# How Pipeline Data Is Rendered in the Career-Ops Go Dashboard TUI Using Bubble Tea and the Catppuccin Theme

> Discover how Career-Ops renders pipeline data in a Go dashboard TUI. Learn about Bubble Tea, Lip-Gloss, and Catppuccin theme integration for a vibrant terminal experience.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-06-07

---

**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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/dashboard/main.go) where the application data is ingested:

```go
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`](https://github.com/santifer/career-ops/blob/main/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:

```go
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`](https://github.com/santifer/career-ops/blob/main/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:

```go
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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/pipeline.go)) orchestrates the final render by delegating to specialized renderers:

```go
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`](https://github.com/santifer/career-ops/blob/main/dashboard/internal/theme/catppuccin.go). The `newCatppuccinMocha()` function (lines 5-24) returns a `Theme` struct with Lip-Gloss color values:

```go
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:

```go
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`](https://github.com/santifer/career-ops/blob/main/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:

```go
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.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) through `ParseApplications()` into a slice of `CareerApplication` structs defined in [`dashboard/internal/model/career.go`](https://github.com/santifer/career-ops/blob/main/dashboard/internal/model/career.go).
- **State management** occurs in `PipelineModel` (defined in [`dashboard/internal/ui/screens/pipeline.go`](https://github.com/santifer/career-ops/blob/main/dashboard/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`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/dashboard/internal/theme/catppuccin.go)) for dark mode or the Latte palette (from [`catppuccin_latte.go`](https://github.com/santifer/career-ops/blob/main/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`](https://github.com/santifer/career-ops/blob/main/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/`.