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}