main ece7682

terminal_test.go RawHistoryFolder

1package webterm
2
3import (
4	"context"
5	"io"
6	"testing"
7	"time"
8
9	"github.com/bnema/vev-vt/html/browser"
10	"github.com/bnema/vev/internal/domain"
11	"github.com/stretchr/testify/require"
12)
13
14func TestTerminalFlushResizeAndClose(t *testing.T) {
15	terminal, err := New(t.Context(), domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
16	require.NoError(t, err)
17	defer terminal.Close()
18	_, err = terminal.Write([]byte("hello"))
19	require.NoError(t, err)
20	require.Equal(t, rune(' '), terminal.Snapshot().Cell(0, 0).Rune)
21	require.NoError(t, terminal.Flush())
22	require.Equal(t, rune('h'), terminal.Snapshot().Cell(0, 0).Rune)
23	old := terminal.Snapshot()
24	geometry := domain.Geometry{Size: domain.Size{Cols: 20, Rows: 8}, PixelWidth: 200, PixelHeight: 160}
25	require.NoError(t, terminal.Resize(geometry))
26	require.Equal(t, geometry, <-terminal.ResizeEvents())
27	require.NoError(t, terminal.Flush())
28	require.Equal(t, 20, terminal.Snapshot().Columns())
29	require.Equal(t, 10, old.Columns())
30	require.Error(t, terminal.Resize(domain.Geometry{Size: domain.Size{Cols: MaxColumns + 1, Rows: 2}}))
31	terminal.Close()
32	_, err = terminal.Write([]byte("x"))
33	require.ErrorIs(t, err, io.ErrClosedPipe)
34}
35
36func TestTerminalDefaultColorQueries(t *testing.T) {
37	terminal, err := New(t.Context(), domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
38	require.NoError(t, err)
39	defer terminal.Close()
40	for _, tt := range []struct{ query, want string }{
41		{"\x1b]10;?\x1b\\", "\x1b]10;rgb:d8d8/d8d8/d8d8\x1b\\"},
42		{"\x1b]11;?\x1b\\", "\x1b]11;rgb:1010/1010/1010\x1b\\"},
43	} {
44		_, err := terminal.Write([]byte(tt.query))
45		require.NoError(t, err)
46		got := make([]byte, len(tt.want))
47		_, err = io.ReadFull(terminal.In(), got)
48		require.NoError(t, err)
49		require.Equal(t, tt.want, string(got))
50	}
51}
52
53func TestTerminalInput(t *testing.T) {
54	tests := []struct {
55		name  string
56		event browser.Event
57		want  string
58	}{
59		{"text", browser.Event{Kind: browser.EventText, Text: &browser.TextEvent{Text: "helloλ"}}, "helloλ"},
60		{"paste sanitizes framing", browser.Event{Kind: browser.EventPaste, Paste: &browser.PasteEvent{Text: "a\x1b[201~\x03\nb"}}, "\x1b[200~a[201~\nb\x1b[201~"},
61		{"palette", browser.Event{Kind: browser.EventKey, Key: &browser.KeyEvent{Key: " ", Modifiers: browser.Modifiers{Alt: true}}}, "\x1b "},
62		{"mouse", browser.Event{Kind: browser.EventPointer, Pointer: &browser.PointerEvent{Action: "down", Button: 0, Row: 1, Column: 2}}, "\x1b[<0;3;2M"},
63	}
64	for _, tt := range tests {
65		t.Run(tt.name, func(t *testing.T) {
66			ctx, cancel := context.WithTimeout(t.Context(), time.Second)
67			defer cancel()
68			terminal, err := New(ctx, domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
69			require.NoError(t, err)
70			defer terminal.Close()
71			_, err = terminal.EnterRaw()
72			require.NoError(t, err)
73			require.NoError(t, terminal.Handle(ctx, tt.event))
74			data := make([]byte, len(tt.want))
75			_, err = io.ReadFull(terminal.In(), data)
76			require.NoError(t, err)
77			require.Equal(t, tt.want, string(data))
78		})
79	}
80}
81
82func TestEncodeKey(t *testing.T) {
83	tests := []struct {
84		name, key string
85		modifiers browser.Modifiers
86		app       bool
87		want      string
88	}{
89		{"cursor", "ArrowUp", browser.Modifiers{}, false, "\x1b[A"},
90		{"application cursor", "ArrowUp", browser.Modifiers{}, true, "\x1bOA"},
91		{"modified cursor", "ArrowLeft", browser.Modifiers{Ctrl: true}, false, "\x1b[1;5D"},
92		{"interrupt", "c", browser.Modifiers{Ctrl: true}, false, "\x03"},
93		{"alt letter", "h", browser.Modifiers{Alt: true}, false, "\x1bh"},
94		{"shift tab", "Tab", browser.Modifiers{Shift: true}, false, "\x1b[Z"},
95		{"alt shift tab", "Tab", browser.Modifiers{Shift: true, Alt: true}, false, "\x1b\x1b[Z"},
96		{"delete", "Delete", browser.Modifiers{}, false, "\x1b[3~"},
97		{"function", "F12", browser.Modifiers{}, false, "\x1b[24~"},
98		{"meta reserved", "c", browser.Modifiers{Meta: true}, false, ""},
99		{"unknown", "Unidentified", browser.Modifiers{}, false, ""},
100	}
101	for _, tt := range tests {
102		t.Run(tt.name, func(t *testing.T) {
103			require.Equal(t, tt.want, encodeKey(browser.KeyEvent{Key: tt.key, Modifiers: tt.modifiers}, tt.app))
104		})
105	}
106}

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