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	// On the full-strength neutral surface an indexed title color cannot be
99	// kept readable, so the active title keeps its RGB secondary text.
100	if !styles.TabActiveTitle.HasBackgroundRGB {
101		styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)
102	}
103	styles.PickerDescription = indexedForeground(styles.PickerDescription, slot)
104	styles.PickerSeparator = indexedForeground(styles.PickerSeparator, slot)
105
106	// Compatibility aliases historically received the scheme-aware indexed
107	// blue fallback too. Keep the strict explicit index on each foreground
108	// decoration without changing any neutral surface background.
109	styles.Border = styles.BorderMuted
110	styles.PaletteDesc = indexedForeground(styles.PaletteDesc, slot)
111	styles.TabTitle = styles.TabInactiveTitle
112	styles.TabTitleActive = styles.TabActiveTitle
113	styles.PickerSelectionMuted = indexedForeground(styles.PickerSelectionMuted, slot)
114	return styles
115}
116
117func indexedForeground(style renderer.Style, slot uint8) renderer.Style {
118	style.HasForegroundRGB = false
119	style.Foreground = int(slot)
120	style.Inverse = false
121	return style
122}
123
124// neutralStyles uses full-strength session surfaces and softer overlay selections.
125// It is used for palette-off, forced themes, incomplete defaults, and RGB-less
126// indexed fallbacks; it never performs a palette slot lookup.
127func neutralStyles(t Theme) Styles {
128	status := renderer.DefaultStyle()
129	accent := inverseStyle()
130	border := renderer.DefaultStyle()
131	overlayDescription := renderer.DefaultStyle()
132	if usable(t) {
133		status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12))
134		accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend))
135		border = neutralBorder(t)
136		overlayDescription = overlaySecondaryText(t)
137	}
138	styles := Styles{
139		SurfaceBar:      status,
140		SurfaceInactive: status,
141		SurfaceRecent:   status,
142		SurfaceActive:   accent,
143		BorderMuted:     border,
144		BorderActive:    border,
145		BorderWarn:      border,
146		NeutralBorder:   border,
147
148		TabInactive:      EmphasisStyle(status, t),
149		TabInactiveTitle: MutedVariantStyle(status, t),
150		TabActive:        EmphasisStyle(accent, t),
151		TabActiveTitle:   MutedVariantStyle(accent, t),
152		MRURecent:        status,
153
154		PickerBase:        renderer.DefaultStyle(),
155		PickerSelection:   EmphasisStyle(accent, t),
156		HintKey:           EmphasisStyle(accent, t),
157		PickerDescription: overlayDescription,
158		PickerSeparator:   overlayDescription,
159		PromptBase:        renderer.DefaultStyle(),
160		CopyStatus:        accent,
161		SearchSelection:   accent,
162	}
163	styles = withLegacyAliases(styles, t)
164	// PaletteDesc is a Phase 3 compatibility alias. It retains its legacy
165	// foreground-only bytes; rendered palette descriptions use PickerDescription.
166	styles.PaletteDesc = legacyMutedText(t)
167	styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent})
168	if usable(t) {
169		// Sessions share the full-strength neutral endpoint with active tabs;
170		// overlay selections retain their softer neutral surface.
171		styles.SurfaceActive = rgbSurface(t.Background, t.Foreground)
172		styles.Accent = styles.SurfaceActive
173		styles.TabActive = EmphasisStyle(styles.SurfaceActive, t)
174		styles.TabNameActive = styles.TabActive
175		styles.TabActiveTitle = MutedVariantStyle(styles.SurfaceActive, t)
176		styles.TabTitleActive = styles.TabActiveTitle
177		// Without a palette accent the history uses the same mruWeight
178		// placement as MRUStyle, between the active tab and the bar.
179		for count := 1; count <= len(styles.mruStyles); count++ {
180			for index := range count {
181				amount := 1 - mruWeight(index, count)
182				background := Blend(t.Foreground, status.BackgroundRGB, amount)
183				foreground, ok := primaryText(t, background)
184				if !ok {
185					foreground, _ = primaryText(Theme{Foreground: renderer.RGB{R: 255, G: 255, B: 255}}, background)
186				}
187				styles.mruStyles[count-1][index] = rgbSurface(foreground, background)
188			}
189		}
190	}
191	return styles
192}
193
194func withMRUStyles(styles Styles, ramp Ramp) Styles {
195	for count := 1; count <= len(styles.mruStyles); count++ {
196		for index := 0; index < count; index++ {
197			styles.mruStyles[count-1][index] = MRUStyle(ramp, index, count)
198		}
199	}
200	return styles
201}
202
203// neutralBorder is the existing non-accent structural border input for pane
204// chrome. It intentionally does not consume a resolved accent and is called
205// only while resolving an immutable style snapshot.
206func neutralBorder(t Theme) renderer.Style {
207	if !usable(t) {
208		return renderer.DefaultStyle()
209	}
210	return foregroundStyle(Blend(t.Foreground, t.Background, 0.40))
211}
212
213func legacyMutedText(t Theme) renderer.Style {
214	if !usable(t) {
215		return renderer.DefaultStyle()
216	}
217	return foregroundStyle(Blend(t.Foreground, t.Background, 0.45))
218}
219
220func withLegacyAliases(styles Styles, t Theme) Styles {
221	styles.StatusBar = styles.SurfaceBar
222	styles.Accent = styles.SurfaceActive
223	styles.Border = styles.BorderMuted
224	styles.Selection = styles.SurfaceActive
225	styles.PaletteDesc = styles.PickerDescription
226	styles.TabName = styles.TabInactive
227	styles.TabNameActive = styles.TabActive
228	styles.TabTitle = styles.TabInactiveTitle
229	styles.TabTitleActive = styles.TabActiveTitle
230	// These aliases retain their pre-semantic-role contracts for render paths
231	// that have not migrated yet.
232	styles.PickerName = EmphasisStyle(renderer.DefaultStyle(), t)
233	styles.PickerSelectionName = EmphasisStyle(styles.Selection, t)
234	styles.PickerSelectionMuted = MutedVariantStyle(styles.Selection, t)
235	return styles
236}

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