Contact

Building a Pomodoro Timer CLI in Rust

November 25, 2025
Nick Paolini
5 min read
RustCLISide ProjectsProductivity
Building a Pomodoro Timer CLI in Rust

Over the past few weeks, I've been learning Rust by building something I actually use every day: a Pomodoro timer. Yes, there are a million Pomodoro apps out there, but building my own has been one of the best learning experiences I've had in a while.

Why a CLI Tool?

I spend most of my day in the terminal anyway, so why switch contexts to a GUI app? Plus, CLI tools are perfect for learning a new language because they're:

  • Simple enough to finish
  • Complex enough to be interesting
  • Actually useful in your workflow

The Basic Requirements

Here's what I wanted:

  • Start a 25-minute focus session
  • 5-minute short break
  • 15-minute long break (after 4 sessions)
  • Desktop notifications when time's up
  • Simple commands: pomo start, pomo break, pomo status

Getting Started with Rust

I'm coming from JavaScript/TypeScript, so Rust's ownership model took some getting used to. Here's a simple timer function:

use std::time::Duration;
use std::thread;
 
fn start_timer(minutes: u64) {
    let duration = Duration::from_secs(minutes * 60);
    println!("Timer started for {} minutes", minutes);
 
    thread::sleep(duration);
 
    println!("Time's up! ⏰");
}

Pretty straightforward! But then I wanted to add a countdown display that updates every second...

The Challenge: Updating the Display

In JavaScript, I'd use setInterval. In Rust, I learned about threads and channels:

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
 
fn start_timer_with_display(minutes: u64) {
    let total_seconds = minutes * 60;
    let start_time = Instant::now();
 
    loop {
        let elapsed = start_time.elapsed().as_secs();
        let remaining = total_seconds.saturating_sub(elapsed);
 
        if remaining == 0 {
            break;
        }
 
        let mins = remaining / 60;
        let secs = remaining % 60;
 
        print!("\r{:02}:{:02} remaining", mins, secs);
        std::io::stdout().flush().unwrap();
 
        thread::sleep(Duration::from_secs(1));
    }
 
    println!("\n\nTime's up! ⏰");
}

That \r character is doing the heavy lifting here - it returns the cursor to the start of the line so we can overwrite the previous time.

Adding Desktop Notifications

This is where Rust's ecosystem really shines. The notify-rust crate makes desktop notifications trivial:

use notify_rust::Notification;
 
fn notify(title: &str, body: &str) {
    Notification::new()
        .summary(title)
        .body(body)
        .timeout(0) // Don't auto-dismiss
        .show()
        .unwrap();
}
 
// Usage
notify("Pomodoro Complete!", "Time for a break 🎉");

Works on macOS, Linux, and Windows. Love it.

Persistent State

I wanted to track how many pomodoros I've completed today, which means persisting state between runs. I went with a simple JSON file in the home directory:

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
 
#[derive(Serialize, Deserialize, Default)]
struct PomodoroState {
    completed_today: u32,
    total_completed: u32,
    current_session: u32,
}
 
fn get_state_path() -> PathBuf {
    let mut path = dirs::home_dir().unwrap();
    path.push(".pomo_state.json");
    path
}
 
fn load_state() -> PomodoroState {
    let path = get_state_path();
 
    if path.exists() {
        let contents = fs::read_to_string(path).unwrap();
        serde_json::from_str(&contents).unwrap()
    } else {
        PomodoroState::default()
    }
}
 
fn save_state(state: &PomodoroState) {
    let path = get_state_path();
    let json = serde_json::to_string_pretty(state).unwrap();
    fs::write(path, json).unwrap();
}

The serde crate handles JSON serialization beautifully. Coming from JavaScript, this feels like magic - type-safe JSON parsing with zero runtime overhead!

The Command Line Interface

For parsing CLI arguments, I used clap. It's incredibly powerful:

use clap::{Parser, Subcommand};
 
#[derive(Parser)]
#[command(name = "pomo")]
#[command(about = "A simple Pomodoro timer", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand)]
enum Commands {
    /// Start a focus session
    Start {
        /// Duration in minutes (default: 25)
        #[arg(short, long, default_value_t = 25)]
        minutes: u64,
    },
    /// Take a break
    Break {
        /// Short break (5 min) or long break (15 min)
        #[arg(short, long)]
        long: bool,
    },
    /// Show current status
    Status,
}

This generates help text automatically and handles all the parsing. Run pomo --help and you get beautiful, formatted documentation for free.

What I Learned

1. The Borrow Checker is Your Friend

At first, the borrow checker felt like fighting the compiler. But after a few days, I realized it was catching real bugs. No more undefined is not a function runtime errors!

2. Error Handling is Explicit

Every function that can fail returns a Result. No silent failures, no forgotten error handling. It forces you to think about what can go wrong:

use std::fs;
use std::io;
 
fn read_config() -> Result<String, io::Error> {
    fs::read_to_string("config.json")
}
 
// You must handle the Result
match read_config() {
    Ok(config) => println!("Config: {}", config),
    Err(e) => eprintln!("Failed to read config: {}", e),
}

3. The Rust Community is Amazing

The documentation is excellent, crates.io is well-organized, and when I got stuck, the Rust Discord was incredibly helpful.

What's Next?

I'm planning to add:

  • Sound alerts (different sounds for breaks vs. sessions)
  • Statistics and charts (using termgraph maybe?)
  • Integration with task management tools
  • Maybe a TUI with ratatui?

Try It Yourself

If you're interested in learning Rust, I highly recommend building a CLI tool. Start with something simple like a todo app or timer, and gradually add features. The instant feedback of a working tool makes learning stick.

Here are some resources I found helpful:

Have you built any CLI tools recently? What's your favorite project to build when learning a new language?