Notes from git-blimey
This document captures interesting patterns and reusable components from git-blimey that we may want to use in the timetracking TUI.
Reusable Components
table.go (READY TO COPY)
- Generic table renderer with fixed-width columns
- Location:
users/Profpatsch/git-blimey/table.go - Features:
TableColumnstruct with Name, Width, Align, RenderCell functionTablestruct manages rendering with automatic last-column width calculationRenderHeader()- Bold column headersRenderRow()- Normal row renderingRenderRowWithBackground()- Highlighted/selected rows- ANSI-aware truncation with ellipsis
- Handles lipgloss styling and alignment
- Dependencies:
lipgloss,x/ansi - Can be copied verbatim - it's a pure utility component
colorized_text.go
- Location:
users/Profpatsch/git-blimey/colorized_text.go - Purpose: Text with ANSI color codes that can be scrolled/paginated
- May be useful if we want colored text in detail views
color_scheme.go
- Location:
users/Profpatsch/git-blimey/color_scheme.go - Purpose: Centralized color palette definitions
- Could be useful for consistent theming
Bubbletea Architecture
Model Structure Pattern (lines 287-356 in main.go)
The Model struct is exceptionally well-commented, showing a clean architecture:
type Model struct {
// View state
viewMode ViewMode
// Core data
blameLines []BlameLine // Main data to display
// Viewport/Navigation
selectedRow int // Currently selected item
viewportStart int // First visible item
pageSize int // Visible items count
// Terminal dimensions
windowWidth int
windowHeight int
// Modal/Detail overlays
showingDetail bool
detailText ColorizedText
detailScroll int
// Loading state
loadingState LoadingState
// Debug mode
debugUI bool
}
Key insight: Separation of concerns with clear sections in the struct.
Bubbletea Interface Methods
Init() - Returns initial command
func (m Model) Init() tea.Cmd {
// Return initial commands (async operations, timers, etc.)
}
Update(msg tea.Msg) - Message handler
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// Handle keyboard input
case tea.WindowSizeMsg:
// Handle terminal resize
// Custom message types...
}
return m, nil
}
View() - Render UI
func (m Model) View() string {
// Build and return UI string
}
Custom Message Types
git-blimey defines custom messages for async operations:
spinnerTickMsg- Animation framesfileChangedMsg- File watcher notificationsFileLoadedMsg- Async file load completeColorizeCompleteMsg- Syntax highlighting done
Pattern: Define custom message types for async operations, send via channels.
UI Features Worth Considering
1. Loading Spinner
spinnerFrame inttracks animation frame (0-9)- Ticker sends
spinnerTickMsgevery 100ms during loading - Simple spinner chars:
⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
2. Modal Overlays
modalText ColorizedText- If non-empty, renders centered modal with border- Used for error messages ("editor not found", etc.)
- Overlays main view
3. Detail View Overlay
showingDetail bool- Toggle between main and detail viewdetailScroll int- Vertical scroll offset in detail- Used for commit details, help screen
- Press 'q' to close detail view
4. Viewport Scrolling
selectedRow- Cursor positionviewportStart- First visible itempageSize- Number of visible items (calculated from window height)- Auto-scroll logic keeps selection visible:
- If
selectedRow < viewportStart: scroll up - If
selectedRow >= viewportStart + pageSize: scroll down
- If
5. Debug Overlay (Ctrl+U)
- Toggle with
debugUI bool - Shows performance metrics:
- Update/View call durations
- Frame times
- Item counts
- Very useful during development!
Navigation Implementation
Keyboard Handling Pattern
case tea.KeyMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "up", "k":
if m.selectedRow > 0 {
m.selectedRow--
}
case "down", "j":
if m.selectedRow < len(m.items)-1 {
m.selectedRow++
}
case "g", "home":
m.selectedRow = 0
case "G", "end":
m.selectedRow = len(m.items) - 1
case "pgup":
m.selectedRow -= m.pageSize
if m.selectedRow < 0 {
m.selectedRow = 0
}
case "pgdown":
m.selectedRow += m.pageSize
if m.selectedRow >= len(m.items) {
m.selectedRow = len(m.items) - 1
}
}
Viewport Auto-scroll Logic
After updating selectedRow, ensure it's visible:
// Scroll viewport to keep selection visible
if m.selectedRow < m.viewportStart {
m.viewportStart = m.selectedRow
}
if m.selectedRow >= m.viewportStart + m.pageSize {
m.viewportStart = m.selectedRow - m.pageSize + 1
}
Build Setup
go.mod Dependencies
module timetrack
go 1.25.0
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/ansi v0.10.1
)
Minimal set - Start with just these three for basic TUI.
default.nix Pattern
{ depot, pkgs, ... }:
let
goDeps = import ./go-deps.nix { inherit depot pkgs; };
unwrapped = depot.nix.buildGo.program {
name = "timetrack";
srcs = [
./main.go
./table.go # Add additional source files here
];
deps = [
goDeps.charmbracelet-bubbletea
goDeps.charmbracelet-lipgloss
goDeps.charmbracelet-x-ansi
];
};
in
unwrapped
go-deps.nix
Can reuse git-blimey's go-deps.nix initially since we're using the same dependencies.
Recommended First Steps
- Copy
table.gofrom git-blimey (no modifications needed) - Create minimal Model struct with:
- Hardcoded time entries
- selectedRow, viewportStart, pageSize
- windowWidth, windowHeight
- Implement basic navigation (j/k, arrows, q to quit)
- Render table with Date, Start, End, Duration columns
- Add summary footer showing total hours
Things to Skip Initially
- Loading spinner (no async operations yet)
- Modal overlays (no errors to show yet)
- Detail view (no details to view yet)
- Debug overlay (nice-to-have)
- File watching (no file persistence yet)
- Custom messages (all sync operations)
Focus on: Data display + Navigation + Table rendering