v0.7.0 4c16dd8

resolved_styles.go RawHistoryFolder

1package theme
2
3import (
4	renderer "github.com/bnema/vev-vt"
5	"github.com/bnema/vev/internal/domain"
6)
7
8const neutralAccentBlend = 0.25
9
10// ResolvedTheme is the immutable, complete chrome style snapshot produced from
11// a terminal theme and typed accent policy.
12type ResolvedTheme struct {
13	Theme  Theme
14	Accent Accent
15	Ramp   Ramp
16	Styles Styles
17}
18
19// Resolve is the single entry point for terminal-owned accent inference and
20// semantic chrome construction.
21func Resolve(t Theme, policy domain.ThemeAccent) ResolvedTheme {
22	accent := ResolveAccent(t, policy)
23	ramp := BuildRamp(t, accent)
24	styles := stylesFromRamp(t, accent, ramp)
25	return ResolvedTheme{Theme: t, Accent: accent, Ramp: ramp, Styles: styles}
26}
27
28func stylesFromRamp(t Theme, accent Accent, ramp Ramp) Styles {
29	if !accent.Known || accent.IndexedOnly || !usable(t) || !ramp.rgb {
30		styles := neutralStyles(t)
31		if accent.IndexedOnly {
32			styles = indexedStyles(styles, accent.Slot)
33		}
34		return styles
35	}
36
37	inactiveTitle := secondarySurface(ramp.SurfaceInactive)
38	activeTitle := secondarySurface(ramp.SurfaceActive)
39	overlayDescription := overlaySecondaryText(t)
40	styles := Styles{
41		SurfaceBar:      ramp.SurfaceBar,
42		SurfaceInactive: ramp.SurfaceInactive,
43		SurfaceRecent:   ramp.SurfaceRecent,
44		SurfaceActive:   ramp.SurfaceActive,
45		BorderMuted:     ramp.BorderMuted,
46		BorderActive:    ramp.BorderActive,
47		BorderWarn:      ramp.BorderWarn,
48		NeutralBorder:   neutralBorder(t),
49
50		TabInactive:      ramp.SurfaceInactive,
51		TabInactiveTitle: inactiveTitle,
52		TabActive:        EmphasisStyle(ramp.SurfaceActive, t),
53		TabActiveTitle:   activeTitle,
54		MRURecent:        ramp.SurfaceRecent,
55
56		PickerBase:        renderer.DefaultStyle(),
57		PickerSelection:   EmphasisStyle(ramp.SurfaceActive, t),
58		HintKey:           EmphasisStyle(ramp.SurfaceActive, t),
59		PickerDescription: overlayDescription,
60		// Separators are secondary text on the terminal background, not
61		// non-text borders; they therefore require the normal 4.5:1 contrast.
62		PickerSeparator: overlayDescription,
63		PromptBase:      renderer.DefaultStyle(),
64		CopyStatus:      ramp.SurfaceBar,
65		SearchSelection: EmphasisStyle(ramp.SurfaceActive, t),
66	}
67	return withMRUStyles(withLegacyAliases(styles, t), ramp)
68}
69
70func overlaySecondaryText(t Theme) renderer.Style {
71	if !usable(t) {
72		return renderer.DefaultStyle()
73	}
74	foreground, ok := secondaryText(t.Foreground, t.Background)
75	if !ok {
76		return foregroundStyle(t.Foreground)
77	}
78	return foregroundStyle(foreground)
79}
80
81func secondarySurface(base renderer.Style) renderer.Style {
82	if !base.HasForegroundRGB || !base.HasBackgroundRGB {
83		return base
84	}
85	foreground, ok := secondaryText(base.ForegroundRGB, base.BackgroundRGB)
86	if !ok {
87		return base
88	}
89	base.ForegroundRGB = foreground
90	return base
91}
92
93func indexedStyles(styles Styles, slot uint8) Styles {
94	styles.BorderMuted = indexedForeground(styles.BorderMuted, slot)
95	styles.BorderActive = indexedForeground(styles.BorderActive, slot)
96	styles.BorderWarn = indexedForeground(styles.BorderWarn, slot)
97	styles.TabInactiveTitle = indexedForeground(styles.TabInactiveTitle, slot)
98	styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)
99	styles.PickerDescription = indexedForeground(styles.PickerDescription, slot)
100	styles.PickerSeparator = indexedForeground(styles.PickerSeparator, slot)
101
102	// Compatibility aliases historically received the scheme-aware indexed
103	// blue fallback too. Keep the strict explicit index on each foreground
104	// decoration without changing any neutral surface background.
105	styles.Border = styles.BorderMuted
106	styles.PaletteDesc = indexedForeground(styles.PaletteDesc, slot)
107	styles.TabTitle = styles.TabInactiveTitle
108	styles.TabTitleActive = styles.TabActiveTitle
109	styles.PickerSelectionMuted = indexedForeground(styles.PickerSelectionMuted, slot)
110	return styles
111}
112
113func indexedForeground(style renderer.Style, slot uint8) renderer.Style {
114	style.HasForegroundRGB = false
115	style.Foreground = int(slot)
116	style.Inverse = false
117	return style
118}
119
120// neutralStyles uses full-strength session surfaces and softer overlay selections.
121// It is used for palette-off, forced themes, incomplete defaults, and RGB-less
122// indexed fallbacks; it never performs a palette slot lookup.
123func neutralStyles(t Theme) Styles {
124	status := renderer.DefaultStyle()
125	accent := inverseStyle()
126	border := renderer.DefaultStyle()
127	overlayDescription := renderer.DefaultStyle()
128	if usable(t) {
129		status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12))
130		accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend))
131		border = neutralBorder(t)
132		overlayDescription = overlaySecondaryText(t)
133	}
134	styles := Styles{
135		SurfaceBar:      status,
136		SurfaceInactive: status,
137		SurfaceRecent:   status,
138		SurfaceActive:   accent,
139		BorderMuted:     border,
140		BorderActive:    border,
141		BorderWarn:      border,
142		NeutralBorder:   border,
143
144		TabInactive:      EmphasisStyle(status, t),
145		TabInactiveTitle: MutedVariantStyle(status, t),
146		TabActive:        EmphasisStyle(accent, t),
147		TabActiveTitle:   MutedVariantStyle(accent, t),
148		MRURecent:        status,
149
150		PickerBase:        renderer.DefaultStyle(),
151		PickerSelection:   EmphasisStyle(accent, t),
152		HintKey:           EmphasisStyle(accent, t),
153		PickerDescription: overlayDescription,
154		PickerSeparator:   overlayDescription,
155		PromptBase:        renderer.DefaultStyle(),
156		CopyStatus:        accent,
157		SearchSelection:   accent,
158	}
159	styles = withLegacyAliases(styles, t)
160	// PaletteDesc is a Phase 3 compatibility alias. It retains its legacy
161	// foreground-only bytes; rendered palette descriptions use PickerDescription.
162	styles.PaletteDesc = legacyMutedText(t)
163	styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent})
164	if usable(t) {
165		// Sessions share the full-strength neutral endpoint with active tabs;
166		// overlay selections retain their softer neutral surface.
167		styles.SurfaceActive = rgbSurface(t.Background, t.Foreground)
168		styles.Accent = styles.SurfaceActive
169		styles.TabActive = EmphasisStyle(styles.SurfaceActive, t)
170		styles.TabNameActive = styles.TabActive
171		styles.TabActiveTitle = MutedVariantStyle(styles.SurfaceActive, t)
172		styles.TabTitleActive = styles.TabActiveTitle
173		// Without a palette accent the history uses the same mruWeight
174		// placement as MRUStyle, between the active tab and the bar.
175		for count := 1; count <= len(styles.mruStyles); count++ {
176			for index := range count {
177				amount := 1 - mruWeight(index, count)
178				background := Blend(t.Foreground, status.BackgroundRGB, amount)
179				foreground, ok := primaryText(t, background)
180				if !ok {
181					foreground, _ = primaryText(Theme{Foreground: renderer.RGB{R: 255, G: 255, B: 255}}, background)
182				}
183				styles.mruStyles[count-1][index] = rgbSurface(foreground, background)
184			}
185		}
186	}
187	return styles
188}
189
190func withMRUStyles(styles Styles, ramp Ramp) Styles {
191	for count := 1; count <= len(styles.mruStyles); count++ {
192		for index := 0; index < count; index++ {
193			styles.mruStyles[count-1][index] = MRUStyle(ramp, index, count)
194		}
195	}
196	return styles
197}
198
199// neutralBorder is the existing non-accent structural border input for pane
200// chrome. It intentionally does not consume a resolved accent and is called
201// only while resolving an immutable style snapshot.
202func neutralBorder(t Theme) renderer.Style {
203	if !usable(t) {
204		return renderer.DefaultStyle()
205	}
206	return foregroundStyle(Blend(t.Foreground, t.Background, 0.40))
207}
208
209func legacyMutedText(t Theme) renderer.Style {
210	if !usable(t) {
211		return renderer.DefaultStyle()
212	}
213	return foregroundStyle(Blend(t.Foreground, t.Background, 0.45))
214}
215
216func withLegacyAliases(styles Styles, t Theme) Styles {
217	styles.StatusBar = styles.SurfaceBar
218	styles.Accent = styles.SurfaceActive
219	styles.Border = styles.BorderMuted
220	styles.Selection = styles.SurfaceActive
221	styles.PaletteDesc = styles.PickerDescription
222	styles.TabName = styles.TabInactive
223	styles.TabNameActive = styles.TabActive
224	styles.TabTitle = styles.TabInactiveTitle
225	styles.TabTitleActive = styles.TabActiveTitle
226	// These aliases retain their pre-semantic-role contracts for render paths
227	// that have not migrated yet.
228	styles.PickerName = EmphasisStyle(renderer.DefaultStyle(), t)
229	styles.PickerSelectionName = EmphasisStyle(styles.Selection, t)
230	styles.PickerSelectionMuted = MutedVariantStyle(styles.Selection, t)
231	return styles
232}

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