v0.8.1 1f54f61

reduced_color_test.go RawHistoryFolder

1package client
2
3import (
4	"regexp"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8
9	ansirenderer "github.com/bnema/vev-vt/ansi"
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/protocol"
12	"github.com/bnema/vev/internal/usecase/picker"
13)
14
15func TestPickerStaysLegibleInReducedColor(t *testing.T) {
16	loop := pickerLoopFixture(t)
17	size := domain.Size{Cols: 100, Rows: 30}
18
19	ansi16 := string(newPickerRenderer(ansirenderer.ColorProfileANSI16).render(loop, size, emptyPickerPreview()))
20	require.NotRegexp(t, `(?:38|48|58);[25];`, ansi16, "ansi16 picker emitted extended colors")
21	require.Regexp(t, `\x1b\[0;[0-9;]*7`, ansi16, "the selected row must stay reverse video")
22
23	mono := string(newPickerRenderer(ansirenderer.ColorProfileMonochrome).render(loop, size, emptyPickerPreview()))
24	require.NotRegexp(t, `\x1b\[[0-9;:]*(?:38|48|58|3[0-7]|4[0-7]|9[0-7]|10[0-7])[;:m]`, mono, "monochrome picker emitted a color")
25	require.Regexp(t, `\x1b\[0;[0-9;]*7`, mono, "the selected row must stay reverse video")
26	require.Contains(t, mono, "work")
27}
28
29func TestToastBorderFollowsColorProfile(t *testing.T) {
30	tests := []struct {
31		name     string
32		profile  ansirenderer.ColorProfile
33		severity domain.NoticeSeverity
34		want     string
35	}{
36		{name: "truecolor error", profile: ansirenderer.ColorProfileTrueColor, severity: domain.NoticeError, want: "\x1b[38;5;167m"},
37		{name: "256 warn", profile: ansirenderer.ColorProfileANSI256, severity: domain.NoticeWarn, want: "\x1b[38;5;179m"},
38		{name: "256 info", profile: ansirenderer.ColorProfileANSI256, severity: domain.NoticeInfo},
39		{name: "16 error", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeError, want: "\x1b[31m"},
40		{name: "16 warn", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeWarn, want: "\x1b[33m"},
41		{name: "16 info", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeInfo},
42		{name: "mono error", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeError, want: "\x1b[1m"},
43		{name: "mono warn", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeWarn, want: "\x1b[1m"},
44		{name: "mono info", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeInfo},
45	}
46	for _, tt := range tests {
47		t.Run(tt.name, func(t *testing.T) {
48			require.Equal(t, tt.want, toastBorderSGRFor(tt.severity, tt.profile))
49		})
50	}
51	// Monochrome borders are closed with normal intensity, not default color.
52	bounds := domain.Rect{Width: 12, Height: 3}
53	lines := clientToastLines(bounds, "hi", toastBorderSGRFor(domain.NoticeError, ansirenderer.ColorProfileMonochrome))
54	require.Contains(t, lines[0], "\x1b[22m")
55	require.NotContains(t, lines[0], "\x1b[39m")
56}
57
58// TestPickerProblemDotsKeepMeaningPerProfile checks how each profile encodes
59// the problem dots: truecolor and 256 keep the fixed indexed colors, ANSI-16 is
60// quantized by the renderer, and monochrome marks error and warning dots bold
61// since no color survives. Informational statuses never gain weight.
62func TestPickerProblemDotsKeepMeaningPerProfile(t *testing.T) {
63	snapshot := pickerSnapshotFixture()
64	snapshot.Lines = []protocol.PickerLine{
65		{Key: "aa/err", Kind: protocol.PickerLineSession, Label: "err", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusError},
66		{Key: "bb/warn", Kind: protocol.PickerLineSession, Label: "warn", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusStale},
67		{Key: "cc/ver", Kind: protocol.PickerLineSession, Label: "ver", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusVersion},
68	}
69	snapshot.Cursor = protocol.PickerCursor{Key: "aa/err"}
70	// dot returns the SGR group in effect for the dot on the row labelled label.
71	dot := func(out, label string) string {
72		row := regexp.MustCompile(`(?s)` + label + `[^●]*(\x1b\[[0-9;]*m)[^●\x1b]*●`)
73		m := row.FindStringSubmatch(out)
74		require.NotNil(t, m, "no problem dot after %q in %q", label, out)
75		return m[1]
76	}
77	tests := []struct {
78		name    string
79		profile ansirenderer.ColorProfile
80		want    map[string]string // label -> exact SGR before the dot
81	}{
82		{name: "truecolor", profile: ansirenderer.ColorProfileTrueColor, want: map[string]string{"err": "\x1b[0;38;5;167m", "warn": "\x1b[0;38;5;179m", "ver": "\x1b[0;38;5;170m"}},
83		{name: "ansi256", profile: ansirenderer.ColorProfileANSI256, want: map[string]string{"err": "\x1b[0;38;5;167m", "warn": "\x1b[0;38;5;179m", "ver": "\x1b[0;38;5;170m"}},
84		{name: "ansi16", profile: ansirenderer.ColorProfileANSI16, want: map[string]string{"err": "", "warn": "", "ver": ""}},
85		{name: "monochrome", profile: ansirenderer.ColorProfileMonochrome, want: map[string]string{"err": "\x1b[0;1m", "warn": "\x1b[0;1m", "ver": "\x1b[0m"}},
86	}
87	for _, tt := range tests {
88		t.Run(tt.name, func(t *testing.T) {
89			loop := pickerLoopFromSnapshot(snapshot, protocol.PickerIntentNavigation, picker.SortRecent)
90			out := string(newPickerRenderer(tt.profile).render(loop, domain.Size{Cols: 100, Rows: 30}, emptyPickerPreview()))
91			for label, want := range tt.want {
92				got := dot(out, label)
93				if want == "" {
94					// ANSI-16 colors are chosen by the renderer's quantizer: only
95					// require a basic or bright palette slot, never extended color.
96					require.Regexp(t, `^\x1b\[0;(3[0-7]|9[0-7])m$`, got, label)
97					continue
98				}
99				require.Equal(t, want, got, label)
100			}
101		})
102	}
103}

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