How to Build a Go Bubble Tea Dashboard TUI for Filtering and Visualizing Application Pipelines

The Career-Ops dashboard uses the Bubble Tea state-machine framework and Lip-gloss styling to load an applications.md tracker, compute aggregate metrics, and render an interactive table supporting real-time filtering, sorting, and search.

The santifer/career-ops repository implements a terminal-user-interface (TUI) for managing job application pipelines. This guide examines how the Go Bubble Tea dashboard TUI parses tracker data, applies dynamic filters across multiple dimensions, and visualizes pipeline metrics through responsive table views and styled components.

Core Architecture

The application follows a layered architecture where data loading, state management, and rendering remain strictly separated. Understanding this structure is essential for extending the filtering and visualization capabilities.

Component Overview

The entry point in dashboard/main.go bootstraps the system by parsing the -path flag, loading application data, and initializing the top-level appModel. This model coordinates three sub-models: pipeline (the table view), viewer (full-report view), and progress (analytics view). The pipeline sub-model, defined in dashboard/internal/ui/screens/pipeline.go, implements the PipelineModel struct that handles all logic for loading data, applying filters, sorting, pagination, and search.

Key data structures reside in dashboard/internal/model/career.go, where the CareerApplication struct represents a single row with fields for company, role, status, score, and date. The data package handles persistence, parsing applications.md through functions like data.ParseApplications and normalizing statuses via data.NormalizeStatus.

Data Flow

The initialization sequence follows a strict pipeline:

  1. Loaddata.ParseApplications reads the tracker file into a slice of CareerApplication structs
  2. Metricsdata.ComputeMetrics and data.ComputeProgressMetrics generate PipelineMetrics and ProgressMetrics aggregates
  3. Model Initscreens.NewPipelineModel(theme, apps, metrics, repoPath, width, height) constructs the view model
  4. Event LoopappModel.Update receives key events, routes them to the active sub-model, and handles custom messages like PipelineRefreshMsg and PipelineOpenReportMsg
  5. RenderappModel.View delegates to the active sub-model's View() method

Filtering and Sorting Logic

The Go Bubble Tea dashboard TUI implements sophisticated filtering through a combination of tab-based status filters, live search queries, and multi-field sorting.

Tab-Based Status Filters

The pipelineTabs slice (lines 87–96 in pipeline.go) defines the available filter categories:

var pipelineTabs = []pipelineTab{
    {filterAll, "ALL"},
    {filterEvaluated, "EVALUATED"},
    {filterApplied, "APPLIED"},
    {filterInterview, "INTERVIEW"},
    {filterTop, "TOP ≥4"},
    {filterSkip, "SKIP"},
    {filterRejected, "REJECTED"},
    {filterDiscarded, "DISCARDED"},
}

When the user presses ←/→ (or h/l), the handleKey method updates activeTab and triggers applyFilterAndSort() (lines 137–141). This function iterates over all applications, performing two-stage validation:

  1. Search MatchmatchesSearch (lines 17–35) checks if the application meets the current search query
  2. Status Filterdata.NormalizeStatus canonicalizes the status field, then a switch statement filters based on the active tab

For the TOP ≥4 filter, the logic specifically checks if app.Score >= 4.0 && norm != "skip" to ensure high-scoring applications are not hidden by skip status.

Multi-Field Sorting

The sortCycle array (lines 98–99) enumerates available sort modes. Pressing s cycles through Score, Date, Company, and Location sorts. The sortLess function (lines 83–115) returns a comparator based on sortMode:

  • Score: Descending numeric order (a.Score > b.Score)
  • Date: Chronological reverse (a.Date > b.Date)
  • Company: Case-insensitive lexical comparison
  • Location: Work mode priority (remote → hybrid → onsite) followed by alphabetical city

The system uses sort.SliceStable twice: first for the flat list, then a secondary stable sort for grouped view (lines 62–78).

Live Search Implementation

Pressing / opens the search input mode. While searchInput is active, the handleSearchInput method consumes keystrokes, updates searchQuery, and calls applyFilterAndSort (lines 22–64). The UI displays a live count via renderSearchBar (lines 31–63). To prevent filesystem thrashing, report previews only reload after the user commits the query with Enter or cancels with Esc, as noted in the implementation comments (lines 17–20).

