Create a Full stack Rust desktop App with Tauri, Yew and Tailwind CSS
Tags: rust, programming, tutorial, learning
Published: 2023-11-16 22:44:08

In this tutorial, we're going to be building a desktop Pomodoro timer app using Tauri and Yew. I especially like this build because it uses rust for every single aspect (except CSS of course). If you're unfamiliar with the the Pomodoro technique, it is a time management method shown to help maintain productivity throughout your day, which is especially important in fields like software development.

We'll be using some community packages; `gloo-timers` for timer functionality and `yew-feather` for icons. Additionally, we'll style our components using Tailwind, and I'll guide you through its setup in this tutorial.

Along with this build I've put together a YouTube video demonstrating the build from start to finish, can see that here:

{% embed https://www.youtube.com/watch?v=7UNT04X-h6I %}

Also you can find the source code [here](https://github.com/max-taylor/Full-Stack-Rust---Pomodoro-Desktop-App/tree/main)

## Project setup

Let's start by setting up the project, for this we will use the Tauri CLI. This command requires `yarn`, which can be installed following the instructions [here](https://yarnpkg.com/getting-started/install).

Run the following command in your console:
```
yarn create tauri-app
```

Answer the prompts as follows:

- **Project name**: `pomodoro-app`
- **Frontend language**: Rust (cargo)
- **UI template**: Yew - (https://yew.rs/)

After completing these steps, you'll have a `pomodoro-app` directory with the necessary boilerplate to kickstart our Tauri app. Note: If any dependencies are missing, the CLI will guide you through the installation.

**Install Dependencies:**

Let's install our additional dependencies now:

```bash
cargo add yew-feather gloo-timers
```

**Enable System Tray:**

To display our timer's status in the system tray, we need to enable Tauri's system-tray functionality. Begin by updating the Tauri dependency in `src-tauri/cargo.toml` to include the `system-tray` feature:

```json
tauri = { version = "1.5", features = ["shell-open", "system-tray"] }
```

Next, configure an icon for the system tray. Modify the `tauri` field in `src-tauri/tauri.conf.json` to specify an icon path:

```json
{
  ...
  "tauri": {
    "systemTray": {
      "iconPath": "icons/icon.png",
      "iconAsTemplate": true
    },
    ...
  }
}
```

Ensure to keep the existing settings intact while adding the `systemTray` object.

After these changes, reinstall the Tauri app packages:

```shell
cargo install --path ./src-tauri
```

**Improve Development Experience:**

Enhancing the development workflow is crucial for efficiency. Currently, closing the window terminates the server, and there's no way to reopen the window once closed. Let's refine our `src-tauri/main.rs` file to address these issues:

```rust
fn main() {
    // Create the system tray object
    let tray = SystemTray::new();

    tauri::Builder::default()
        .on_window_event(|event| {
            match event.event() {
                // Prevents the dev server from exiting when we close the window
                tauri::WindowEvent::CloseRequested { api, .. } => {
                    event.window().hide().unwrap();
                    api.prevent_close();
                }
                _ => {}
            }
        })
        // Include the system tray and open the window when the icon is pressed
        .system_tray(tray)
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick {
                position: _,
                size: _,
                ..
            } => {
                let window = app.get_window("main").unwrap();
                window.show().unwrap();
            }
            _ => (),
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

This update ensures that closing the window hides it instead of shutting down the server, and clicking on the system tray icon will reopen the window.

**Setup Tailwind:**

Integrating Tailwind CSS into our project will give us a powerful yet simple way to style our components. First, ensure you have `npx` installed, a tool that allows executing npm package binaries. It's a part of npm, and you can learn more about it [here](https://www.npmjs.com/package/npx).

To create a Tailwind configuration file, run:

```shell
npx tailwindcss init
```

This command generates a `tailwind.config.js` file in your project's root directory. Modify this file to include our Rust files:

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.rs"],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Next, create a CSS file named `src/input.css` and insert the following Tailwind directives:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

To integrate this stylesheet into your app, update the `index.html` file in the root directory:

```html
<link href="output.css" data-trunk rel="css" />
```

This ensures that the generated CSS is included in the app.

**Start Services:**

With the setup complete, it's time to start the development server and the Tailwind watcher service. Open two separate terminals and execute the following commands:

Tauri Dev Server:

```bash
cargo tauri dev
```

Tailwind Watcher Service:

```bash
npx tailwindcss -i ./src/input.css -o ./output.css --watch
```

These commands will start the Tauri development server and initiate Tailwind's process to watch for any changes in your CSS file, ensuring real-time updates.

---

## Timer

**State hooks:**

To manage our timer, we'll use three state hooks in `src/app.rs`. These hooks are crucial for handling different aspects of our timer's functionality:

- `session_length`: Determines the duration of a session, allowing users to adjust it with buttons.
- `timer_duration`: Tracks the elapsed time in a work or break session.
- `timer_state`: Manages three distinct states of the timer - Paused, Running, and Break.

Update `src/app.rs` with the following code to define these states:

```rust
use yew::prelude::*;

#[derive(PartialEq, Copy, Clone, Debug)]
pub enum TimerState {
    Paused,
    Running,
    Break,
}

#[function_component(App)]
pub fn app() -> Html {
    let session_length = use_state(|| 25 * 60); // Default 25 minutes
    let timer_duration = use_state(|| 0);
    let timer_state = use_state(|| TimerState::Paused);

    html! {
        <div class={classes!("flex", "items-center", "justify-center", "flex-col", "h-screen")}>
            {"Pomodoro Timer"}
        </div>
    }
}
```

**Timer Logic:**

Next, we'll implement the logic to update `timer_duration` every second. We'll use a `use_effect_with_deps` to create a timer that updates our state:

```rust
use_effect_with_deps(
    move |props| {
        let (timer_duration, timer_state, _) = props.clone();

        let timeout = Timeout::new(1_000, move || {
            if *timer_state != TimerState::Paused {
                timer_duration.set(*timer_duration + 1);
            }
        });
        
        move || {
	        timeout.cancel();
        }
    },
    (
        timer_duration.clone(),
        timer_state.clone(),
        session_length.clone(),
    ),
);
```

This sets up a one-second timeout updating the `timer_duration`, unless the timer is paused. When the component unmounts, the timeout is canceled. We also need to clone our states multiple times, so we can move the ownership into the `use_effect_with_deps` closure and then the `Timeout::new` closure.

**Timer UI:**

For the timer display, we'll be formatting the time into MM:SS format, create `src/helpers.rs` and add the following function for time formatting:

```rust
pub fn format_time(seconds: u32) -> String {
    let minutes = seconds / 60;
    let seconds = seconds % 60;
    format!("{:02}:{:02}", minutes, seconds)
}
```

Include this helper in `src/main.rs`:

```rust
mod helpers;
```

Now let's create a directory to hold our UI components: `src/components` and create the file `src/components/timer_display.rs` for the timer UI. This component will display the timer and the buttons required to increase our session length. Let's first define the imports, props and integrate the MM::SS formatting:

```rust
use yew::prelude::*;
use crate::{app::TimerState, helpers::format_time};

// Define the component's properties
#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub timer_state: UseStateHandle<TimerState>,
    pub timer_duration: UseStateHandle<u32>,
    pub session_length: UseStateHandle<u32>,
}

#[function_component]
pub fn TimerDisplay(props: &Props) -> Html {
	let is_expired = *props.timer_duration > *props.session_length;
	
    let get_session_display: String = {
        let Props {
            timer_duration,
            session_length,
            ..
        } = props.clone();

        if is_expired {
            format_time(*timer_duration)
        } else {
            format_time(*session_length - *timer_duration)
        }
    };

    html! {
        <div class={classes!("flex", "flex-col", "space-y-2", "items-center", "mb-3")}>
          <p class={classes!("text-5xl", "min-w-[50px]", "text-right")}>
              {get_session_length}
          </p>
        </div>
    }
}
```

We need to allow our app to have access to this component, do this by creating the file `src/components.rs` and exposing the `timer_display` module:

```rust
pub mod timer_display;
```

Finally, integrate the `TimerDisplay` component in `src/app.rs`:

```rust
html! {
    <div class={classes!("flex", "items-center", "justify-center", "flex-col", "h-screen")}>
        <TimerDisplay timer_state={timer_state.clone()} timer_duration={timer_duration.clone()} session_length={session_length.clone()} />
    </div>
}
```

Congratulations 🎉, we now have a really basic viewer for our time remaining:

![Basic view of Pomodoro timer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkylmx41csxyqr96lwpv.png)

**Session indicator:**

Currently it's a bit confusing what is going on with the timer, let's resolve this by creating a simple indicator to display the timer state. This requires an additional method in `timer_display.rs`:

```rust
let session_state_display = {
    let timer_state = props.timer_state.clone();

    match *timer_state {
        TimerState::Paused => "Paused".to_string(),
        TimerState::Break => {
            if isExpired {
                "Finished break".to_string()
            } else {
                "On break".to_string()
            }
        }
        TimerState::Running => {
            if isExpired {
                "Finished session".to_string()
            } else {
                "In session".to_string()
            }
        }
    }
};
```

We will integrate this method into our UI later on in the tutorial.

**Adjusting Session Length:**

For dynamic control over the session length, we will use Callback closures. These methods will be triggered by buttons to increase and decrease the session length, let's define them:

```rust
let increase_session_length = {
    let session_length = props.session_length.clone();
    
    Callback::from(move |_: ()| {
        session_length.set(*session_length + 60 * 5); // Increase by 5 minutes
    })
};

let decrease_session_length = {
    let session_length = props.session_length.clone();
    Callback::from(move |_: ()| {
        session_length.set(*session_length - 60 * 5); // Decrease by 5 minutes
    })
};
```

Finally, we integrate all the logic we've developed into our UI with proper styling:

```rust
html! {
    <div class={classes!("flex", "flex-col", "space-y-2", "items-center")}>
        <div class={classes!("flex", "flex-row", "space-x-3")}>
            <button onclick={move |_| {
                decrease_session_length.emit(());
            }} class={classes!("p-2", "border-2", "border-red-500")}>
                { "- 5" }
            </button>
            <p class={classes!("text-5xl")}>
                {get_session_length}
            </p>
            <button onclick={move |_| {
                increase_session_length.emit(());
            }} class={classes!("p-2", "border-2", "border-green-500")}>
                { "+ 5" }
            </button>
        </div>
        {session_state_display}
    </div>
}
```

Our application will now have improved controls, allowing us to increase and decrease the timer state and it will also display more information, making the UI easier to understand.


![Pomodoro image including increment decrement buttons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f667hhf0z3clse6a99y.png)

---

## System Tray Updating:

To manage and display the timer's status in the system tray, we need to define the necessary data structures and functions. Start by declaring a struct for setting the tray title and the required wasm method:

```rust
#[derive(Serialize, Deserialize)]
struct SetTitleArgs<'a> {
    title: &'a str,
}

// Defines an async Rust function to call Tauri, used for updating the system tray state.
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "tauri"])]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
```

Then, create a function that determines the appropriate tray title based on the current state:

```rust
pub fn get_tray_title(timer_state: TimerState, timer_duration: u32, session_length: u32) -> String {
    match timer_state {
        TimerState::Paused => String::from("Paused"),
        TimerState::Running => {
            if timer_duration >= session_length {
                return format!("Finished session: {}", format_time(timer_duration));
            }
            return format!(
                "In session: {}",
                format_time(session_length - timer_duration)
            );
        }
        TimerState::Break => {
            if timer_duration >= session_length {
                return format!("Finished break: {}", format_time(timer_duration));
            }
            return format!("Break: {}", format_time(session_length - timer_duration));
        }
    }
}
```

Inside `src/app.rs`, update the `use_effect_with_deps` to handle tray title updates, add the below snippet under the `let timeout = ...` declaration:

```rust
let (timer_duration, timer_state, session_length) = props.clone();

