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}