Timer CLI is a Rust-based terminal countdown timer and stopwatch with ASCII art display, sound alerts, and hardware PC speaker support. It provides a simple, elegant way to set countdown timers or run a stopwatch with large, centered FIGlet text output.
Repository: https://github.com/pando85/timer
Binary name: timer
Package name: timer-cli
src/
├── main.rs # Entry point, CLI parsing, signal handling, main loop
├── opts.rs # CLI options structure (clap derive)
├── timer.rs # Core countdown logic, time string parsing
├── stopwatch.rs # Core stopwatch logic, state machine, keyboard handling
├── time.rs # Time struct, formatting, FIGlet ASCII rendering
├── ui.rs # Terminal UI (crossterm): setup, draw, restore
├── beep.rs # Linux PC speaker hardware beep (ioctl)
├── constants.rs # Timing constants for beep/sound
├── utils.rs # Thread utilities with timeout
├── figlet/
│ ├── mod.rs # FIGlet ASCII art text conversion
│ └── univers.flf # FIGlet font file (embedded)
└── sound/
├── mod.rs # Audio playback via rodio
└── beep.ogg # Embedded notification sound
- Parses CLI arguments using
clap - Routes to countdown or stopwatch based on subcommand
- Converts input time to
OffsetDateTimetarget (for countdown) - Sets up terminal (alternate screen, hidden cursor)
- Spawns countdown thread
- Handles Unix signals (SIGWINCH for resize, SIGTERM/SIGINT/SIGQUIT for exit)
- Supports loop mode (
--loopflag)
Key functions:
parse_time(input_time: &str) -> Option<OffsetDateTime>- Parses duration or target timehandle_countdown(w, end, opts) -> Result<()>- Runs countdown with terminal restorationrun_stopwatch()- Runs stopwatch moderun_countdown(opts)- Runs countdown modemain()- Entry point orchestrating everything
Defines the Opts struct with clap derive:
pub struct Opts {
pub command: Option<Command>, // Subcommand (stopwatch)
pub r#loop: bool, // -l, --loop: Repeat countdown
pub silence: bool, // -s, --silence: Disable sounds
pub terminal_bell: bool, // -t, --terminal-bell: Enable bell character
pub time: Vec<String>, // Positional: time input
}
pub enum Command {
Stopwatch, // Start a stopwatch (counts up from zero)
}Supported time formats (countdown):
- Duration:
10s,5m,2h10m,1h30m45s,15min,10(seconds) - Target time:
12:00,08:25,13:45:43
parse_counter_time(s: &str) -> Option<Duration>- Parses duration strings using regexparse_end_time(s: &str) -> Option<OffsetDateTime>- Parses target time stringscountdown(w, end, opts) -> Result<()>- Main tail-recursive countdown loopresize_term(w, end) -> Result<()>- Redraws on terminal resizeplay_beep() -> Result<()>- Plays hardware PC speaker beepplay_sound() -> Result<()>- Plays audio file with timeout
Countdown algorithm: Uses tail-call optimization (tailcall crate) to recursively count down, drawing the UI each second until the end time is reached.
run(w) -> Result<()>- Main stopwatch loop with keyboard handlingStateenum - State machine (Running/Paused) tracking elapsed timehandle_key(key) -> Action- Maps keyboard input to actions
Stopwatch controls:
Space/p- Toggle pause/resumel/Enter- Record lap timer- Reset stopwatchq/Ctrl+C- Quit
State machine:
enum State {
Running { start: Instant, accumulated: Duration },
Paused { accumulated: Duration },
}Defines the Time struct:
pub struct Time {
hours: u64,
minutes: u8,
seconds: u8,
}Key functions:
From<&Duration> for Time- Convertstime::DurationtoTimeformat() -> String- Returns formatted string like2h 10m 5srender(size: (u16, u16)) -> String- Renders as centered FIGlet ASCII art
Rendering strategy (graceful degradation):
- Full format:
2h 10m 5s - If too wide:
2h 10m(omit seconds) - If still too wide:
2h(omit minutes) - If still too wide: plain text (no FIGlet)
Uses crossterm for terminal operations:
draw(w, counter: Duration) -> Result<()>- Clears screen, sets title, renders timedraw_with_laps(w, elapsed, laps, is_running) -> Result<()>- Draws stopwatch with lap timesset_up_terminal(w) -> Result<()>- Enters alternate screen, hides cursorrestore_terminal(w) -> Result<()>- Shows cursor, leaves alternate screen
Linux-specific hardware beep via ioctl:
- Searches for beep devices:
/dev/input/by-path/platform-pcspkr-event-spkr,/dev/console, TTYs - Two driver strategies: Console driver (KIOCSOUND) and Evdev driver (input_event)
beep(freq: i32, time: Duration) -> Result<()>- Plays beep at frequency
Uses rodio crate:
- Embeds
beep.oggat compile time Sound::new() -> Result<Sound>- Opens audio streamSound::play(&self) -> Result<()>- Plays the embedded sound
FIGlet font rendering:
- Embeds
univers.flffont at compile time Figlet::convert(&self, s: &str) -> String- Converts text to ASCII art
pub const BEEP_DELAY: u64 = 100; // ms between beeps
pub const BEEP_DURATION: u64 = 400; // ms per beep
pub const BEEP_REPETITIONS: usize = 5; // number of beeps
pub const BEEP_FREQ: i32 = 440; // Hz (A4 note)JoinWithTimeout<T>- Thread handle with timeout supportspawn_thread(f) -> JoinWithTimeout<T>- Spawns thread with completion signaljoin(timeout) -> Option<T>- Waits with timeout
| Crate | Purpose |
|---|---|
clap |
CLI argument parsing with derive macros |
crossterm |
Cross-platform terminal manipulation |
time |
Date/time handling and formatting |
regex |
Time input string parsing |
rodio |
Audio playback (vorbis support) |
signal-hook |
Unix signal handling |
tailcall |
Tail-call optimization for recursion |
libc / nix |
Low-level Linux ioctl for PC speaker |
glob |
Finding TTY devices |
1. main()
├── Parse CLI args → Opts
├── parse_time(input) → OffsetDateTime
│ ├── Try parse_counter_time() for "5m30s" style
│ └── Or parse_end_time() for "12:30" style
├── ui::set_up_terminal()
├── Spawn countdown thread
│ └── timer::countdown() [tail-recursive]
│ ├── Calculate remaining = end - now
│ ├── ui::draw(remaining)
│ ├── sleep(1 second)
│ └── Recurse until done, then play alerts
└── Main thread: Signal handler loop
├── SIGWINCH → resize_term()
└── SIGTERM/SIGINT/SIGQUIT → cleanup & exit
1. main()
├── Parse CLI args → Opts
├── Match Command::Stopwatch
├── ui::set_up_terminal()
└── stopwatch::run()
├── Initialize State::Running
└── Loop:
├── Calculate elapsed time
├── ui::draw_with_laps(elapsed, laps)
├── Poll keyboard events (100ms timeout)
└── Handle key → Action (Quit/Pause/Lap/Reset)
Run tests with:
cargo testcargo build --releaseThe binary is optimized with LTO and symbol stripping (see Cargo.toml profile).
- Uses Rust 2024 edition
- Tail-call optimization for recursive functions
- Embedded assets (font, sound) at compile time
- Error handling via
Box<dyn std::error::Error> - Modular separation of concerns
When adding new features:
- New CLI options: Add to
Optsstruct inopts.rs - New time formats: Extend parsing in
timer.rs - UI changes: Modify
ui.rsandtime.rsrendering - New alerts: Add to
beep.rsorsound/mod.rs - Subcommands: Consider using clap subcommands in
opts.rs