// Spawn a thread so that it can await the async call
spawn_local(async move {
    let args = to_value(&SetTitleArgs {
        title: get_tray_title(*timer_state, *timer_duration, *session_length).as_str(),
    })
    .unwrap();

    invoke("set_title", args).await;
});
```

For the Tauri application in `src-tauri/src/main.rs`, implement the `set_title` command to update the system tray:

```rust
#[tauri::command]
async fn set_title(app_handle: tauri::AppHandle, title: String) {
    if let Err(e) = app_handle.tray_handle().set_title(&title) {
        eprintln!("error updating timer: {}", e);
    }
}
```

Add this to your `tauri::builder::default()` under `.on_system_tray_event`:

```rust
.invoke_handler(tauri::generate_handler![set_title])
```

---

## Timer Controls:

We will now implement the core timer controls, allowing us to start, pause, reset the timer, and initiate or finish a break. Create the file `src/components/timer_controls.rs` and let's first define our imports, required props and TimerControl declaration:

```rust
use yew::prelude::*;
use yew_feather::{Coffee, Pause, Play, RefreshCcw};
use crate::app::TimerState;

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub timer_state: UseStateHandle<TimerState>,
    pub timer_duration: UseStateHandle<u32>,
    pub session_length: UseStateHandle<u32>,
}