Visual Rendering Pipeline

The dashboard uses Lip-gloss for all terminal styling, creating a consistent visual language across the interface.

Lip-gloss Styling System

The dashboard/internal/theme/theme.go package supplies colour palettes (e.g., catppuccin, catppuccin-mocha) that drive the visual appearance. These themes are injected into the PipelineModel during initialization and applied throughout the rendering methods.

Table Layout Components

The View() method composes six distinct sections:

  • Header (renderHeader, lines 66–84): Displays "CAREER PIPELINE" with total offer count and average score
  • Tabs (renderTabs, lines 87–112): Shows filter tabs with dynamic counts per status
  • Metrics Bar (renderMetrics, lines 39–58): Status-wise counts with colour coding
  • Sort Bar (renderSortBar, lines 61–71): Current sort mode, view mode, and item count
  • Column Headers (renderColumnHeader, lines 80–106): Adaptive titles based on terminal width
  • Body Rows (renderAppLine, lines 8–78): Individual application rows with colour-coded scores, statuses, and pay ranges

The grouped view (viewMode == "grouped") inserts status headers before each block (lines 89–102). Scroll handling via adjustScroll and cursorLineEstimate ensures the cursor remains visible when group headers shift line numbers.

Interactive Features

Beyond passive visualization, the TUI supports direct manipulation of application data.

Status Updates

Pressing c triggers the statusPicker overlay (lines 66–80). When the user selects a new status, the system emits a PipelineUpdateStatusMsg (lines 69–74), which persists the change via data.UpdateApplicationStatus. This updates the underlying applications.md file without requiring a restart.

Report Preview Loading

When a selected row contains a ReportPath, the loadCurrentReport method (lines 2–15) emits a PipelineLoadReportMsg. The main appModel.Update handler reads the report summary using data.LoadReportSummary and caches it in PipelineModel.reportCache (lines 69–73), enabling instant preview rendering.

Summary

  • The Go Bubble Tea dashboard TUI in santifer/career-ops uses a state-machine architecture with separate models for data, UI state, and rendering
  • Filtering combines tab-based status selection via applyFilterAndSort with live search through matchesSearch and handleSearchInput
  • Sorting supports multiple fields (Score, Date, Company, Location) using stable sorts and custom comparators in sortLess
  • Visualization relies on Lip-gloss styling from the theme package, with components like renderHeader, renderTabs, and renderAppLine building the terminal interface
  • Interactivity includes status updates persisted to disk and lazy-loaded report previews cached in reportCache

Frequently Asked Questions

How does the Career-Ops TUI handle real-time search filtering?

The system enters search mode when the user presses /, activating handleSearchInput to capture keystrokes. Each keystroke updates searchQuery and immediately calls applyFilterAndSort, which re-evaluates the full dataset against both the search string and active tab filter. The UI shows a live count in the search bar, though filesystem reads for report previews are deferred until the user commits the query with Enter.

What sorting options are available in the pipeline view?

Pressing s cycles through four sort modes defined in sortCycle: Score (descending), Date (most recent first), Company (alphabetical), and Location (by work mode priority then city). The sortLess function in dashboard/internal/ui/screens/pipeline.go implements the comparison logic, using sort.SliceStable to maintain consistent ordering within equal elements.

How does the dashboard update application statuses without restarting?

When the user presses c and selects a status from the statusPicker overlay, the system creates a PipelineUpdateStatusMsg containing the application ID and new status. The main model's Update method handles this message by calling data.UpdateApplicationStatus, which writes the change back to applications.md. The PipelineModel then refreshes its local data slice to reflect the update immediately.

Can the TUI display reports for specific applications?

Yes, when an application has a ReportPath field populated, pressing the selection key triggers loadCurrentReport, which emits a PipelineLoadReportMsg. The system reads the report summary via data.LoadReportSummary and stores it in PipelineModel.reportCache. This cached content renders in the preview panel (via renderPreview) without blocking the main event loop, allowing users to scan report content before opening the full viewer with Enter.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →