This document captures interesting patterns and reusable components from git-blimey that we may want to use in the timetracking TUI.

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.

func (m Model) Init() tea.Cmd {
    // Return initial commands (async operations, timers, etc.)
}

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
}

func (m Model) View() string {
    // Build and return UI string
}

git-blimey defines custom messages for async operations:

Pattern: Define custom message types for async operations, send via channels.

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
        }
    }

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
}

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.

{ 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

Can reuse git-blimey's go-deps.nix initially since we're using the same dependencies.

  1. Copy table.go from git-blimey (no modifications needed)
  2. Create minimal Model struct with:
    • Hardcoded time entries
    • selectedRow, viewportStart, pageSize
    • windowWidth, windowHeight
  3. Implement basic navigation (j/k, arrows, q to quit)
  4. Render table with Date, Start, End, Duration columns
  5. Add summary footer showing total hours

Focus on: Data display + Navigation + Table rendering