#[function_component]
pub fn TimerControls(props: &Props) -> Html {
	let Props {
		timer_state,
		timer_duration,
		session_length,
	} = props;
}
```

**Callbacks:**

To handle the various user interactions, we will need to declare a variety of `Callback<()>` instances, each altering the state in a unique way:

```rust
let start_timer: Callback<()> = {
	let timer_state = timer_state.clone();

	Callback::from(move |_| {
		timer_state.set(TimerState::Running);
	})
};

let pause_timer: Callback<()> = {
	let timer_state = timer_state.clone();

	Callback::from(move |_| {
		timer_state.set(TimerState::Paused);
	})
};

let reset_timer: Callback<()> = {
	let timer_state = timer_state.clone();
	let timer_duration = timer_duration.clone();
	let session_length = session_length.clone();

	Callback::from(move |_| {
		timer_state.set(TimerState::Paused);
		timer_duration.set(0);
		session_length.set(25 * 60); // Reset to 25 minute session time
	})
};

let take_break: Callback<()> = {
	let timer_state = timer_state.clone();
	let timer_duration = timer_duration.clone();
	let session_length = session_length.clone();

	Callback::from(move |_| {
		timer_state.set(TimerState::Break);
		timer_duration.set(0);
		session_length.set(5 * 60); // 5 minute break time
	})
};

