summaryrefslogtreecommitdiff
path: root/internal/tui/common
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
commit1561987330cb898f5ff64383a9c78e7e6559f118 (patch)
tree69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/common
parent96225fb6159212a8851043a08d781aba721b4e78 (diff)
parent110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff)
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/common')
-rw-r--r--internal/tui/common/doc.go2
-rw-r--r--internal/tui/common/keys.go17
-rw-r--r--internal/tui/common/keys_test.go24
-rw-r--r--internal/tui/common/styles.go128
-rw-r--r--internal/tui/common/styles_test.go39
-rw-r--r--internal/tui/common/viewport.go26
-rw-r--r--internal/tui/common/viewport_test.go76
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" }