core

package
v0.14.2 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package core defines the contracts every TUI view is built on: the Section interface, the shared ProgramContext, the async task manager, the section registry and the typed message set. The root App orchestrates Sections without knowing what data any of them holds, so adding a view (boards, epics, worklogs) is a matter of implementing Section and registering a factory — no change to the orchestration.

Index

Constants

View Source
const TopChromeRows = 2

TopChromeRows is the rows above the body: the tab row and its bottom rule. Exported because a section hit-testing absolute mouse coordinates (wheel routing) needs the body's screen offset.

Variables

This section is empty.

Functions

func HintSegment added in v0.12.0

func HintSegment(styles Styles, hintKey, desc string) string

HintSegment renders one key/description hint. A single-letter key (with or without a modifier prefix) highlights its mnemonic inside the description — the "(t)ransition" style — so the eye reads the word and the key at once; anything else falls back to "key desc". Shared by the footer hint line and the sections' own hint rows so the two can never drift apart.

func NewDialogShell added in v0.12.1

func NewDialogShell(styles Styles, margin int, bar scrollbar.Styles) dialog.Shell

NewDialogShell builds the frame a dialog stack draws around a box-less dialog, from the current theme styles. The 66-column cap keeps every modal at a readable measure (pairing with the action controller's 60-column inner text width); margin and scrollbar styling are the owner's — the App's stack hugs the screen edges, the section stacks keep their historical wider margin. Owners rebuild the shell whenever the styles re-derive (theme preview or config reload) so an open dialog never renders with stale chrome.

Types

type App

type App struct {

	// Reconfigure is the wiring layer's hook for a config hot-reload: given the
	// fresh config it re-registers config-derived sections and returns the new
	// tab order plus the section IDs whose cached instances must be rebuilt
	// (their config may have changed). Nil disables structural reloads — the
	// App still swaps ctx.Config so value-only settings (refresh interval)
	// apply.
	Reconfigure func(ctx *ProgramContext, registry *Registry, cfg *config.Config) (order, invalidate []SectionID)
	// contains filtered or unexported fields
}

App is the root Bubble Tea model. It is data-agnostic: it owns the shared context, the section registry, the task manager and the live section instances, and routes messages without knowing what any section displays. Value semantics match the rest of the TUI; the pointer/map fields are shared references so mutations persist across the value copies Bubble Tea makes.

func NewApp

func NewApp(ctx *ProgramContext, registry *Registry, order []SectionID) App

NewApp builds the root model. The first ID in order is the initially active section. The context's StartTask is wired to the task manager so any section can launch generation-tracked async work.

func (App) CurrentSection

func (a App) CurrentSection() Section

CurrentSection exposes the active section for tests and wiring.

func (App) Init

func (a App) Init() tea.Cmd

Init starts the active section, then eagerly starts every other registered section so background tabs fetch immediately and the tab bar shows real counts without a visit. It also arms the auto-refresh timer. The returned App is discarded by the caller (Bubble Tea keeps the model it already holds), but the started-map mutations persist because the map is shared.

func (App) Update

func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update routes messages by kind:

  • WindowSizeMsg recomputes the shared layout and is broadcast to every built section so backgrounded views reflow before they are shown.
  • Global keys (quit, section switch) are handled here.
  • An accepted TaskFinishedMsg is broadcast to every section so the one that owns the scope applies it even if the user has since switched views; a superseded result is dropped.
  • Everything else (input) goes to the active section only.

func (App) View

func (a App) View() tea.View

View composes the tab bar, the section body and the footer. The sidebar split is part of the section body via ProgramContext; the App only owns chrome.

type ConfigReloadedMsg

type ConfigReloadedMsg struct{ Config *config.Config }

ConfigReloadedMsg carries a freshly re-read config file. The App swaps the shared config, asks its Reconfigure hook to rebuild the section order, and re-broadcasts the message so sections (e.g. settings) can refresh their view.

type Counter

type Counter interface {
	Count() (n int, ok bool)
}

Counter is an optional Section capability: a section that knows how many items it holds reports them for the tab bar ("Issues (6)"). ok is false until the section has loaded, so an unvisited tab shows no count.

type ErrorMsg

type ErrorMsg struct{ Err error }

ErrorMsg carries a non-fatal error to the App, which stores it on the context and renders it in the footer. Errors never block the loop and are cleared by the next successful task.

The action/selection/search message vocabulary (transition, comment, assign, issue-selected, JQL-submitted, ...) is intentionally not defined here: those messages are introduced alongside the sections and the action controller that produce and consume them, so the foundation never carries receiver-less types.

type Lens

type Lens struct {
	Name string
	JQL  string
}

Lens is a named quick-filter JQL for the issues section.

type PreviewPosition

type PreviewPosition string

PreviewPosition controls where the issue sidebar sits relative to the list.

const (
	// PreviewRight docks the sidebar to the right of the list (wide terminals).
	PreviewRight PreviewPosition = "right"
	// PreviewLeft docks the sidebar to the left of the list.
	PreviewLeft PreviewPosition = "left"
	// PreviewBottom docks the sidebar below the list (narrow terminals).
	PreviewBottom PreviewPosition = "bottom"
	// PreviewHidden closes the sidebar (config value only; SidebarOpen is the
	// runtime flag).
	PreviewHidden PreviewPosition = "hidden"
	// PreviewAuto picks right or bottom based on terminal width.
	PreviewAuto PreviewPosition = "auto"
)

type ProgramContext

type ProgramContext struct {
	// Terminal geometry.
	ScreenWidth  int
	ScreenHeight int

	// Computed body regions. MainWidth/Height is the list area; Preview* is the
	// sidebar. Preview dimensions are zero when the sidebar is closed.
	// BodyHeight is the full region between the chrome (the whole list+sidebar
	// area), used by full-body views like the issue detail.
	MainWidth     int
	MainHeight    int
	PreviewWidth  int
	PreviewHeight int
	BodyHeight    int

	// SidebarOpen toggles the issue preview pane.
	SidebarOpen bool

	Styles Styles
	Keys   keys.Map
	// Lenses are the config-supplied quick-filters for the issues section
	// ([[tui.lenses]]); empty means the section's built-ins apply.
	// DefaultLens names the landing lens by title (case-insensitive).
	Lenses      []Lens
	DefaultLens string
	// Recent is the app-wide recently-viewed issue jumplist (ctrl+o).
	Recent *RecentList
	// Activity is the app-wide record of user-facing mutations, surfaced in the
	// footer status slot and the operation-log overlay. Sections write to it
	// (Start/Finish/Fail); only the footer and log read it back.
	Activity *activity.Registry
	Config   *config.Config
	View     SectionID
	Err      error
	Services Services

	// Profile context for the footer chrome: which profile/project/board the
	// dashboard is pointed at. All optional; empty fields are omitted.
	ProfileName string
	Project     string
	Board       string
	// Version is the build version shown right of the tab bar.
	// Empty hides the brand label.
	Version string
	// ConfigPath is the loaded config file's location, used by the settings
	// section to display it and to watch it for hot-reload. Empty disables
	// reloading.
	ConfigPath string
	// BaseURL is the Jira site root (e.g. https://acme.atlassian.net), used to
	// build issue links for "open in browser" and "copy url".
	BaseURL string
	// DefaultIssueType is the profile's issue type for creates ("" means the
	// caller picks a fallback).
	DefaultIssueType string
	// WorkdaySeconds is the active profile's working-day length, used to parse
	// relative worklog durations like "1d". Zero falls back to 8 hours.
	WorkdaySeconds int

	// Base is the program's root context, used to cancel in-flight Jira calls
	// launched from sections when the app shuts down.
	Base context.Context //nolint:containedctx // sections launch async Jira calls from Update, which has no context parameter; the program's root context is shared state by design

	// StartTask is wired by the App so any Section can launch async work that
	// flows back as a TaskFinishedMsg with generation tracking.
	StartTask func(TaskSpec) tea.Cmd
	// contains filtered or unexported fields
}

ProgramContext is the single shared state broadcast to every Section. The App owns one *ProgramContext and mutates it (notably on resize); Sections hold the pointer and read it during View, so layout, theme and config changes need no prop-drilling.

func NewProgramContext

func NewProgramContext(svc Services, cfg *config.Config) *ProgramContext

NewProgramContext builds a context with default styles, key map and the given services and config. Either may be nil for tests or for a chrome-only run.

func (*ProgramContext) AdjustPreviewRatio

func (c *ProgramContext) AdjustPreviewRatio(delta float64)

AdjustPreviewRatio grows or shrinks the preview's share of the split by delta, clamped, and re-runs the layout.

func (*ProgramContext) PreviewPosition

func (c *ProgramContext) PreviewPosition() PreviewPosition

PreviewPosition resolves "auto" to the concrete position for the current width. It is exported so a Section can render its sidebar on the correct edge.

func (*ProgramContext) RebindKeys

func (c *ProgramContext) RebindKeys(cfg *config.Config) error

RebindKeys rebuilds the key map from defaults plus the config's tui.keys overrides, so a removed override returns to its default on hot-reload. On an invalid override (unknown action) the current map is kept and the error returned for the caller to surface.

func (*ProgramContext) SetLenses

func (c *ProgramContext) SetLenses(cfg *config.Config)

SetLenses applies the config's [[tui.lenses]] entries and tui.default_lens. Entries missing a title or JQL are skipped rather than rendering a blank chip; an empty surviving list clears back to the section's built-ins.

func (*ProgramContext) SetPreviewFromConfig

func (c *ProgramContext) SetPreviewFromConfig(v string)

SetPreviewFromConfig applies a tui.preview value: right/left/bottom dock the open sidebar there, "hidden" closes it, "auto" opens it with width-resolved placement. An empty or unknown value leaves the current state alone, so a config without the key never fights the p-key cycle on reload.

func (*ProgramContext) SetPreviewPosition

func (c *ProgramContext) SetPreviewPosition(p PreviewPosition)

SetPreviewPosition sets the configured sidebar preference and recomputes the layout for the current terminal size.

func (*ProgramContext) SetPreviewRatioPercent

func (c *ProgramContext) SetPreviewRatioPercent(p int)

SetPreviewRatioPercent applies the configured tui.preview_size (percent of the body given to the preview) and re-runs the layout, mirroring AdjustPreviewRatio. Zero/absent keeps the current ratio; out-of-range values clamp.

func (*ProgramContext) SetSize

func (c *ProgramContext) SetSize(w, h int)

SetSize records the terminal size and splits it into the list and sidebar regions. With the sidebar closed the list takes the full body; with it open the split follows the resolved preview position (right ≈ half width, bottom ≈ half height). All width/height math lives here so layout can never drift between components.

func (*ProgramContext) ToggleZoom

func (c *ProgramContext) ToggleZoom()

ToggleZoom flips the tmux-style zoom: the main pane takes the full body, and toggling again restores the previous split. With the sidebar closed there is nothing to zoom away, so the key is a no-op — latent zoom state would otherwise make a reopened preview invisibly collapse.

func (*ProgramContext) Zoomed

func (c *ProgramContext) Zoomed() bool

Zoomed reports whether the main pane is zoomed.

type RecentIssue

type RecentIssue struct {
	Key     string
	Summary string
}

RecentIssue is one jumplist entry: the issue key plus the summary shown in the picker.

type RecentList

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

RecentList is the app-wide recently-viewed issue history, most recent first. It is shared through the ProgramContext so every section feeds and reads the same jumplist. Not safe for concurrent use; the TUI mutates it from the single Update goroutine.

func NewRecentList

func NewRecentList() *RecentList

NewRecentList returns an empty history.

func (*RecentList) List

func (r *RecentList) List() []RecentIssue

List returns the history, most recent first.

func (*RecentList) Touch

func (r *RecentList) Touch(key, summary string)

Touch records a visit: the issue moves to (or enters at) the front with the given summary. Empty keys are ignored; an empty summary keeps the one already recorded (jumping to an issue from history opens it via a bare-key stub, which must not wipe the known summary).

type RefreshTickMsg

type RefreshTickMsg struct{}

RefreshTickMsg is the dashboard's auto-refresh heartbeat. The App arms a timer from tui.refresh_interval, broadcasts this on each firing so every section may refetch if idle, then re-arms.

type Registry

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

Registry maps section IDs to their factories. It is the single extension point: registering a factory makes a view reachable; the App needs no change.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Build

func (r *Registry) Build(id SectionID, ctx *ProgramContext) (Section, bool)

Build constructs the section for an ID, returning false if it is unregistered.

func (*Registry) Has

func (r *Registry) Has(id SectionID) bool

Has reports whether an ID is registered.

func (*Registry) Register

func (r *Registry) Register(id SectionID, f SectionFactory)

Register associates an ID with a factory, overwriting any previous one. It lazily initializes the map so a zero-value Registry is usable.

type RestyleMsg added in v0.12.0

type RestyleMsg struct{}

RestyleMsg tells sections the derived styles changed under them (a theme preview): re-render cached content from data already in hand — never refetch, a preview must stay free.

type Section

type Section interface {
	// ID is the stable identifier used for tab routing and the registry.
	ID() SectionID
	// Title is the human label shown in the tab bar.
	Title() string
	// Init wires the shared context and returns any startup command
	// (typically the first data fetch).
	Init(ctx *ProgramContext) tea.Cmd
	// Update handles a message and returns the (possibly new) Section value
	// plus a command. Value semantics keep Sections cheap to copy and easy
	// to test.
	Update(msg tea.Msg) (Section, tea.Cmd)
	// View renders the section body. The App owns the surrounding chrome
	// (tab bar, footer) and the sidebar split lives in ProgramContext.
	View() string
	// HelpBindings returns the bindings shown in the contextual help bar for
	// this section in its current state.
	HelpBindings() []key.Binding
	// CapturesInput reports whether the section is currently consuming raw key
	// input (e.g. a filter or editor is focused). When true, the App routes
	// keys to the section before applying global shortcuts, so typing a query
	// containing "q" or "tab" does not quit or switch views.
	CapturesInput() bool
}

Section is one top-level view. It is a real Bubble Tea component: it owns its state, reacts to messages, and renders into the body region the App lays out. The App passes the shared *ProgramContext into Init and the Section keeps the pointer, so layout and theme changes are observed on the next View without any prop-drilling.

type SectionFactory

type SectionFactory func(ctx *ProgramContext) Section

SectionFactory builds a Section bound to the shared context. Sections are constructed lazily on first navigation so unused views cost nothing.

func NewPlaceholderSection

func NewPlaceholderSection(id SectionID, title string) SectionFactory

NewPlaceholderSection returns a factory for a do-nothing section with the given id and title.

type SectionID

type SectionID string

SectionID identifies a view (e.g. "issues", "search").

type SectionMsg

type SectionMsg interface {
	Section() SectionID
}

SectionMsg is a message addressed to a specific section. The App broadcasts these to every section instead of routing them to the active one; the addressee matches the ID itself.

type Services

type Services interface {
	Issues() jira.IssueService
	Search() jira.SearchService
	JQL() jira.JQLService
	Users() jira.UserService
	Worklogs() jira.WorklogService
	// Projects backs the create form's issue-type list (ListIssueTypes).
	Projects() jira.ProjectService
	// Labels backs the create form's label suggestions.
	Labels() jira.LabelService
}

Services is the narrow seam Sections use to reach Jira. It exposes only the service interfaces the TUI needs, so sections can be unit-tested with fakes without constructing a real client.

Only the interface lives in core: it depends solely on the jira domain package. The concrete adapter that wraps the CLI's service factory belongs in the wiring layer (added at cutover), so core never imports the cli packages and no import cycle can form.

type Styles

type Styles struct {
	Header             lipgloss.Style
	HeaderRule         lipgloss.Style // wraps the tab row, drawing the bottom divider
	TabActive          lipgloss.Style // selected section pill
	TabInactive        lipgloss.Style // unselected section pill
	Brand              lipgloss.Style // product label right of the tab row
	Footer             lipgloss.Style
	FooterRule         lipgloss.Style // dim style for the labeled-border dashes
	HintKey            lipgloss.Style // key glyphs in the footer hint line
	HintDesc           lipgloss.Style // descriptions in the footer hint line
	Error              lipgloss.Style
	Sidebar            lipgloss.Style
	SidebarBorder      lipgloss.Style // left-edge divider (sidebar docked right)
	SidebarBorderRight lipgloss.Style // right-edge divider (sidebar docked left)
	SidebarBorderTop   lipgloss.Style // top-edge divider (sidebar docked bottom)
	Overlay            lipgloss.Style // bordered modal box for the action controller
	HelpBox            lipgloss.Style // bordered box for the full-keymap help sheet
}

Styles bundles the chrome styles the App and Sections share. Content styles (status colors, priorities, entity colors) stay in the theme package; this bundle is just the structural chrome that the new architecture owns.

func DefaultStyles

func DefaultStyles() Styles

DefaultStyles derives the chrome bundle from the shared clib theme so a theme swap reskins the new TUI alongside the old one.

type TaskFinishedMsg

type TaskFinishedMsg = task.FinishedMsg

TaskFinishedMsg carries a task result, tagged with its generation.

type TaskManager

type TaskManager = task.Manager

TaskManager hands out monotonic generations per scope.

func NewTaskManager

func NewTaskManager() *TaskManager

NewTaskManager returns an empty manager.

type TaskScope

type TaskScope = task.Scope

TaskScope groups async work so a newer task supersedes an older one.

type TaskSpec

type TaskSpec = task.Spec

TaskSpec describes a unit of async work run off the UI loop.

type ThemePreviewMsg added in v0.12.0

type ThemePreviewMsg struct{ Name string }

ThemePreviewMsg applies a theme by name without touching the config file — the settings picker sends one per cursor move so the dashboard shows the highlighted theme live. Committing then saves the same name (the reload sees it already applied and no-ops); backing out previews the original name back.

Jump to

Keyboard shortcuts

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