sprout A habit tracker that lives in your terminal.
Sprout is a habit tracker for the terminal. It has a full-screen TUI for daily use and a CLI for quick logging without opening the interface. Just a binary and a SQLite file.
Installation
macOS / Linux: Homebrew
brew tap kb019/sprout
brew trust kb019/sprout
brew install sprout
macOS / Linux: install script
curl -sSf https://raw.githubusercontent.com/kb019/sprout/main/install.sh | sh
Installs to ~/.local/bin. Detects OS and architecture automatically.
Windows: Scoop
scoop bucket add sprout https://github.com/kb019/scoop-sprout
scoop install sprout
Cargo
cargo install sprout
Manual download
Download the prebuilt binary for your platform from the latest GitHub Release, extract it, and place it on your PATH.
habit.db to the same directory as the
binary, not the working directory. Keep the binary somewhere writable, or symlink it.
Build from source
git clone https://github.com/kb019/sprout
cd sprout
just setup
Running with Cargo (development)
Pass arguments to the binary after a -- separator so Cargo does not
consume them:
cargo run # open TUI
cargo run -- list # list habits
cargo run -- add --name "Reading" --daily-goal 30
cargo run -- log reading 30
CLI overview
Every Sprout subcommand runs directly in the shell and exits. No TUI is launched.
Running sprout with no arguments (or sprout sample) is the
only way to open the full-screen interface.
sprout # open the TUI
sprout list # list all habits with today's progress
sprout status # today's ✓ / ✗ completion summary
sprout streak [name] # current and best streaks
sprout heatmap <name> # activity grid for the year
sprout add --name <n> … # create a new habit
sprout log <name> [value] # record progress for a habit
sprout delete <name> # delete a habit and its history
sprout edit <name> … # rename or update goals
sprout sample # open TUI with demo data
All name arguments are case-insensitive.
Run sprout --help or sprout <command> --help
for the full flag reference.
Launching the TUI
sprout
The full-screen TUI opens in your current terminal. Press q, Esc, or Ctrl-C to quit at any time.
Sprout requires a true terminal (TTY). Running it inside a pipe or a non-TTY context prints a warning but still attempts to start.
CLI commands
list
sprout list
Prints all habits in a table with today's progress, and the weekly, monthly, and
yearly goal targets. The Today column shows logged/goal when a
goal is set, or the raw value when there is no goal.
ID Name Today Weekly Monthly Yearly
──── ──────────────────── ───────── ─────── ──────── ───────
1 Reading 15/30 ─ ─ ─
2 Pushups 0/50 ─ ─ ─
3 Meditation 0 ─ ─ ─
status
sprout status
Shows a quick completion summary for today. Each habit is marked ✓ (completed) or ✗ (not yet done), along with the logged value vs. the daily goal.
Today Saturday, 2026-09-06 (1/3)
────────────────────────────────────────
✓ Reading 15 / 30
✗ Pushups 0 / 50
✗ Meditation not logged
streak
sprout streak # show all habits
sprout streak reading # show a single habit
Displays the current streak and all-time best streak for each habit. The best-streak row also shows the date range it covered.
Habit Current Best
────────────────────── ───────── ──────────
Reading 3 days 14 days
2026-01-01 → 2026-01-14
Pushups 0 days ─
heatmap
sprout heatmap <name> [--year YYYY]
Prints a full-year activity grid directly in the terminal using the same
■ square and five-level green gradient as the TUI heatmap.
Intensity is derived by normalising each day's logged value against the
min and max values recorded in the selected year, identical to the TUI
heatmap calculation. Darkest means no data; brightest means the highest
recorded value for that year. Binary habits (no goals) use only two levels:
not logged vs. done. Month labels and day-of-week initials are shown for
orientation. When the terminal is too narrow to show all twelve months, the
display centres on the current month and trims accordingly.
Reading - 2026
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Mon ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Tue ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Wed ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Thu ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Fri ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Sat ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
Sun ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ …
■ none ■ low ■ mid ■ high ■ done (colour darkest to brightest)
32 active days in 2026
Omit --year to show the current year.
sprout heatmap reading # current year
sprout heatmap reading --year 2025 # a previous year
add
sprout add --name <name> [--daily-goal <n>] [--weekly-goal <n>] [--monthly-goal <n>] [--yearly-goal <n>]
Creates a new habit. All goals are optional and default to 0 (no goal).
| Flag | Required | Default | Description |
|---|---|---|---|
--name / -n | Yes | n/a | Unique habit name |
--daily-goal | No | 0 | Daily target value |
--weekly-goal | No | 0 | Weekly target value |
--monthly-goal | No | 0 | Monthly target value |
--yearly-goal | No | 0 | Yearly target value |
sprout add --name "Reading" --daily-goal 30
sprout add --name "Pushups" --daily-goal 50 --weekly-goal 300
sprout add --name "Meditation" # no goal, tracked as done/not-done
log
sprout log <name> [value]
Records progress for a habit today. value defaults to 1.
If the logged value meets or exceeds the daily goal, the habit is marked completed.
Running log again on the same day overwrites the
previous entry. It does not add to it.
sprout log reading 30 # log 30; marks complete if daily goal ≤ 30
sprout log meditation # logs 1; marks complete for a no-goal habit
0 (not done) or 1 (done). Negative values are rejected
for all habits.
delete
sprout delete <name> [--yes]
Deletes the habit and all its log history.
Without --yes, a [y/N] prompt is shown first.
Pass --yes / -y to skip the prompt in scripts.
sprout delete reading # prompts for confirmation
sprout delete reading --yes # deletes immediately
edit
sprout edit <name> [--rename <new-name>] [--daily-goal <n>] [--weekly-goal <n>] [--monthly-goal <n>] [--yearly-goal <n>]
Updates a habit in place. Only the flags you pass are changed; everything else keeps its current value. At least one flag is required.
| Flag | Description |
|---|---|
--rename | Set a new name |
--daily-goal | Update the daily target |
--weekly-goal | Update the weekly target |
--monthly-goal | Update the monthly target |
--yearly-goal | Update the yearly target |
sprout edit reading --daily-goal 50
sprout edit reading --rename "Books" --daily-goal 20
--weekly-goal
is rejected. Binary habits (no goals at all) cannot have goals added via
edit; recreate the habit instead.
sample
sprout sample
Seeds four example habits with 90 days of history into a temporary database,
then opens the TUI against that file. Your real habit.db is never touched.
The temp file is deleted when you exit.
Navigation
Sprout uses a cyclic focus model. Tab moves focus forward through panes in order, and Shift+Tab moves it backward. The cycle always includes the sidebar menu, so pressing Tab enough times from any pane eventually returns to the menu. Only the focused pane responds to ↑↓ and ◂▸.
A coloured border and a ▶ marker indicate which element is focused.
Press m from anywhere to jump straight back to the sidebar.
Dashboard
The default screen. Split into a left column (habits) and a right column (stats).
Active habits
Lists every tracked habit. Each row shows the habit name, current streak (with a flame indicator), and whether it has been completed today. A spinner appears on a row while a log request is in flight.
| Key | Action |
|---|---|
| ↑ / k | Previous habit |
| ↓ / j | Next habit |
| Enter | Toggle complete, or open the progress modal if goals are set |
| a | Add a new habit |
| e | Edit the selected habit |
| d | Delete the selected habit |
Best streaks
Shows the all-time best streak per habit (longest consecutive completed days), sorted longest first. Refreshes automatically after any log action.
| Key | Action |
|---|---|
| ↑ / k | Scroll up |
| ↓ / j | Scroll down |
Goal progress
Four tabs: Daily, Weekly, Monthly, Yearly. Each tab lists only the habits that have a goal for that period, with a progress bar showing units logged vs. target.
| Key | Action |
|---|---|
| ◂ / h | Previous tab |
| ▸ / l | Next tab |
| ↑ / k | Scroll list up |
| ↓ / j | Scroll list down |
Stat cards
Four summary cards on the right column: current streak, weekly average, total active days, and number of habits tracked.
Heatmap
A GitHub-style activity grid, one cell per calendar day. Cell shade ranges from empty through four intensity levels derived from the logged value relative to the habit's daily goal.
| Key | Action |
|---|---|
| ◂ / h | Previous habit tile |
| ▸ / l | Next habit tile |
| ↑ / k | Previous year |
| ↓ / j | Next year |
The habit selector at the top shows one tile per habit active in the selected year. The year list on the right shows every year that contains log data. The active-days count below the grid reflects the selected habit and year, not a global total.
A spinner appears in the title while heatmap data is being loaded from the database.
Stats
Reached via the sidebar (item 3). Shows aggregated statistics across all habits.
Streak leaderboard
Ranked list of all habits by current streak length, with a horizontal bar chart.
| Key | Action |
|---|---|
| ↑ / k | Scroll up |
| ↓ / j | Scroll down |
Settings
Four rows navigated with ↑↓. Options on each row are cycled with ◂▸. Changes are saved automatically to the database after a ~3 second debounce. A spinner next to the title indicates a save is in flight. Settings persist across restarts.
| Row | Setting | Options |
|---|---|---|
| 1 | Accent theme | Sprout · Amber · Mono · Ocean · Paper |
| 2 | Cursor blink | On · Off |
| 3 | Notifications | All · Errors · Off |
| 4 | Reset all data | Press Enter to confirm |
Modals
All modals are dismissed with Esc and confirmed with Enter (where applicable). While a modal is open, the rest of the UI does not respond to keys.
Modal navigation
Applies to text-input modals (Add habit, Edit habit, Log progress). Confirmation-only modals (Delete habit, Reset all data) only respond to Enter and Esc.
| Key | Action |
|---|---|
| Tab / ↓ | Next input field |
| Shift+Tab / ↑ | Previous input field |
| ← / → | Move text cursor within the active field |
| Backspace | Delete character before the cursor |
| Enter | Confirm / submit |
| Esc | Cancel and close |
Add habit
Opened with a from the habit list.
| Field | Required | Description |
|---|---|---|
| Name | Yes | Unique identifier for the habit |
| Daily goal | No | Target per day, used to shade the heatmap and track progress |
| Weekly goal | No | Target per week |
| Monthly goal | No | Target per month |
| Yearly goal | No | Target per year |
A habit with no goals is treated as a simple done/not-done tracker. Goals only affect the heatmap shading and the goal-progress pane.
Edit habit
Opened with e from the habit list. Same fields as Add. The creation date is preserved.
Log progress
Opens automatically when you press Enter on a habit that has at least one goal. Type the number of units completed today (digits only) and press Enter to save. Use ← / → to move the cursor and Backspace to delete. If the value meets or exceeds the daily goal, the habit is marked completed.
Delete habit
Opened with d from the habit list. Asks for confirmation. Deletes the habit definition and all its log history.
Reset all data
Opened from the Settings screen (navigate to the Reset row, press Enter). Removes every habit and every log entry.
Keybind reference
Universal
| Key | Action |
|---|---|
| q / Esc | Quit |
| Ctrl-C | Quit |
| Tab | Move focus forward through panes |
| Shift+Tab | Move focus backward through panes |
| m | Return focus to sidebar menu |
Sidebar menu
| Key | Action |
|---|---|
| ↑ / k | Previous item |
| ↓ / j | Next item |
| Tab | Enter the selected screen at the first pane |
| Shift+Tab | Enter the selected screen at the last pane |
Dashboard: habit list
| Key | Action |
|---|---|
| ↑↓ / jk | Navigate list |
| Enter | Toggle complete / open progress modal |
| a | Add habit |
| e | Edit habit |
| d | Delete habit |
Dashboard: goal progress
| Key | Action |
|---|---|
| ◂▸ / hl | Switch period tab |
| ↑↓ / jk | Scroll habit list |
Dashboard: best streaks
| Key | Action |
|---|---|
| ↑ / k | Scroll up |
| ↓ / j | Scroll down |
Modals (text-input: Add, Edit, Log progress)
| Key | Action |
|---|---|
| Tab / ↓ | Next field |
| Shift+Tab / ↑ | Previous field |
| ← / → | Move cursor in active field |
| Backspace | Delete character before the cursor |
| Enter | Confirm |
| Esc | Cancel |
Modals (confirmation: Delete, Reset)
| Key | Action |
|---|---|
| Enter | Confirm |
| Esc | Cancel |
Heatmap
| Key | Action |
|---|---|
| ◂▸ / hl | Switch habit |
| ↑↓ / jk | Switch year |
Settings
| Key | Action |
|---|---|
| ↑↓ / jk | Navigate rows |
| ◂▸ / hl | Cycle option |
| Enter | Confirm (Reset row only) |
Settings reference
| Setting | Default | Persisted |
|---|---|---|
| Accent theme | Sprout (green) | Yes |
| Cursor blink | On | Yes |
| Notifications | Errors only | Yes |
Accent themes
| Name | Accent colour | Style |
|---|---|---|
| Sprout | Green #4be374 | Dark, green tinted borders and heatmap |
| Amber | Amber #e3b341 | Dark, warm amber tinted borders and heatmap |
| Mono | Light grey #cfe9d4 | Dark, neutral grey borders and heatmap |
| Ocean | Cyan-blue #41c3eb | Dark, cool blue tinted borders and heatmap |
| Paper | Green #289b55 | Light, cream background with dark text |
Notification levels
| Level | What is shown |
|---|---|
| All | Success messages and errors |
| Errors | Only error messages (default) |
| Off | Nothing |
Data storage
Everything is stored in a single SQLite file named habit.db,
placed in the same directory as the sprout binary.
The file is created automatically on first run.
| Table | Contents |
|---|---|
habit | Habit definitions: name, goals (daily / weekly / monthly / yearly), created_at |
habit_log | One row per day per habit: completed flag and raw progress value |
settings | Key/value pairs for persistent user preferences |
To wipe everything manually, delete habit.db.
To reset from inside the app, use the Reset option in Settings.
To reset from the CLI, use sprout delete <name> for individual habits.
Architecture
Sprout is built around a single mpsc channel and a
unidirectional data flow. Every piece of work (a key press, a database
result, a timer tick) arrives at the main loop as an AppEvent
and is routed to the appropriate handler.
crossterm controller background thread
(Key, Mouse, 1. calls model (SQLite)
Resize, Tick) 2. gets result
│ 3. sends AppEvent via sender
v │
┌────────────────────────────────────────────── v ──────┐
│ EventHandler │
│ thread: polls crossterm, emits terminal AppEvents │
│ channel: single mpsc shared with controller threads │
│ next(): blocks until any AppEvent arrives │
└──────────────────────────────┬────────────────────────┘
│ AppEvent (any source)
v
┌──────────────────────────────────────────────┐
│ Main Loop │
│ routes every AppEvent to a handler │
└───────────────┬──────────────────────────────┘
│
┌─────────────┼──────────────┐
v v v
Key/Mouse DB result Tick
│ │ │
v v v
┌─────────┐ ┌──────────┐ ┌─────────┐
│ update │ │ handlers │ │ view │
│ key │ │ state │ │ render │
│ logic │ │ patches │ │ │
└────┬────┘ └──────────┘ └─────────┘
│
│ dispatches async op
v
┌────────────┐ spawns ┌──────────────────────────┐
│ controller │ ---------> │ background thread │
│ Actions │ thread │ 1. calls model (SQLite) │
└────────────┘ │ 2. gets result │
│ 3. sends AppEvent via │
│ shared mpsc sender │
└──────────────────────────┘
┌──────────────────────────┐
│ App + States │
│ single source of truth │ mutated by update / handlers
│ read by view at render │
└──────────────────────────┘
Module roles
| Module | Role |
|---|---|
src/event/ |
Defines the AppEvent enum: terminal events
(Key, Tick, Resize) and one
variant per domain operation (AddHabit,
LogHabit, …). Also contains EventHandler,
a dedicated thread that polls crossterm and forwards events into the
channel.
|
src/app.rs |
The App struct: habit list, focus flags, modal state,
active theme index, settings values, and the tick counter used for
spinners. Everything the rest of the app needs at runtime lives here.
|
src/state/ |
UI selection state: one ListState per scrollable pane,
plus per-operation async states that track whether a fetch is idle,
loading, or complete. Kept separate from App so render
functions can take &mut States without borrowing all
of App.
|
src/update/ |
Pure functions that mutate App and States.
handle routes key events to screen-specific handlers.
Each handle_*_event function applies a database result
to the relevant state field. Update functions call
controller when they need to start an async DB
operation.
|
src/controller/ |
The Actions struct. Each method spawns a background
thread, opens a short-lived model connection, runs the
query, then sends the result back as an AppEvent over
the shared mpsc channel. No application logic lives here.
|
src/model/ |
HabitDb and SettingsDb: raw SQLite queries
via rusqlite. Creates the schema on first open, exposes one method
per operation. No app state, no channels, just data access.
|
src/view/ |
Stateless render functions. Given &App and
&mut States, they build ratatui widgets and draw
them to the terminal frame. Called once per tick; never mutate state.
|
src/cli/ |
Standalone CLI command handlers. Each opens its own short-lived DB connection, formats output to stdout, and exits. No TUI, no channels. |
AppEvent lifecycle
Every operation follows the same path through the system:
-
The user presses a key.
EventHandlerwraps it inAppEvent::Keyand sends it into the mpsc channel. -
The main loop receives it and calls
update::handle, which runs the key-binding logic for the focused pane. -
If data is needed (e.g. logging a habit),
updatecallsActions::log_habit. The controller spawns a thread, opens a DB connection, and runs the SQL. -
The background thread sends
AppEvent::LogHabit(…)back through the same channel. -
The main loop receives it and calls
handle_log_habit_event, which updates the relevant fields inAppandStates. -
On the next
Tick, the view re-renders from the updated state.
License
Sprout is released under the MIT License. Copyright © 2026.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.