v0.9.0 e5e4ded

terminal_keys_test.go RawHistoryFolder

1package client
2
3import (
4	"context"
5	"io"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/protocol"
13)
14
15func TestTerminalKeysNormalizeOnlyOnceEnabled(t *testing.T) {
16	tests := []struct {
17		name     string
18		enabled  bool
19		focus    domain.TerminalFocus
20		in       string
21		want     string
22		wantHeld bool
23	}{
24		{name: "disabled passes through", in: "\x1b[57442;5u", want: "\x1b[57442;5u"},
25		{name: "enabled drops ctrl press", enabled: true, in: "\x1b[57442;5u", want: "", wantHeld: true},
26		{name: "enabled rewrites text", enabled: true, in: "\x1b[97;;97u", want: "a"},
27		{name: "lost focus forgets ctrl", enabled: true, focus: domain.TerminalFocusUnfocused, in: "\x1b[57442;5u", want: ""},
28	}
29	for _, tt := range tests {
30		t.Run(tt.name, func(t *testing.T) {
31			keys := newTerminalKeys()
32			if tt.enabled {
33				keys.enable()
34			}
35			require.Equal(t, tt.want, string(keys.normalize([]byte(tt.in), tt.focus)))
36			held, _ := keys.ctrl.Watch()
37			require.Equal(t, tt.wantHeld, held)
38		})
39	}
40}
41
42func TestCtrlReporterReportsHoldsAfterDelayAndReleasesAtOnce(t *testing.T) {
43	type step struct {
44		held bool // set the state, then handle the change
45		fire bool // instead, fire the hold timer
46		want *protocol.ModifierState
47	}
48	held, released := &protocol.ModifierState{Ctrl: true}, &protocol.ModifierState{}
49	tests := []struct {
50		name  string
51		steps []step
52	}{
53		{name: "hold then release", steps: []step{{held: true}, {fire: true, want: held}, {held: false, want: released}}},
54		{name: "quick tap sends nothing", steps: []step{{held: true}, {held: false}, {fire: true}}},
55		{name: "hold reported once", steps: []step{{held: true}, {fire: true, want: held}, {fire: true}}},
56	}
57	for _, tt := range tests {
58		t.Run(tt.name, func(t *testing.T) {
59			state := newTerminalCtrlState()
60			clock := newSupervisorTestClock()
61			r := newCtrlReporter(state, clock)
62			var timer *supervisorTestTimer
63			for i, s := range tt.steps {
64				var message protocol.ModifierState
65				var ok bool
66				if s.fire {
67					if r.timerC() == nil {
68						require.Nil(t, s.want, "step %d: no timer to fire", i)
69						continue
70					}
71					timer.ch <- time.Time{}
72					<-r.timerC()
73					message, ok = r.due()
74				} else {
75					state.set(s.held)
76					<-r.changed
77					message, ok = r.next()
78					if s.held {
79						timer = clock.awaitTimer(t)
80						require.Equal(t, ctrlHoldDelay, timer.delay)
81					}
82				}
83				require.Equal(t, s.want != nil, ok, "step %d", i)
84				if s.want != nil {
85					require.Equal(t, *s.want, message, "step %d", i)
86				}
87			}
88			r.stop()
89			require.True(t, clock.allStopped())
90		})
91	}
92}
93
94// TestAttachmentReportsHeldCtrl drives a real pump: once kitty key events are
95// enabled, a held Ctrl reaches the daemon as ModifierState and its release
96// follows, while the dropped modifier events never reach the session as input.
97func TestAttachmentReportsHeldCtrl(t *testing.T) {
98	pr, pw := io.Pipe()
99	defer pw.Close()
100	pump := newTerminalInputPump(pr)
101	pump.keys.enable()
102	pump.start()
103	defer pump.stop()
104	stream := newSessionTestStream()
105	host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
106	cfg := sessionTestWorkerConfig(sessionTestRequest(true))
107	clock := newSupervisorTestClock()
108	cfg.Clock = clock
109	worker, err := newSessionAttachmentWorker(cfg)
110	require.NoError(t, err)
111	ctx, cancel := context.WithCancel(context.Background())
112	defer cancel()
113	go func() { _, _ = host.Run(ctx, AttachmentToken{Generation: 4, Attempt: 1}, worker, stream) }()
114
115	awaitHello(t, stream)
116	stream.deliver(protocol.Welcome{SessionName: "alpha"})
117	stream.deliver(sessionTestOutput(1, "\x1b[Hready"))
118
119	sent := func(want protocol.ModifierState) func() bool {
120		return func() bool {
121			for _, message := range stream.messages() {
122				if message == want {
123					return true
124				}
125			}
126			return false
127		}
128	}
129	_, err = pw.Write([]byte("\x1b[57442;5u"))
130	require.NoError(t, err)
131	var timer *supervisorTestTimer
132	require.Eventually(t, func() bool {
133		select {
134		case created := <-clock.created:
135			if created.delay == ctrlHoldDelay {
136				timer = created
137			}
138		default:
139		}
140		return timer != nil
141	}, 5*time.Second, time.Millisecond, "a Ctrl press arms the hold delay")
142	timer.ch <- time.Time{}
143	require.Eventually(t, sent(protocol.ModifierState{Ctrl: true}), 5*time.Second, time.Millisecond)
144
145	_, err = pw.Write([]byte("\x1b[57442;1:3u"))
146	require.NoError(t, err)
147	require.Eventually(t, sent(protocol.ModifierState{}), 5*time.Second, time.Millisecond)
148	for _, message := range stream.messages() {
149		_, isInput := message.(protocol.Input)
150		require.False(t, isInput, "modifier events are never session input")
151	}
152}

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