ui

package
v0.0.0-...-cad1c48 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 36 Imported by: 0

Documentation

Overview

highlight.go implements syntax highlighting for file previews using glamour. Results are cached in an LRU keyed by path+width+content-hash to avoid re-rendering when re-selecting a previously viewed file.

Package ui implements the Bubble Tea TUI for browsing GitLab projects, pipelines, merge requests, and repository files.

The UI is a mode-based state machine built on the Elm architecture. A single Model value owns all state and transitions between modes (projects, explorer, pipelines, multi-panel) via key events. All I/O is performed through tea.Cmd functions that return typed messages; the Model.Update method routes each message to the appropriate handler which produces the next state and optional follow-up commands.

Rendering is pure: Model.View reads state and returns a string. No side effects occur during rendering, which makes the UI predictable and testable.

state_explorer.go manages the three-pane file browser state: navigation stack (push/pop directories), preview loading, and clipboard operations.

state_mrs.go manages merge request state: clipboard operations for MR URLs and discussion comments.

state_pipelines.go manages pipeline view state: opening/closing the view, page navigation, stage/job/bridge/log fetching, retry logic, and the auto-refresh cascade that keeps pipeline data live.

state_projects.go manages project list state: selection, pagination, search filtering, favorites, pipeline status prefetching, visible-projects caching, and detail-pane render caching.

text_helpers.go contains pure text manipulation functions used across multiple UI views: truncation, wrapping, fuzzy matching, ANSI-aware clamping, column normalization, and modal overlay compositing.

view_explorer.go renders the three-pane file browser: parent directory listing, current directory listing, and file/directory preview with syntax highlighting.

view_pipelines.go renders the pipeline view: the pipeline list pane, the stages/jobs matrix pane, the log preview pane, and the retry confirmation modal.

view_projects.go renders the project list pane, the detail sidebar (project metadata + pipeline status), the search bar, and the pagination progress bar.

Index

Constants

View Source
const (

	// MinTerminalWidth and MinTerminalHeight define the smallest terminal
	// dimensions where the TUI can render usefully. Below these thresholds
	// all modes show a "too small" message and block key input.
	MinTerminalWidth  = 63 // minSidebarWidth + detailMinWidth + paneGap + borderCharsH*2
	MinTerminalHeight = 10 // practical floor for any useful rendering
)

Layout constants for the multi-panel view.

Variables

SidebarPanels lists the left-side panels in display order. The Detail pane is intentionally excluded — it is navigated to via h/l, not Tab.

Functions

func ThemeLabel

func ThemeLabel(t ThemeName) string

ThemeLabel returns the display name for a theme.

Types

type AsyncCache

type AsyncCache[K comparable, V any] struct {
	// contains filtered or unexported fields
}

AsyncCache is a generic map-based cache that tracks loading and error state per key. It replaces the repeated (map[K]V, map[K]bool, map[K]error) triplet used throughout pipelineViewState.

Each key transitions through a simple state machine:

idle → SetLoading → (Set | SetErr) → idle

Set clears loading/error, so callers don't need to track transitions manually. This is not concurrency-safe; it relies on Bubble Tea's single-goroutine Update loop for all mutations.

func NewAsyncCache

func NewAsyncCache[K comparable, V any]() AsyncCache[K, V]

NewAsyncCache returns an initialized, ready-to-use cache.

func (*AsyncCache[K, V]) Clear

func (c *AsyncCache[K, V]) Clear()

Clear resets the cache to empty.

func (*AsyncCache[K, V]) Delete

func (c *AsyncCache[K, V]) Delete(key K)

Delete removes all state for a key.

func (*AsyncCache[K, V]) Err

func (c *AsyncCache[K, V]) Err(key K) error

Err returns the error for a key, or nil.

func (*AsyncCache[K, V]) Get

func (c *AsyncCache[K, V]) Get(key K) (V, bool)

Get returns the cached value and whether it exists.

func (*AsyncCache[K, V]) IsLoading

func (c *AsyncCache[K, V]) IsLoading(key K) bool

IsLoading reports whether the key is currently loading.

func (*AsyncCache[K, V]) Keys

func (c *AsyncCache[K, V]) Keys() []K

Keys returns all keys that have cached values.

func (*AsyncCache[K, V]) Len

func (c *AsyncCache[K, V]) Len() int

Len returns the number of cached values.

func (*AsyncCache[K, V]) LoadingMap

func (c *AsyncCache[K, V]) LoadingMap() map[K]bool

LoadingMap exposes the underlying loading map for backward compatibility with shouldFetchPipelineData guards. Prefer IsLoading for new code.

