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
- Variables
- func ThemeLabel(t ThemeName) string
- type AsyncCache
- func (c *AsyncCache[K, V]) Clear()
- func (c *AsyncCache[K, V]) Delete(key K)
- func (c *AsyncCache[K, V]) Err(key K) error
- func (c *AsyncCache[K, V]) Get(key K) (V, bool)
- func (c *AsyncCache[K, V]) IsLoading(key K) bool
- func (c *AsyncCache[K, V]) Keys() []K
- func (c *AsyncCache[K, V]) Len() int
- func (c *AsyncCache[K, V]) LoadingMap() map[K]bool
- func (c *AsyncCache[K, V]) Set(key K, value V)
- func (c *AsyncCache[K, V]) SetErr(key K, err error)
- func (c *AsyncCache[K, V]) SetLoading(key K)
- type FocusState
- type LRUCache
- type LayoutMode
- type Logger
- type Mode
- type Model
- type Options
- type PanelID
- type ScreenMode
- type ThemeName
Constants ¶
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 ¶
var SidebarPanels = []PanelID{ PanelProjects, PanelPipelines, PanelStages, PanelMRs, }
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 ¶
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 ¶
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]) Peek ¶
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.
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 ¶
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 ¶
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 ¶
Update is the central message router. It handles three categories of messages:
- System messages (WindowSizeMsg) — resize all sub-components.
- Key messages — dispatched to the mode-specific key handler. Help toggle and error clearing are handled globally before mode dispatch.
- 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.
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 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.
Source Files
¶
- async_cache.go
- cache.go
- clipboard_cmd.go
- error_format.go
- favorites.go
- glab_emit.go
- highlight.go
- icons.go
- info_bar.go
- keys.go
- keys_detail_panel.go
- keys_mrs_panel.go
- keys_multipanel.go
- keys_pipelines_panel.go
- keys_projects_panel.go
- keys_stages_panel.go
- layout.go
- lru_cache.go
- matrix.go
- messages_explorer.go
- messages_mrs.go
- messages_pipelines.go
- messages_projects.go
- panel.go
- panel_mrs.go
- preferences.go
- project_list_cmds.go
- project_list_helpers.go
- project_list_keys.go
- project_list_model.go
- project_list_style.go
- project_list_view.go
- state_explorer.go
- state_mrs.go
- state_pipelines.go
- state_projects.go
- text_helpers.go
- theme.go
- view_explorer.go
- view_glab.go
- view_multipanel.go
- view_pipelines.go
- view_projects.go