Pomodoro Timer ⏲️

Preview Image

A simple CLI pomodoro timer.

This program runs a pomodoro timer in the user’s terminal. This program was built on Arch Linux, and has currently only been tested on that platform.

This guide assumes that you have Rust installed on your system, if you do not, follow the instructions for installing here: https://doc.rust-lang.org/book/ch01-01-installation.html

First, create your new Rust project for this program by running the following:

cargo new pomodoro

Now, change directories into your new ‘pomodoro’ directory.

cd pomodoro

We will now edit our ‘Cargo.toml’ file to include the proper crates for this project. Open ‘Cargo.toml’ in your text editor of choice, then add the following:

[package]
name = "pomodoro"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
soloud = "1"
colored = "2"
figlet-rs = "0.1.5"

There aren’t many dependencies for this project, as most libraries used in this project are from the Rust standard library. The ‘soloud’ library is included to allow audio files to be used in this program. The ‘colored’ and ‘figlet-rs’ libraries are used to display colored text in the terminal, and to display text in an ASCII art format, respectively.

Now, here is the source code for the program. The only other prerequisite to run the program in its current form is to have an audio file in the ’/src’ directory. This file can be called whatever you want, just make sure its name matches in your file system and in the source code for this program.

use colored::Colorize;
use figlet_rs::FIGfont;
use soloud::*;
use std::io;
use std::io::{stdout, Write};
use std::thread;
use std::time::Duration;


fn main() {
    //this block is not strictly necessary for the program,
    //I just thought it looked cool
    let standard_font = FIGfont::standard().unwrap();
    let figure = standard_font.convert("          CLI");
    let figure2 = standard_font.convert("POMODORO");
    println!("{}", figure.unwrap());
    println!("{}", figure2.unwrap());
    //

    let mut user_input = String::new();

    println!("{} 
        \n1: Four 25 minute study sessions with 5 minute breaks, with a 30 minute break every 4 sessions
        \n2: Two 50 minute study sessions with 10 minute breaks, with a 45 minute break every 2 sessions",
    "Choose your mode: ".green());
    println!("\n\nPlease enter your choice here, type 1 or 2:");

    io::stdin()
        .read_line(&mut user_input)
        .expect("Failed to read line.");

    let user_input: u8 = user_input.trim().parse().expect("Please type 1 or 2!");
    match user_input {
        //this match expression determines the amount of seconds and sessions that will be
        //passed to the main_loop function
        1 => main_loop(1500, 300, 1800, 4),
        2 => main_loop(3000, 600, 2700, 2),
        _ => println!("idk"), //figure out how to make them input
                              //either a 1 or a 2 here and repeatedly
                              //prompt them if they don't give the
                              //correct input. Currently, the console will
                              //just print "idk" and end the program
    }
}

//general timer function. abstracted this from the main_loop function because there are many uses
//and possibilities for a timer in this program 
fn timer(time: &mut i32) {
    let mut stdout = stdout();
    let minutes = *time / 60;
    println!(
        "\nStart timer: {} {}",
        minutes.to_string().blue(),
        "minutes".blue()
    );
    while *time > 0 {
        *time -= 1;
        let minutes = *time / 60;
        let seconds = *time % 60;

        //using ':0>2' in the formatters (aka these: {}) keeps the leading 0
        //if the value is less than 10 (ie 9 becomes 09)
        print!("\rRemaining time: {:0>2}:{:0>2} ", minutes, seconds);
        stdout.flush().unwrap();
        thread::sleep(Duration::from_secs(1));
    }
    println!("\n{}", "Time's up!".red());

    alarm();
    thread::sleep(Duration::from_secs(2));
}

//Function to play an alarm sound. If you don't want this functionality,
//it can be removed from the program relatively easily. Just delete this function
//and remove any references to it in the source code
fn alarm() {
    //Creating a new thread to play the alarm sound without interrupting
    //the timer. This referenced .mp3 file lives in
    //the /src folder of this project
    thread::spawn(|| {
        let sl = Soloud::default().unwrap();
        let mut wav = audio::Wav::default();
        wav.load_mem(include_bytes!("ghetto_smosh.mp3")).unwrap();
        sl.play(&wav);
        while sl.voice_count() > 0 {
            std::thread::sleep(std::time::Duration::from_millis(100));
        }
    });
}

//this function is a 30 second timer to give the user time to prepare
//for either a break or a study session
fn between_timers(next_segment: String) {
    let mut stdout = stdout();
    let mut time = 30;
    while time > 0 {
        time -= 1;
        if time > 5 {
            print!(
                "\r{} begins in {:0>2} seconds ",
                next_segment,
                time.to_string().green()
            );
        } else {
            print!(
                "\r{} begins in {:0>2} seconds ",
                next_segment,
                time.to_string().red()
            );
        }
        stdout.flush().unwrap();
        thread::sleep(Duration::from_secs(1))
    }
}

fn main_loop(study: i32, regular_break: i32, long_break: i32, countdown: i32) {
    let mut session_counter = 0;
    loop {
        let mut countdown = countdown;

        while countdown > 0 {
            println!(
                "\n{} Sessions remaining: {}",
                "Study time!".green(),
                countdown.to_string().green()
            );

            between_timers(String::from("Study session"));

            let mut study_time = study;
            timer(&mut study_time);
            session_counter += 1;

            if countdown == 1 {
                let mut long_break_time = long_break;
                println!("\n{}", "Long break time!".green());
                between_timers(String::from("Long break"));
                println!(
                    "\nCompleted sessions: {}",
                    session_counter.to_string().green()
                );
                timer(&mut long_break_time);
            } else {
                println!("\n{}", "Break time!".green());
                between_timers(String::from("Break"));
                let mut break_time = regular_break;
                println!(
                    "\nCompleted sessions: {}",
                    session_counter.to_string().green()
                );
                timer(&mut break_time);
            }
            countdown -= 1
        }
    }
}

There you have it. You can run this program from the ’/src’ directory by entering the command ‘cargo run’ from the ’/src’ directory. If you want to compile this program to have an executable file, simply type ‘cargo build’ in either the ’/src’ directory, or run the same command in the root directory of the project ’/pomodoro’. This will create an executable file called ‘pomodoro’ in the ’/pomodoro/target/debug’ directory. In Linux, this can be ran by changing directories to ’/pomodoro/target/debug’ and entering the command ’./pomodoro’.

As you can see, this project is fairly barebones, and some features are either ommitted or annotated with comments to implement later. However, for my purposes, this program provides the functionality that I require in a pomodoro timer, and I learned a lot about Rust in the process of creating it.