diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
| commit | 1561987330cb898f5ff64383a9c78e7e6559f118 (patch) | |
| tree | 69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/common | |
| parent | 96225fb6159212a8851043a08d781aba721b4e78 (diff) | |
| parent | 110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff) | |
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/common')
| -rw-r--r-- | internal/tui/common/doc.go | 2 | ||||
| -rw-r--r-- | internal/tui/common/keys.go | 17 | ||||
| -rw-r--r-- | internal/tui/common/keys_test.go | 24 | ||||
| -rw-r--r-- | internal/tui/common/styles.go | 128 | ||||
| -rw-r--r-- | internal/tui/common/styles_test.go | 39 | ||||
| -rw-r--r-- | internal/tui/common/viewport.go | 26 | ||||
| -rw-r--r-- | internal/tui/common/viewport_test.go | 76 |
7 files changed, 257 insertions, 55 deletions
diff --git a/internal/tui/common/doc.go b/internal/tui/common/doc.go new file mode 100644 index 0000000..e15ceb7 --- /dev/null +++ b/internal/tui/common/doc.go @@ -0,0 +1,2 @@ +// Package common provides shared TUI styling, keymaps, and viewport utilities. +package common diff --git a/internal/tui/common/keys.go b/internal/tui/common/keys.go index ba17998..ab9865d 100644 --- a/internal/tui/common/keys.go +++ b/internal/tui/common/keys.go @@ -1,6 +1,6 @@ package common -import "github.com/charmbracelet/bubbles/key" +import "charm.land/bubbles/v2/key" // HelpSection groups related key bindings under a shared heading. type HelpSection struct { @@ -38,12 +38,12 @@ func DefaultKeyMap() KeyMap { return KeyMap{ Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next tab")), ShiftTab: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev tab")), - One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "overview")), - Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "syscalls")), - Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "files")), - Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "processes")), - Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "lat+gaps")), - Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "stream")), + One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "flame")), + Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "overview")), + Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "syscalls")), + Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "files")), + Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "processes")), + Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "lat+gaps")), Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "stream")), DirGroup: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "dir group")), SelectPID: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "select pid")), @@ -83,6 +83,7 @@ func (k KeyMap) DashboardStatusHelpSections() []HelpSection { k.Four, k.Five, k.Six, + k.Seven, k.SelectPID, k.SelectTID, k.Probes, @@ -126,7 +127,7 @@ func (k KeyMap) DashboardFullHelp() [][]key.Binding { controls = append(controls, k.DirGroup, k.SelectPID, k.SelectTID, k.Probes, k.Refresh, k.Quit) return [][]key.Binding{ - {k.One, k.Two, k.Three, k.Four, k.Five, k.Six}, + {k.One, k.Two, k.Three, k.Four, k.Five, k.Six, k.Seven}, controls, { helpTextBinding("space", "stream pause"), diff --git a/internal/tui/common/keys_test.go b/internal/tui/common/keys_test.go index 42e47ab..4284faf 100644 --- a/internal/tui/common/keys_test.go +++ b/internal/tui/common/keys_test.go @@ -23,6 +23,11 @@ func TestDefaultKeyMapIncludesDirGroupBinding(t *testing.T) { if selectTIDHelp.Key != "t" || selectTIDHelp.Desc != "select tid" { t.Fatalf("unexpected select tid binding help: key=%q desc=%q", selectTIDHelp.Key, selectTIDHelp.Desc) } + + flameHelp := keys.One.Help() + if flameHelp.Key != "1" || flameHelp.Desc != "flame" { + t.Fatalf("unexpected flame binding help: key=%q desc=%q", flameHelp.Key, flameHelp.Desc) + } } func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) { @@ -33,6 +38,7 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) { } found := false + foundOne := false for _, binding := range groups[1] { help := binding.Help() if help.Key == "d" && help.Desc == "dir group" { @@ -44,6 +50,17 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) { t.Fatalf("expected dir group binding in dashboard full help controls") } + for _, binding := range groups[0] { + help := binding.Help() + if help.Key == "1" && help.Desc == "flame" { + foundOne = true + break + } + } + if !foundOne { + t.Fatalf("expected flame tab binding in dashboard full help tabs") + } + found = false for _, binding := range groups[1] { help := binding.Help() @@ -86,6 +103,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { short := keys.DashboardStatusHelp() found := false foundSelectTID := false + foundOne := false for _, binding := range short { help := binding.Help() if help.Key == "o" && help.Desc == "probes" { @@ -94,6 +112,9 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { if help.Key == "t" && help.Desc == "select tid" { foundSelectTID = true } + if help.Key == "1" && help.Desc == "flame" { + foundOne = true + } } if !found { t.Fatalf("expected probes binding in dashboard short help") @@ -101,4 +122,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) { if !foundSelectTID { t.Fatalf("expected select tid binding in dashboard short help") } + if !foundOne { + t.Fatalf("expected flame tab binding in dashboard short help") + } } diff --git a/internal/tui/common/styles.go b/internal/tui/common/styles.go index d4c75ff..a71ef81 100644 --- a/internal/tui/common/styles.go +++ b/internal/tui/common/styles.go @@ -1,59 +1,117 @@ package common -import "github.com/charmbracelet/lipgloss" +import ( + "image/color" + + "charm.land/lipgloss/v2" +) + +// Palette defines themed colors shared across the TUI package. +type Palette struct { + Background color.Color + Panel color.Color + Primary color.Color + Accent color.Color + Muted color.Color + Text color.Color + Danger color.Color +} + +// NewPalette returns a color palette for dark or light terminal backgrounds. +func NewPalette(isDark bool) Palette { + if isDark { + return Palette{ + Background: lipgloss.Color("235"), + Panel: lipgloss.Color("238"), + Primary: lipgloss.Color("75"), + Accent: lipgloss.Color("222"), + Muted: lipgloss.Color("246"), + Text: lipgloss.Color("255"), + Danger: lipgloss.Color("203"), + } + } + + return Palette{ + Background: lipgloss.Color("255"), + Panel: lipgloss.Color("250"), + Primary: lipgloss.Color("26"), + Accent: lipgloss.Color("88"), + Muted: lipgloss.Color("242"), + Text: lipgloss.Color("235"), + Danger: lipgloss.Color("160"), + } +} var ( // Palette colors shared across the TUI package. - ColorBackground = lipgloss.Color("235") - ColorPanel = lipgloss.Color("238") - ColorPrimary = lipgloss.Color("75") - ColorAccent = lipgloss.Color("222") - ColorMuted = lipgloss.Color("246") - ColorText = lipgloss.Color("255") - ColorDanger = lipgloss.Color("203") + ColorBackground color.Color + ColorPanel color.Color + ColorPrimary color.Color + ColorAccent color.Color + ColorMuted color.Color + ColorText color.Color + ColorDanger color.Color ) var ( // ScreenStyle is the base style for full-screen models. - ScreenStyle = lipgloss.NewStyle(). - Foreground(ColorText) + ScreenStyle lipgloss.Style // HeaderStyle is used by top-level titles and screen headers. - HeaderStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColorPrimary) + HeaderStyle lipgloss.Style // TabActiveStyle is applied to the currently-selected tab. - TabActiveStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColorBackground). - Background(ColorPrimary). - Padding(0, 1) + TabActiveStyle lipgloss.Style // TabInactiveStyle is applied to non-selected tabs. - TabInactiveStyle = lipgloss.NewStyle(). - Foreground(ColorMuted). - Padding(0, 1) + TabInactiveStyle lipgloss.Style // PanelStyle is used for boxed sections. - PanelStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder()). - BorderForeground(ColorPanel). - Padding(0, 1) + PanelStyle lipgloss.Style // HelpBarStyle is used for keybinding hints at the bottom. - HelpBarStyle = lipgloss.NewStyle(). - Foreground(ColorMuted). - BorderTop(true). - BorderForeground(ColorPanel) + HelpBarStyle lipgloss.Style // HighlightStyle emphasizes inline values. - HighlightStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColorAccent) + HighlightStyle lipgloss.Style // ErrorStyle is used for fatal or warning messages. - ErrorStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColorDanger) + ErrorStyle lipgloss.Style ) + +// ApplyPalette updates shared colors and styles to match the provided theme. +func ApplyPalette(isDark bool) { + palette := NewPalette(isDark) + ColorBackground = palette.Background + ColorPanel = palette.Panel + ColorPrimary = palette.Primary + ColorAccent = palette.Accent + ColorMuted = palette.Muted + ColorText = palette.Text + ColorDanger = palette.Danger + + ScreenStyle = lipgloss.NewStyle().Foreground(ColorText) + HeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorPrimary) + TabActiveStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(ColorBackground). + Background(ColorPrimary). + Padding(0, 1) + TabInactiveStyle = lipgloss.NewStyle(). + Foreground(ColorMuted). + Padding(0, 1) + PanelStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(ColorPanel). + Padding(0, 1) + HelpBarStyle = lipgloss.NewStyle(). + Foreground(ColorMuted). + BorderTop(true). + BorderForeground(ColorPanel) + HighlightStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent) + ErrorStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorDanger) +} + +func init() { + ApplyPalette(true) +} diff --git a/internal/tui/common/styles_test.go b/internal/tui/common/styles_test.go new file mode 100644 index 0000000..c0900b3 --- /dev/null +++ b/internal/tui/common/styles_test.go @@ -0,0 +1,39 @@ +package common + +import ( + "testing" + + "charm.land/lipgloss/v2" +) + +func TestNewPaletteRendersDistinctThemes(t *testing.T) { + dark := NewPalette(true) + light := NewPalette(false) + + darkRender := lipgloss.NewStyle(). + Foreground(dark.Text). + Background(dark.Background). + Render("ior") + lightRender := lipgloss.NewStyle(). + Foreground(light.Text). + Background(light.Background). + Render("ior") + + if darkRender == lightRender { + t.Fatalf("expected dark and light palettes to render differently") + } +} + +func TestApplyPaletteUpdatesSharedStyles(t *testing.T) { + t.Cleanup(func() { ApplyPalette(true) }) + + ApplyPalette(true) + dark := ScreenStyle.Render("ior") + + ApplyPalette(false) + light := ScreenStyle.Render("ior") + + if dark == light { + t.Fatalf("expected ScreenStyle render to differ between dark and light palettes") + } +} diff --git a/internal/tui/common/viewport.go b/internal/tui/common/viewport.go index e1729db..d54c886 100644 --- a/internal/tui/common/viewport.go +++ b/internal/tui/common/viewport.go @@ -11,20 +11,22 @@ const ( defaultViewportHeight = 24 ) +var queryTerminalSize = func() (int, int, error) { + return xterm.GetSize(os.Stdout.Fd()) +} + // EffectiveViewport returns a usable terminal viewport size. Missing or invalid -// dimensions are resolved from the active terminal when possible. +// dimensions fall back to defaults. func EffectiveViewport(width, height int) (int, int) { - if width > 0 && height > 0 { - return width, height - } - - termWidth, termHeight, err := xterm.GetSize(os.Stdout.Fd()) - if err == nil { - if width <= 0 && termWidth > 0 { - width = termWidth - } - if height <= 0 && termHeight > 0 { - height = termHeight + if width <= 0 || height <= 0 { + terminalWidth, terminalHeight, err := queryTerminalSize() + if err == nil { + if width <= 0 && terminalWidth > 0 { + width = terminalWidth + } + if height <= 0 && terminalHeight > 0 { + height = terminalHeight + } } } diff --git a/internal/tui/common/viewport_test.go b/internal/tui/common/viewport_test.go new file mode 100644 index 0000000..2dda81b --- /dev/null +++ b/internal/tui/common/viewport_test.go @@ -0,0 +1,76 @@ +package common + +import "testing" + +func TestEffectiveViewport(t *testing.T) { + originalQuery := queryTerminalSize + t.Cleanup(func() { + queryTerminalSize = originalQuery + }) + queryTerminalSize = func() (int, int, error) { + return 132, 41, nil + } + + tests := []struct { + name string + width int + height int + wantWidth int + wantHeight int + }{ + { + name: "provided dimensions", + width: 120, + height: 40, + wantWidth: 120, + wantHeight: 40, + }, + { + name: "both missing use terminal size", + width: 0, + height: 0, + wantWidth: 132, + wantHeight: 41, + }, + { + name: "missing height uses terminal size", + width: 100, + height: 0, + wantWidth: 100, + wantHeight: 41, + }, + { + name: "missing width uses terminal size", + width: -1, + height: 30, + wantWidth: 132, + wantHeight: 30, + }, + } + + for _, tt := range tests { + gotWidth, gotHeight := EffectiveViewport(tt.width, tt.height) + if gotWidth != tt.wantWidth || gotHeight != tt.wantHeight { + t.Fatalf("%s: got (%d,%d), want (%d,%d)", tt.name, gotWidth, gotHeight, tt.wantWidth, tt.wantHeight) + } + } +} + +func TestEffectiveViewportFallsBackToDefaultsWhenTerminalQueryFails(t *testing.T) { + originalQuery := queryTerminalSize + t.Cleanup(func() { + queryTerminalSize = originalQuery + }) + queryTerminalSize = func() (int, int, error) { + return 0, 0, assertiveError{} + } + + gotWidth, gotHeight := EffectiveViewport(0, 0) + if gotWidth != defaultViewportWidth || gotHeight != defaultViewportHeight { + t.Fatalf("got (%d,%d), want (%d,%d)", gotWidth, gotHeight, defaultViewportWidth, defaultViewportHeight) + } +} + +type assertiveError struct{} + +func (assertiveError) Error() string { return "terminal query failed" } |
