main ece7682

terminal_keys.go RawHistoryFolder

1package client
2
3import (
4	"sync"
5	"sync/atomic"
6	"time"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/ports"
10	"github.com/bnema/vev/internal/protocol"
11	"github.com/bnema/vev/internal/usecase/keys/kittykey"
12)
13
14// Held Ctrl.
15//
16// With the kitty keyboard protocol the client asks its terminal for every key
17// event, modifier presses and releases included (kittykey.RequestFlags). The
18// terminal input pump rewrites that stream back to the encoding the rest of
19// vev reads (kittykey.OuterFlags) and records whether Ctrl is held on its own
20// in a terminalCtrlState. The attached loop reports a hold that lasted
21// ctrlHoldDelay to the daemon, which then shows recent-session ranks.
22
23// ctrlHoldDelay keeps a quick Ctrl shortcut from flashing the ranks.
24const ctrlHoldDelay = 150 * time.Millisecond
25
26// terminalKeys is the pump's key normalization. Only the pump's reader
27// goroutine calls normalize; enable may run on any goroutine.
28type terminalKeys struct {
29	enabled    atomic.Bool
30	normalizer kittykey.Normalizer
31	ctrl       *terminalCtrlState
32}
33
34func newTerminalKeys() *terminalKeys {
35	return &terminalKeys{ctrl: newTerminalCtrlState()}
36}
37
38// enable starts normalizing. Call it before asking the terminal for
39// kittykey.RequestFlags; input still in the old encoding passes unchanged.
40func (k *terminalKeys) enable() { k.enabled.Store(true) }
41
42// disable stops normalizing, for a terminal that refused the flags.
43func (k *terminalKeys) disable() { k.enabled.Store(false) }
44
45// normalize rewrites one read. focus is the terminal focus after the read:
46// a window that lost focus may never report the Ctrl release.
47func (k *terminalKeys) normalize(data []byte, focus domain.TerminalFocus) []byte {
48	if !k.enabled.Load() {
49		return data
50	}
51	out := k.normalizer.Normalize(data)
52	if focus == domain.TerminalFocusUnfocused {
53		k.normalizer.ReleaseCtrl()
54	}
55	k.ctrl.set(k.normalizer.CtrlHeld())
56	return out
57}
58
59// terminalCtrlState is the client's one held-Ctrl state. The terminal input
60// pump is its only writer; it outlives attachments.
61type terminalCtrlState struct {
62	mu      sync.Mutex
63	held    bool
64	changed chan struct{}
65}
66
67func newTerminalCtrlState() *terminalCtrlState {
68	return &terminalCtrlState{changed: make(chan struct{})}
69}
70
71// Watch returns whether Ctrl is held and a channel closed on the next change.
72// A nil state is never held and never changes.
73func (s *terminalCtrlState) Watch() (bool, <-chan struct{}) {
74	if s == nil {
75		return false, nil
76	}
77	s.mu.Lock()
78	defer s.mu.Unlock()
79	return s.held, s.changed
80}
81
82func (s *terminalCtrlState) set(held bool) {
83	s.mu.Lock()
84	defer s.mu.Unlock()
85	if held == s.held {
86		return
87	}
88	s.held = held
89	close(s.changed)
90	s.changed = make(chan struct{})
91}
92
93// attachmentCtrlForeground is the optional seam through which an attached
94// loop reads the held-Ctrl state.
95type attachmentCtrlForeground interface {
96	terminalCtrl() *terminalCtrlState
97}
98
99func (f *attachmentForeground) terminalCtrl() *terminalCtrlState {
100	if f == nil || f.input == nil {
101		return nil
102	}
103	return f.input.keys.ctrl
104}
105
106func attachmentTerminalCtrl(fg AttachmentForeground) *terminalCtrlState {
107	if ctrl, ok := fg.(attachmentCtrlForeground); ok {
108		return ctrl.terminalCtrl()
109	}
110	return nil
111}
112
113// ctrlReporter turns the held-Ctrl state into ModifierState messages: a hold
114// is reported once it lasted ctrlHoldDelay, a release at once, and only after
115// a reported hold.
116type ctrlReporter struct {
117	state   *terminalCtrlState
118	clock   ports.Clock
119	sent    bool
120	changed <-chan struct{}
121	timer   ports.Timer
122}
123
124func newCtrlReporter(state *terminalCtrlState, clock ports.Clock) *ctrlReporter {
125	r := &ctrlReporter{state: state, clock: clock}
126	r.next()
127	return r
128}
129
130// timerC fires when a pending hold is due.
131func (r *ctrlReporter) timerC() <-chan time.Time {
132	if r.timer == nil {
133		return nil
134	}
135	return r.timer.C()
136}
137
138// next handles a state change and returns the message to send now, if any.
139func (r *ctrlReporter) next() (protocol.ModifierState, bool) {
140	held, changed := r.state.Watch()
141	r.changed = changed
142	if held {
143		if !r.sent && r.timer == nil {
144			r.timer = r.clock.NewTimer(ctrlHoldDelay)
145		}
146		return protocol.ModifierState{}, false
147	}
148	r.stop()
149	if !r.sent {
150		return protocol.ModifierState{}, false
151	}
152	r.sent = false
153	return protocol.ModifierState{Ctrl: false}, true
154}
155
156// due handles the hold timer and returns the hold to report, if still held.
157func (r *ctrlReporter) due() (protocol.ModifierState, bool) {
158	r.stop()
159	if held, _ := r.state.Watch(); !held || r.sent {
160		return protocol.ModifierState{}, false
161	}
162	r.sent = true
163	return protocol.ModifierState{Ctrl: true}, true
164}
165
166func (r *ctrlReporter) stop() {
167	if r.timer != nil {
168		r.timer.Stop()
169		r.timer = nil
170	}
171}

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