v0.9.0 e5e4ded

attachment_echo.go RawHistoryFolder

1package client
2
3import (
4	"fmt"
5	"time"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/ports"
9	"github.com/bnema/vev/internal/protocol"
10)
11
12// echoTickInterval re-culls pending guesses while a timing trigger may still
13// fire without output, as mosh's PredictionEngine::wait_time.
14const echoTickInterval = 50 * time.Millisecond
15
16// attachmentEcho connects the prediction engine to one attached loop: it feeds
17// sent input and applied output to the engine and writes the guesses through
18// the foreground. It is nil when the attachment does not predict (local
19// attachments, scripted foregrounds, echo.predict = never), and every method
20// is a no-op on nil.
21type attachmentEcho struct {
22	predictor *echoPredictor
23	out       attachmentPredictionForeground
24	clock     ports.Clock
25	tick      ports.Timer
26}
27
28func newAttachmentEcho(w *sessionAttachmentWorker, fg AttachmentForeground) *attachmentEcho {
29	if w.cfg.Request.Local || w.cfg.EchoPredict == domain.EchoPredictNever {
30		return nil
31	}
32	out, ok := fg.(attachmentPredictionForeground)
33	if !ok {
34		return nil
35	}
36	return &attachmentEcho{predictor: newEchoPredictor(w.cfg.EchoPredict), out: out, clock: w.cfg.Clock}
37}
38
39// seed mirrors output written before the loop started (the initial frame).
40func (e *attachmentEcho) seed(output protocol.Output) {
41	if e == nil {
42		return
43	}
44	e.predictor.applyOutput(output, e.clock.Now())
45}
46
47// input records one session Input sent with seq.
48func (e *attachmentEcho) input(seq uint64, data []byte) error {
49	if e == nil {
50		return nil
51	}
52	e.predictor.input(seq, data, e.clock.Now())
53	return e.draw()
54}
55
56// beforeOutput erases drawn guesses before the foreground writes a daemon
57// frame, so the frame never scrolls or overwrites around a guess.
58func (e *attachmentEcho) beforeOutput(output protocol.Output) error {
59	if e == nil || len(output.Data) == 0 {
60		return nil
61	}
62	data := e.predictor.undraw()
63	if len(data) == 0 {
64		return nil
65	}
66	if _, err := e.out.writePrediction(data); err != nil {
67		return fmt.Errorf("erasing predicted echo: %w", err)
68	}
69	return nil
70}
71
72// output mirrors one Output after the foreground wrote it.
73func (e *attachmentEcho) output(output protocol.Output) error {
74	if e == nil {
75		return nil
76	}
77	e.predictor.applyOutput(output, e.clock.Now())
78	return e.draw()
79}
80
81// resized handles a terminal resize. A new size makes the daemon repaint
82// everything, so the guesses are simply forgotten. The same size brings no
83// repaint, so drawn guesses are erased from the mirror instead.
84func (e *attachmentEcho) resized(size domain.Size) error {
85	if e == nil {
86		return nil
87	}
88	screen := e.predictor.screen
89	if size.Cols != screen.Columns() || size.Rows != screen.Rows() {
90		e.predictor.forget()
91		return nil
92	}
93	e.predictor.reset()
94	return e.draw()
95}
96
97// tickC fires while a pending guess waits on a timing trigger.
98func (e *attachmentEcho) tickC() <-chan time.Time {
99	if e == nil || e.tick == nil {
100		return nil
101	}
102	return e.tick.C()
103}
104
105func (e *attachmentEcho) ticked() error {
106	if e == nil {
107		return nil
108	}
109	e.tick = nil
110	e.predictor.cull(e.clock.Now())
111	return e.draw()
112}
113
114func (e *attachmentEcho) close() {
115	if e != nil && e.tick != nil {
116		e.tick.Stop()
117		e.tick = nil
118	}
119}
120
121func (e *attachmentEcho) draw() error {
122	defer e.armTick()
123	data := e.predictor.render()
124	if len(data) == 0 {
125		return nil
126	}
127	written, err := e.out.writePrediction(data)
128	if err != nil {
129		return fmt.Errorf("writing predicted echo: %w", err)
130	}
131	if !written {
132		// An overlay owns the terminal: its box covers the guesses and the
133		// daemon repaints everything once it closes.
134		e.predictor.forget()
135	}
136	return nil
137}
138
139func (e *attachmentEcho) armTick() {
140	if e.tick != nil || !e.predictor.needsTick() {
141		return
142	}
143	e.tick = e.clock.NewTimer(echoTickInterval)
144}

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