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.

Sprout demo


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.

Note: Sprout writes 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).

FlagRequiredDefaultDescription
--name / -nYesn/aUnique habit name
--daily-goalNo0Daily target value
--weekly-goalNo0Weekly target value
--monthly-goalNo0Monthly target value
--yearly-goalNo0Yearly 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
Binary habits (created with no goals at all) only accept 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
Warning: Deletion is permanent. There is no undo.

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.

FlagDescription
--renameSet a new name
--daily-goalUpdate the daily target
--weekly-goalUpdate the weekly target
--monthly-goalUpdate the monthly target
--yearly-goalUpdate the yearly target
sprout edit reading --daily-goal 50
sprout edit reading --rename "Books" --daily-goal 20
Constraints: Only goals that were set when the habit was created can be changed. For example, if a habit was created with no weekly goal, --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.


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.

Tip: Arrow keys and vim keys (h j k l) are interchangeable throughout the app.

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.

KeyAction
/ kPrevious habit
/ jNext habit
EnterToggle complete, or open the progress modal if goals are set
aAdd a new habit
eEdit the selected habit
dDelete 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.

KeyAction
/ kScroll up
/ jScroll 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.

KeyAction
/ hPrevious tab
/ lNext tab
/ kScroll list up
/ jScroll 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.

KeyAction
/ hPrevious habit tile
/ lNext habit tile
/ kPrevious year
/ jNext 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.

KeyAction
/ kScroll up
/ jScroll 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.

RowSettingOptions
1Accent themeSprout · Amber · Mono · Ocean · Paper
2Cursor blinkOn · Off
3NotificationsAll · Errors · Off
4Reset all dataPress Enter to confirm
Warning: Reset deletes every habit and every log entry permanently. There is no undo.

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.

KeyAction
Tab / Next input field
Shift+Tab / Previous input field
/ Move text cursor within the active field
BackspaceDelete character before the cursor
EnterConfirm / submit
EscCancel and close

Add habit

Opened with a from the habit list.

FieldRequiredDescription
NameYesUnique identifier for the habit
Daily goalNoTarget per day, used to shade the heatmap and track progress
Weekly goalNoTarget per week
Monthly goalNoTarget per month
Yearly goalNoTarget 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

KeyAction
q / EscQuit
Ctrl-CQuit
TabMove focus forward through panes
Shift+TabMove focus backward through panes
mReturn focus to sidebar menu

Sidebar menu

KeyAction
/ kPrevious item
/ jNext item
TabEnter the selected screen at the first pane
Shift+TabEnter the selected screen at the last pane

Dashboard: habit list

KeyAction
↑↓ / jkNavigate list
EnterToggle complete / open progress modal
aAdd habit
eEdit habit
dDelete habit

Dashboard: goal progress

KeyAction
◂▸ / hlSwitch period tab
↑↓ / jkScroll habit list

Dashboard: best streaks

KeyAction
/ kScroll up
/ jScroll down

Modals (text-input: Add, Edit, Log progress)

KeyAction
Tab / Next field
Shift+Tab / Previous field
/ Move cursor in active field
BackspaceDelete character before the cursor
EnterConfirm
EscCancel

Modals (confirmation: Delete, Reset)

KeyAction
EnterConfirm
EscCancel

Heatmap

KeyAction
◂▸ / hlSwitch habit
↑↓ / jkSwitch year

Settings

KeyAction
↑↓ / jkNavigate rows
◂▸ / hlCycle option
EnterConfirm (Reset row only)

Settings reference

SettingDefaultPersisted
Accent themeSprout (green)Yes
Cursor blinkOnYes
NotificationsErrors onlyYes

Accent themes

NameAccent colourStyle
SproutGreen #4be374Dark, green tinted borders and heatmap
AmberAmber #e3b341Dark, warm amber tinted borders and heatmap
MonoLight grey #cfe9d4Dark, neutral grey borders and heatmap
OceanCyan-blue #41c3ebDark, cool blue tinted borders and heatmap
PaperGreen #289b55Light, cream background with dark text

Notification levels

LevelWhat is shown
AllSuccess messages and errors
ErrorsOnly error messages (default)
OffNothing

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.

TableContents
habitHabit definitions: name, goals (daily / weekly / monthly / yearly), created_at
habit_logOne row per day per habit: completed flag and raw progress value
settingsKey/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

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

  1. The user presses a key. EventHandler wraps it in AppEvent::Key and sends it into the mpsc channel.
  2. The main loop receives it and calls update::handle, which runs the key-binding logic for the focused pane.
  3. If data is needed (e.g. logging a habit), update calls Actions::log_habit. The controller spawns a thread, opens a DB connection, and runs the SQL.
  4. The background thread sends AppEvent::LogHabit(…) back through the same channel.
  5. The main loop receives it and calls handle_log_habit_event, which updates the relevant fields in App and States.
  6. 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.