func (*AsyncCache[K, V]) Set

func (c *AsyncCache[K, V]) Set(key K, value V)

Set stores a value and clears loading/error for that key.

func (*AsyncCache[K, V]) SetErr

func (c *AsyncCache[K, V]) SetErr(key K, err error)

SetErr stores an error and clears loading for that key.

func (*AsyncCache[K, V]) SetLoading

func (c *AsyncCache[K, V]) SetLoading(key K)

SetLoading marks a key as loading and clears its error.

type FocusState

type FocusState struct {
	Active     PanelID    // Currently focused panel
	ScreenMode ScreenMode // Normal/half/full layout mode
	PrevActive PanelID    // Sidebar panel to return to from Detail
	LayoutMode LayoutMode // Default (30/70) or Wide (50/50) split
}

FocusState tracks which panel owns keyboard input and how the layout is configured. PrevActive is set when entering the Detail pane so that "back" (h/left) returns to the correct sidebar panel, and so the Detail pane knows which context to render (pipeline logs vs MR content).

func (*FocusState) NextScreenMode

func (f *FocusState) NextScreenMode()

NextScreenMode cycles through screen modes.

func (*FocusState) ToggleLayoutMode

func (f *FocusState) ToggleLayoutMode()

ToggleLayoutMode switches between Default and Wide layout modes.

type LRUCache

type LRUCache[K comparable, V any] struct {
	// contains filtered or unexported fields
}

LRUCache is a generic least-recently-used cache with a fixed maximum size. When the cache is full, the least recently used entry is evicted to make room for new ones. Get promotes an entry to the most recently used position.

All operations are O(1): a doubly-linked list (container/list) tracks order from most-recently-used (front) to least-recently-used (back), and a map keyed by the user key holds *list.Element references for direct removal and promotion without a linear scan.

Get reorders the LRU list as a side effect; use Peek for read-only access from render paths so View() never mutates shared state.

This is not concurrency-safe; it relies on Bubble Tea's single-goroutine Update loop for all mutations.

func NewLRUCache

func NewLRUCache[K comparable, V any](maxSize int) LRUCache[K, V]

NewLRUCache returns an initialized cache that holds at most maxSize entries. A maxSize of zero or negative means no entries will ever be stored.

func (*LRUCache[K, V]) Clear

func (c *LRUCache[K, V]) Clear()

Clear removes all entries from the cache.

func (*LRUCache[K, V]) Delete

func (c *LRUCache[K, V]) Delete(key K)

Delete removes a key from the cache.

func (*LRUCache[K, V]) Get

func (c *LRUCache[K, V]) Get(key K) (V, bool)

Get returns the cached value for key and reports whether it was found. A hit promotes the key to the most recently used position.

func (*LRUCache[K, V]) Len

func (c *LRUCache[K, V]) Len() int

Len returns the number of entries in the cache.

func (*LRUCache[K, V]) Peek

func (c *LRUCache[K, V]) Peek(key K) (V, bool)

Peek returns the cached value for key without touching the LRU order. Use from render paths (View) where mutation would smuggle Update-style side effects into the read-only View contract.

func (*LRUCache[K, V]) Range

func (c *LRUCache[K, V]) Range(fn func(K, V) bool)

Range iterates over all entries from oldest to newest. If fn returns false, iteration stops early. The traversal walks the list back-to-front because the back holds the oldest entry under the front-is-newest convention.

func (*LRUCache[K, V]) Set

func (c *LRUCache[K, V]) Set(key K, value V)

Set stores a value for key. If the key already exists, its value is updated and it is promoted to the most recently used position. If the cache is full, the least recently used entry is evicted first.

type LayoutMode

type LayoutMode int

LayoutMode controls the sidebar-to-detail width ratio.

const (
	LayoutDefault LayoutMode = iota // 30/70 split
	LayoutWide                      // 50/50 split
)

type Logger

type Logger interface {
	Debug(msg string, args ...any)
	Error(msg string, args ...any)
	Info(msg string, args ...any)
}

Logger abstracts structured logging so callers can inject slog.Logger (or a test spy) without coupling the UI package to a concrete implementation. In tests, a nil Logger is safe — all call sites check before logging.

type Mode

type Mode int

Mode represents which top-level screen the user is viewing. Each mode has its own key handler, view function, and subset of Model state that it "owns". Transitioning between modes resets the destination mode's state (e.g., opening pipelines clears previous pipeline data) to avoid showing stale content from a prior visit.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the root Bubble Tea model. It is a value type following Bubble Tea conventions: Update returns a new Model by value, and View is a pure read.

