# How to Browse and Filter the Pipeline in the Go Bubble Tea Dashboard TUI

> Learn how the Go Bubble Tea dashboard TUI browses and filters the pipeline. Discover its PipelineModel, tab filters, live search, and multi-column sorting for efficient navigation. Get insights now.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: how-to-guide
- Published: 2026-08-19

---

**The career-ops TUI implements browsing and filtering through a central `PipelineModel` that maintains a full dataset slice and a derived `filtered` slice, applying tab-based filters, live search, and multi-column sorts via the `applyFilterAndSort` method.**

The `santifer/career-ops` repository provides a terminal-based dashboard for managing job applications. The pipeline view is a sophisticated Go Bubble Tea model that combines keyboard-driven navigation with real-time filtering and sorting capabilities. Understanding its internal mechanics reveals how to efficiently manage large application lists without leaving the terminal.

## Core Data Structures

The pipeline’s state management centers on a few key structures defined in [`dashboard/internal/ui/screens/pipeline.go`](https://github.com/santifer/career-ops/blob/main/dashboard/internal/ui/screens/pipeline.go).

**`PipelineModel`** (lines 59‑94) serves as the primary Bubble Tea model. It stores the complete dataset in `apps` (a slice of `model.CareerApplication`) and maintains the currently visible subset in `filtered`. The model also tracks UI state including cursor position, scroll offset, active tab index, current sort mode, and the live search query string.

**`pipelineTab`** (lines 41‑44) pairs a filter key—such as `filterAll`, `filterEvaluated`, or `filterApplied`—with its display label. These tabs drive the primary filtering interface when users press **f**, **h**, or the arrow keys.

**`colDef`** (lines 76‑84) describes optional columns that users can toggle via the **C** key, while `reportSummary` (lines 8‑13) caches parsed report data for the preview pane.

## Filtering Logic

All filtering happens inside **`applyFilterAndSort`** (lines 1138‑1160), which rebuilds the `filtered` slice whenever the user changes tabs or modifies the search query.

### Tab-Based Filtering

Each tab maps to a specific filter constant. When `applyFilterAndSort` runs, it retrieves the current filter from `getPipelineTabs()[m.activeTab].filter` and iterates through `m.apps`:

```go
func (m *PipelineModel) applyFilterAndSort() {
    var filtered []model.CareerApplication
    currentFilter := getPipelineTabs()[m.activeTab].filter

    for _, app := range m.apps {
        if !matchesSearch(app, m.searchQuery) { continue }
        norm := data.NormalizeStatus(app.Status)
        switch currentFilter {
        case filterAll:
            filtered = append(filtered, app)
        case filterTop:
            if app.Score >= 4.0 && norm != "skip" { filtered = append(filtered, app) }
        default:
            if norm == currentFilter { filtered = append(filtered, app) }
        }
    }
    // Sorting logic follows...
    m.filtered = filtered
}

```

The **`data.NormalizeStatus`** function standardizes status strings before comparison, ensuring that variations like "Interview Scheduled" and "interview" match consistently.

### Live Search

Pressing **/** activates the search bar. As the user types, `handleSearchInput` (lines 33‑85) updates `m.searchQuery`, and **`matchesSearch`** (lines 21‑35) performs a case‑insensitive substring match across company name, role title, and notes fields. An empty query matches all records, allowing seamless toggling between filtered and unfiltered views.

## Sorting Implementation

The model supports seven sort modes cycled with the **s** key:

```go
var sortCycle = []string{sortScore, sortDate, sortCompany,
    sortStatus, sortLocation, sortPay, sortLast}

```

**`sortLess`** (lines 86‑115) returns a comparator function based on the active mode:

```go
func (m PipelineModel) sortLess() func(a, b model.CareerApplication) bool {
    switch m.sortMode {
    case sortDate:      return func(a, b model.CareerApplication) bool { return a.Date > b.Date }
    case sortCompany:   return func(a, b model.CareerApplication) bool { return strings.ToLower(a.Company) < strings.ToLower(b.Company) }
    case sortStatus:    return func(a, b model.CareerApplication) bool { return data.StatusPriority(a.Status) < data.StatusPriority(b.Status) }
    case sortLocation:  return func(a, b model.CareerApplication) bool { … }
    case sortPay:       return func(a, b model.CareerApplication) bool { return a.PayMax > b.PayMax }
    case sortLast:      return func(a, b model.CareerApplication) bool { return a.LastContact > b.LastContact }
    default:            return func(a, b model.CareerApplication) bool { return a.Score > b.Score }
    }
}

```

When the view mode is set to **grouped**, the function first applies the selected comparator, then performs a stable sort by `data.StatusPriority` (lines 68‑78) to keep status blocks visually separated while maintaining internal order within each group.

## Navigation and Scrolling

**`handleKey`** (lines 15‑70) processes all keyboard input for cursor movement:

- **j** / **down**: Move cursor down one row
- **k** / **up**: Move cursor up one row  
- **PageDown** / **ctrl+d**: Jump half a page down
- **PageUp** / **ctrl+u**: Jump half a page up
- **g**: Jump to first row
- **G**: Jump to last row

After each movement, **`adjustScroll`** (lines 51‑69) recalculates the visible window based on terminal height, fixed header/footer rows, and the estimated preview height, ensuring the cursor remains visible. The method then calls **`loadCurrentReport`** (lines 103‑115) to lazy‑load the report preview for the newly selected application.

## Advanced Features

### Column Picker

Press **C** to open the column picker overlay. The **`handleColPicker`** function (lines 71‑88) iterates through `getOptionalCols()` and toggles visibility flags in `m.visibleCols`. These booleans directly influence **`columnWidths`** (lines 54‑79), which dynamically adjusts the rendered table layout.

### Status and Discard Flows

Pressing **c** opens the status picker via **`handleStatusPicker`** (lines 89‑117). Selecting `Discarded` or `Skip` triggers **`startDiscardFlow`**, which loads predicted reasons from the report using `data.LoadReportDiscardReasons`, merges them with `canonicalDiscardReasons`, and displays a secondary picker. Confirming a reason emits a `PipelineUpdateStatusAndNotesMsg` that atomically updates the status and appends a `DISCARD:` or `SKIP:` tag to the tracker notes.

### Report Preview and PDF Actions

When the cursor lands on an entry with a `ReportPath`, **`loadCurrentReport`** emits a `PipelineLoadReportMsg`. Press **Enter** to open the report viewer, **d** to open the newest generated PDF, and **D** to regenerate the PDF from source HTML via `PipelineGeneratePDFMsg`.

## Summary

- The **Go Bubble Tea dashboard TUI** stores all applications in `PipelineModel.apps` and renders a derived `filtered` slice.
- **Filtering** combines tab-based category filters with real-time substring search via `applyFilterAndSort`.
- **Sorting** supports seven modes (score, date, company, status, location, pay, last contact) and can group results by status priority.
- **Navigation** uses vim-style keybindings processed by `handleKey`, with automatic scroll adjustment through `adjustScroll`.
- **Column visibility**, **status updates**, and **PDF generation** are handled through dedicated picker overlays and command messages.

## Frequently Asked Questions

### How do I filter applications by status in the career-ops TUI?

Press **f** to cycle forward or **h** to cycle backward through the tab bar. Each tab maps to a filter constant (`filterEvaluated`, `filterApplied`, etc.) that `applyFilterAndSort` uses to include only matching applications in the `filtered` slice. You can also press **/** to activate live search and filter by company name, role, or notes.

### What keyboard shortcuts move the cursor in the pipeline view?

Use **j** or **down arrow** to move down one row, **k** or **up arrow** to move up, **PageDown**/**ctrl+d** for half-page jumps, and **g** or **G** to jump to the first or last row respectively. These bindings are handled in `handleKey` (lines 38‑70), which automatically adjusts the scroll viewport to keep your selection visible.

### How does the TUI handle sorting when I press the 's' key?

Pressing **s** cycles through `sortCycle`, updating `m.sortMode`. The model then calls `applyFilterAndSort`, which retrieves a comparator from `sortLess` (lines 86‑115). In grouped view mode, the function performs a stable secondary sort by `data.StatusPriority` to keep applications clustered by status regardless of the primary sort key.

### Can I customize which columns appear in the application table?

Yes. Press **C** to open the column picker managed by `handleColPicker` (lines 71‑88). Navigate with **j**/**k** and toggle columns with **Space**. Your selections are stored in `m.visibleCols` and consulted by `columnWidths` (lines 54‑79) to render the table with only your chosen data fields.