Tiny Timer CLI is a command-line Pomodoro timer written in Go. It features:
- Countdown timer mode (default): tracks time remaining with an animated progress bar
- Count-up mode (
--count-up): tracks elapsed time and allows logging tasks to SQLite - TUI built with Bubble Tea framework (bubbletea + bubbles + lipgloss)
- SQLite persistence for task logging
- Cross-platform support (macOS notifications on darwin)
Generate a summary and commit to VCS with jj:
jj status
jj diff
jj commit -m "feat: implement keyboard shortcuts"Never run git for any reason. Only use jj.
Trust that the commit was effective; do not run any follow up commands unless the commit fails.
make build # Build binary (outputs: tiny-timer)
make install # Install to GOPATH/bin
make clean # Clean build cache and test cachemake test # Run all tests (go test ./...)
go test -v # Run with verbose outputgo run . # Run directly without building
make list # Show all available make targetsThe codebase uses multiple files organized by responsibility:
main.go: Entry point and application initializationmodel.go: Model struct and state managementhandlers.go: Event handlers (Update function with separate update* handlers)view.go: View rendering logicdatabase.go: Database operationsutils.go: Helper functions (formatting, notifications)constants.go: Constants and configuration valuestiny_timer_test.go: Comprehensive test suite- Follows Go testing conventions (
*_test.gosuffix) - Uses
testifyassertions - Tests organized by feature with clear names
- Follows Go testing conventions (
Uses Bubble Tea MVC pattern:
- Model:
type model structholds all state (progress, timer, UI mode, input buffer, etc.) - Update:
func (m model) Update(msg tea.Msg)handles all events and state changes- Dispatches to specialized handlers:
updateKey(),updatePercent(),updateWindowSize(),handlePromptInput() - Returns new model and command (e.g.,
tickCmd())
- Dispatches to specialized handlers:
- View:
func (m model) View()renders UI based on current state
type model struct {
progress progress.Model // Animated progress bar
startTime int64 // Unix timestamp (never changes, used for all time calculations)
targetDuration int64 // Countdown target or count-up max (in seconds)
title string // Task title (displayed at top, set by prompt or --title flag)
mode viewMode // timerView or tableView
countUpMode bool // true = count elapsed time, false = countdown
inputBuffer string // Text being typed in prompt
promptActive bool // Whether prompt is currently visible
promptType int // 0 = log+reset (d), 1 = title-only (D)
table table.Model // Recent sessions display
}Always use absolute time, never cached progress:
elapsed := time.Now().Unix() - m.startTime // ✓ Correct - always current
remaining := m.targetDuration - elapsed // ✓ Correct - calculated freshThis is critical because:
- Timer can be suspended (Ctrl-Z) and resumed later
- startTime is set once and never changes
- Progress percentage is cached for UI animation but cannot be relied upon for logic
- Write all ordered lists starting with
1.
- Multiple
.gofiles organized by responsibility (model, handlers, view, database, utils, constants) - All files are in the same package (main package)
- Each file should have a clear, focused purpose
- Related functionality should be grouped together logically
- Types: PascalCase (
viewMode,model,session,promptMsg,tickMsg) - Constants: UPPER_SNAKE_CASE with descriptive names (
defaultDurationInMinutes,defaultCountUpDuration,sqlite_db_file_path) - Functions: camelCase, descriptive names indicating behavior (
updatePercent,handlePromptInput,getRecentSessions) - UI strings: In constants at top (color codes, default values)
- Error handling:
if err := ...; err != nil { return err }pattern - Defer for cleanup:
defer db.Close(),defer rows.Close() - Interface satisfaction:
modelimplements tea.Model implicitly - Return error as last value: standard Go convention
- Unix timestamps (
int64) for all time values to avoid timezone issues
- Uses lipgloss for terminal styling
- Three main colors defined as constants:
colorGrey = "#626262"- help text, headerscolorCream = "#fefdbc"- progress/highlightscolorMontezumaGold = "#f0c442"- progress bar gradient
- Help text always uses
helpStyle()which islipgloss.NewStyle().Foreground(...).Render
- Each feature has multiple test cases with descriptive names
- Test-driven approach: create model with specific state, call Update, assert result
- Uses
testify/assertfor readable assertions - Database tests: Create temp SQLite DB in temp dir for isolation
- Time-based tests: Use
testing.Testing()to skip platform-specific code (e.g., macOS notifications)
- Any change to key handling must include key binding tests
- Any change to UI rendering must verify View() output
- Any database changes must verify save/read operations
- All tests must pass before considering work complete:
make test
SQLite database stored at ~/.config/tiny-timer/tiny-timer.db:
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime DATETIME DEFAULT CURRENT_TIMESTAMP,
duration INTEGER, -- elapsed seconds
completed BOOLEAN,
title TEXT -- task title (can be empty string)
)- Save:
saveSessionToDB(duration, completed, title)- creates table if missing - Read:
getRecentSessions(limit)- returns sorted by datetime DESC
- Start:
tiny-timer [minutes](default 25) - Keys:
- 'r' = reset timer
- 't' = show recent sessions table
- Ctrl-Z = suspend/resume
- Any other key = quit
- On completion: Saves session to DB with elapsed duration
- Start:
tiny-timer --count-up [--title "Task"] - Default duration: 1 hour (3600s) - just for progress bar scaling
- Keys:
- 'd' = prompt for title, log session, reset timer to 0:00
- 'D' = prompt for title only (continue counting without logging)
- 'r' = reset timer to 0:00 without logging
- 't' = show recent sessions table
- Ctrl-Z = suspend/resume
- Any other key = quit
- Prompt handling: Text input with backspace, Enter to confirm, Esc to cancel
startTimeis never modified except at initialization- When resuming from Ctrl-Z suspend, elapsed time is recalculated:
time.Now().Unix() - m.startTime - This automatically handles gaps from suspension without special logic
- Gotcha: Don't cache elapsed time - always recalculate in handlers
- All handlers return
(tea.Model, tea.Cmd)- old model is discarded - Updates are "modal" functional style, not mutating:
m.title = ""; return m, cmd - Gotcha: Don't modify m, then use old references - always work with returned model
- Notifications only work on macOS (darwin)
- Code checks
runtime.GOOS != "darwin"to skip on other platforms - Tests check
testing.Testing()to avoid trying notifications during test runs - Gotcha: Notification failures are logged but don't crash the app
promptType: 0= task logging (d key) - saves to DB and resetspromptType: 1= title-only (D key) - no DB save, timer continues- Both activate prompt UI, but
handlePromptInputbehaves differently - Gotcha: Easy to mix up which type does what - check code carefully
mode: timerView(default) = shows timer and progress barmode: tableView= shows recent sessions table (read-only, any key exits)- Only 'r', 't', and Ctrl-Z are handled in timer view
- All other keys quit the app (design choice: keep app lightweight)
- Gotcha: Adding new features shouldn't break this "few keys" philosophy
- Stored at
os.Getenv("HOME")/.config/tiny-timer/tiny-timer.db - Directory is created automatically if missing:
os.MkdirAll(..., os.ModePerm) - Gotcha: Tests use temp db path (set in test via os.Setenv) to avoid polluting user DB
- Progress bar still shows 0-100%, but capped at 1 hour by default
- Elapsed time displayed as MM:SS regardless of mode
- Gotcha: Progress bar will show "full" after 1 hour in count-up mode (visual limitation)
Before submitting changes:
- All tests pass:
make test - Code builds:
make build - Changes follow Conventional Commits format
- New features have corresponding tests
- Database changes verified (save/read operations tested)
- UI changes verified visually (View() logic tested)
- No hardcoded paths (use constants like
sqlite_db_file_path) - Time calculations use
time.Now().Unix() - m.startTimepattern - Error handling present and logged (don't silently fail)
- JOURNAL.md updated for significant work
- JOURNAL.md: Historical record of significant work (debugging, features, fixes)
- Use "## YYYY-MM-DD HH:MM - Title" headers
- Document problem, solution, and testing
- Helpful for understanding why decisions were made
- README.md: User-facing documentation (installation, usage, features)
- AGENTS.md (this file): Developer guidelines for working in the codebase