palette_scan_test.go RawHistoryFolder

1package theme
2
3import (
4	"bytes"
5	"reflect"
6	"strconv"
7	"testing"
8
9	renderer "github.com/bnema/vev-vt"
10)
11
12type paletteEvent struct {
13	slot int
14	rgb  renderer.RGB
15}
16
17func TestScannerExtractsPaletteResponses(t *testing.T) {
18	tests := []struct {
19		name        string
20		chunks      []string
21		wantPalette []paletteEvent
22		wantColors  []int
23		wantOut     string
24	}{
25		{
26			name:        "BEL hash and ST rgb",
27			chunks:      []string{"a\x1b]4;0;#112233\x07b\x1b]4;15;rgb:4455/6677/8899\x1b\\c"},
28			wantPalette: []paletteEvent{{slot: 0, rgb: renderer.RGB{R: 0x11, G: 0x22, B: 0x33}}, {slot: 15, rgb: renderer.RGB{R: 0x44, G: 0x66, B: 0x88}}},
29			wantOut:     "abc",
30		},
31		{
32			name:        "split prefix body and terminator",
33			chunks:      []string{"a\x1b]4;", "1;rgb:12", "34/abcd/ffff\x1b", "\\b"},
34			wantPalette: []paletteEvent{{slot: 1, rgb: renderer.RGB{R: 0x12, G: 0xab, B: 0xff}}},
35			wantOut:     "ab",
36		},
37		{
38			name:        "interleaved defaults and multiple replies",
39			chunks:      []string{"a\x1b]10;#010203\x07\x1b]4;2;#102030\x07\x1b]11;#040506\x07\x1b]4;2;rgb:aa/bb/cc\x07z"},
40			wantPalette: []paletteEvent{{slot: 2, rgb: renderer.RGB{R: 0x10, G: 0x20, B: 0x30}}, {slot: 2, rgb: renderer.RGB{R: 0xaa, G: 0xbb, B: 0xcc}}},
41			wantColors:  []int{10, 11},
42			wantOut:     "az",
43		},
44	}
45
46	for _, tt := range tests {
47		t.Run(tt.name, func(t *testing.T) {
48			var s Scanner
49			var palette []paletteEvent
50			var colors []int
51			var out bytes.Buffer
52			for _, chunk := range tt.chunks {
53				s.Scan([]byte(chunk), func(kind int, rgb renderer.RGB) {
54					colors = append(colors, kind)
55				}, func(slot int, rgb renderer.RGB) {
56					palette = append(palette, paletteEvent{slot: slot, rgb: rgb})
57				}, func(bool) {}, func(b []byte) {
58					out.Write(b)
59				})
60			}
61			if !reflect.DeepEqual(palette, tt.wantPalette) {
62				t.Fatalf("palette=%+v want %+v", palette, tt.wantPalette)
63			}
64			if !reflect.DeepEqual(colors, tt.wantColors) {
65				t.Fatalf("colors=%v want %v", colors, tt.wantColors)
66			}
67			if out.String() != tt.wantOut {
68				t.Fatalf("passthrough=%q want %q", out.String(), tt.wantOut)
69			}
70		})
71	}
72}
73
74func TestScannerReportsEveryValidPaletteSlot(t *testing.T) {
75	var data bytes.Buffer
76	for slot := 0; slot < 16; slot++ {
77		data.WriteString("\x1b]4;")
78		data.WriteString(strconv.Itoa(slot))
79		data.WriteString(";#010203\x07")
80	}
81
82	var s Scanner
83	var slots []int
84	var out bytes.Buffer
85	s.Scan(data.Bytes(), func(int, renderer.RGB) {}, func(slot int, rgb renderer.RGB) {
86		if rgb != (renderer.RGB{R: 1, G: 2, B: 3}) {
87			t.Fatalf("slot %d rgb=%+v", slot, rgb)
88		}
89		slots = append(slots, slot)
90	}, func(bool) {}, func(b []byte) {
91		out.Write(b)
92	})
93
94	if want := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; !reflect.DeepEqual(slots, want) {
95		t.Fatalf("slots=%v want %v", slots, want)
96	}
97	if out.Len() != 0 {
98		t.Fatalf("passthrough=%q want empty", out.String())
99	}
100}
101
102func TestScannerForwardsMalformedPaletteResponses(t *testing.T) {
103	const malformed = "\x1b]4;;#010203\x07" +
104		"\x1b]4;-1;#010203\x07" +
105		"\x1b]4;cat;#010203\x1b\\" +
106		"\x1b]4;16;#010203\x07" +
107		"\x1b]4;999999999999999999;#010203\x07" +
108		"\x1b]4;3;not-a-color\x07"
109
110	var s Scanner
111	var out bytes.Buffer
112	paletteCalls := 0
113	s.Scan([]byte("before"+malformed+"after"), func(int, renderer.RGB) {}, func(int, renderer.RGB) {
114		paletteCalls++
115	}, func(bool) {}, func(b []byte) {
116		out.Write(b)
117	})
118
119	if paletteCalls != 0 {
120		t.Fatalf("palette callbacks=%d want 0", paletteCalls)
121	}
122	if got, want := out.String(), "before"+malformed+"after"; got != want {
123		t.Fatalf("passthrough=%q want %q", got, want)
124	}
125}
126
127func TestScannerFlushesTruncatedPaletteResponseAtPendingLimit(t *testing.T) {
128	var s Scanner
129	var out bytes.Buffer
130	partial := append([]byte("\x1b]4;15;rgb:"), bytes.Repeat([]byte("a"), maxPending)...)
131	s.Scan(partial, func(int, renderer.RGB) {}, func(int, renderer.RGB) {}, func(bool) {}, func(b []byte) {
132		out.Write(b)
133	})
134	s.Scan([]byte("z"), func(int, renderer.RGB) {}, func(int, renderer.RGB) {}, func(bool) {}, func(b []byte) {
135		out.Write(b)
136	})
137
138	if got, want := out.String(), string(partial)+"z"; got != want {
139		t.Fatalf("passthrough=%q want %q", got, want)
140	}
141}

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