Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# AGENTS.md - Timer CLI Codebase Guide

## Project Overview

**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`

## Architecture

```
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
```

## Module Responsibilities

### `main.rs` - Application Entry Point
- Parses CLI arguments using `clap`
- Routes to countdown or stopwatch based on subcommand
- Converts input time to `OffsetDateTime` target (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 (`--loop` flag)

**Key functions:**
- `parse_time(input_time: &str) -> Option<OffsetDateTime>` - Parses duration or target time
- `handle_countdown(w, end, opts) -> Result<()>` - Runs countdown with terminal restoration
- `run_stopwatch()` - Runs stopwatch mode
- `run_countdown(opts)` - Runs countdown mode
- `main()` - Entry point orchestrating everything

### `opts.rs` - CLI Options
Defines the `Opts` struct with clap derive:
```rust
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`

### `timer.rs` - Core Countdown Logic
- `parse_counter_time(s: &str) -> Option<Duration>` - Parses duration strings using regex
- `parse_end_time(s: &str) -> Option<OffsetDateTime>` - Parses target time strings
- `countdown(w, end, opts) -> Result<()>` - Main tail-recursive countdown loop
- `resize_term(w, end) -> Result<()>` - Redraws on terminal resize
- `play_beep() -> Result<()>` - Plays hardware PC speaker beep
- `play_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.

### `stopwatch.rs` - Core Stopwatch Logic
- `run(w) -> Result<()>` - Main stopwatch loop with keyboard handling
- `State` enum - State machine (Running/Paused) tracking elapsed time
- `handle_key(key) -> Action` - Maps keyboard input to actions

**Stopwatch controls:**
- `Space` / `p` - Toggle pause/resume
- `l` / `Enter` - Record lap time
- `r` - Reset stopwatch
- `q` / `Ctrl+C` - Quit

**State machine:**
```rust
enum State {
Running { start: Instant, accumulated: Duration },
Paused { accumulated: Duration },
}
```

### `time.rs` - Time Representation & Rendering
Defines the `Time` struct:
```rust
pub struct Time {
hours: u64,
minutes: u8,
seconds: u8,
}
```

**Key functions:**
- `From<&Duration> for Time` - Converts `time::Duration` to `Time`
- `format() -> String` - Returns formatted string like `2h 10m 5s`
- `render(size: (u16, u16)) -> String` - Renders as centered FIGlet ASCII art

**Rendering strategy (graceful degradation):**
1. Full format: `2h 10m 5s`
2. If too wide: `2h 10m` (omit seconds)
3. If still too wide: `2h` (omit minutes)
4. If still too wide: plain text (no FIGlet)

### `ui.rs` - Terminal UI Management
Uses `crossterm` for terminal operations:
- `draw(w, counter: Duration) -> Result<()>` - Clears screen, sets title, renders time
- `draw_with_laps(w, elapsed, laps, is_running) -> Result<()>` - Draws stopwatch with lap times
- `set_up_terminal(w) -> Result<()>` - Enters alternate screen, hides cursor
- `restore_terminal(w) -> Result<()>` - Shows cursor, leaves alternate screen

### `beep.rs` - Hardware PC Speaker
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

### `sound/mod.rs` - Audio Playback
Uses `rodio` crate:
- Embeds `beep.ogg` at compile time
- `Sound::new() -> Result<Sound>` - Opens audio stream
- `Sound::play(&self) -> Result<()>` - Plays the embedded sound

### `figlet/mod.rs` - ASCII Art Text
FIGlet font rendering:
- Embeds `univers.flf` font at compile time
- `Figlet::convert(&self, s: &str) -> String` - Converts text to ASCII art

### `constants.rs` - Timing Constants
```rust
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)
```

### `utils.rs` - Thread Utilities
- `JoinWithTimeout<T>` - Thread handle with timeout support
- `spawn_thread(f) -> JoinWithTimeout<T>` - Spawns thread with completion signal
- `join(timeout) -> Option<T>` - Waits with timeout

## Key Dependencies

| 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 |

## Application Flow

### Countdown Flow
```
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
```

### Stopwatch Flow
```
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)
```

## Testing

Run tests with:
```bash
cargo test
```

## Building

```bash
cargo build --release
```

The binary is optimized with LTO and symbol stripping (see `Cargo.toml` profile).

## Code Style

- 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

## Extension Points

When adding new features:
- **New CLI options:** Add to `Opts` struct in `opts.rs`
- **New time formats:** Extend parsing in `timer.rs`
- **UI changes:** Modify `ui.rs` and `time.rs` rendering
- **New alerts:** Add to `beep.rs` or `sound/mod.rs`
- **Subcommands:** Consider using clap subcommands in `opts.rs`
30 changes: 29 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod constants;
mod figlet;
mod opts;
mod sound;
mod stopwatch;
mod time;
mod timer;
mod ui;
Expand All @@ -11,7 +12,7 @@ mod utils;
extern crate time as time_crate;

use crate::beep::beep;
use crate::opts::Opts;
use crate::opts::{Command, Opts};

use std::io;
use std::process::exit;
Expand Down Expand Up @@ -49,6 +50,33 @@ fn handle_countdown<W: io::Write>(w: &mut W, end: OffsetDateTime, opts: &Opts) {
fn main() {
let opts: Opts = Opts::parse();

// Handle stopwatch subcommand
if let Some(Command::Stopwatch) = opts.command {
run_stopwatch();
return;
}

// Handle countdown (default behavior)
run_countdown(opts);
}

fn run_stopwatch() {
let mut stdout = io::stdout();
ui::set_up_terminal(&mut stdout).unwrap();

match stopwatch::run(&mut stdout) {
Ok(_) => {
ui::restore_terminal(&mut stdout).unwrap();
}
Err(e) => {
ui::restore_terminal(&mut stdout).unwrap();
eprintln!("Error: {e:?}");
exit(1);
}
}
}

fn run_countdown(opts: Opts) {
let input_time = opts.time.join(" ");
let end = match parse_time(input_time.as_str()) {
Some(x) => x,
Expand Down
11 changes: 10 additions & 1 deletion src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{ArgAction, Parser, crate_authors, crate_description, crate_version};
use clap::{ArgAction, Parser, Subcommand, crate_authors, crate_description, crate_version};

#[derive(Parser)]
#[command(
Expand All @@ -8,6 +8,9 @@ use clap::{ArgAction, Parser, crate_authors, crate_description, crate_version};
author = crate_authors!("\n"),
)]
pub struct Opts {
#[command(subcommand)]
pub command: Option<Command>,

/// Repeat countdown infinitely
#[arg(short, long)]
pub r#loop: bool,
Expand All @@ -24,6 +27,12 @@ pub struct Opts {
pub time: Vec<String>,
}

#[derive(Subcommand)]
pub enum Command {
/// Start a stopwatch (counts up from zero)
Stopwatch,
}

#[test]
fn verify_cli() {
use clap::CommandFactory;
Expand Down
Loading
Loading