let finish_break: Callback<()> = {
	let timer_state = timer_state.clone();
	let timer_duration = timer_duration.clone();
	let session_length = session_length.clone();

	Callback::from(move |_| {
		timer_state.set(TimerState::Running);
		timer_duration.set(0);
		session_length.set(25 * 60); // Reset state to 25 minutes
	})
};
```

**Rendering the buttons:**

The returned HTML depends on the `timer_state`, we want to return varying controls to the user depending on whether the timer is; Running, Paused or Break. Let's use a match block to handle this:

```rust
match **timer_state {
	TimerState::Running => {
		html!(
		  <div class={classes!("flex", "flex-row", "space-x-2")}>
			<button class={classes!("p-3")} onclick={move |_| {
			  take_break.emit(());
			}}>
			  <Coffee />
			</button>
			<button class={classes!("p-3")} onclick={move |_| {
			  pause_timer.emit(());
			}}>
			  <Pause />
			</button>
			<button class={classes!("p-3")} onclick={move |_| {
			  reset_timer.emit(());
			}}>
			  <RefreshCcw />
			</button>
		  </div>
		)
	}
	TimerState::Paused => {
		html!(
		  <div class={classes!("flex", "flex-row", "space-x-2")}>
			<button class={classes!("p-3")} onclick={move |_| {
			  take_break.emit(());
			}}>
			  <Coffee />
			</button>
			<button class={classes!("p-3")} onclick={move |_| {
			  start_timer.emit(());
			}}>
			  <Play />
			</button>
			<button class={classes!("p-3")} onclick={move |_| {
			  reset_timer.emit(());
			}}>
			  <RefreshCcw />
			</button>
		  </div>
		)
	}
	TimerState::Break => {
		html!(
		  <div class={classes!("flex", "flex-row", "space-x-2")}>
			<button class={classes!("p-3")} onclick={move |_| {
			  finish_break.emit(());
			}}>
			  <Play />
			</button>
		  </div>
		)
	}
}
```

And finally let's integrate this component into our `src/app.rs` by adding the following line after our `<TimerDisplay />` component:

```rust
<TimerControls session_length={session_length.clone()} timer_state={timer_state.clone()} timer_duration={timer_duration.clone()} />
```

Great work! You will now have a fully functional UI and a useful tool that you can continue to iterate on over time assisting you in your software development


![Final image of pomodoro app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8tsh72wlps2pnhl6xgk.png)


---

## Conclusion:

Congratulations! 🎉 You've just built a full-stack Rust desktop app using Tauri and Yew, complete with a functional Pomodoro timer. This app not only serves as a practical tool for managing work and break periods but also demonstrates the power and flexibility of Rust in creating desktop applications.

I hope this tutorial has been insightful and enjoyable. Rust's growing ecosystem offers a unique approach to desktop application development, combining performance, safety, and modern tooling.

If you enjoyed the tutorial and want to see more of my other work checkout the links:
[Twitter](https://twitter.com/max_t_dev)
[YouTube](https://www.youtube.com/channel/UCNnu7xjW0hyUgRxOegrS86A)