detached 5c7684b

refactor(theme): move chrome style set into theme package

commit
5c7684b8d48a7105a7fe33efafd59d5110623d82
author
brice · · 2026-07-17 07:59 UTC
parent
4ab3b08
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +169 -127
M internal/usecase/daemon/copymode.go +3 -2
@@ -10,6 +10,7 @@ import (
1010 scopy "github.com/bnema/vev/internal/usecase/copy"
1111 "github.com/bnema/vev/internal/usecase/keys"
1212 "github.com/bnema/vev/internal/usecase/mouse"
13+ themeui "github.com/bnema/vev/internal/usecase/theme"
1314 "github.com/bnema/vev/internal/usecase/ui"
1415 "github.com/bnema/vev/internal/usecase/visualsearch"
1516 "github.com/bnema/vev/pkg/renderer"
@@ -415,8 +416,8 @@ func composeCopyClientFrame(mode *scopy.Mode, target domain.Rect, frame renderer
415416 if mode == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 {
416417 return frame, nil
417418 }
418- styles := newThemeStyles(bars.theme)
419- copyFrame := mode.Render(styles.copyStatus, styles.selection)
419+ styles := themeui.NewStyles(bars.theme)
420+ copyFrame := mode.Render(styles.CopyStatus, styles.Selection)
420421 bodyRows := max(copyFrame.Height-1, 0)
421422 for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ {
422423 dstX := max(target.X, 0)
@@ -10,6 +10,7 @@ import (
10 scopy "github.com/bnema/vev/internal/usecase/copy"10 scopy "github.com/bnema/vev/internal/usecase/copy"
11 "github.com/bnema/vev/internal/usecase/keys"11 "github.com/bnema/vev/internal/usecase/keys"
12 "github.com/bnema/vev/internal/usecase/mouse"12 "github.com/bnema/vev/internal/usecase/mouse"
13+ themeui "github.com/bnema/vev/internal/usecase/theme"
13 "github.com/bnema/vev/internal/usecase/ui"14 "github.com/bnema/vev/internal/usecase/ui"
14 "github.com/bnema/vev/internal/usecase/visualsearch"15 "github.com/bnema/vev/internal/usecase/visualsearch"
15 "github.com/bnema/vev/pkg/renderer"16 "github.com/bnema/vev/pkg/renderer"
@@ -415,8 +416,8 @@ func composeCopyClientFrame(mode *scopy.Mode, target domain.Rect, frame renderer
415 if mode == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 {416 if mode == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 {
416 return frame, nil417 return frame, nil
417 }418 }
418- styles := newThemeStyles(bars.theme)419+ styles := themeui.NewStyles(bars.theme)
419- copyFrame := mode.Render(styles.copyStatus, styles.selection)420+ copyFrame := mode.Render(styles.CopyStatus, styles.Selection)
420 bodyRows := max(copyFrame.Height-1, 0)421 bodyRows := max(copyFrame.Height-1, 0)
421 for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ {422 for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ {
422 dstX := max(target.X, 0)423 dstX := max(target.X, 0)
M internal/usecase/daemon/floating_render.go +1 -1
@@ -117,7 +117,7 @@ func composeCapturedFloatingFrame(input floatingComposeInput) (renderer.Frame, [
117117 }
118118 popupChanged := !cache.valid || cache.floatingGeneration != floating.generation || cache.floatingGeometry != geometry
119119 titleChanged := popupChanged || cache.floatingTitleGeneration != floating.titleGeneration
120- drawFloatingBorder(frame, geometry.Bounds, floating.title, newThemeStyles(theme).border)
120+ drawFloatingBorder(frame, geometry.Bounds, floating.title, themeui.NewStyles(theme).Border)
121121 if full || popupChanged {
122122 return frame, []renderer.Damage{renderer.FullRedraw()}
123123 }
@@ -117,7 +117,7 @@ func composeCapturedFloatingFrame(input floatingComposeInput) (renderer.Frame, [
117 }117 }
118 popupChanged := !cache.valid || cache.floatingGeneration != floating.generation || cache.floatingGeometry != geometry118 popupChanged := !cache.valid || cache.floatingGeneration != floating.generation || cache.floatingGeometry != geometry
119 titleChanged := popupChanged || cache.floatingTitleGeneration != floating.titleGeneration119 titleChanged := popupChanged || cache.floatingTitleGeneration != floating.titleGeneration
120- drawFloatingBorder(frame, geometry.Bounds, floating.title, newThemeStyles(theme).border)120+ drawFloatingBorder(frame, geometry.Bounds, floating.title, themeui.NewStyles(theme).Border)
121 if full || popupChanged {121 if full || popupChanged {
122 return frame, []renderer.Damage{renderer.FullRedraw()}122 return frame, []renderer.Damage{renderer.FullRedraw()}
123 }123 }
M internal/usecase/daemon/modal_compose.go +3 -2
@@ -2,12 +2,13 @@ package daemon
22
33 import (
44 "github.com/bnema/vev/internal/domain"
5+ themeui "github.com/bnema/vev/internal/usecase/theme"
56 "github.com/bnema/vev/internal/usecase/ui"
67 "github.com/bnema/vev/pkg/renderer"
78 )
89
9-func composeModalClientFrame(base renderer.Frame, modal ui.Modal, styles themeStyles, contentStyle renderer.Style, renderInner func(domain.Size, ...renderer.Style) renderer.Frame) (renderer.Frame, []renderer.Damage) {
10- inner := modal.Composite(base, styles.border)
10+func composeModalClientFrame(base renderer.Frame, modal ui.Modal, styles themeui.Styles, contentStyle renderer.Style, renderInner func(domain.Size, ...renderer.Style) renderer.Frame) (renderer.Frame, []renderer.Damage) {
11+ inner := modal.Composite(base, styles.Border)
1112 modalFrame := renderInner(domain.Size{Cols: inner.Width, Rows: inner.Height}, contentStyle)
1213 for y := range min(inner.Height, modalFrame.Height) {
1314 for x := range min(inner.Width, modalFrame.Width) {
@@ -2,12 +2,13 @@ package daemon
2 2
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5+ themeui "github.com/bnema/vev/internal/usecase/theme"
5 "github.com/bnema/vev/internal/usecase/ui"6 "github.com/bnema/vev/internal/usecase/ui"
6 "github.com/bnema/vev/pkg/renderer"7 "github.com/bnema/vev/pkg/renderer"
7 )8 )
8 9
9-func composeModalClientFrame(base renderer.Frame, modal ui.Modal, styles themeStyles, contentStyle renderer.Style, renderInner func(domain.Size, ...renderer.Style) renderer.Frame) (renderer.Frame, []renderer.Damage) {10+func composeModalClientFrame(base renderer.Frame, modal ui.Modal, styles themeui.Styles, contentStyle renderer.Style, renderInner func(domain.Size, ...renderer.Style) renderer.Frame) (renderer.Frame, []renderer.Damage) {
10- inner := modal.Composite(base, styles.border)11+ inner := modal.Composite(base, styles.Border)
11 modalFrame := renderInner(domain.Size{Cols: inner.Width, Rows: inner.Height}, contentStyle)12 modalFrame := renderInner(domain.Size{Cols: inner.Width, Rows: inner.Height}, contentStyle)
12 for y := range min(inner.Height, modalFrame.Height) {13 for y := range min(inner.Height, modalFrame.Height) {
13 for x := range min(inner.Width, modalFrame.Width) {14 for x := range min(inner.Width, modalFrame.Width) {
M internal/usecase/daemon/palette.go +5 -4
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/vev/internal/usecase/layout"
1010 "github.com/bnema/vev/internal/usecase/palette"
1111 "github.com/bnema/vev/internal/usecase/picker"
12+ themeui "github.com/bnema/vev/internal/usecase/theme"
1213 "github.com/bnema/vev/internal/usecase/ui"
1314 "github.com/bnema/vev/pkg/renderer"
1415 )
@@ -496,10 +497,10 @@ func (e paletteExec) JumpRecentSession(rank int) error {
496497 return nil
497498 }
498499
499-func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeStyles) (renderer.Frame, []renderer.Damage) {
500- styleSet := resolveThemeStyles(styles)
500+func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) {
501+ styleSet := resolveStyles(styles)
501502 modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg)
502- return composeModalClientFrame(base, modal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame {
503- return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Selection: styleSet.selection, Description: styleSet.paletteDesc}, Guidance: guidance})
503+ return composeModalClientFrame(base, modal, styleSet, styleSet.Selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame {
504+ return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Selection: styleSet.Selection, Description: styleSet.PaletteDesc}, Guidance: guidance})
504505 })
505506 }
@@ -9,6 +9,7 @@ import (
9 "github.com/bnema/vev/internal/usecase/layout"9 "github.com/bnema/vev/internal/usecase/layout"
10 "github.com/bnema/vev/internal/usecase/palette"10 "github.com/bnema/vev/internal/usecase/palette"
11 "github.com/bnema/vev/internal/usecase/picker"11 "github.com/bnema/vev/internal/usecase/picker"
12+ themeui "github.com/bnema/vev/internal/usecase/theme"
12 "github.com/bnema/vev/internal/usecase/ui"13 "github.com/bnema/vev/internal/usecase/ui"
13 "github.com/bnema/vev/pkg/renderer"14 "github.com/bnema/vev/pkg/renderer"
14 )15 )
@@ -496,10 +497,10 @@ func (e paletteExec) JumpRecentSession(rank int) error {
496 return nil497 return nil
497 }498 }
498 499
499-func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeStyles) (renderer.Frame, []renderer.Damage) {500+func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) {
500- styleSet := resolveThemeStyles(styles)501+ styleSet := resolveStyles(styles)
501 modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg)502 modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg)
502- return composeModalClientFrame(base, modal, styleSet, styleSet.selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame {503+ return composeModalClientFrame(base, modal, styleSet, styleSet.Selection, func(size domain.Size, _ ...renderer.Style) renderer.Frame {
503- return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Selection: styleSet.selection, Description: styleSet.paletteDesc}, Guidance: guidance})504+ return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Selection: styleSet.Selection, Description: styleSet.PaletteDesc}, Guidance: guidance})
504 })505 })
505 }506 }
M internal/usecase/daemon/render.go +2 -50
@@ -357,59 +357,11 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool, lease *att
357357 d.emitFrame(sess, ac, state, composed, &marks)
358358 }
359359
360-type themeStyles struct {
361- statusBar renderer.Style
362- accent renderer.Style
363- border renderer.Style
364- selection renderer.Style
365- copyStatus renderer.Style
366- paletteDesc renderer.Style
367-
368- // Top bar tab label segments: name (bold) and parenthesized pane-title
369- // (muted), one pair per base style. No-ops to statusBar/accent on
370- // non-truecolor themes.
371- tabName renderer.Style
372- tabNameActive renderer.Style
373- tabTitle renderer.Style
374- tabTitleActive renderer.Style
375-
376- // Session picker row segments. Detail reuses paletteDesc (both are
377- // themeui.MutedTextStyle(t)); pickerName/pickerSelection* are picker-only.
378- pickerName renderer.Style
379- pickerSelectionName renderer.Style
380- pickerSelectionMuted renderer.Style
381- pickerSeparator renderer.Style
382-}
383-
384-func newThemeStyles(t themeui.Theme) themeStyles {
385- statusBar := themeui.StatusBarStyle(t)
386- accent := themeui.AccentStyle(t)
387- selection := themeui.SelectionStyle(t)
388- return themeStyles{
389- statusBar: statusBar,
390- accent: accent,
391- border: themeui.BorderStyle(t),
392- selection: selection,
393- copyStatus: selection,
394- paletteDesc: themeui.MutedTextStyle(t),
395-
396- tabName: themeui.EmphasisStyle(statusBar, t),
397- tabNameActive: themeui.EmphasisStyle(accent, t),
398- tabTitle: themeui.MutedVariantStyle(statusBar, t),
399- tabTitleActive: themeui.MutedVariantStyle(accent, t),
400-
401- pickerName: themeui.EmphasisStyle(renderer.DefaultStyle(), t),
402- pickerSelectionName: themeui.EmphasisStyle(selection, t),
403- pickerSelectionMuted: themeui.MutedVariantStyle(selection, t),
404- pickerSeparator: themeui.MutedTextStyle(t),
405- }
406-}
407-
408-func resolveThemeStyles(styles []themeStyles) themeStyles {
360+func resolveStyles(styles []themeui.Styles) themeui.Styles {
409361 if len(styles) > 0 {
410362 return styles[0]
411363 }
412- return newThemeStyles(themeui.Theme{})
364+ return themeui.NewStyles(themeui.Theme{})
413365 }
414366
415367 type barCache struct {
@@ -357,59 +357,11 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool, lease *att
357 d.emitFrame(sess, ac, state, composed, &marks)357 d.emitFrame(sess, ac, state, composed, &marks)
358 }358 }
359 359
360-type themeStyles struct {360+func resolveStyles(styles []themeui.Styles) themeui.Styles {
361- statusBar renderer.Style
362- accent renderer.Style
363- border renderer.Style
364- selection renderer.Style
365- copyStatus renderer.Style
366- paletteDesc renderer.Style
367-
368- // Top bar tab label segments: name (bold) and parenthesized pane-title
369- // (muted), one pair per base style. No-ops to statusBar/accent on
370- // non-truecolor themes.
371- tabName renderer.Style
372- tabNameActive renderer.Style
373- tabTitle renderer.Style
374- tabTitleActive renderer.Style
375-
376- // Session picker row segments. Detail reuses paletteDesc (both are
377- // themeui.MutedTextStyle(t)); pickerName/pickerSelection* are picker-only.
378- pickerName renderer.Style
379- pickerSelectionName renderer.Style
380- pickerSelectionMuted renderer.Style
381- pickerSeparator renderer.Style
382-}
383-
384-func newThemeStyles(t themeui.Theme) themeStyles {
385- statusBar := themeui.StatusBarStyle(t)
386- accent := themeui.AccentStyle(t)
387- selection := themeui.SelectionStyle(t)
388- return themeStyles{
389- statusBar: statusBar,
390- accent: accent,
391- border: themeui.BorderStyle(t),
392- selection: selection,
393- copyStatus: selection,
394- paletteDesc: themeui.MutedTextStyle(t),
395-
396- tabName: themeui.EmphasisStyle(statusBar, t),
397- tabNameActive: themeui.EmphasisStyle(accent, t),
398- tabTitle: themeui.MutedVariantStyle(statusBar, t),
399- tabTitleActive: themeui.MutedVariantStyle(accent, t),
400-
401- pickerName: themeui.EmphasisStyle(renderer.DefaultStyle(), t),
402- pickerSelectionName: themeui.EmphasisStyle(selection, t),
403- pickerSelectionMuted: themeui.MutedVariantStyle(selection, t),
404- pickerSeparator: themeui.MutedTextStyle(t),
405- }
406-}
407-
408-func resolveThemeStyles(styles []themeStyles) themeStyles {
409 if len(styles) > 0 {361 if len(styles) > 0 {
410 return styles[0]362 return styles[0]
411 }363 }
412- return newThemeStyles(themeui.Theme{})364+ return themeui.NewStyles(themeui.Theme{})
413 }365 }
414 366
415 type barCache struct {367 type barCache struct {
M internal/usecase/daemon/render_pipeline.go +11 -11
@@ -77,14 +77,14 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...
7777 copy(frame.Row(y), in.frame.Row(y))
7878 }
7979 }
80- styles := newThemeStyles(state.theme)
80+ styles := themeui.NewStyles(state.theme)
8181 defaultDimmer := themeui.NewDimmer(state.theme)
8282 inactivePaneDimmer := themeui.NewDimmer(state.theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))
8383 drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles)
8484 drawStatusBarState(frame.Row(rows+1), state.bars, styles)
8585 content := domain.Rect{Y: 1, Width: width, Height: rows}
8686 if state.layout.valid && state.layout.root != nil {
87- drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.border))
87+ drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.Border))
8888 }
8989
9090 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible
@@ -157,10 +157,10 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...
157157 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}
158158 }
159159
160-func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeStyles, dimmer themeui.Dimmer) {
161- style := styles.border
160+func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeui.Styles, dimmer themeui.Dimmer) {
161+ style := styles.Border
162162 if focused {
163- style = styles.statusBar
163+ style = styles.StatusBar
164164 } else {
165165 style = dimmer.Dim(style)
166166 }
@@ -191,15 +191,15 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
191191 if snap.copyPane != nil {
192192 o.copyPaneID = snap.copyPane.id
193193 }
194- styles := newThemeStyles(state.theme)
194+ styles := themeui.NewStyles(state.theme)
195195 size := domain.Size{Cols: state.layout.area.Width, Rows: state.layout.area.Height + 2}
196196 if snap.copySearchModel != nil {
197197 o.copySearch.modal = copySearchModal
198- o.copySearch.inner = snap.copySearchModel.Render(rectSize(copySearchModal.Inner(size)), styles.selection)
198+ o.copySearch.inner = snap.copySearchModel.Render(rectSize(copySearchModal.Inner(size)), styles.Selection)
199199 }
200200 if snap.pickerActive && snap.pickerModel != nil {
201201 o.picker.modal = pickerModal
202- renderStyles := picker.RenderStyles{Selection: styles.selection, SelectionName: styles.pickerSelectionName, SelectionMuted: styles.pickerSelectionMuted, Name: styles.pickerName, Detail: styles.paletteDesc, Base: renderer.DefaultStyle(), Separator: styles.pickerSeparator}
202+ renderStyles := picker.RenderStyles{Selection: styles.Selection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PaletteDesc, Base: renderer.DefaultStyle(), Separator: styles.PickerSeparator}
203203 o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles)
204204 }
205205 if snap.paletteActive && snap.paletteModel != nil {
@@ -209,11 +209,11 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
209209 guidance = snap.paletteHints.Feedback
210210 }
211211 o.paletteGuidance = snap.paletteFeedback
212- o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Selection: styles.selection, Description: styles.paletteDesc}, Guidance: guidance, Feedback: snap.paletteFeedback})
212+ o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Selection: styles.Selection, Description: styles.PaletteDesc}, Guidance: guidance, Feedback: snap.paletteFeedback})
213213 }
214214 if snap.promptActive && snap.promptModel != nil {
215215 o.prompt.modal = promptModalFor(snap.promptModel.Title())
216- o.prompt.inner = snap.promptModel.Render(rectSize(o.prompt.modal.Inner(size)), styles.accent)
216+ o.prompt.inner = snap.promptModel.Render(rectSize(o.prompt.modal.Inner(size)), styles.Accent)
217217 }
218218 state.cursor.hiddenByOverlay = o.active()
219219 }
@@ -248,7 +248,7 @@ func composeCapturedOverlays(state capturedRenderState, frame renderer.Frame, da
248248 if modal.inner.Width == 0 && modal.inner.Height == 0 {
249249 continue
250250 }
251- inner := modal.modal.Composite(frame, newThemeStyles(state.theme).border)
251+ inner := modal.modal.Composite(frame, themeui.NewStyles(state.theme).Border)
252252 for y := range min(inner.Height, modal.inner.Height) {
253253 copy(frame.Row(inner.Y + y)[inner.X:inner.X+min(inner.Width, modal.inner.Width)], modal.inner.Row(y)[:min(inner.Width, modal.inner.Width)])
254254 }
@@ -77,14 +77,14 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...
77 copy(frame.Row(y), in.frame.Row(y))77 copy(frame.Row(y), in.frame.Row(y))
78 }78 }
79 }79 }
80- styles := newThemeStyles(state.theme)80+ styles := themeui.NewStyles(state.theme)
81 defaultDimmer := themeui.NewDimmer(state.theme)81 defaultDimmer := themeui.NewDimmer(state.theme)
82 inactivePaneDimmer := themeui.NewDimmer(state.theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))82 inactivePaneDimmer := themeui.NewDimmer(state.theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))
83 drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles)83 drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles)
84 drawStatusBarState(frame.Row(rows+1), state.bars, styles)84 drawStatusBarState(frame.Row(rows+1), state.bars, styles)
85 content := domain.Rect{Y: 1, Width: width, Height: rows}85 content := domain.Rect{Y: 1, Width: width, Height: rows}
86 if state.layout.valid && state.layout.root != nil {86 if state.layout.valid && state.layout.root != nil {
87- drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.border))87+ drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.Border))
88 }88 }
89 89
90 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible90 full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible
@@ -157,10 +157,10 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...
157 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}157 return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()}
158 }158 }
159 159
160-func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeStyles, dimmer themeui.Dimmer) {160+func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeui.Styles, dimmer themeui.Dimmer) {
161- style := styles.border161+ style := styles.Border
162 if focused {162 if focused {
163- style = styles.statusBar163+ style = styles.StatusBar
164 } else {164 } else {
165 style = dimmer.Dim(style)165 style = dimmer.Dim(style)
166 }166 }
@@ -191,15 +191,15 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
191 if snap.copyPane != nil {191 if snap.copyPane != nil {
192 o.copyPaneID = snap.copyPane.id192 o.copyPaneID = snap.copyPane.id
193 }193 }
194- styles := newThemeStyles(state.theme)194+ styles := themeui.NewStyles(state.theme)
195 size := domain.Size{Cols: state.layout.area.Width, Rows: state.layout.area.Height + 2}195 size := domain.Size{Cols: state.layout.area.Width, Rows: state.layout.area.Height + 2}
196 if snap.copySearchModel != nil {196 if snap.copySearchModel != nil {
197 o.copySearch.modal = copySearchModal197 o.copySearch.modal = copySearchModal
198- o.copySearch.inner = snap.copySearchModel.Render(rectSize(copySearchModal.Inner(size)), styles.selection)198+ o.copySearch.inner = snap.copySearchModel.Render(rectSize(copySearchModal.Inner(size)), styles.Selection)
199 }199 }
200 if snap.pickerActive && snap.pickerModel != nil {200 if snap.pickerActive && snap.pickerModel != nil {
201 o.picker.modal = pickerModal201 o.picker.modal = pickerModal
202- renderStyles := picker.RenderStyles{Selection: styles.selection, SelectionName: styles.pickerSelectionName, SelectionMuted: styles.pickerSelectionMuted, Name: styles.pickerName, Detail: styles.paletteDesc, Base: renderer.DefaultStyle(), Separator: styles.pickerSeparator}202+ renderStyles := picker.RenderStyles{Selection: styles.Selection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PaletteDesc, Base: renderer.DefaultStyle(), Separator: styles.PickerSeparator}
203 o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles)203 o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles)
204 }204 }
205 if snap.paletteActive && snap.paletteModel != nil {205 if snap.paletteActive && snap.paletteModel != nil {
@@ -209,11 +209,11 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
209 guidance = snap.paletteHints.Feedback209 guidance = snap.paletteHints.Feedback
210 }210 }
211 o.paletteGuidance = snap.paletteFeedback211 o.paletteGuidance = snap.paletteFeedback
212- o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Selection: styles.selection, Description: styles.paletteDesc}, Guidance: guidance, Feedback: snap.paletteFeedback})212+ o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Selection: styles.Selection, Description: styles.PaletteDesc}, Guidance: guidance, Feedback: snap.paletteFeedback})
213 }213 }
214 if snap.promptActive && snap.promptModel != nil {214 if snap.promptActive && snap.promptModel != nil {
215 o.prompt.modal = promptModalFor(snap.promptModel.Title())215 o.prompt.modal = promptModalFor(snap.promptModel.Title())
216- o.prompt.inner = snap.promptModel.Render(rectSize(o.prompt.modal.Inner(size)), styles.accent)216+ o.prompt.inner = snap.promptModel.Render(rectSize(o.prompt.modal.Inner(size)), styles.Accent)
217 }217 }
218 state.cursor.hiddenByOverlay = o.active()218 state.cursor.hiddenByOverlay = o.active()
219 }219 }
@@ -248,7 +248,7 @@ func composeCapturedOverlays(state capturedRenderState, frame renderer.Frame, da
248 if modal.inner.Width == 0 && modal.inner.Height == 0 {248 if modal.inner.Width == 0 && modal.inner.Height == 0 {
249 continue249 continue
250 }250 }
251- inner := modal.modal.Composite(frame, newThemeStyles(state.theme).border)251+ inner := modal.modal.Composite(frame, themeui.NewStyles(state.theme).Border)
252 for y := range min(inner.Height, modal.inner.Height) {252 for y := range min(inner.Height, modal.inner.Height) {
253 copy(frame.Row(inner.Y + y)[inner.X:inner.X+min(inner.Width, modal.inner.Width)], modal.inner.Row(y)[:min(inner.Width, modal.inner.Width)])253 copy(frame.Row(inner.Y + y)[inner.X:inner.X+min(inner.Width, modal.inner.Width)], modal.inner.Row(y)[:min(inner.Width, modal.inner.Width)])
254 }254 }
M internal/usecase/daemon/status.go +15 -15
@@ -51,18 +51,18 @@ func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) {
5151 return style, true
5252 }
5353
54-func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, topRight string, styles themeStyles) {
54+func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, topRight string, styles themeui.Styles) {
5555 clearStatusRow(row)
5656 x := 0
5757 labels := fitTabLabels(status.tabs, len(row), topRight)
5858 for i, w := range status.tabs {
59- baseStyle := styles.statusBar
60- nameStyle := styles.tabName
61- titleStyle := styles.tabTitle
59+ baseStyle := styles.StatusBar
60+ nameStyle := styles.TabName
61+ titleStyle := styles.TabTitle
6262 if w.active {
63- baseStyle = styles.accent
64- nameStyle = styles.tabNameActive
65- titleStyle = styles.tabTitleActive
63+ baseStyle = styles.Accent
64+ nameStyle = styles.TabNameActive
65+ titleStyle = styles.TabTitleActive
6666 }
6767 label := labels[i]
6868 writeStatusText(row, &x, " "+label.text[:label.nameLen], nameStyle)
@@ -73,30 +73,30 @@ func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, t
7373 writeStatusText(row, &x, label.text[label.nameLen:], titleStyle)
7474 writeStatusText(row, &x, " ", baseStyle)
7575 }
76- drawRightPlainText(row, topRight, x, styles.statusBar)
76+ drawRightPlainText(row, topRight, x, styles.StatusBar)
7777 }
7878
79-func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles) {
79+func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styles) {
8080 clearStatusRow(row)
8181 x := 0
8282 rightText := composeBottomRightText(state.bottomRight, state.copyFeedback)
83- writeStatusText(row, &x, " "+state.status.session+" ", styles.accent)
83+ writeStatusText(row, &x, " "+state.status.session+" ", styles.Accent)
8484 if state.rankedRecent != nil {
8585 for _, sess := range fitRankedRecent(state.rankedRecent, len(row), x, rightText) {
86- style := styles.statusBar // contextual ranks deliberately do not fade.
86+ style := styles.StatusBar // contextual ranks deliberately do not fade.
8787 if sess.selected {
88- style = styles.accent
88+ style = styles.Accent
8989 }
9090 drawRankedStatusSessionEntry(row, &x, sess, style, state.attentionFrame)
9191 }
9292 } else {
9393 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
9494 for i, sess := range fittedMRU {
95- style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))
95+ style := mruStyle(styles.StatusBar, state.theme, i, len(fittedMRU))
9696 drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)
9797 }
9898 }
99- drawRightPlainText(row, rightText, x, styles.statusBar)
99+ drawRightPlainText(row, rightText, x, styles.StatusBar)
100100 }
101101
102102 type rankedRecent struct {
@@ -170,7 +170,7 @@ type barState struct {
170170 attentionFrame int
171171 // theme is the client's terminal theme, if reported. Its zero value
172172 // (Theme{}, Known: false) is a valid "no theme" default that resolves to
173- // the pre-theme fallback styles (see newThemeStyles / theme.usable).
173+ // the pre-theme fallback styles (see themeui.NewStyles / theme.usable).
174174 theme themeui.Theme
175175 }
176176
@@ -51,18 +51,18 @@ func pulseStyle(frame int, base renderer.Style) (renderer.Style, bool) {
51 return style, true51 return style, true
52 }52 }
53 53
54-func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, topRight string, styles themeStyles) {54+func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, topRight string, styles themeui.Styles) {
55 clearStatusRow(row)55 clearStatusRow(row)
56 x := 056 x := 0
57 labels := fitTabLabels(status.tabs, len(row), topRight)57 labels := fitTabLabels(status.tabs, len(row), topRight)
58 for i, w := range status.tabs {58 for i, w := range status.tabs {
59- baseStyle := styles.statusBar59+ baseStyle := styles.StatusBar
60- nameStyle := styles.tabName60+ nameStyle := styles.TabName
61- titleStyle := styles.tabTitle61+ titleStyle := styles.TabTitle
62 if w.active {62 if w.active {
63- baseStyle = styles.accent63+ baseStyle = styles.Accent
64- nameStyle = styles.tabNameActive64+ nameStyle = styles.TabNameActive
65- titleStyle = styles.tabTitleActive65+ titleStyle = styles.TabTitleActive
66 }66 }
67 label := labels[i]67 label := labels[i]
68 writeStatusText(row, &x, " "+label.text[:label.nameLen], nameStyle)68 writeStatusText(row, &x, " "+label.text[:label.nameLen], nameStyle)
@@ -73,30 +73,30 @@ func drawTopBarSnapshot(row []renderer.Cell, status statusSnapshot, frame int, t
73 writeStatusText(row, &x, label.text[label.nameLen:], titleStyle)73 writeStatusText(row, &x, label.text[label.nameLen:], titleStyle)
74 writeStatusText(row, &x, " ", baseStyle)74 writeStatusText(row, &x, " ", baseStyle)
75 }75 }
76- drawRightPlainText(row, topRight, x, styles.statusBar)76+ drawRightPlainText(row, topRight, x, styles.StatusBar)
77 }77 }
78 78
79-func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles) {79+func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styles) {
80 clearStatusRow(row)80 clearStatusRow(row)
81 x := 081 x := 0
82 rightText := composeBottomRightText(state.bottomRight, state.copyFeedback)82 rightText := composeBottomRightText(state.bottomRight, state.copyFeedback)
83- writeStatusText(row, &x, " "+state.status.session+" ", styles.accent)83+ writeStatusText(row, &x, " "+state.status.session+" ", styles.Accent)
84 if state.rankedRecent != nil {84 if state.rankedRecent != nil {
85 for _, sess := range fitRankedRecent(state.rankedRecent, len(row), x, rightText) {85 for _, sess := range fitRankedRecent(state.rankedRecent, len(row), x, rightText) {
86- style := styles.statusBar // contextual ranks deliberately do not fade.86+ style := styles.StatusBar // contextual ranks deliberately do not fade.
87 if sess.selected {87 if sess.selected {
88- style = styles.accent88+ style = styles.Accent
89 }89 }
90 drawRankedStatusSessionEntry(row, &x, sess, style, state.attentionFrame)90 drawRankedStatusSessionEntry(row, &x, sess, style, state.attentionFrame)
91 }91 }
92 } else {92 } else {
93 fittedMRU := fitMRU(state.mru, len(row), x, rightText)93 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
94 for i, sess := range fittedMRU {94 for i, sess := range fittedMRU {
95- style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))95+ style := mruStyle(styles.StatusBar, state.theme, i, len(fittedMRU))
96 drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)96 drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)
97 }97 }
98 }98 }
99- drawRightPlainText(row, rightText, x, styles.statusBar)99+ drawRightPlainText(row, rightText, x, styles.StatusBar)
100 }100 }
101 101
102 type rankedRecent struct {102 type rankedRecent struct {
@@ -170,7 +170,7 @@ type barState struct {
170 attentionFrame int170 attentionFrame int
171 // theme is the client's terminal theme, if reported. Its zero value171 // theme is the client's terminal theme, if reported. Its zero value
172 // (Theme{}, Known: false) is a valid "no theme" default that resolves to172 // (Theme{}, Known: false) is a valid "no theme" default that resolves to
173- // the pre-theme fallback styles (see newThemeStyles / theme.usable).173+ // the pre-theme fallback styles (see themeui.NewStyles / theme.usable).
174 theme themeui.Theme174 theme themeui.Theme
175 }175 }
176 176
M internal/usecase/daemon/status_test.go +42 -42
@@ -267,9 +267,9 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
267267 Known: true,
268268 TrueColor: true,
269269 }
270- styles := newThemeStyles(theme)
271- require.False(t, styles.tabTitle.Equal(styles.statusBar), "sanity: muted title style must differ from the base style on a truecolor theme")
272- require.False(t, styles.tabTitleActive.Equal(styles.accent), "sanity: muted active-title style must differ from the base accent style")
270+ styles := themeui.NewStyles(theme)
271+ require.False(t, styles.TabTitle.Equal(styles.StatusBar), "sanity: muted title style must differ from the base style on a truecolor theme")
272+ require.False(t, styles.TabTitleActive.Equal(styles.Accent), "sanity: muted active-title style must differ from the base accent style")
273273
274274 labels := fitTabLabels(tabs, rowLen, "")
275275
@@ -286,17 +286,17 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
286286 nameEnd := x + 1 + labels[0].nameLen
287287 for i := x; i < nameEnd; i++ {
288288 require.True(t, row[i].Style.Bold, "cell %d: name segment must be bold", i)
289- require.True(t, row[i].Style.Equal(styles.tabName), "cell %d: name segment style mismatch", i)
289+ require.True(t, row[i].Style.Equal(styles.TabName), "cell %d: name segment style mismatch", i)
290290 }
291291 x = nameEnd
292292 titleLen := len(labels[0].text) - labels[0].nameLen
293293 titleEnd := x + titleLen
294294 for i := x; i < titleEnd; i++ {
295295 require.False(t, row[i].Style.Bold, "cell %d: title segment must not be bold", i)
296- require.True(t, row[i].Style.Equal(styles.tabTitle), "cell %d: title segment style mismatch", i)
296+ require.True(t, row[i].Style.Equal(styles.TabTitle), "cell %d: title segment style mismatch", i)
297297 }
298298 x = titleEnd
299- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 0 trailing space must keep the base style", x)
299+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 0 trailing space must keep the base style", x)
300300 x++
301301
302302 // tab 1: active, attention, no title: name, attention space+bell (both
@@ -304,16 +304,16 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
304304 nameEnd = x + 1 + labels[1].nameLen
305305 for i := x; i < nameEnd; i++ {
306306 require.True(t, row[i].Style.Bold, "cell %d: active name segment must be bold", i)
307- require.True(t, row[i].Style.Equal(styles.tabNameActive), "cell %d: active name segment style mismatch", i)
307+ require.True(t, row[i].Style.Equal(styles.TabNameActive), "cell %d: active name segment style mismatch", i)
308308 }
309309 require.Equal(t, len(labels[1].text), labels[1].nameLen, "tab 1 has no pane title, so its whole label is the name segment")
310310 x = nameEnd
311- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 attention leading space must keep the active base style", x)
311+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 attention leading space must keep the active base style", x)
312312 x++
313313 tab1Bell := x
314- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 blank bell must keep the active base style, not DefaultStyle", x)
314+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 blank bell must keep the active base style, not DefaultStyle", x)
315315 x++
316- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 trailing space must keep the active base style", x)
316+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 trailing space must keep the active base style", x)
317317 x++
318318
319319 // tab 2: inactive, attention, with title: name, attention space+bell
@@ -321,22 +321,22 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
321321 nameEnd = x + 1 + labels[2].nameLen
322322 for i := x; i < nameEnd; i++ {
323323 require.True(t, row[i].Style.Bold, "cell %d: tab 2 name segment must be bold", i)
324- require.True(t, row[i].Style.Equal(styles.tabName), "cell %d: tab 2 name segment style mismatch", i)
324+ require.True(t, row[i].Style.Equal(styles.TabName), "cell %d: tab 2 name segment style mismatch", i)
325325 }
326326 x = nameEnd
327- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 attention leading space must keep the base style", x)
327+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 attention leading space must keep the base style", x)
328328 x++
329329 tab2Bell := x
330- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 blank bell must keep the base style, not DefaultStyle", x)
330+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 blank bell must keep the base style, not DefaultStyle", x)
331331 x++
332332 titleLen = len(labels[2].text) - labels[2].nameLen
333333 titleEnd = x + titleLen
334334 for i := x; i < titleEnd; i++ {
335335 require.False(t, row[i].Style.Bold, "cell %d: tab 2 title segment must not be bold", i)
336- require.True(t, row[i].Style.Equal(styles.tabTitle), "cell %d: tab 2 title segment style mismatch", i)
336+ require.True(t, row[i].Style.Equal(styles.TabTitle), "cell %d: tab 2 title segment style mismatch", i)
337337 }
338338 x = titleEnd
339- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 trailing space must keep the base style", x)
339+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 trailing space must keep the base style", x)
340340
341341 // On a visible pulse frame, the bell glyph itself must keep the same
342342 // background as its tab's base style.
@@ -344,18 +344,18 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
344344 drawTopBarSnapshot(visibleRow, status, 1, "", styles)
345345 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune)
346346 require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB)
347- require.Equal(t, styles.accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background")
347+ require.Equal(t, styles.Accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background")
348348 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab2Bell].Rune)
349349 require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB)
350- require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background")
350+ require.Equal(t, styles.StatusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background")
351351 })
352352
353353 t.Run("non-usable theme matches the pre-change fallback styles", func(t *testing.T) {
354- styles := resolveThemeStyles(nil)
355- require.True(t, styles.tabName.Equal(styles.statusBar))
356- require.True(t, styles.tabTitle.Equal(styles.statusBar))
357- require.True(t, styles.tabNameActive.Equal(styles.accent))
358- require.True(t, styles.tabTitleActive.Equal(styles.accent))
354+ styles := resolveStyles(nil)
355+ require.True(t, styles.TabName.Equal(styles.StatusBar))
356+ require.True(t, styles.TabTitle.Equal(styles.StatusBar))
357+ require.True(t, styles.TabNameActive.Equal(styles.Accent))
358+ require.True(t, styles.TabTitleActive.Equal(styles.Accent))
359359
360360 row := make([]renderer.Cell, rowLen)
361361 drawTopBarSnapshot(row, status, 0, "", styles)
@@ -371,15 +371,15 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
371371 require.Equal(t, tab0+tab1+tab2+strings.Repeat(" ", rowLen-len(tab0)-len(tab1)-len(tab2)), rowText(row))
372372
373373 for i, c := range row[:len(tab0)] {
374- require.True(t, c.Style.Equal(styles.statusBar), "cell %d should use the plain base style", i)
374+ require.True(t, c.Style.Equal(styles.StatusBar), "cell %d should use the plain base style", i)
375375 }
376376 tab1Start := len(tab0)
377377 for i := tab1Start; i < tab1Start+len(tab1); i++ {
378- require.True(t, row[i].Style.Equal(styles.accent), "cell %d should use the plain accent style, including the blank bell cell", i)
378+ require.True(t, row[i].Style.Equal(styles.Accent), "cell %d should use the plain accent style, including the blank bell cell", i)
379379 }
380380 tab2Start := tab1Start + len(tab1)
381381 for i := tab2Start; i < tab2Start+len(tab2); i++ {
382- require.True(t, row[i].Style.Equal(styles.statusBar), "cell %d should use the plain base style, including the blank bell cell", i)
382+ require.True(t, row[i].Style.Equal(styles.StatusBar), "cell %d should use the plain base style, including the blank bell cell", i)
383383 }
384384 })
385385 }
@@ -847,7 +847,7 @@ func TestTopBarRightAnchor(t *testing.T) {
847847 t.Run(tt.name, func(t *testing.T) {
848848 row := make([]renderer.Cell, tt.width)
849849
850- drawTopBarSnapshot(row, tt.status, 0, tt.topRight, resolveThemeStyles(nil))
850+ drawTopBarSnapshot(row, tt.status, 0, tt.topRight, resolveStyles(nil))
851851
852852 require.Equal(t, tt.want, rowText(row))
853853 if tt.continuationCell > 0 {
@@ -1093,7 +1093,7 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
10931093 draw := func(state capturedRenderState) string {
10941094 t.Helper()
10951095 row := make([]renderer.Cell, 32)
1096- drawStatusBarState(row, state.bars, resolveThemeStyles(nil))
1096+ drawStatusBarState(row, state.bars, resolveStyles(nil))
10971097 return rowText(row)
10981098 }
10991099 normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "vty"}, {name: "misc"}}}
@@ -1139,7 +1139,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
11391139 }
11401140 row := make([]renderer.Cell, 24)
11411141
1142- drawStatusBarState(row, state, resolveThemeStyles(nil))
1142+ drawStatusBarState(row, state, resolveStyles(nil))
11431143
11441144 require.Equal(t, " cur fresh tmp  ", rowText(row))
11451145 for _, c := range row {
@@ -1153,19 +1153,19 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
11531153
11541154 func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) {
11551155 theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, TrueColor: true, Known: true}
1156- styles := newThemeStyles(theme)
1156+ styles := themeui.NewStyles(theme)
11571157 row := make([]renderer.Cell, 16)
11581158
11591159 drawStatusBarState(row, barState{status: statusSnapshot{session: "cur"}, theme: theme}, styles)
11601160
11611161 for _, idx := range []int{0, 1, 2, 3, 4} {
1162- require.True(t, row[idx].Style.Equal(styles.accent), "cell %d should use accent style", idx)
1162+ require.True(t, row[idx].Style.Equal(styles.Accent), "cell %d should use accent style", idx)
11631163 }
1164- require.NotEqual(t, styles.statusBar.BackgroundRGB, styles.accent.BackgroundRGB)
1164+ require.NotEqual(t, styles.StatusBar.BackgroundRGB, styles.Accent.BackgroundRGB)
11651165 }
11661166
11671167 func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testing.T) {
1168- styles := resolveThemeStyles(nil)
1168+ styles := resolveStyles(nil)
11691169 row := make([]renderer.Cell, 18)
11701170 state := barState{
11711171 status: statusSnapshot{session: "cur"},
@@ -1190,8 +1190,8 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi
11901190 if cell.Rune == '2' {
11911191 found = true
11921192 require.True(t, cell.Style.Bold)
1193- require.Equal(t, styles.accent.Foreground, cell.Style.Foreground)
1194- require.Equal(t, styles.accent.Background, cell.Style.Background)
1193+ require.Equal(t, styles.Accent.Foreground, cell.Style.Foreground)
1194+ require.Equal(t, styles.Accent.Background, cell.Style.Background)
11951195 break
11961196 }
11971197 }
@@ -1200,7 +1200,7 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi
12001200
12011201 func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
12021202 theme := themeui.Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 20, G: 20, B: 20}, HasFG: true, HasBG: true, TrueColor: true, Known: true}
1203- styles := newThemeStyles(theme)
1203+ styles := themeui.NewStyles(theme)
12041204 state := barState{status: statusSnapshot{session: "c"}, theme: theme, mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
12051205 row := make([]renderer.Cell, 16)
12061206
@@ -1209,14 +1209,14 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
12091209 firstFG := row[4].Style.ForegroundRGB.R
12101210 secondFG := row[7].Style.ForegroundRGB.R
12111211 thirdFG := row[10].Style.ForegroundRGB.R
1212- require.Equal(t, styles.statusBar.ForegroundRGB.R, firstFG)
1212+ require.Equal(t, styles.StatusBar.ForegroundRGB.R, firstFG)
12131213 require.Greater(t, firstFG, secondFG)
12141214 require.Greater(t, secondFG, thirdFG)
12151215
12161216 firstBG := row[4].Style.BackgroundRGB.R
12171217 secondBG := row[7].Style.BackgroundRGB.R
12181218 thirdBG := row[10].Style.BackgroundRGB.R
1219- require.Equal(t, styles.statusBar.BackgroundRGB.R, firstBG)
1219+ require.Equal(t, styles.StatusBar.BackgroundRGB.R, firstBG)
12201220 require.Greater(t, firstBG, secondBG)
12211221 require.Greater(t, secondBG, thirdBG)
12221222
@@ -1229,7 +1229,7 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
12291229 state := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}}
12301230 row := make([]renderer.Cell, 21)
12311231
1232- drawStatusBarState(row, state, resolveThemeStyles(nil))
1232+ drawStatusBarState(row, state, resolveStyles(nil))
12331233
12341234 text := rowText(row)
12351235 require.Contains(t, text, "fresh")
@@ -1273,7 +1273,7 @@ func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
12731273 t.Run(tt.name, func(t *testing.T) {
12741274 row := make([]renderer.Cell, tt.cols)
12751275
1276- drawStatusBarState(row, state, resolveThemeStyles(nil))
1276+ drawStatusBarState(row, state, resolveStyles(nil))
12771277
12781278 text := rowText(row)
12791279 for _, want := range tt.wantShown {
@@ -1346,7 +1346,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
13461346 t.Run(tt.name, func(t *testing.T) {
13471347 row := make([]renderer.Cell, tt.width)
13481348
1349- drawStatusBarState(row, tt.state, resolveThemeStyles(nil))
1349+ drawStatusBarState(row, tt.state, resolveStyles(nil))
13501350
13511351 text := rowText(row)
13521352 if tt.want != "" {
@@ -1372,7 +1372,7 @@ func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
13721372 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
13731373 row := make([]renderer.Cell, 20)
13741374
1375- drawStatusBarState(row, state, resolveThemeStyles(nil))
1375+ drawStatusBarState(row, state, resolveStyles(nil))
13761376
13771377 text := rowText(row)
13781378 require.Contains(t, text, " a")
@@ -1395,7 +1395,7 @@ func TestStatusBarCopyFeedbackBoundaryWidths(t *testing.T) {
13951395 t.Run(tt.name, func(t *testing.T) {
13961396 row := make([]renderer.Cell, tt.cols)
13971397
1398- drawStatusBarState(row, state, resolveThemeStyles(nil))
1398+ drawStatusBarState(row, state, resolveStyles(nil))
13991399
14001400 require.Equal(t, tt.want, rowText(row))
14011401 })
@@ -267,9 +267,9 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
267 Known: true,267 Known: true,
268 TrueColor: true,268 TrueColor: true,
269 }269 }
270- styles := newThemeStyles(theme)270+ styles := themeui.NewStyles(theme)
271- require.False(t, styles.tabTitle.Equal(styles.statusBar), "sanity: muted title style must differ from the base style on a truecolor theme")271+ require.False(t, styles.TabTitle.Equal(styles.StatusBar), "sanity: muted title style must differ from the base style on a truecolor theme")
272- require.False(t, styles.tabTitleActive.Equal(styles.accent), "sanity: muted active-title style must differ from the base accent style")272+ require.False(t, styles.TabTitleActive.Equal(styles.Accent), "sanity: muted active-title style must differ from the base accent style")
273 273
274 labels := fitTabLabels(tabs, rowLen, "")274 labels := fitTabLabels(tabs, rowLen, "")
275 275
@@ -286,17 +286,17 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
286 nameEnd := x + 1 + labels[0].nameLen286 nameEnd := x + 1 + labels[0].nameLen
287 for i := x; i < nameEnd; i++ {287 for i := x; i < nameEnd; i++ {
288 require.True(t, row[i].Style.Bold, "cell %d: name segment must be bold", i)288 require.True(t, row[i].Style.Bold, "cell %d: name segment must be bold", i)
289- require.True(t, row[i].Style.Equal(styles.tabName), "cell %d: name segment style mismatch", i)289+ require.True(t, row[i].Style.Equal(styles.TabName), "cell %d: name segment style mismatch", i)
290 }290 }
291 x = nameEnd291 x = nameEnd
292 titleLen := len(labels[0].text) - labels[0].nameLen292 titleLen := len(labels[0].text) - labels[0].nameLen
293 titleEnd := x + titleLen293 titleEnd := x + titleLen
294 for i := x; i < titleEnd; i++ {294 for i := x; i < titleEnd; i++ {
295 require.False(t, row[i].Style.Bold, "cell %d: title segment must not be bold", i)295 require.False(t, row[i].Style.Bold, "cell %d: title segment must not be bold", i)
296- require.True(t, row[i].Style.Equal(styles.tabTitle), "cell %d: title segment style mismatch", i)296+ require.True(t, row[i].Style.Equal(styles.TabTitle), "cell %d: title segment style mismatch", i)
297 }297 }
298 x = titleEnd298 x = titleEnd
299- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 0 trailing space must keep the base style", x)299+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 0 trailing space must keep the base style", x)
300 x++300 x++
301 301
302 // tab 1: active, attention, no title: name, attention space+bell (both302 // tab 1: active, attention, no title: name, attention space+bell (both
@@ -304,16 +304,16 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
304 nameEnd = x + 1 + labels[1].nameLen304 nameEnd = x + 1 + labels[1].nameLen
305 for i := x; i < nameEnd; i++ {305 for i := x; i < nameEnd; i++ {
306 require.True(t, row[i].Style.Bold, "cell %d: active name segment must be bold", i)306 require.True(t, row[i].Style.Bold, "cell %d: active name segment must be bold", i)
307- require.True(t, row[i].Style.Equal(styles.tabNameActive), "cell %d: active name segment style mismatch", i)307+ require.True(t, row[i].Style.Equal(styles.TabNameActive), "cell %d: active name segment style mismatch", i)
308 }308 }
309 require.Equal(t, len(labels[1].text), labels[1].nameLen, "tab 1 has no pane title, so its whole label is the name segment")309 require.Equal(t, len(labels[1].text), labels[1].nameLen, "tab 1 has no pane title, so its whole label is the name segment")
310 x = nameEnd310 x = nameEnd
311- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 attention leading space must keep the active base style", x)311+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 attention leading space must keep the active base style", x)
312 x++312 x++
313 tab1Bell := x313 tab1Bell := x
314- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 blank bell must keep the active base style, not DefaultStyle", x)314+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 blank bell must keep the active base style, not DefaultStyle", x)
315 x++315 x++
316- require.True(t, row[x].Style.Equal(styles.accent), "cell %d: tab 1 trailing space must keep the active base style", x)316+ require.True(t, row[x].Style.Equal(styles.Accent), "cell %d: tab 1 trailing space must keep the active base style", x)
317 x++317 x++
318 318
319 // tab 2: inactive, attention, with title: name, attention space+bell319 // tab 2: inactive, attention, with title: name, attention space+bell
@@ -321,22 +321,22 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
321 nameEnd = x + 1 + labels[2].nameLen321 nameEnd = x + 1 + labels[2].nameLen
322 for i := x; i < nameEnd; i++ {322 for i := x; i < nameEnd; i++ {
323 require.True(t, row[i].Style.Bold, "cell %d: tab 2 name segment must be bold", i)323 require.True(t, row[i].Style.Bold, "cell %d: tab 2 name segment must be bold", i)
324- require.True(t, row[i].Style.Equal(styles.tabName), "cell %d: tab 2 name segment style mismatch", i)324+ require.True(t, row[i].Style.Equal(styles.TabName), "cell %d: tab 2 name segment style mismatch", i)
325 }325 }
326 x = nameEnd326 x = nameEnd
327- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 attention leading space must keep the base style", x)327+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 attention leading space must keep the base style", x)
328 x++328 x++
329 tab2Bell := x329 tab2Bell := x
330- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 blank bell must keep the base style, not DefaultStyle", x)330+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 blank bell must keep the base style, not DefaultStyle", x)
331 x++331 x++
332 titleLen = len(labels[2].text) - labels[2].nameLen332 titleLen = len(labels[2].text) - labels[2].nameLen
333 titleEnd = x + titleLen333 titleEnd = x + titleLen
334 for i := x; i < titleEnd; i++ {334 for i := x; i < titleEnd; i++ {
335 require.False(t, row[i].Style.Bold, "cell %d: tab 2 title segment must not be bold", i)335 require.False(t, row[i].Style.Bold, "cell %d: tab 2 title segment must not be bold", i)
336- require.True(t, row[i].Style.Equal(styles.tabTitle), "cell %d: tab 2 title segment style mismatch", i)336+ require.True(t, row[i].Style.Equal(styles.TabTitle), "cell %d: tab 2 title segment style mismatch", i)
337 }337 }
338 x = titleEnd338 x = titleEnd
339- require.True(t, row[x].Style.Equal(styles.statusBar), "cell %d: tab 2 trailing space must keep the base style", x)339+ require.True(t, row[x].Style.Equal(styles.StatusBar), "cell %d: tab 2 trailing space must keep the base style", x)
340 340
341 // On a visible pulse frame, the bell glyph itself must keep the same341 // On a visible pulse frame, the bell glyph itself must keep the same
342 // background as its tab's base style.342 // background as its tab's base style.
@@ -344,18 +344,18 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
344 drawTopBarSnapshot(visibleRow, status, 1, "", styles)344 drawTopBarSnapshot(visibleRow, status, 1, "", styles)
345 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune)345 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab1Bell].Rune)
346 require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB)346 require.True(t, visibleRow[tab1Bell].Style.HasBackgroundRGB)
347- require.Equal(t, styles.accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background")347+ require.Equal(t, styles.Accent.BackgroundRGB, visibleRow[tab1Bell].Style.BackgroundRGB, "tab 1's visible bell must keep the active base background")
348 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab2Bell].Rune)348 require.Equal(t, rune(ui.AttentionGlyph), visibleRow[tab2Bell].Rune)
349 require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB)349 require.True(t, visibleRow[tab2Bell].Style.HasBackgroundRGB)
350- require.Equal(t, styles.statusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background")350+ require.Equal(t, styles.StatusBar.BackgroundRGB, visibleRow[tab2Bell].Style.BackgroundRGB, "tab 2's visible bell must keep the plain base background")
351 })351 })
352 352
353 t.Run("non-usable theme matches the pre-change fallback styles", func(t *testing.T) {353 t.Run("non-usable theme matches the pre-change fallback styles", func(t *testing.T) {
354- styles := resolveThemeStyles(nil)354+ styles := resolveStyles(nil)
355- require.True(t, styles.tabName.Equal(styles.statusBar))355+ require.True(t, styles.TabName.Equal(styles.StatusBar))
356- require.True(t, styles.tabTitle.Equal(styles.statusBar))356+ require.True(t, styles.TabTitle.Equal(styles.StatusBar))
357- require.True(t, styles.tabNameActive.Equal(styles.accent))357+ require.True(t, styles.TabNameActive.Equal(styles.Accent))
358- require.True(t, styles.tabTitleActive.Equal(styles.accent))358+ require.True(t, styles.TabTitleActive.Equal(styles.Accent))
359 359
360 row := make([]renderer.Cell, rowLen)360 row := make([]renderer.Cell, rowLen)
361 drawTopBarSnapshot(row, status, 0, "", styles)361 drawTopBarSnapshot(row, status, 0, "", styles)
@@ -371,15 +371,15 @@ func TestDrawTopBarSnapshotStylesTabNameAndTitle(t *testing.T) {
371 require.Equal(t, tab0+tab1+tab2+strings.Repeat(" ", rowLen-len(tab0)-len(tab1)-len(tab2)), rowText(row))371 require.Equal(t, tab0+tab1+tab2+strings.Repeat(" ", rowLen-len(tab0)-len(tab1)-len(tab2)), rowText(row))
372 372
373 for i, c := range row[:len(tab0)] {373 for i, c := range row[:len(tab0)] {
374- require.True(t, c.Style.Equal(styles.statusBar), "cell %d should use the plain base style", i)374+ require.True(t, c.Style.Equal(styles.StatusBar), "cell %d should use the plain base style", i)
375 }375 }
376 tab1Start := len(tab0)376 tab1Start := len(tab0)
377 for i := tab1Start; i < tab1Start+len(tab1); i++ {377 for i := tab1Start; i < tab1Start+len(tab1); i++ {
378- require.True(t, row[i].Style.Equal(styles.accent), "cell %d should use the plain accent style, including the blank bell cell", i)378+ require.True(t, row[i].Style.Equal(styles.Accent), "cell %d should use the plain accent style, including the blank bell cell", i)
379 }379 }
380 tab2Start := tab1Start + len(tab1)380 tab2Start := tab1Start + len(tab1)
381 for i := tab2Start; i < tab2Start+len(tab2); i++ {381 for i := tab2Start; i < tab2Start+len(tab2); i++ {
382- require.True(t, row[i].Style.Equal(styles.statusBar), "cell %d should use the plain base style, including the blank bell cell", i)382+ require.True(t, row[i].Style.Equal(styles.StatusBar), "cell %d should use the plain base style, including the blank bell cell", i)
383 }383 }
384 })384 })
385 }385 }
@@ -847,7 +847,7 @@ func TestTopBarRightAnchor(t *testing.T) {
847 t.Run(tt.name, func(t *testing.T) {847 t.Run(tt.name, func(t *testing.T) {
848 row := make([]renderer.Cell, tt.width)848 row := make([]renderer.Cell, tt.width)
849 849
850- drawTopBarSnapshot(row, tt.status, 0, tt.topRight, resolveThemeStyles(nil))850+ drawTopBarSnapshot(row, tt.status, 0, tt.topRight, resolveStyles(nil))
851 851
852 require.Equal(t, tt.want, rowText(row))852 require.Equal(t, tt.want, rowText(row))
853 if tt.continuationCell > 0 {853 if tt.continuationCell > 0 {
@@ -1093,7 +1093,7 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
1093 draw := func(state capturedRenderState) string {1093 draw := func(state capturedRenderState) string {
1094 t.Helper()1094 t.Helper()
1095 row := make([]renderer.Cell, 32)1095 row := make([]renderer.Cell, 32)
1096- drawStatusBarState(row, state.bars, resolveThemeStyles(nil))1096+ drawStatusBarState(row, state.bars, resolveStyles(nil))
1097 return rowText(row)1097 return rowText(row)
1098 }1098 }
1099 normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "vty"}, {name: "misc"}}}1099 normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "vty"}, {name: "misc"}}}
@@ -1139,7 +1139,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
1139 }1139 }
1140 row := make([]renderer.Cell, 24)1140 row := make([]renderer.Cell, 24)
1141 1141
1142- drawStatusBarState(row, state, resolveThemeStyles(nil))1142+ drawStatusBarState(row, state, resolveStyles(nil))
1143 1143
1144 require.Equal(t, " cur fresh tmp  ", rowText(row))1144 require.Equal(t, " cur fresh tmp  ", rowText(row))
1145 for _, c := range row {1145 for _, c := range row {
@@ -1153,19 +1153,19 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
1153 1153
1154 func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) {1154 func TestStatusBarCurrentSessionUsesAccentStyle(t *testing.T) {
1155 theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, TrueColor: true, Known: true}1155 theme := themeui.Theme{Foreground: renderer.RGB{R: 220, G: 220, B: 220}, Background: renderer.RGB{R: 10, G: 10, B: 10}, HasFG: true, HasBG: true, TrueColor: true, Known: true}
1156- styles := newThemeStyles(theme)1156+ styles := themeui.NewStyles(theme)
1157 row := make([]renderer.Cell, 16)1157 row := make([]renderer.Cell, 16)
1158 1158
1159 drawStatusBarState(row, barState{status: statusSnapshot{session: "cur"}, theme: theme}, styles)1159 drawStatusBarState(row, barState{status: statusSnapshot{session: "cur"}, theme: theme}, styles)
1160 1160
1161 for _, idx := range []int{0, 1, 2, 3, 4} {1161 for _, idx := range []int{0, 1, 2, 3, 4} {
1162- require.True(t, row[idx].Style.Equal(styles.accent), "cell %d should use accent style", idx)1162+ require.True(t, row[idx].Style.Equal(styles.Accent), "cell %d should use accent style", idx)
1163 }1163 }
1164- require.NotEqual(t, styles.statusBar.BackgroundRGB, styles.accent.BackgroundRGB)1164+ require.NotEqual(t, styles.StatusBar.BackgroundRGB, styles.Accent.BackgroundRGB)
1165 }1165 }
1166 1166
1167 func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testing.T) {1167 func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testing.T) {
1168- styles := resolveThemeStyles(nil)1168+ styles := resolveStyles(nil)
1169 row := make([]renderer.Cell, 18)1169 row := make([]renderer.Cell, 18)
1170 state := barState{1170 state := barState{
1171 status: statusSnapshot{session: "cur"},1171 status: statusSnapshot{session: "cur"},
@@ -1190,8 +1190,8 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi
1190 if cell.Rune == '2' {1190 if cell.Rune == '2' {
1191 found = true1191 found = true
1192 require.True(t, cell.Style.Bold)1192 require.True(t, cell.Style.Bold)
1193- require.Equal(t, styles.accent.Foreground, cell.Style.Foreground)1193+ require.Equal(t, styles.Accent.Foreground, cell.Style.Foreground)
1194- require.Equal(t, styles.accent.Background, cell.Style.Background)1194+ require.Equal(t, styles.Accent.Background, cell.Style.Background)
1195 break1195 break
1196 }1196 }
1197 }1197 }
@@ -1200,7 +1200,7 @@ func TestStatusBarContextualRanksPreserveOriginalRanksAndSelectedAccent(t *testi
1200 1200
1201 func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {1201 func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
1202 theme := themeui.Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 20, G: 20, B: 20}, HasFG: true, HasBG: true, TrueColor: true, Known: true}1202 theme := themeui.Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 20, G: 20, B: 20}, HasFG: true, HasBG: true, TrueColor: true, Known: true}
1203- styles := newThemeStyles(theme)1203+ styles := themeui.NewStyles(theme)
1204 state := barState{status: statusSnapshot{session: "c"}, theme: theme, mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}1204 state := barState{status: statusSnapshot{session: "c"}, theme: theme, mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
1205 row := make([]renderer.Cell, 16)1205 row := make([]renderer.Cell, 16)
1206 1206
@@ -1209,14 +1209,14 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
1209 firstFG := row[4].Style.ForegroundRGB.R1209 firstFG := row[4].Style.ForegroundRGB.R
1210 secondFG := row[7].Style.ForegroundRGB.R1210 secondFG := row[7].Style.ForegroundRGB.R
1211 thirdFG := row[10].Style.ForegroundRGB.R1211 thirdFG := row[10].Style.ForegroundRGB.R
1212- require.Equal(t, styles.statusBar.ForegroundRGB.R, firstFG)1212+ require.Equal(t, styles.StatusBar.ForegroundRGB.R, firstFG)
1213 require.Greater(t, firstFG, secondFG)1213 require.Greater(t, firstFG, secondFG)
1214 require.Greater(t, secondFG, thirdFG)1214 require.Greater(t, secondFG, thirdFG)
1215 1215
1216 firstBG := row[4].Style.BackgroundRGB.R1216 firstBG := row[4].Style.BackgroundRGB.R
1217 secondBG := row[7].Style.BackgroundRGB.R1217 secondBG := row[7].Style.BackgroundRGB.R
1218 thirdBG := row[10].Style.BackgroundRGB.R1218 thirdBG := row[10].Style.BackgroundRGB.R
1219- require.Equal(t, styles.statusBar.BackgroundRGB.R, firstBG)1219+ require.Equal(t, styles.StatusBar.BackgroundRGB.R, firstBG)
1220 require.Greater(t, firstBG, secondBG)1220 require.Greater(t, firstBG, secondBG)
1221 require.Greater(t, secondBG, thirdBG)1221 require.Greater(t, secondBG, thirdBG)
1222 1222
@@ -1229,7 +1229,7 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
1229 state := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}}1229 state := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}}
1230 row := make([]renderer.Cell, 21)1230 row := make([]renderer.Cell, 21)
1231 1231
1232- drawStatusBarState(row, state, resolveThemeStyles(nil))1232+ drawStatusBarState(row, state, resolveStyles(nil))
1233 1233
1234 text := rowText(row)1234 text := rowText(row)
1235 require.Contains(t, text, "fresh")1235 require.Contains(t, text, "fresh")
@@ -1273,7 +1273,7 @@ func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
1273 t.Run(tt.name, func(t *testing.T) {1273 t.Run(tt.name, func(t *testing.T) {
1274 row := make([]renderer.Cell, tt.cols)1274 row := make([]renderer.Cell, tt.cols)
1275 1275
1276- drawStatusBarState(row, state, resolveThemeStyles(nil))1276+ drawStatusBarState(row, state, resolveStyles(nil))
1277 1277
1278 text := rowText(row)1278 text := rowText(row)
1279 for _, want := range tt.wantShown {1279 for _, want := range tt.wantShown {
@@ -1346,7 +1346,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
1346 t.Run(tt.name, func(t *testing.T) {1346 t.Run(tt.name, func(t *testing.T) {
1347 row := make([]renderer.Cell, tt.width)1347 row := make([]renderer.Cell, tt.width)
1348 1348
1349- drawStatusBarState(row, tt.state, resolveThemeStyles(nil))1349+ drawStatusBarState(row, tt.state, resolveStyles(nil))
1350 1350
1351 text := rowText(row)1351 text := rowText(row)
1352 if tt.want != "" {1352 if tt.want != "" {
@@ -1372,7 +1372,7 @@ func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
1372 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}1372 state := barState{status: statusSnapshot{session: "cur"}, copyFeedback: "copied", mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
1373 row := make([]renderer.Cell, 20)1373 row := make([]renderer.Cell, 20)
1374 1374
1375- drawStatusBarState(row, state, resolveThemeStyles(nil))1375+ drawStatusBarState(row, state, resolveStyles(nil))
1376 1376
1377 text := rowText(row)1377 text := rowText(row)
1378 require.Contains(t, text, " a")1378 require.Contains(t, text, " a")
@@ -1395,7 +1395,7 @@ func TestStatusBarCopyFeedbackBoundaryWidths(t *testing.T) {
1395 t.Run(tt.name, func(t *testing.T) {1395 t.Run(tt.name, func(t *testing.T) {
1396 row := make([]renderer.Cell, tt.cols)1396 row := make([]renderer.Cell, tt.cols)
1397 1397
1398- drawStatusBarState(row, state, resolveThemeStyles(nil))1398+ drawStatusBarState(row, state, resolveStyles(nil))
1399 1399
1400 require.Equal(t, tt.want, rowText(row))1400 require.Equal(t, tt.want, rowText(row))
1401 })1401 })
A internal/usecase/theme/styles.go +48 -0
@@ -0,0 +1,48 @@
1+package theme
2+
3+import "github.com/bnema/vev/pkg/renderer"
4+
5+// Styles is the resolved set of terminal chrome styles for a client theme.
6+type Styles struct {
7+ StatusBar renderer.Style
8+ Accent renderer.Style
9+ Border renderer.Style
10+ Selection renderer.Style
11+ CopyStatus renderer.Style
12+ PaletteDesc renderer.Style
13+
14+ TabName renderer.Style
15+ TabNameActive renderer.Style
16+ TabTitle renderer.Style
17+ TabTitleActive renderer.Style
18+
19+ PickerName renderer.Style
20+ PickerSelectionName renderer.Style
21+ PickerSelectionMuted renderer.Style
22+ PickerSeparator renderer.Style
23+}
24+
25+// NewStyles composes the terminal chrome styles from the semantic theme styles.
26+func NewStyles(t Theme) Styles {
27+ statusBar := StatusBarStyle(t)
28+ accent := AccentStyle(t)
29+ selection := SelectionStyle(t)
30+ return Styles{
31+ StatusBar: statusBar,
32+ Accent: accent,
33+ Border: BorderStyle(t),
34+ Selection: selection,
35+ CopyStatus: selection,
36+ PaletteDesc: MutedTextStyle(t),
37+
38+ TabName: EmphasisStyle(statusBar, t),
39+ TabNameActive: EmphasisStyle(accent, t),
40+ TabTitle: MutedVariantStyle(statusBar, t),
41+ TabTitleActive: MutedVariantStyle(accent, t),
42+
43+ PickerName: EmphasisStyle(renderer.DefaultStyle(), t),
44+ PickerSelectionName: EmphasisStyle(selection, t),
45+ PickerSelectionMuted: MutedVariantStyle(selection, t),
46+ PickerSeparator: MutedTextStyle(t),
47+ }
48+}
@@ -0,0 +1,48 @@
1+package theme
2+
3+import "github.com/bnema/vev/pkg/renderer"
4+
5+// Styles is the resolved set of terminal chrome styles for a client theme.
6+type Styles struct {
7+ StatusBar renderer.Style
8+ Accent renderer.Style
9+ Border renderer.Style
10+ Selection renderer.Style
11+ CopyStatus renderer.Style
12+ PaletteDesc renderer.Style
13+
14+ TabName renderer.Style
15+ TabNameActive renderer.Style
16+ TabTitle renderer.Style
17+ TabTitleActive renderer.Style
18+
19+ PickerName renderer.Style
20+ PickerSelectionName renderer.Style
21+ PickerSelectionMuted renderer.Style
22+ PickerSeparator renderer.Style
23+}
24+
25+// NewStyles composes the terminal chrome styles from the semantic theme styles.
26+func NewStyles(t Theme) Styles {
27+ statusBar := StatusBarStyle(t)
28+ accent := AccentStyle(t)
29+ selection := SelectionStyle(t)
30+ return Styles{
31+ StatusBar: statusBar,
32+ Accent: accent,
33+ Border: BorderStyle(t),
34+ Selection: selection,
35+ CopyStatus: selection,
36+ PaletteDesc: MutedTextStyle(t),
37+
38+ TabName: EmphasisStyle(statusBar, t),
39+ TabNameActive: EmphasisStyle(accent, t),
40+ TabTitle: MutedVariantStyle(statusBar, t),
41+ TabTitleActive: MutedVariantStyle(accent, t),
42+
43+ PickerName: EmphasisStyle(renderer.DefaultStyle(), t),
44+ PickerSelectionName: EmphasisStyle(selection, t),
45+ PickerSelectionMuted: MutedVariantStyle(selection, t),
46+ PickerSeparator: MutedTextStyle(t),
47+ }
48+}
A internal/usecase/theme/styles_test.go +39 -0
@@ -0,0 +1,39 @@
1+package theme
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/pkg/renderer"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestNewStylesComposesSemanticStyles(t *testing.T) {
11+ theme := Theme{
12+ Foreground: renderer.RGB{R: 0xc8, G: 0xc8, B: 0xc8},
13+ Background: renderer.RGB{R: 0x0a, G: 0x14, B: 0x1e},
14+ HasFG: true,
15+ HasBG: true,
16+ Known: true,
17+ TrueColor: true,
18+ }
19+
20+ styles := NewStyles(theme)
21+ statusBar := StatusBarStyle(theme)
22+ accent := AccentStyle(theme)
23+ selection := SelectionStyle(theme)
24+
25+ require.True(t, styles.StatusBar.Equal(statusBar))
26+ require.True(t, styles.Accent.Equal(accent))
27+ require.True(t, styles.Border.Equal(BorderStyle(theme)))
28+ require.True(t, styles.Selection.Equal(selection))
29+ require.True(t, styles.CopyStatus.Equal(selection))
30+ require.True(t, styles.PaletteDesc.Equal(MutedTextStyle(theme)))
31+ require.True(t, styles.TabName.Equal(EmphasisStyle(statusBar, theme)))
32+ require.True(t, styles.TabNameActive.Equal(EmphasisStyle(accent, theme)))
33+ require.True(t, styles.TabTitle.Equal(MutedVariantStyle(statusBar, theme)))
34+ require.True(t, styles.TabTitleActive.Equal(MutedVariantStyle(accent, theme)))
35+ require.True(t, styles.PickerName.Equal(EmphasisStyle(renderer.DefaultStyle(), theme)))
36+ require.True(t, styles.PickerSelectionName.Equal(EmphasisStyle(selection, theme)))
37+ require.True(t, styles.PickerSelectionMuted.Equal(MutedVariantStyle(selection, theme)))
38+ require.True(t, styles.PickerSeparator.Equal(MutedTextStyle(theme)))
39+}
@@ -0,0 +1,39 @@
1+package theme
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/pkg/renderer"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestNewStylesComposesSemanticStyles(t *testing.T) {
11+ theme := Theme{
12+ Foreground: renderer.RGB{R: 0xc8, G: 0xc8, B: 0xc8},
13+ Background: renderer.RGB{R: 0x0a, G: 0x14, B: 0x1e},
14+ HasFG: true,
15+ HasBG: true,
16+ Known: true,
17+ TrueColor: true,
18+ }
19+
20+ styles := NewStyles(theme)
21+ statusBar := StatusBarStyle(theme)
22+ accent := AccentStyle(theme)
23+ selection := SelectionStyle(theme)
24+
25+ require.True(t, styles.StatusBar.Equal(statusBar))
26+ require.True(t, styles.Accent.Equal(accent))
27+ require.True(t, styles.Border.Equal(BorderStyle(theme)))
28+ require.True(t, styles.Selection.Equal(selection))
29+ require.True(t, styles.CopyStatus.Equal(selection))
30+ require.True(t, styles.PaletteDesc.Equal(MutedTextStyle(theme)))
31+ require.True(t, styles.TabName.Equal(EmphasisStyle(statusBar, theme)))
32+ require.True(t, styles.TabNameActive.Equal(EmphasisStyle(accent, theme)))
33+ require.True(t, styles.TabTitle.Equal(MutedVariantStyle(statusBar, theme)))
34+ require.True(t, styles.TabTitleActive.Equal(MutedVariantStyle(accent, theme)))
35+ require.True(t, styles.PickerName.Equal(EmphasisStyle(renderer.DefaultStyle(), theme)))
36+ require.True(t, styles.PickerSelectionName.Equal(EmphasisStyle(selection, theme)))
37+ require.True(t, styles.PickerSelectionMuted.Equal(MutedVariantStyle(selection, theme)))
38+ require.True(t, styles.PickerSeparator.Equal(MutedTextStyle(theme)))
39+}

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