layout

package
v0.1.0-rc3 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 5 Imported by: 0

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

Constants

This section is empty.

Variables

PageAPresets is the ordered list of presets for Page A (Music). Index 0 is the default (Full Dashboard).

View Source
var PageBPresets = []Preset{PresetNerdStatus}

PageBPresets is the ordered list of presets for Page B (Nerd Status).

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func FormatFilterLabel(query string, budget int) string

FormatFilterLabel returns the most informative filter label that fits within the given column budget. Tries variants from widest to narrowest:

  1. f(rock) — preferred; full query in unquoted form
  2. f(ro…) — truncated; trims the query rune-by-rune with a trailing …
  3. f(…) — minimal; signals an active filter without showing query
  4. "" — 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

func PadRight(s string, width int) string

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

func PaneBorderColor(id PaneID, t theme.Theme) lipgloss.Color

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

func Truncate(s string, maxWidth int) string

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

func TruncateOrPad(s string, width int) string

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 Action

type Action struct {
	Key   string // e.g., "f"
	Label string // e.g., "filter"
}

Action describes a pane-specific shortcut shown in the border.

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 Cell

type Cell struct {
	PaneID      PaneID
	WidthWeight int
}

Cell represents a pane slot in a row with its relative width.

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

func (m *Manager) ActivePage() PageID

ActivePage returns the current page.

func (*Manager) ActivePresetIndex

func (m *Manager) ActivePresetIndex() int

ActivePresetIndex returns the current preset index for the active page.

func (*Manager) ActivePresetName

func (m *Manager) ActivePresetName() string

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

func (m *Manager) FocusedPane() PaneID

FocusedPane returns the PaneID that currently has keyboard focus.

func (*Manager) IsPaneVisible

func (m *Manager) IsPaneVisible(id PaneID) bool

IsPaneVisible returns whether a pane is currently visible.

func (*Manager) PaneAt

func (m *Manager) PaneAt(x, y int) PaneID

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

func (m *Manager) PaneRect(id PaneID) Rect

PaneRect returns the computed Rect for a pane. Returns zero Rect if hidden.

func (*Manager) Resize

func (m *Manager) Resize(width, height int)

Resize updates terminal dimensions and recomputes all pane rects.

func (*Manager) RotateFocus

func (m *Manager) RotateFocus(forward bool)

RotateFocus moves focus to the next (forward=true) or previous visible pane. Wraps around. Uses focusOrder built during recompute().

func (*Manager) SetFocus

func (m *Manager) SetFocus(id PaneID)

SetFocus sets focus to a specific pane. No-op if pane is not visible.

func (*Manager) SetPreset

func (m *Manager) SetPreset(index int)

SetPreset sets a specific preset index. Resets manual toggles.

func (*Manager) TogglePage

func (m *Manager) TogglePage()

TogglePage switches between PageA and PageB. Resets hidden map and recomputes layout.

func (*Manager) TogglePane

func (m *Manager) TogglePane(id PaneID)

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

func (m *Manager) VisiblePanes() []PaneID

VisiblePanes returns all visible PaneIDs in layout order (top-left to bottom-right).

type PageID

type PageID int

PageID identifies a page (group of panes).

const (
	PageA PageID = iota // Music (8 panes)
	PageB               // Nerd Status (2 panes)
)

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

type Preset struct {
	Name    string
	Visible map[PaneID]bool
	Grid    []Row
}

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

func (r Rect) ContentHeight() int

ContentHeight returns the usable height inside borders. Returns 0 if height is less than 2 (cannot fit borders).

func (Rect) ContentWidth

func (r Rect) ContentWidth() int

ContentWidth returns the usable width inside borders. Returns 0 if width is less than 2 (cannot fit borders).

type Row

type Row struct {
	HeightWeight int
	Cells        []Cell
}

Row represents a horizontal strip of cells in the grid with its relative height.

Jump to

Keyboard shortcuts

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