Internally it acts as a state machine whose current Mode determines which key handler, view function, and subset of state are active. The mode-specific state structs (explorerState, pipelineViewState, mrViewState, etc.) are only meaningful when Model.mode matches; transition functions (openExplorer, openPipelineView, etc.) reinitialize them from scratch.

Shared mutable state (pipelineStatus map, favorites map) is referenced by both the Model and its list delegates. This is intentional: the delegate reads the same map instance so pipeline status icons refresh without reconstructing the delegate on every tick. The trade-off is that callers must not replace these maps after construction — only mutate them in place or call SetDelegate with the new map.

func NewModel

func NewModel(ctx context.Context, client gitlab.Service, opts Options) Model

NewModel returns a ready-to-run Bubble Tea model. It applies defaults to zero-valued Options fields, initializes Bubble Tea sub-components (spinner, help, paginator, project list), and sets up on-disk caches for projects and favorites. Cache initialization errors are logged but non-fatal — the app falls back to API-only mode.

The provided ctx is stored in the Model and used as the base context for all subsequent API calls. Callers should pass a context derived from [signal.NotifyContext] or similar so that in-flight requests are cancelled on shutdown.

The returned Model starts in modeMultiPanel and begins loading on Model.Init.

func (Model) Init

func (m Model) Init() tea.Cmd

Init kicks off initial data loading. If an on-disk project cache exists, it is loaded first for instant startup; otherwise a foreground API fetch begins. Favorites are loaded in parallel. The spinner tick is always started so the loading indicator animates immediately.

func (Model) Update

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update is the central message router. It handles three categories of messages:

  1. System messages (WindowSizeMsg) — resize all sub-components.
  2. Key messages — dispatched to the mode-specific key handler. Help toggle and error clearing are handled globally before mode dispatch.
  3. Async result messages — each typed message is routed to its handler (e.g., projectsLoadedMsg -> handleProjectsLoaded). Handlers update state and may return follow-up commands for cascading fetches.

The spinner is only ticked when something is actively loading, to avoid unnecessary redraws in idle state.

func (Model) View

func (m Model) View() string

View is the top-level Bubble Tea render entry point. It dispatches to the active mode's renderer and composites modal overlays (help, retry confirm) on top of the base view when needed.

type Options

type Options struct {
	ProjectsPerPage int
	Logger          Logger
	Host            string
	APITimeout      time.Duration
	PipelineTimeout time.Duration
	// DiffContextLines controls how many unified-diff lines surround each
	// positioned MR comment in the Comments tab. Defaults to 10 if unset.
	// Set to 0 to disable inline diff context entirely.
	DiffContextLines int
}

Options configures the model at creation time. Zero values are replaced with sensible defaults in NewModel:

  • ProjectsPerPage defaults to 30.
  • APITimeout defaults to 15s (used for project listing, tree, file fetches).
  • PipelineTimeout defaults to 20s (used for stages, jobs, logs, retries).
  • Host is the GitLab instance URL, used as the cache key for on-disk project/favorites storage. An empty Host is valid (defaults apply).
  • Logger may be nil; when set it receives structured debug/error output without ever including the API token.

type PanelID

type PanelID int

PanelID identifies a panel in the multi-panel layout.

const (
	PanelProjects  PanelID = iota // Left sidebar: project list
	PanelPipelines                // Left sidebar: pipeline list
	PanelStages                   // Left sidebar: stages/jobs
	PanelMRs                      // Left sidebar: merge requests
	PanelDetail                   // Right area: contextual content (not a sidebar panel)
)

type ScreenMode

type ScreenMode int

ScreenMode controls how much space the focused panel consumes.

const (
	ScreenNormal ScreenMode = iota // All panels visible, accordion sizing
	ScreenHalf                     // Focused panel takes half, others share rest
	ScreenFull                     // Focused panel takes full sidebar
)

type ThemeName

type ThemeName int

ThemeName identifies a color theme preset. Persisted as an int in the preferences JSON; values must remain stable across versions.

const (
	ThemeRosePineMoon ThemeName = iota
	ThemeTokyoNight
	ThemeCatppuccinMocha
	ThemeGruvboxDark
	ThemeDracula
	ThemeNord
	ThemeSolarizedDark
	ThemeKanagawa
	ThemeEverforestDark
	ThemeOneDark
)

The available theme presets, ordered as the user cycles through them with the ~ hotkey. Their iota values are the persisted preference, so existing entries must keep their order; append new presets before themeCount.

func NextTheme

func NextTheme(t ThemeName) ThemeName

NextTheme returns the next theme in the cycle, wrapping around.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL