v0.17.0 949637b

theme_inherited_test.go RawHistoryFolder

1package tui
2
3import (
4	"bytes"
5	"strings"
6	"testing"
7)
8
9func TestDetectTrueColor(t *testing.T) {
10	tests := []struct {
11		name      string
12		term      string
13		colorTerm string
14		want      bool
15	}{
16		{name: "COLORTERM truecolor", term: "screen", colorTerm: "truecolor", want: true},
17		{name: "COLORTERM 24bit", term: "screen", colorTerm: "24bit", want: true},
18		{name: "direct TERM", term: "xterm-direct", want: true},
19		{name: "direct TERM suffix", term: "screen-direct", want: true},
20		{name: "256 color TERM", term: "xterm-256color", colorTerm: "", want: false},
21		{name: "unknown TERM", term: "dumb", colorTerm: "", want: false},
22	}
23	for _, tt := range tests {
24		t.Run(tt.name, func(t *testing.T) {
25			if got := DetectTrueColor(tt.term, tt.colorTerm); got != tt.want {
26				t.Fatalf("DetectTrueColor(%q, %q) = %v, want %v", tt.term, tt.colorTerm, got, tt.want)
27			}
28		})
29	}
30}
31
32func TestParseOSCColorResponse(t *testing.T) {
33	tests := []struct {
34		name string
35		raw  string
36		kind int
37		slot int
38		want TerminalColor
39	}{
40		{
41			name: "foreground hash",
42			raw:  "\x1b]10;#123456\x07",
43			kind: 10,
44			want: ColorRGB(0x12, 0x34, 0x56),
45		},
46		{
47			name: "background rgb",
48			raw:  "\x1b]11;rgb:ffff/8000/0000\x1b\\",
49			kind: 11,
50			want: ColorRGB(255, 127, 0),
51		},
52		{
53			name: "palette slot",
54			raw:  "\x1b]4;12;rgb:12/34/56\x07",
55			kind: 4,
56			slot: 12,
57			want: ColorRGB(0x12, 0x34, 0x56),
58		},
59	}
60	for _, tt := range tests {
61		t.Run(tt.name, func(t *testing.T) {
62			got, ok := parseOSCColorResponse([]byte(tt.raw))
63			if !ok {
64				t.Fatal("parseOSCColorResponse returned !ok")
65			}
66			if got.kind != tt.kind || got.slot != tt.slot || got.color != tt.want {
67				t.Fatalf("response = %#v, want kind=%d slot=%d color=%#v", got, tt.kind, tt.slot, tt.want)
68			}
69		})
70	}
71}
72
73func TestANSIThemeColorsAdaptToTargetSGR(t *testing.T) {
74	if got := Dark.FGColor(ColorANSI(100), "x"); got != "\x1b[90mx\x1b[0m" {
75		t.Fatalf("ANSI foreground = %q, want bright-black foreground", got)
76	}
77	if got := Dark.BG(ColorANSI(31), "x"); got != "\x1b[41mx\x1b[0m" {
78		t.Fatalf("ANSI background = %q, want red background", got)
79	}
80}
81
82func TestNearestXtermColorCacheIsStable(t *testing.T) {
83	colors := [][3]int{
84		{0, 0, 0},
85		{66, 69, 75},
86		{200, 100, 50},
87		{-20, 300, 128},
88	}
89	for _, color := range colors {
90		first := nearestXtermColor(color[0], color[1], color[2])
91		second := nearestXtermColor(color[0], color[1], color[2])
92		if first != second {
93			t.Fatalf("nearest color changed for %#v: first=%d second=%d", color, first, second)
94		}
95		if first < 0 || first > 255 {
96			t.Fatalf("nearest color out of range for %#v: %d", color, first)
97		}
98	}
99}
100
101func inheritedTestTheme(trueColor bool) Theme {
102	detected := Dark
103	detected.Terminal = TerminalProfile{
104		Foreground:    ColorRGB(220, 210, 200),
105		Background:    ColorRGB(12, 18, 24),
106		HasForeground: true,
107		HasBackground: true,
108		TrueColor:     trueColor,
109	}
110	detected.Terminal.Palette[10] = ColorRGB(20, 220, 80)
111	detected.Terminal.Palette[12] = ColorRGB(40, 100, 240)
112	detected.Terminal.Palette[13] = ColorRGB(220, 70, 210)
113	detected.Terminal.PaletteKnown = 1<<10 | 1<<12 | 1<<13
114	return InheritedTheme(detected)
115}
116
117func TestInheritedThemeUsesTrueColorAndTerminalPalette(t *testing.T) {
118	th := inheritedTestTheme(true)
119	if !th.Inherited || !th.Terminal.TrueColor {
120		t.Fatalf("inherited theme mode = (inherited=%v, trueColor=%v), want truecolor", th.Inherited, th.Terminal.TrueColor)
121	}
122	if got := th.FGColor(th.Accent, "x"); !strings.Contains(got, "\x1b[38;2;40;100;240m") {
123		t.Fatalf("accent did not use the reported terminal palette: %q", got)
124	}
125	if got := th.FGColor(th.FG, "x"); !strings.Contains(got, "\x1b[38;2;220;210;200m") {
126		t.Fatalf("foreground did not inherit the terminal default: %q", got)
127	}
128	if got := th.UserBubble("x", 3); !strings.Contains(got, "\x1b[48;2;") {
129		t.Fatalf("user bubble did not use truecolor background: %q", got)
130	}
131	if got := strings.Join(th.HighlightCode("func main() {}", "go"), "\n"); !strings.Contains(got, "\x1b[38;2;") {
132		t.Fatalf("syntax highlighting did not use truecolor: %q", got)
133	}
134	if got := th.DimColor(th.FG, 45); got.Mode != terminalColorRGB {
135		t.Fatalf("DimColor mode = %v, want RGB", got.Mode)
136	}
137}
138
139func TestInheritedThemeFallsBackTo256Colors(t *testing.T) {
140	th := inheritedTestTheme(false)
141	if !th.Inherited || th.Terminal.TrueColor {
142		t.Fatalf("inherited theme mode = (inherited=%v, trueColor=%v), want 256", th.Inherited, th.Terminal.TrueColor)
143	}
144	if got := th.FGColor(th.Accent, "x"); !strings.Contains(got, "\x1b[38;5;12m") || strings.Contains(got, "\x1b[38;2;") {
145		t.Fatalf("accent did not use 256-color fallback: %q", got)
146	}
147	if got := th.UserBubble("x", 3); strings.Contains(got, "\x1b[48;2;") || !strings.Contains(got, "\x1b[48;5;") {
148		t.Fatalf("user bubble did not quantize its background: %q", got)
149	}
150	if got := th.DimColor(th.FG, 45); got.Mode != terminalColor256 {
151		t.Fatalf("DimColor mode = %v, want 256", got.Mode)
152	}
153	if got := strings.Join(th.HighlightCode("func main() {}", "go"), "\n"); strings.Contains(got, "\x1b[38;2;") {
154		t.Fatalf("syntax highlighting unexpectedly used truecolor: %q", got)
155	}
156}
157
158func TestExplicitThemesFollowTerminalCapability(t *testing.T) {
159	if got, want := Dark.FGColor(Dark.Accent, "x"), "\x1b[38;5;111mx\x1b[0m"; got != want {
160		t.Fatalf("explicit theme output = %q, want %q", got, want)
161	}
162	if got := Dark.UserBubble("x", 1); !strings.Contains(got, "\x1b[48;5;") || strings.Contains(got, "\x1b[48;2;") {
163		t.Fatalf("non-truecolor RGB bubble did not quantize: %q", got)
164	}
165
166	trueColor := withTerminalProfile(Dark, Theme{Terminal: TerminalProfile{TrueColor: true}})
167	if got := trueColor.FGColor(trueColor.Accent, "x"); !strings.Contains(got, "\x1b[38;2;") {
168		t.Fatalf("truecolor explicit theme did not emit RGB: %q", got)
169	}
170	if got := trueColor.UserBubble("x", 1); !strings.Contains(got, "\x1b[48;2;66;69;75m") {
171		t.Fatalf("truecolor explicit RGB bubble did not emit RGB: %q", got)
172	}
173}
174
175func TestAvailableThemesIncludesInherited(t *testing.T) {
176	options := AvailableThemes(t.TempDir())
177	for _, option := range options {
178		if option.Value == "inherited" {
179			if option.Label != "inherited (from terminal)" || !option.Builtin {
180				t.Fatalf("unexpected inherited option: %#v", option)
181			}
182			return
183		}
184	}
185	t.Fatal("available themes did not include inherited")
186}
187
188func TestIsLightThemePreservesExplicitDarkTheme(t *testing.T) {
189	dark := Dark
190	dark.Terminal.SchemeKnown = true
191	dark.Terminal.Light = true
192	if IsLightTheme(dark) {
193		t.Fatal("explicit dark theme was reclassified from the terminal scheme")
194	}
195
196	inherited := InheritedTheme(dark)
197	if !IsLightTheme(inherited) {
198		t.Fatal("inherited light terminal theme was not recognized as light")
199	}
200}
201
202func TestRendererDetectsInheritedSelectionBackground(t *testing.T) {
203	var buf bytes.Buffer
204	r := NewRenderer(&buf)
205	r.Resize(20, 2)
206	r.SetTheme(inheritedTestTheme(true))
207	selected := r.theme.SelectionStyle() + "selected" + reset
208	r.Draw([]string{"", selected}, -1, -1)
209	buf.Reset()
210
211	r.Draw([]string{"", "plain"}, -1, -1)
212	if !strings.Contains(buf.String(), MoveTo(1, 1)) {
213		t.Fatalf("selection repaint did not include unchanged rows: %q", buf.String())
214	}
215}

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 1

Tags 476

↑↓ moveenter openesc close