How Pipeline Data Is Rendered in the Career-Ops Go Dashboard TUI Using Bubble Tea and the Catppuccin Theme
Career-Ops renders job pipeline data in a terminal UI by parsing Markdown applications into a PipelineModel, then composing views with Lip-Gloss styles driven by the Catppuccin color palette, all orchestrated by the Bubble Tea framework.
The Career-Ops project provides a terminal-based dashboard for tracking job applications using a custom Go TUI built with Bubble Tea. This article examines exactly how pipeline data is rendered in the Go dashboard TUI using Bubble Tea and the Catppuccin theme, tracing the flow from raw Markdown files to the final styled terminal output.
Overview of the Rendering Architecture
The rendering pipeline consists of three distinct layers: data ingestion, UI state management, and visual composition. First, data.ParseApplications reads data/applications.md and constructs a slice of model.CareerApplication. Next, screens.NewPipelineModel initializes a PipelineModel that stores applications, filtering state, and a report cache. Finally, PipelineModel.View() composes the terminal output using Lip-Gloss styles populated by the Catppuccin color theme.
Step-by-Step Data Flow
Loading Raw Application Data
The process begins in dashboard/main.go where the application data is ingested:
apps := data.ParseApplications(careerOpsPath) // main.go:61-62
metrics := data.ComputeMetrics(apps) // main.go:68-69
The ParseApplications function (located in dashboard/internal/data/career.go) returns a slice of CareerApplication structs containing fields like Company, Role, Status, and Score. These metrics feed into the UI model to populate headers and status bars.
Initializing the PipelineModel with Theme Context
After data loading, the Bubble Tea model is instantiated with the Catppuccin theme:
t := theme.NewTheme("auto") // main.go:71
pm := screens.NewPipelineModel(t, apps, metrics,
careerOpsPath, 120, 40) // main.go:73
The NewPipelineModel function (lines 23-39 in dashboard/internal/ui/screens/pipeline.go) accepts the theme, application slice, metrics, terminal dimensions, and initializes an empty reportCache map for lazy-loaded report previews.
Caching Report Summaries for Lazy Loading
For applications containing a ReportPath, summary data is loaded asynchronously and cached:
archetype, tldr, remote, comp := data.LoadReportSummary(
careerOpsPath, app.ReportPath) // main.go:75-76
pm.EnrichReport(app.ReportPath, archetype, tldr, remote, comp) // main.go:81-82
The EnrichReport method (lines 65-73 in pipeline.go) populates the cache, ensuring that report previews are fetched only once per application.
Composing the View with Lip-Gloss and Catppuccin
The View() method (lines 61-71 in pipeline.go) orchestrates the final render by delegating to specialized renderers:
header := m.renderHeader()
tabs := m.renderTabs()
metrics := m.renderMetrics()
sortBar := m.renderSortBar()
search := m.renderSearchBar()
body := m.renderBody()
preview := m.renderPreview()
help := m.renderHelp()
Each component uses Lip-Gloss styles configured with colors from the Catppuccin theme struct.
Deep Dive into the View Rendering Pipeline
The Catppuccin Theme Definition
The theme is defined in dashboard/internal/theme/catppuccin.go. The newCatppuccinMocha() function (lines 5-24) returns a Theme struct with Lip-Gloss color values:
func newCatppuccinMocha() Theme {
return Theme{
Base: lipgloss.Color("#1e1e2e"),
Surface: lipgloss.Color("#313244"),
Overlay: lipgloss.Color("#45475a"),
Text: lipgloss.Color("#cdd6f4"),
Subtext: lipgloss.Color("#a6adc8"),
Blue: lipgloss.Color("#89b4fa"),
Green: lipgloss.Color("#a6e3a1"),
Yellow: lipgloss.Color("#f9e2af"),
}
}
The theme.NewTheme("auto") function automatically selects between Mocha (dark) and Latte (light) variants based on the terminal's color mode.
Header and Metrics Bar
The header displays total offers and average scores using bold styling with theme.Text foreground and theme.Surface background:
style := lipgloss.NewStyle().
Bold(true).Foreground(m.theme.Text).
Background(m.theme.Surface).Width(m.width).Padding(0, 2)
title := lipgloss.NewStyle().Bold(true).Foreground(m.theme.Blue).
Render("CAREER PIPELINE")
The metrics bar (renderMetrics(), lines 110-130 in pipeline.go) iterates through status groups, applying color-coded tokens via statusColorMap()—for example, mapping Interview status to theme.Green.
Interactive Tabs and Sort Controls
The renderTabs() method (lines 58-88) draws navigation tabs (ALL, EVALUATED, etc.) with active states highlighted in theme.Blue and inactive states in theme.Subtext. The underline uses theme.Overlay for visual separation.
The sort bar (renderSortBar(), lines 132-140) displays the current sort mode (sortScore, sortDate) and view mode (grouped/flat), indicating the number of filtered rows.
Body and Application Rows
The body rendering (renderBody(), lines 82-121) handles two view modes: grouped and flat. In grouped mode, it inserts status headers whenever data.NormalizeStatus detects a change. Individual rows are rendered by renderAppLine() (lines 86-121), which aligns columns and applies conditional styling:
func (m PipelineModel) renderAppLine(app model.CareerApplication, selected bool) string {
scoreStyle := m.scoreStyle(app.Score)
statusColor := m.statusColorMap()[data.NormalizeStatus(app.Status)]
line := fmt.Sprintf(" %s %s %s %s %s %s %s",
numStyle.Render(truncateRunes(numText, numW)),
scoreStyle.Render(fmt.Sprintf("%.1f", app.Score)),
dateStyle.Render(truncateRunes(dateText, dateW)),
companyStyle.Render(company),
roleStyle.Render(role),
lipgloss.NewStyle().Foreground(statusColor).Width(statusW).Render(statusLabel(norm)),
compText,
)
if selected {
selStyle := lipgloss.NewStyle().
Background(m.theme.Overlay).
Width(m.width - 4)
return selStyle.Render(line)
}
return line
}
Preview Pane and Help Bar
When a row is selected, renderPreview() (lines 154-176) displays cached report data including archetype, TL;DR, remote status, and compensation, or falls back to application notes.
The help bar (renderHelp(), lines 202-225) renders keyboard shortcuts using theme.Subtext for the background and theme.Text for key combinations.
Handling User Interactions
Bubble Tea drives the UI through the Update method in PipelineModel (lines 33-50). When navigation keys move the cursor, loadCurrentReport() dispatches a PipelineLoadReportMsg that triggers data.LoadReportSummary and caches the result in the model's reportCache, ensuring the preview pane updates without blocking the main thread.
Summary
- Data flows from
data/applications.mdthroughParseApplications()into a slice ofCareerApplicationstructs defined indashboard/internal/model/career.go. - State management occurs in
PipelineModel(defined indashboard/internal/ui/screens/pipeline.go), which maintains applications, filters, sort state, and a report cache. - Visual styling uses Lip-Gloss with colors from the Catppuccin theme (
dashboard/internal/theme/catppuccin.go). - View composition happens in
View(), which delegates to specialized renderers for headers, tabs, metrics, the application list, and previews. - Lazy loading of report summaries prevents UI blocking while maintaining responsive previews.
Frequently Asked Questions
How does the Career-Ops dashboard automatically detect dark or light mode?
The theme.NewTheme("auto") function checks the terminal's color capabilities and returns either the Catppuccin Mocha palette (defined in dashboard/internal/theme/catppuccin.go) for dark mode or the Latte palette (from catppuccin_latte.go) for light mode, ensuring appropriate contrast ratios.
What is the purpose of the reportCache in PipelineModel?
The reportCache map stores loaded report summaries (archetype, TL;DR, remote status, compensation) keyed by ReportPath. This prevents redundant disk I/O when users navigate between applications, as each report is loaded once via data.LoadReportSummary and reused via EnrichReport() (lines 65-73).
How are application rows color-coded in the pipeline view?
The renderAppLine() method applies dynamic styling based on data values: scores receive gradient colors via scoreStyle(), while status labels are colored using statusColorMap() which maps normalized statuses (like "Interview" or "Applied") to specific Catppuccin colors such as theme.Green or theme.Yellow.
Can I run the Career-Ops dashboard without the Catppuccin theme?
While the theme is hardcoded in main.go through theme.NewTheme("auto"), the architecture supports theme injection. The PipelineModel accepts any Theme struct satisfying the color interface, though the repository currently only implements Catppuccin Mocha and Latte variants in dashboard/internal/theme/.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →