feat(palette): add responsive command rail
- commit
- a72295f72577e870c82dabc7598d060a51d35b9e
- author
- brice · · 2026-07-09 23:30 UTC
- parent
- 916eeb2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +38 -9
M internal/usecase/daemon/palette.go +17 -1
| @@ -9,8 +9,23 @@ import ( | |||
| 9 | 9 | "github.com/bnema/vev/pkg/renderer" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | const ( | |
| 13 | + | paletteRailBreakpoint = 96 | |
| 14 | + | paletteRailWidth = 64 | |
| 15 | + | ) | |
| 16 | + | ||
| 12 | 17 | var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1} | |
| 13 | 18 | ||
| 19 | + | func paletteModalFor(size domain.Size) ui.Modal { | |
| 20 | + | modal := paletteModal | |
| 21 | + | if size.Cols >= paletteRailBreakpoint { | |
| 22 | + | modal.FixedWidth = paletteRailWidth | |
| 23 | + | modal.HorizontalAnchor = ui.HorizontalAnchorRight | |
| 24 | + | modal.RightMargin = 1 | |
| 25 | + | } | |
| 26 | + | return modal | |
| 27 | + | } | |
| 28 | + | ||
| 14 | 29 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | |
| 15 | 30 | d.closePalette(ac) | |
| 16 | 31 | ac.overlays.paletteMu.Lock() | |
| @@ -277,7 +292,8 @@ func (e paletteExec) OpenSessionPicker() error { | |||
| 277 | 292 | ||
| 278 | 293 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, styles ...themeStyles) (renderer.Frame, []renderer.Damage) { | |
| 279 | 294 | styleSet := resolveThemeStyles(styles) | |
| 280 | - | return composeModalClientFrame(base, paletteModal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame { | |
| 295 | + | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}) | |
| 296 | + | return composeModalClientFrame(base, modal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame { | |
| 281 | 297 | return model.Render(size, palette.RenderStyles{Selection: styleSet.selection, Description: styleSet.paletteDesc}) | |
| 282 | 298 | }) | |
| 283 | 299 | } | |
| @@ -9,8 +9,23 @@ import ( | |||||
| 9 | "github.com/bnema/vev/pkg/renderer" | 9 | "github.com/bnema/vev/pkg/renderer" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | const ( | |||
| 13 | + | paletteRailBreakpoint = 96 | |||
| 14 | + | paletteRailWidth = 64 | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 12 | var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1} | 17 | var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: ui.AnchorBottom, BottomMargin: 1} | ||
| 13 | 18 | ||||
| 19 | + | func paletteModalFor(size domain.Size) ui.Modal { | |||
| 20 | + | modal := paletteModal | |||
| 21 | + | if size.Cols >= paletteRailBreakpoint { | |||
| 22 | + | modal.FixedWidth = paletteRailWidth | |||
| 23 | + | modal.HorizontalAnchor = ui.HorizontalAnchorRight | |||
| 24 | + | modal.RightMargin = 1 | |||
| 25 | + | } | |||
| 26 | + | return modal | |||
| 27 | + | } | |||
| 28 | + | ||||
| 14 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | 29 | func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | ||
| 15 | d.closePalette(ac) | 30 | d.closePalette(ac) | ||
| 16 | ac.overlays.paletteMu.Lock() | 31 | ac.overlays.paletteMu.Lock() | ||
| @@ -277,7 +292,8 @@ func (e paletteExec) OpenSessionPicker() error { | |||||
| 277 | 292 | ||||
| 278 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, styles ...themeStyles) (renderer.Frame, []renderer.Damage) { | 293 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, styles ...themeStyles) (renderer.Frame, []renderer.Damage) { | ||
| 279 | styleSet := resolveThemeStyles(styles) | 294 | styleSet := resolveThemeStyles(styles) | ||
| 280 | - | return composeModalClientFrame(base, paletteModal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame { | 295 | + | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}) |
| 296 | + | return composeModalClientFrame(base, modal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame { | |||
| 281 | return model.Render(size, palette.RenderStyles{Selection: styleSet.selection, Description: styleSet.paletteDesc}) | 297 | return model.Render(size, palette.RenderStyles{Selection: styleSet.selection, Description: styleSet.paletteDesc}) | ||
| 282 | }) | 298 | }) | ||
| 283 | } | 299 | } | ||
M internal/usecase/daemon/palette_test.go +21 -8
| @@ -217,14 +217,27 @@ func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) { | |||
| 217 | 217 | } | |
| 218 | 218 | ||
| 219 | 219 | func TestPaletteModalGeometry(t *testing.T) { | |
| 220 | - | require.Equal(t, 100, paletteModal.WidthPct) | |
| 221 | - | require.Equal(t, 11, paletteModal.FixedHeight) | |
| 222 | - | require.Equal(t, ui.AnchorBottom, paletteModal.Anchor) | |
| 223 | - | require.Equal(t, 1, paletteModal.BottomMargin) | |
| 224 | - | require.Equal(t, 32, paletteModal.MinWidth) | |
| 225 | - | ||
| 226 | - | bounds := paletteModal.Bounds(domain.Size{Cols: 120, Rows: 40}) | |
| 227 | - | require.Equal(t, domain.Rect{X: 0, Y: 28, Width: 120, Height: 11}, bounds) | |
| 220 | + | tests := []struct { | |
| 221 | + | name string | |
| 222 | + | size domain.Size | |
| 223 | + | want domain.Rect | |
| 224 | + | }{ | |
| 225 | + | {name: "95 column shelf", size: domain.Size{Cols: 95, Rows: 40}, want: domain.Rect{X: 0, Y: 28, Width: 95, Height: 11}}, | |
| 226 | + | {name: "96 column rail", size: domain.Size{Cols: 96, Rows: 40}, want: domain.Rect{X: 31, Y: 28, Width: 64, Height: 11}}, | |
| 227 | + | {name: "120 column rail", size: domain.Size{Cols: 120, Rows: 40}, want: domain.Rect{X: 55, Y: 28, Width: 64, Height: 11}}, | |
| 228 | + | {name: "tiny terminal clamps", size: domain.Size{Cols: 20, Rows: 6}, want: domain.Rect{X: 0, Y: 0, Width: 20, Height: 6}}, | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | for _, tt := range tests { | |
| 232 | + | t.Run(tt.name, func(t *testing.T) { | |
| 233 | + | modal := paletteModalFor(tt.size) | |
| 234 | + | require.Equal(t, tt.want, modal.Bounds(tt.size)) | |
| 235 | + | require.Equal(t, " Commands ", modal.Title) | |
| 236 | + | require.Equal(t, 11, modal.FixedHeight) | |
| 237 | + | require.Equal(t, ui.AnchorBottom, modal.Anchor) | |
| 238 | + | require.Equal(t, 1, modal.BottomMargin) | |
| 239 | + | }) | |
| 240 | + | } | |
| 228 | 241 | } | |
| 229 | 242 | ||
| 230 | 243 | func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | |
| @@ -217,14 +217,27 @@ func TestPaletteCtrlNAndCtrlPNavigate(t *testing.T) { | |||||
| 217 | } | 217 | } | ||
| 218 | 218 | ||||
| 219 | func TestPaletteModalGeometry(t *testing.T) { | 219 | func TestPaletteModalGeometry(t *testing.T) { | ||
| 220 | - | require.Equal(t, 100, paletteModal.WidthPct) | 220 | + | tests := []struct { |
| 221 | - | require.Equal(t, 11, paletteModal.FixedHeight) | 221 | + | name string |
| 222 | - | require.Equal(t, ui.AnchorBottom, paletteModal.Anchor) | 222 | + | size domain.Size |
| 223 | - | require.Equal(t, 1, paletteModal.BottomMargin) | 223 | + | want domain.Rect |
| 224 | - | require.Equal(t, 32, paletteModal.MinWidth) | 224 | + | }{ |
| 225 | - | 225 | + | {name: "95 column shelf", size: domain.Size{Cols: 95, Rows: 40}, want: domain.Rect{X: 0, Y: 28, Width: 95, Height: 11}}, | |
| 226 | - | bounds := paletteModal.Bounds(domain.Size{Cols: 120, Rows: 40}) | 226 | + | {name: "96 column rail", size: domain.Size{Cols: 96, Rows: 40}, want: domain.Rect{X: 31, Y: 28, Width: 64, Height: 11}}, |
| 227 | - | require.Equal(t, domain.Rect{X: 0, Y: 28, Width: 120, Height: 11}, bounds) | 227 | + | {name: "120 column rail", size: domain.Size{Cols: 120, Rows: 40}, want: domain.Rect{X: 55, Y: 28, Width: 64, Height: 11}}, |
| 228 | + | {name: "tiny terminal clamps", size: domain.Size{Cols: 20, Rows: 6}, want: domain.Rect{X: 0, Y: 0, Width: 20, Height: 6}}, | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | for _, tt := range tests { | |||
| 232 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 233 | + | modal := paletteModalFor(tt.size) | |||
| 234 | + | require.Equal(t, tt.want, modal.Bounds(tt.size)) | |||
| 235 | + | require.Equal(t, " Commands ", modal.Title) | |||
| 236 | + | require.Equal(t, 11, modal.FixedHeight) | |||
| 237 | + | require.Equal(t, ui.AnchorBottom, modal.Anchor) | |||
| 238 | + | require.Equal(t, 1, modal.BottomMargin) | |||
| 239 | + | }) | |||
| 240 | + | } | |||
| 228 | } | 241 | } | ||
| 229 | 242 | ||||
| 230 | func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | 243 | func TestPaletteUTF8PendingCompletesFilter(t *testing.T) { | ||