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
- func HintSegment(styles Styles, hintKey, desc string) string
- func NewDialogShell(styles Styles, margin int, bar scrollbar.Styles) dialog.Shell
- type App
- type ConfigReloadedMsg
- type Counter
- type ErrorMsg
- type Lens
- type PreviewPosition
- type ProgramContext
- func (c *ProgramContext) AdjustPreviewRatio(delta float64)
- func (c *ProgramContext) PreviewPosition() PreviewPosition
- func (c *ProgramContext) RebindKeys(cfg *config.Config) error
- func (c *ProgramContext) SetLenses(cfg *config.Config)
- func (c *ProgramContext) SetPreviewFromConfig(v string)
- func (c *ProgramContext) SetPreviewPosition(p PreviewPosition)
- func (c *ProgramContext) SetPreviewRatioPercent(p int)
- func (c *ProgramContext) SetSize(w, h int)
- func (c *ProgramContext) ToggleZoom()
- func (c *ProgramContext) Zoomed() bool
- type RecentIssue
- type RecentList
- type RefreshTickMsg
- type Registry
- type RestyleMsg
- type Section
- type SectionFactory
- type SectionID
- type SectionMsg
- type Services
- type Styles
- type TaskFinishedMsg
- type TaskManager
- type TaskScope
- type TaskSpec
- type ThemePreviewMsg
Constants ¶
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
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
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 ¶
CurrentSection exposes the active section for tests and wiring.
func (App) Init ¶
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 ¶
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.
type ConfigReloadedMsg ¶
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 ¶
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 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 ¶
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 (*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 (*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) 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 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
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 ¶
TaskManager hands out monotonic generations per scope.
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.