Overview
toni is built using the Bubble Tea TUI framework, following a clean separation of concerns across database, model, UI, and command layers. The architecture emphasizes:- Local-first data: All state persists to SQLite
- Modal interaction: Vim-style navigation and insert modes
- Message-driven updates: Elm-inspired unidirectional data flow
- Component isolation: Each screen manages its own state and rendering
Framework: Bubble Tea
Bubble Tea is an Elm-inspired TUI framework that implements the Model-Update-View (MVU) pattern:Layer Architecture
Database Layer (internal/db/)
Provides typed database operations with zero ORM overhead:
db.go: Database initialization, schema management, and connection handlingrestaurants.go: Restaurant CRUD operations and aggregation queriesvisits.go: Visit tracking with joined queries for list viewswant_to_visit.go: Wishlist managementundo_helpers.go: Support for undo/redo operations
database/sql for maximum control and performance.
Key patterns:
- Functions accept
*sql.DBas first parameter - Return domain types from
internal/model - No database-specific types leak into upper layers
Model Layer (internal/model/)
Defines domain types and Bubble Tea messages:
types.go:
- Domain entities:
Restaurant,Visit,WantToVisit - Row types for list views:
VisitRow,RestaurantRow,WantToVisitRow - Mutation types:
NewVisit,UpdateVisit, etc.
msg.go:
- Bubble Tea messages for state transitions
- Screen and Mode enums
- Data-loading messages:
VisitsLoadedMsg,RestaurantDetailLoadedMsg - Action messages:
VisitSavedMsg,DeleteVisitMsg,FormCancelledMsg
Messages are the only way to communicate state changes in Bubble Tea. They’re sent via
tea.Cmd and processed in the Update function.UI Layer (internal/ui/)
Implements all screens, forms, and visual components:
app.go: Root model with routing, mode handling, and global state (internal/ui/app.go:24)visits.go,restaurants.go,want_to_visit.go: List screen viewsvisit_detail.go,restaurant_detail.go: Detail screensrestaurant_form.go,visit_form.go: Input forms with validationtable_controls.go: Reusable table component with sorting, filtering, column managementstyles.go: Lip Gloss styling definitionskeys.go: Keybinding definitions for navigation and formsgraphics.go: ASCII art and visual elementshelp.go: Context-aware help systemprefs.go: User preferences persistence
Command Layer (cmd/)
Handles CLI argument parsing and initialization:
root.go: Flag parsing, database path resolution, Yelp API configuration (cmd/root.go:20)onboarding.go: First-run experience and settings collection
Supporting Layers
internal/search/: Yelp Fusion API client for restaurant autocomplete (internal/search/yelp.go:14)
internal/util/: Formatting utilities for dates, ratings, and user-facing strings (internal/util/format.go:1)
State Management
Screen Navigation
toni implements a screen-based state machine with navigation and insert modes:Model.screen and determines:
- Which keybindings are active
- Which sub-model handles updates
- What’s rendered in
View()
Mode System
Navigation Mode (ModeNav):
- Vim-style movement:
j/k,gg,G,Ctrl+d/Ctrl+u - Actions:
a(add),e(edit),d(delete),v(log visit) - Screen switching:
r(restaurants),w(want to visit)
ModeInsert):
- Active during form editing
- Tab/Shift+Tab for field navigation
- Ctrl+S to save, Esc to cancel
- Restaurant autocomplete in visit forms
Message Passing
All state changes flow through messages:Undo/Redo System
toni implements operation-level undo with action stacking:- Each destructive operation (save, delete) creates an
undoAction - Actions capture “before” and “after” state
utriggers undo,Ctrl+rtriggers redo- Undo actions stored in
Model.undoStackandModel.redoStack
Terminal Capabilities
toni detects terminal features at startup:- True color support: Enables 24-bit color in supported terminals
- Unicode support: Falls back to ASCII if needed
- Dimensions: Adapts layout to available width/height
Data Flow Example
Adding a new visit:- User presses
aon visits screen handleVisitsNav()setsmode = ModeInsert,screen = ScreenVisitFormVisitFormModelrenders with empty fields- User types restaurant name → triggers Yelp autocomplete
- User fills fields and presses Ctrl+S
- Form validation runs
saveVisitCmd()executes database insertVisitSavedMsgreceived → mode returns toModeNavloadVisitsCmd()refreshes the visits list- View re-renders with new visit visible
Key Design Decisions
Pure Go
No CGO dependencies via modernc.org/sqlite - single binary, easy distribution
Single Database
All state in one SQLite file for trivial backup and portability
Optional Cloud
Yelp API enhances UX but app works fully offline
Modal UI
Vim-inspired navigation keeps hands on keyboard
Performance Characteristics
- Startup time: <100ms (database open + schema init + first query)
- Render latency: 1-2ms for typical screen (thanks to Lip Gloss caching)
- Database queries: <10ms for list views with <10k records
- Autocomplete: 100-500ms depending on network (Yelp API)
LIMIT to cap result size.