detached 60de555

feat(bar): add bar.background option for transparent top and bottom bars

commit
60de5551af97145edad96df48d46b7c9cc487d15
author
brice · · 2026-10-08 08:42 UTC
parent
0254710
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +118 -5
M docs/configuration.md +3 -0
@@ -78,6 +78,8 @@ snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,
7878 bar.top-right =
7979 bar.bottom-right =
8080 bar.interval = 5s
81+# Fill the top and bottom bars with the theme surface; off keeps the terminal background.
82+bar.background = on
8183
8284 # Command palette codes: 2-3 letters or digits.
8385 code.new-tab = CNT
@@ -215,6 +217,7 @@ If the daemon refuses to start because its session data is broken, do not edit t
215217 | `theme.palette = off` | Neutral colors, no accent. |
216218 | `theme.accent = auto` | Pick an accent from your terminal's ANSI colors (blue if unsure). |
217219 | `theme.accent = 0`–`15` | Use exactly that ANSI color as the accent. |
220+| `bar.background = off` | Keep the terminal background behind the top and bottom bars (useful with a transparent terminal). Tabs and recent sessions keep their colors. |
218221
219222 - vev only colors its own UI (bars, borders, palette). Pane content is never recolored.
220223 - Tinted backgrounds need a truecolor or 256-color terminal. On 16-color and monochrome terminals vev uses bold, reverse, and faint instead. See [terminal colors](terminal.md#colors).
@@ -78,6 +78,8 @@ snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,
78 bar.top-right =78 bar.top-right =
79 bar.bottom-right =79 bar.bottom-right =
80 bar.interval = 5s80 bar.interval = 5s
81+# Fill the top and bottom bars with the theme surface; off keeps the terminal background.
82+bar.background = on
81 83
82 # Command palette codes: 2-3 letters or digits.84 # Command palette codes: 2-3 letters or digits.
83 code.new-tab = CNT85 code.new-tab = CNT
@@ -215,6 +217,7 @@ If the daemon refuses to start because its session data is broken, do not edit t
215 | `theme.palette = off` | Neutral colors, no accent. |217 | `theme.palette = off` | Neutral colors, no accent. |
216 | `theme.accent = auto` | Pick an accent from your terminal's ANSI colors (blue if unsure). |218 | `theme.accent = auto` | Pick an accent from your terminal's ANSI colors (blue if unsure). |
217 | `theme.accent = 0`–`15` | Use exactly that ANSI color as the accent. |219 | `theme.accent = 0`–`15` | Use exactly that ANSI color as the accent. |
220+| `bar.background = off` | Keep the terminal background behind the top and bottom bars (useful with a transparent terminal). Tabs and recent sessions keep their colors. |
218 221
219 - vev only colors its own UI (bars, borders, palette). Pane content is never recolored.222 - vev only colors its own UI (bars, borders, palette). Pane content is never recolored.
220 - Tinted backgrounds need a truecolor or 256-color terminal. On 16-color and monochrome terminals vev uses bold, reverse, and faint instead. See [terminal colors](terminal.md#colors).223 - Tinted backgrounds need a truecolor or 256-color terminal. On 16-color and monochrome terminals vev uses bold, reverse, and faint instead. See [terminal colors](terminal.md#colors).
M internal/adapters/config/config.go +7 -0
@@ -122,6 +122,13 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
122122 interval = domain.MinBarInterval
123123 }
124124 cfg.Bar.Interval = interval
125+ case key == "bar.background":
126+ on, ok := parseOnOff(value)
127+ if !ok {
128+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid bar.background %q", value)})
129+ continue
130+ }
131+ cfg.Bar.Transparent = !on
125132 case key == "palette.anchor":
126133 warnings = warnDuplicateKey(warnings, seenPaletteKeys, key, lineNo)
127134 if strings.EqualFold(strings.TrimSpace(value), "auto") {
@@ -122,6 +122,13 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) {
122 interval = domain.MinBarInterval122 interval = domain.MinBarInterval
123 }123 }
124 cfg.Bar.Interval = interval124 cfg.Bar.Interval = interval
125+ case key == "bar.background":
126+ on, ok := parseOnOff(value)
127+ if !ok {
128+ warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid bar.background %q", value)})
129+ continue
130+ }
131+ cfg.Bar.Transparent = !on
125 case key == "palette.anchor":132 case key == "palette.anchor":
126 warnings = warnDuplicateKey(warnings, seenPaletteKeys, key, lineNo)133 warnings = warnDuplicateKey(warnings, seenPaletteKeys, key, lineNo)
127 if strings.EqualFold(strings.TrimSpace(value), "auto") {134 if strings.EqualFold(strings.TrimSpace(value), "auto") {
M internal/adapters/config/config_test.go +33 -0
@@ -428,6 +428,39 @@ func TestParseThemePalette(t *testing.T) {
428428 }
429429 }
430430
431+func TestParseBarBackground(t *testing.T) {
432+ t.Parallel()
433+
434+ tests := []struct {
435+ name string
436+ input string
437+ wantTransparent bool
438+ wantWarnings []domain.Warning
439+ }{
440+ {name: "on keeps the theme surface", input: "bar.background = on\n"},
441+ {name: "off makes bars transparent", input: "bar.background = off\n", wantTransparent: true},
442+ {
443+ name: "invalid value warns and keeps default",
444+ input: "bar.background = none\n",
445+ wantWarnings: []domain.Warning{
446+ {Line: 1, Msg: "invalid bar.background \"none\""},
447+ },
448+ },
449+ {name: "absent key keeps default", input: "bar.interval = 2s\n"},
450+ }
451+
452+ for _, tt := range tests {
453+ t.Run(tt.name, func(t *testing.T) {
454+ t.Parallel()
455+
456+ got, warnings, err := Parse(strings.NewReader(tt.input))
457+ require.NoError(t, err)
458+ require.Equal(t, tt.wantTransparent, got.Bar.Transparent)
459+ require.Equal(t, tt.wantWarnings, warnings)
460+ })
461+ }
462+}
463+
431464 func TestDefaultsThemePalette(t *testing.T) {
432465 t.Parallel()
433466
@@ -428,6 +428,39 @@ func TestParseThemePalette(t *testing.T) {
428 }428 }
429 }429 }
430 430
431+func TestParseBarBackground(t *testing.T) {
432+ t.Parallel()
433+
434+ tests := []struct {
435+ name string
436+ input string
437+ wantTransparent bool
438+ wantWarnings []domain.Warning
439+ }{
440+ {name: "on keeps the theme surface", input: "bar.background = on\n"},
441+ {name: "off makes bars transparent", input: "bar.background = off\n", wantTransparent: true},
442+ {
443+ name: "invalid value warns and keeps default",
444+ input: "bar.background = none\n",
445+ wantWarnings: []domain.Warning{
446+ {Line: 1, Msg: "invalid bar.background \"none\""},
447+ },
448+ },
449+ {name: "absent key keeps default", input: "bar.interval = 2s\n"},
450+ }
451+
452+ for _, tt := range tests {
453+ t.Run(tt.name, func(t *testing.T) {
454+ t.Parallel()
455+
456+ got, warnings, err := Parse(strings.NewReader(tt.input))
457+ require.NoError(t, err)
458+ require.Equal(t, tt.wantTransparent, got.Bar.Transparent)
459+ require.Equal(t, tt.wantWarnings, warnings)
460+ })
461+ }
462+}
463+
431 func TestDefaultsThemePalette(t *testing.T) {464 func TestDefaultsThemePalette(t *testing.T) {
432 t.Parallel()465 t.Parallel()
433 466
M internal/domain/config.go +4 -1
@@ -47,11 +47,14 @@ func DefaultSnapshotRestoreProcesses() []string {
4747 return append([]string(nil), defaultSnapshotRestoreProcesses...)
4848 }
4949
50-// BarConfig contains user-configurable bar right-anchor settings.
50+// BarConfig contains user-configurable bar settings.
5151 type BarConfig struct {
5252 TopRight string
5353 BottomRight string
5454 Interval time.Duration
55+ // Transparent keeps the terminal background behind the top and bottom
56+ // bars instead of the theme surface color (bar.background = off).
57+ Transparent bool
5558 }
5659
5760 // CopyConfig contains copy-mode selection and mouse motion settings.
@@ -47,11 +47,14 @@ func DefaultSnapshotRestoreProcesses() []string {
47 return append([]string(nil), defaultSnapshotRestoreProcesses...)47 return append([]string(nil), defaultSnapshotRestoreProcesses...)
48 }48 }
49 49
50-// BarConfig contains user-configurable bar right-anchor settings.50+// BarConfig contains user-configurable bar settings.
51 type BarConfig struct {51 type BarConfig struct {
52 TopRight string52 TopRight string
53 BottomRight string53 BottomRight string
54 Interval time.Duration54 Interval time.Duration
55+ // Transparent keeps the terminal background behind the top and bottom
56+ // bars instead of the theme surface color (bar.background = off).
57+ Transparent bool
55 }58 }
56 59
57 // CopyConfig contains copy-mode selection and mouse motion settings.60 // CopyConfig contains copy-mode selection and mouse motion settings.
A internal/usecase/daemon/bar_background_test.go +55 -0
@@ -0,0 +1,55 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+
8+ renderer "github.com/bnema/vev-vt"
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/domain/terminalcap"
11+ themeui "github.com/bnema/vev/internal/usecase/theme"
12+)
13+
14+func TestBarBackgroundConfigResolvesBarSurface(t *testing.T) {
15+ paletteColors := [16]renderer.RGB{}
16+ paletteColors[4] = renderer.RGB{R: 40, G: 90, B: 220}
17+ raw := themeui.Theme{
18+ Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10},
19+ HasFG: true, HasBG: true, Known: true, UsePalette: true,
20+ Palette: paletteColors, PaletteKnown: 1 << 4,
21+ }
22+
23+ tests := []struct {
24+ name string
25+ transparent bool
26+ color terminalcap.ColorCapabilities
27+ }{
28+ {name: "truecolor themed", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}},
29+ {name: "truecolor transparent", transparent: true, color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}},
30+ {name: "ansi16 transparent", transparent: true, color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ cfg := domain.Defaults()
35+ cfg.Bar.Transparent = tt.transparent
36+ d := newTestDaemon(t, nil, stubClock{})
37+ d.ApplyConfig(cfg)
38+
39+ themed := themeui.ResolveForColor(raw, cfg.ThemeAccent, tt.color).Styles
40+ got := d.resolveAppliedTheme(raw, tt.color).Resolved.Styles
41+
42+ if tt.transparent {
43+ require.True(t, got.SurfaceBar.Equal(renderer.DefaultStyle()), "bar fill uses the terminal background")
44+ } else {
45+ require.True(t, got.SurfaceBar.Equal(themed.SurfaceBar))
46+ }
47+ // Every other role keeps its theme colors.
48+ require.True(t, got.SurfaceInactive.Equal(themed.SurfaceInactive))
49+ require.True(t, got.TabActive.Equal(themed.TabActive))
50+ require.True(t, got.MRUStyle(0, 2).Equal(themed.MRUStyle(0, 2)))
51+ require.True(t, got.CopyStatus.Equal(themed.CopyStatus))
52+ require.True(t, got.StatusBar.Equal(themed.StatusBar))
53+ })
54+ }
55+}
@@ -0,0 +1,55 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+
8+ renderer "github.com/bnema/vev-vt"
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/domain/terminalcap"
11+ themeui "github.com/bnema/vev/internal/usecase/theme"
12+)
13+
14+func TestBarBackgroundConfigResolvesBarSurface(t *testing.T) {
15+ paletteColors := [16]renderer.RGB{}
16+ paletteColors[4] = renderer.RGB{R: 40, G: 90, B: 220}
17+ raw := themeui.Theme{
18+ Foreground: renderer.RGB{R: 230, G: 230, B: 230}, Background: renderer.RGB{R: 8, G: 9, B: 10},
19+ HasFG: true, HasBG: true, Known: true, UsePalette: true,
20+ Palette: paletteColors, PaletteKnown: 1 << 4,
21+ }
22+
23+ tests := []struct {
24+ name string
25+ transparent bool
26+ color terminalcap.ColorCapabilities
27+ }{
28+ {name: "truecolor themed", color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}},
29+ {name: "truecolor transparent", transparent: true, color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}},
30+ {name: "ansi16 transparent", transparent: true, color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI16}},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ cfg := domain.Defaults()
35+ cfg.Bar.Transparent = tt.transparent
36+ d := newTestDaemon(t, nil, stubClock{})
37+ d.ApplyConfig(cfg)
38+
39+ themed := themeui.ResolveForColor(raw, cfg.ThemeAccent, tt.color).Styles
40+ got := d.resolveAppliedTheme(raw, tt.color).Resolved.Styles
41+
42+ if tt.transparent {
43+ require.True(t, got.SurfaceBar.Equal(renderer.DefaultStyle()), "bar fill uses the terminal background")
44+ } else {
45+ require.True(t, got.SurfaceBar.Equal(themed.SurfaceBar))
46+ }
47+ // Every other role keeps its theme colors.
48+ require.True(t, got.SurfaceInactive.Equal(themed.SurfaceInactive))
49+ require.True(t, got.TabActive.Equal(themed.TabActive))
50+ require.True(t, got.MRUStyle(0, 2).Equal(themed.MRUStyle(0, 2)))
51+ require.True(t, got.CopyStatus.Equal(themed.CopyStatus))
52+ require.True(t, got.StatusBar.Equal(themed.StatusBar))
53+ })
54+ }
55+}
M internal/usecase/daemon/config.go +8 -4
@@ -21,9 +21,10 @@ var commandCodePattern = regexp.MustCompile(`^[A-Z0-9]{2,3}$`)
2121 // themeConfigSnapshot is the theme slice of runtimeConfig. Its zero value is
2222 // the default theme configuration: automatic mode with palette inheritance on.
2323 type themeConfigSnapshot struct {
24- mode domain.ThemeMode
25- paletteOff bool
26- accent domain.ThemeAccent
24+ mode domain.ThemeMode
25+ paletteOff bool
26+ accent domain.ThemeAccent
27+ barTransparent bool
2728 }
2829
2930 // runtimeConfig is the immutable derived configuration published by
@@ -96,7 +97,7 @@ func (d *Daemon) ApplyConfig(cfg domain.Config) {
9697 tabs: cfg.Tabs,
9798 ephemeral: cfg.Ephemeral,
9899 scrollback: scrollback,
99- theme: themeConfigSnapshot{mode: cfg.Theme, paletteOff: !cfg.ThemePalette, accent: cfg.ThemeAccent},
100+ theme: themeConfigSnapshot{mode: cfg.Theme, paletteOff: !cfg.ThemePalette, accent: cfg.ThemeAccent, barTransparent: cfg.Bar.Transparent},
100101 }
101102 d.bindings.Store(bindings)
102103 d.config.Store(snapshot)
@@ -359,6 +360,9 @@ func (d *Daemon) resolveAppliedTheme(raw theme.Theme, color terminalcap.ColorCap
359360 config := d.currentThemeConfig()
360361 effective := effectiveThemeForConfig(raw, config)
361362 resolved := theme.ResolveForColor(effective, config.accent, color)
363+ if config.barTransparent {
364+ resolved.Styles = resolved.Styles.WithTransparentBar()
365+ }
362366 return appliedTheme{Raw: resolved.Theme, Resolved: resolved}
363367 }
364368
@@ -21,9 +21,10 @@ var commandCodePattern = regexp.MustCompile(`^[A-Z0-9]{2,3}$`)
21 // themeConfigSnapshot is the theme slice of runtimeConfig. Its zero value is21 // themeConfigSnapshot is the theme slice of runtimeConfig. Its zero value is
22 // the default theme configuration: automatic mode with palette inheritance on.22 // the default theme configuration: automatic mode with palette inheritance on.
23 type themeConfigSnapshot struct {23 type themeConfigSnapshot struct {
24- mode domain.ThemeMode24+ mode domain.ThemeMode
25- paletteOff bool25+ paletteOff bool
26- accent domain.ThemeAccent26+ accent domain.ThemeAccent
27+ barTransparent bool
27 }28 }
28 29
29 // runtimeConfig is the immutable derived configuration published by30 // runtimeConfig is the immutable derived configuration published by
@@ -96,7 +97,7 @@ func (d *Daemon) ApplyConfig(cfg domain.Config) {
96 tabs: cfg.Tabs,97 tabs: cfg.Tabs,
97 ephemeral: cfg.Ephemeral,98 ephemeral: cfg.Ephemeral,
98 scrollback: scrollback,99 scrollback: scrollback,
99- theme: themeConfigSnapshot{mode: cfg.Theme, paletteOff: !cfg.ThemePalette, accent: cfg.ThemeAccent},100+ theme: themeConfigSnapshot{mode: cfg.Theme, paletteOff: !cfg.ThemePalette, accent: cfg.ThemeAccent, barTransparent: cfg.Bar.Transparent},
100 }101 }
101 d.bindings.Store(bindings)102 d.bindings.Store(bindings)
102 d.config.Store(snapshot)103 d.config.Store(snapshot)
@@ -359,6 +360,9 @@ func (d *Daemon) resolveAppliedTheme(raw theme.Theme, color terminalcap.ColorCap
359 config := d.currentThemeConfig()360 config := d.currentThemeConfig()
360 effective := effectiveThemeForConfig(raw, config)361 effective := effectiveThemeForConfig(raw, config)
361 resolved := theme.ResolveForColor(effective, config.accent, color)362 resolved := theme.ResolveForColor(effective, config.accent, color)
363+ if config.barTransparent {
364+ resolved.Styles = resolved.Styles.WithTransparentBar()
365+ }
362 return appliedTheme{Raw: resolved.Theme, Resolved: resolved}366 return appliedTheme{Raw: resolved.Theme, Resolved: resolved}
363 }367 }
364 368
M internal/usecase/theme/styles.go +8 -0
@@ -127,6 +127,14 @@ type Styles struct {
127127 PickerSelectionMuted renderer.Style
128128 }
129129
130+// WithTransparentBar returns s with the top and bottom bar fill on the
131+// terminal's default colors. Tabs, MRU entries, and every other role keep
132+// their theme colors.
133+func (s Styles) WithTransparentBar() Styles {
134+ s.SurfaceBar = renderer.DefaultStyle()
135+ return s
136+}
137+
130138 // NewStyles applies the default automatic policy. New callers that own a
131139 // policy should use Resolve and retain its complete immutable result.
132140 func NewStyles(t Theme) Styles {
@@ -127,6 +127,14 @@ type Styles struct {
127 PickerSelectionMuted renderer.Style127 PickerSelectionMuted renderer.Style
128 }128 }
129 129
130+// WithTransparentBar returns s with the top and bottom bar fill on the
131+// terminal's default colors. Tabs, MRU entries, and every other role keep
132+// their theme colors.
133+func (s Styles) WithTransparentBar() Styles {
134+ s.SurfaceBar = renderer.DefaultStyle()
135+ return s
136+}
137+
130 // NewStyles applies the default automatic policy. New callers that own a138 // NewStyles applies the default automatic policy. New callers that own a
131 // policy should use Resolve and retain its complete immutable result.139 // policy should use Resolve and retain its complete immutable result.
132 func NewStyles(t Theme) Styles {140 func NewStyles(t Theme) Styles {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close