Documentation
¶
Overview ¶
Package layout provides the grid-based layout engine for Spotnik's btop-inspired UI. It computes pane positions (Rect values) from preset definitions and terminal dimensions, and manages page switching, preset cycling, pane toggling, and focus rotation. The Manager does not render anything — rendering is handled by Feature 42 (border renderer).
Index ¶
- Variables
- func FormatFilterLabel(query string, budget int) string
- func PadRight(s string, width int) string
- func PaneBorderColor(id PaneID, t theme.Theme) lipgloss.Color
- func RenderPaneBorder(content string, cfg BorderConfig) string
- func Truncate(s string, maxWidth int) string
- func TruncateOrPad(s string, width int) string
- type Action
- type BorderConfig
- type Cell
- type FilterQueryPane
- type FilterablePane
- type Manager
- func (m *Manager) ActivePage() PageID
- func (m *Manager) ActivePresetIndex() int
- func (m *Manager) ActivePresetName() string
- func (m *Manager) CyclePreset()
- func (m *Manager) FocusedPane() PaneID
- func (m *Manager) IsPaneVisible(id PaneID) bool
- func (m *Manager) PaneAt(x, y int) PaneID
- func (m *Manager) PaneRect(id PaneID) Rect
- func (m *Manager) Resize(width, height int)
- func (m *Manager) RotateFocus(forward bool)
- func (m *Manager) SetFocus(id PaneID)
- func (m *Manager) SetPreset(index int)
- func (m *Manager) TogglePage()
- func (m *Manager) TogglePane(id PaneID)
- func (m *Manager) VisiblePanes() []PaneID
- type PageID
- type Pane
- type PaneID
- type Preset
- type Rect
- type Row
Constants ¶
This section is empty.
Variables ¶
var PageAPresets = []Preset{PresetDashboard, PresetListening, PresetLibrary, PresetDiscovery}
PageAPresets is the ordered list of presets for Page A (Music). Index 0 is the default (Full Dashboard).
var PageBPresets = []Preset{PresetNerdStatus}
PageBPresets is the ordered list of presets for Page B (Nerd Status).
var PresetDashboard = Preset{ Name: "Full Dashboard", Visible: map[PaneID]bool{ PaneNowPlaying: true, PaneQueue: true, PanePlaylists: true, PaneAlbums: true, PaneLikedSongs: true, PaneRecentlyPlayed: true, PaneTopTracks: true, PaneTopArtists: true, }, Grid: []Row{ {HeightWeight: 2, Cells: []Cell{{PaneID: PaneNowPlaying, WidthWeight: 1}}}, {HeightWeight: 3, Cells: []Cell{ {PaneID: PanePlaylists, WidthWeight: 1}, {PaneID: PaneAlbums, WidthWeight: 1}, {PaneID: PaneLikedSongs, WidthWeight: 1}, }}, {HeightWeight: 3, Cells: []Cell{ {PaneID: PaneQueue, WidthWeight: 1}, {PaneID: PaneRecentlyPlayed, WidthWeight: 1}, {PaneID: PaneTopTracks, WidthWeight: 1}, {PaneID: PaneTopArtists, WidthWeight: 1}, }}, }, }
PresetDashboard shows all 8 Page A panes across 3 rows.
var PresetDiscovery = Preset{ Name: "Discovery", Visible: map[PaneID]bool{ PaneNowPlaying: true, PaneTopTracks: true, PaneTopArtists: true, PaneRecentlyPlayed: true, }, Grid: []Row{ {HeightWeight: 1, Cells: []Cell{{PaneID: PaneNowPlaying, WidthWeight: 1}}}, {HeightWeight: 2, Cells: []Cell{ {PaneID: PaneTopTracks, WidthWeight: 1}, {PaneID: PaneTopArtists, WidthWeight: 1}, }}, {HeightWeight: 2, Cells: []Cell{{PaneID: PaneRecentlyPlayed, WidthWeight: 1}}}, }, }
PresetDiscovery shows a compact NowPlaying strip with discovery panes below.
var PresetLibrary = Preset{ Name: "Library", Visible: map[PaneID]bool{ PaneNowPlaying: true, PanePlaylists: true, PaneAlbums: true, PaneLikedSongs: true, }, Grid: []Row{ {HeightWeight: 1, Cells: []Cell{{PaneID: PaneNowPlaying, WidthWeight: 1}}}, {HeightWeight: 4, Cells: []Cell{ {PaneID: PanePlaylists, WidthWeight: 1}, {PaneID: PaneAlbums, WidthWeight: 1}, {PaneID: PaneLikedSongs, WidthWeight: 1}, }}, }, }
PresetLibrary shows a compact NowPlaying strip with the full library below.
var PresetListening = Preset{ Name: "Listening", Visible: map[PaneID]bool{ PaneNowPlaying: true, PaneQueue: true, PaneRecentlyPlayed: true, }, Grid: []Row{ {HeightWeight: 3, Cells: []Cell{{PaneID: PaneNowPlaying, WidthWeight: 1}}}, {HeightWeight: 2, Cells: []Cell{ {PaneID: PaneQueue, WidthWeight: 1}, {PaneID: PaneRecentlyPlayed, WidthWeight: 1}, }}, }, }
PresetListening shows NowPlaying expanded with Queue and RecentlyPlayed below.
var PresetNerdStatus = Preset{ Name: "Nerd Status", Visible: map[PaneID]bool{ PaneNowPlaying: true, PaneGatewayHealth: true, PanePollingTraffic: true, PaneGatewayLive: true, PaneNetworkLog: true, }, Grid: []Row{ {HeightWeight: 1, Cells: []Cell{ {PaneID: PaneNowPlaying, WidthWeight: 1}, }}, {HeightWeight: 3, Cells: []Cell{ {PaneID: PaneGatewayHealth, WidthWeight: 1}, {PaneID: PanePollingTraffic, WidthWeight: 1}, {PaneID: PaneGatewayLive, WidthWeight: 3}, }}, {HeightWeight: 2, Cells: []Cell{ {PaneID: PaneNetworkLog, WidthWeight: 1}, }}, }, }
PresetNerdStatus shows NowPlaying strip, three diagnostic panes side-by-side (Health, Traffic, Live with weights 1:1:3 → ~20%/20%/60%), and NetworkLog full-width below. All five panes are individually toggleable via keys 1-5.
Functions ¶
func FormatFilterLabel ¶
FormatFilterLabel returns the most informative filter label that fits within the given column budget. Tries variants from widest to narrowest:
- f(rock) — preferred; full query in unquoted form
- f(ro…) — truncated; trims the query rune-by-rune with a trailing …
- f(…) — minimal; signals an active filter without showing query
- "" — drop; even f(…) does not fit
The returned string is unstyled — the caller is responsible for applying theme colours via mutedStyle.
budget is in terminal columns. The right segment in filter mode is exactly this label — no close-notch is rendered. Esc is a global key documented in the help overlay (`?`), not repeated per-pane.
func PadRight ¶
PadRight pads s with trailing spaces so that its terminal column width equals width. If s is already at least width columns wide, it is returned unchanged (use Truncate first if you need to guarantee the output fits).
func PaneBorderColor ¶
PaneBorderColor returns the accent color for a given PaneID from the Theme. This maps PaneID constants to the corresponding PaneBorder*() Theme method. Falls back to Theme.ActiveBorder() for unknown PaneIDs.
func RenderPaneBorder ¶
func RenderPaneBorder(content string, cfg BorderConfig) string
RenderPaneBorder wraps content in a btop-style border.
The top border line contains the toggle key superscript, title, dash fill, and action shortcuts (or filter query when active). Border characters always use the pane's AccentColor: focused = full brightness + bold title; unfocused = dimmed (Faint on top of AccentColor) so each pane retains its identity color.
Content should be pre-sized to Width-2 × Height-2 (the interior dimensions). Lines are padded or truncated to fit exactly inside the border.
func Truncate ¶
Truncate truncates s to at most maxWidth terminal columns. If s is wider than maxWidth, it is truncated and "…" (U+2026) is appended. Uses lipgloss.Width() for accurate rendered-width measurement so that CJK characters (2 columns), emoji, combining marks, and ANSI escape sequences are all handled correctly.
func TruncateOrPad ¶
TruncateOrPad ensures s is exactly width terminal columns wide. Long strings are truncated (with "…" appended); short strings are padded with trailing spaces. Equivalent to PadRight(Truncate(s, width), width).
Types ¶
type BorderConfig ¶
type BorderConfig struct {
// Width is the total border width in terminal columns (includes the 2 border columns).
Width int
// Height is the total border height in terminal rows (includes the 2 border rows).
Height int
// Title is the pane title shown in the top border (e.g., "Playlists").
Title string
// ToggleKey is the number key (1-8) shown as a superscript before the title.
// Pass 0 for panes that have no toggle key (e.g. Page B panes).
ToggleKey int
// Actions are pane-specific shortcuts shown in the top-right of the border.
// Displayed in corner-notch format: ╮key label╭, separated by ─.
Actions []Action
// AccentColor is the per-pane border accent color (from Theme.PaneBorder*()).
AccentColor lipgloss.Color
// Focused controls whether the pane has keyboard focus.
// Focused: full AccentColor + Bold title; unfocused: AccentColor + Faint (dimmed but colored).
Focused bool
// FilterQuery is non-empty when filter mode is active.
// When set, replaces the action shortcuts with the graded f(query) label
// (see FormatFilterLabel). No close-notch — Esc is a global key documented
// in the help overlay.
FilterQuery string
// Theme provides KeyHint() and TextMuted() colors for action rendering.
Theme theme.Theme
// Glyph overrides for the six structural border characters. When any field
// is empty, the unicode default is used. PaneChrome.Render populates these
// via uikit.GlyphFor so that the active GlyphMode is honoured without
// creating an import cycle (layout → uikit → layout).
//
// Direct callers of RenderPaneBorder that do not set these fields always
// receive unicode rounded corners — the existing behaviour before S2.
CornerTL string // top-left corner (default ╭, ascii +)
CornerTR string // top-right corner (default ╮, ascii +)
CornerBL string // bottom-left corner (default ╰, ascii +)
CornerBR string // bottom-right corner (default ╯, ascii +)
HRule string // horizontal rule (default ─, ascii -)
VRule string // vertical rule (default │, ascii |)
// ToggleKeyStr overrides the auto-derived superscript for ToggleKey. When
// non-empty this string is used verbatim as the key prefix rendered before
// the title. PaneChrome.Render sets this to a plain "N " (digit + space) in
// ascii mode instead of the default unicode superscript. Direct callers that
// only set ToggleKey (int) continue to receive the unicode superscript.
ToggleKeyStr string
}
BorderConfig holds all data needed to render a btop-style pane border.
type FilterQueryPane ¶
type FilterQueryPane interface {
ActiveFilterQuery() string
}
FilterQueryPane is implemented by panes that expose the live filter query for display in the pane border. When ActiveFilterQuery() returns a non-empty string, the border renderer shows f(query) in the top-right corner using the graded-shrink helper (FormatFilterLabel). No close-notch is rendered — Esc is a global key documented in the help overlay.
type FilterablePane ¶
type FilterablePane interface {
HasActiveFilter() bool
}
FilterablePane is implemented by panes that support in-pane text filtering. When HasActiveFilter() returns true, the routing layer sends all key events directly to the pane, bypassing global shortcuts.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager computes pane positions from a grid definition and terminal size. It manages page switching, preset cycling, pane toggling, and focus rotation. The Manager is purely a layout engine — it does not render anything.
func NewManager ¶
func NewManager() *Manager
NewManager creates a Manager with default presets and Page A active.
func (*Manager) ActivePage ¶
ActivePage returns the current page.
func (*Manager) ActivePresetIndex ¶
ActivePresetIndex returns the current preset index for the active page.
func (*Manager) ActivePresetName ¶
ActivePresetName returns the name of the current preset.
func (*Manager) CyclePreset ¶
func (m *Manager) CyclePreset()
CyclePreset advances to the next preset on the active page. Wraps to first preset after the last. Resets manual toggles.
func (*Manager) FocusedPane ¶
FocusedPane returns the PaneID that currently has keyboard focus.
func (*Manager) IsPaneVisible ¶
IsPaneVisible returns whether a pane is currently visible.
func (*Manager) PaneAt ¶
PaneAt returns the PaneID at terminal coordinates (x, y). Returns PaneID(-1) if no pane is at that position. Coordinates are 0-based from top-left of terminal. The header occupies y=0 and the status bar occupies y=height-1.
func (*Manager) PaneRect ¶
PaneRect returns the computed Rect for a pane. Returns zero Rect if hidden.
func (*Manager) RotateFocus ¶
RotateFocus moves focus to the next (forward=true) or previous visible pane. Wraps around. Uses focusOrder built during recompute().
func (*Manager) TogglePage ¶
func (m *Manager) TogglePage()
TogglePage switches between PageA and PageB. Resets hidden map and recomputes layout.
func (*Manager) TogglePane ¶
TogglePane toggles visibility of a pane (keys 1-8 on Page A, 1-5 on Page B). Does nothing if the pane is not part of the current preset. If toggling would hide ALL panes, the toggle is rejected. NOTE: Preset membership is the sole authority for whether a pane is toggleable. This naturally handles cross-page safety: panes not in the active preset are rejected, including Page A panes on Page B and vice versa (with the exception of PaneNowPlaying which appears in both pages' presets and is intentionally toggleable on both).
func (*Manager) VisiblePanes ¶
VisiblePanes returns all visible PaneIDs in layout order (top-left to bottom-right).
type Pane ¶
type Pane interface {
tea.Model
// SetSize sets the content area dimensions (inside borders).
SetSize(width, height int)
// SetFocused updates the keyboard focus state.
SetFocused(focused bool)
// IsFocused returns whether this pane currently has keyboard focus.
IsFocused() bool
// ID returns the PaneID for this slot in the grid.
ID() PaneID
// Title returns the display title shown in the pane border.
Title() string
// ToggleKey returns the number key for btop-style pane toggling
// (1-8 on Page A, 1-5 on Page B). Returns 0 for panes that are not
// individually toggleable.
ToggleKey() int
// Actions returns pane-specific shortcut hints displayed in the border.
Actions() []Action
// SetTheme updates the pane's theme reference for runtime theme switching.
// Table-based panes must rebuild their tables with new column colors.
SetTheme(th theme.Theme)
}
Pane is the interface every grid pane must implement. It extends tea.Model with layout and focus management methods.
type PaneID ¶
type PaneID int
PaneID uniquely identifies a pane slot in the grid.
const ( PaneNowPlaying PaneID = iota // Page A pane 1 (toggle key 1) PaneQueue // Page A pane 2 (toggle key 2) PanePlaylists // Page A pane 3 (toggle key 3) PaneAlbums // Page A pane 4 (toggle key 4) PaneLikedSongs // Page A pane 5 (toggle key 5) PaneRecentlyPlayed // Page A pane 6 (toggle key 6) PaneTopTracks // Page A pane 7 (toggle key 7) PaneTopArtists // Page A pane 8 (toggle key 8) PaneNetworkLog // Page B pane 5 (toggle key 5) PaneGatewayHealth // Page B pane 2 (toggle key 2) PanePollingTraffic // Page B pane 3 (toggle key 3) PaneGatewayLive // Page B pane 4 (toggle key 4) )
type Preset ¶
Preset is a named grid configuration — a bitmask of visible panes plus the grid layout.
type Rect ¶
type Rect struct {
X, Y int // Top-left corner (relative to content area)
Width, Height int // Dimensions including borders
}
Rect describes a pane's position and size in terminal cells.
func (Rect) ContentHeight ¶
ContentHeight returns the usable height inside borders. Returns 0 if height is less than 2 (cannot fit borders).
func (Rect) ContentWidth ¶
ContentWidth returns the usable width inside borders. Returns 0 if width is less than 2 (cannot fit borders).