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// output mirrors one Output after the foreground wrote it.
57func (e *attachmentEcho) output(output protocol.Output) error {
58 if e == nil {
59 return nil
60 }
61 e.predictor.applyOutput(output, e.clock.Now())
62 return e.draw()
63}
64
65// resized handles a terminal resize. A new size makes the daemon repaint
66// everything, so the guesses are simply forgotten. The same size brings no
67// repaint, so drawn guesses are erased from the mirror instead.
68func (e *attachmentEcho) resized(size domain.Size) error {
69 if e == nil {
70 return nil
71 }
72 screen := e.predictor.screen
73 if size.Cols != screen.Columns() || size.Rows != screen.Rows() {
74 e.predictor.forget()
75 return nil
76 }
77 e.predictor.reset()
78 return e.draw()
79}
80
81// tickC fires while a pending guess waits on a timing trigger.
82func (e *attachmentEcho) tickC() <-chan time.Time {
83 if e == nil || e.tick == nil {
84 return nil
85 }
86 return e.tick.C()
87}
88
89func (e *attachmentEcho) ticked() error {
90 if e == nil {
91 return nil
92 }
93 e.tick = nil
94 e.predictor.cull(e.clock.Now())
95 return e.draw()
96}
97
98func (e *attachmentEcho) close() {
99 if e != nil && e.tick != nil {
100 e.tick.Stop()
101 e.tick = nil
102 }
103}
104
105func (e *attachmentEcho) draw() error {
106 defer e.armTick()
107 data := e.predictor.render()
108 if len(data) == 0 {
109 return nil
110 }
111 written, err := e.out.writePrediction(data)
112 if err != nil {
113 return fmt.Errorf("writing predicted echo: %w", err)
114 }
115 if !written {
116 // An overlay owns the terminal: its box covers the guesses and the
117 // daemon repaints everything once it closes.
118 e.predictor.forget()
119 }
120 return nil
121}
122
123func (e *attachmentEcho) armTick() {
124 if e.tick != nil || !e.predictor.needsTick() {
125 return
126 }
127 e.tick = e.clock.NewTimer(echoTickInterval)
128}