1package webterm
2
3import (
4 "context"
5 "fmt"
6 "strings"
7 "unicode/utf8"
8
9 "github.com/bnema/vev-vt/html/browser"
10 "github.com/bnema/vev/internal/domain"
11)
12
13// Handle translates validated browser events into ordinary terminal input.
14// Paste never admits embedded terminal framing or C0 commands other than text
15// whitespace. The client retains its normal bracketed-paste policy.
16func (t *Terminal) Handle(ctx context.Context, event browser.Event) error {
17 snapshot := t.Snapshot()
18 modes := snapshot.Modes()
19 var data string
20 switch event.Kind {
21 case browser.EventResize:
22 e := event.Resize
23 return t.Resize(domain.Geometry{Size: domain.Size{Cols: e.Columns, Rows: e.Rows}, PixelWidth: int(e.PixelWidth), PixelHeight: int(e.PixelHeight)})
24 case browser.EventText:
25 data = cleanText(event.Text.Text, false)
26 case browser.EventPaste:
27 data = cleanText(event.Paste.Text, true)
28 if modes.BracketedPaste {
29 data = "\x1b[200~" + data + "\x1b[201~"
30 }
31 case browser.EventKey:
32 data = encodeKey(*event.Key, snapshot.ApplicationCursorMode())
33 case browser.EventPointer:
34 e := event.Pointer
35 if modes.MouseTracking == 0 || !modes.MouseSGR || e.Row < 0 || e.Column < 0 || e.Row >= snapshot.Rows() || e.Column >= snapshot.Columns() {
36 return nil
37 }
38 button := e.Button
39 final := "M"
40 switch e.Action {
41 case "down":
42 if button < 0 || button > 2 {
43 return nil
44 }
45 case "up":
46 if button < 0 || button > 2 {
47 return nil
48 }
49 final = "m"
50 case "move":
51 if modes.MouseTracking != 1003 && (modes.MouseTracking != 1002 || e.Buttons == 0) {
52 return nil
53 }
54 button = 3
55 if e.Buttons&1 != 0 {
56 button = 0
57 } else if e.Buttons&4 != 0 {
58 button = 1
59 } else if e.Buttons&2 != 0 {
60 button = 2
61 }
62 button += 32
63 default:
64 return nil
65 }
66 data = fmt.Sprintf("\x1b[<%d;%d;%d%s", button+mouseModifiers(e.Modifiers), e.Column+1, e.Row+1, final)
67 case browser.EventWheel:
68 e := event.Wheel
69 if modes.MouseTracking == 0 || !modes.MouseSGR || e.Row < 0 || e.Column < 0 || e.Row >= snapshot.Rows() || e.Column >= snapshot.Columns() {
70 t.resetWheel()
71 return nil
72 }
73 if e.DeltaY == 0 {
74 return nil
75 }
76 reports, button := t.consumeWheel(e.DeltaY, e.DeltaMode, e.Modifiers.Shift)
77 if reports == 0 {
78 return nil
79 }
80 // Shift is consumed as the x10 fast-scroll multiplier, so it must
81 // not also leak into the SGR modifiers (buttons 68/69), which
82 // applications would not recognize as wheel reports.
83 modifiers := e.Modifiers
84 if e.Modifiers.Shift {
85 modifiers.Shift = false
86 }
87 var sb strings.Builder
88 for i := 0; i < reports; i++ {
89 fmt.Fprintf(&sb, "\x1b[<%d;%d;%dM", button+mouseModifiers(modifiers), e.Column+1, e.Row+1)
90 }
91 data = sb.String()
92 case browser.EventFocus:
93 // The browser page stays out of focus reporting: web clients keep
94 // unknown focus and count as possibly seen.
95 return nil
96 }
97 return t.Send(ctx, []byte(data))
98}
99
100func mouseModifiers(m browser.Modifiers) int {
101 n := 0
102 if m.Shift {
103 n += 4
104 }
105 if m.Alt {
106 n += 8
107 }
108 if m.Ctrl {
109 n += 16
110 }
111 return n
112}
113
114func cleanText(text string, whitespace bool) string {
115 return strings.Map(func(r rune) rune {
116 if whitespace && (r == '\n' || r == '\r' || r == '\t') {
117 return r
118 }
119 if r < 32 || r >= 127 && r <= 159 {
120 return -1
121 }
122 return r
123 }, text)
124}
125
126func encodeKey(e browser.KeyEvent, applicationCursor bool) string {
127 if e.Modifiers.Meta {
128 return ""
129 }
130 modifier := 1
131 if e.Modifiers.Shift {
132 modifier += 1
133 }
134 if e.Modifiers.Alt {
135 modifier += 2
136 }
137 if e.Modifiers.Ctrl {
138 modifier += 4
139 }
140 var final string
141 switch e.Key {
142 case "ArrowUp":
143 final = "A"
144 case "ArrowDown":
145 final = "B"
146 case "ArrowRight":
147 final = "C"
148 case "ArrowLeft":
149 final = "D"
150 case "Home":
151 final = "H"
152 case "End":
153 final = "F"
154 }
155 if final != "" {
156 if modifier > 1 {
157 return fmt.Sprintf("\x1b[1;%d%s", modifier, final)
158 }
159 if applicationCursor {
160 return "\x1bO" + final
161 }
162 return "\x1b[" + final
163 }
164 number := 0
165 switch e.Key {
166 case "Insert":
167 number = 2
168 case "Delete":
169 number = 3
170 case "PageUp":
171 number = 5
172 case "PageDown":
173 number = 6
174 case "F5":
175 number = 15
176 case "F6":
177 number = 17
178 case "F7":
179 number = 18
180 case "F8":
181 number = 19
182 case "F9":
183 number = 20
184 case "F10":
185 number = 21
186 case "F11":
187 number = 23
188 case "F12":
189 number = 24
190 }
191 if number != 0 {
192 if modifier > 1 {
193 return fmt.Sprintf("\x1b[%d;%d~", number, modifier)
194 }
195 return fmt.Sprintf("\x1b[%d~", number)
196 }
197 if len(e.Key) == 2 && e.Key[0] == 'F' && e.Key[1] >= '1' && e.Key[1] <= '4' {
198 final := string(rune('P' + e.Key[1] - '1'))
199 if modifier > 1 {
200 return fmt.Sprintf("\x1b[1;%d%s", modifier, final)
201 }
202 return "\x1bO" + final
203 }
204 var key string
205 switch e.Key {
206 case "Enter":
207 key = "\r"
208 case "Escape":
209 key = "\x1b"
210 case "Backspace":
211 key = "\x7f"
212 case "Tab":
213 if e.Modifiers.Shift {
214 if e.Modifiers.Alt {
215 return "\x1b\x1b[Z"
216 }
217 return "\x1b[Z"
218 }
219 key = "\t"
220 default:
221 if utf8.RuneCountInString(e.Key) != 1 {
222 return ""
223 }
224 key = cleanText(e.Key, false)
225 if e.Modifiers.Ctrl {
226 upper := strings.ToUpper(key)
227 if upper == " " || upper == "@" {
228 key = "\x00"
229 } else if len(upper) == 1 && upper[0] >= 'A' && upper[0] <= '_' {
230 key = string([]byte{upper[0] & 31})
231 } else {
232 return ""
233 }
234 }
235 }
236 if e.Modifiers.Alt {
237 key = "\x1b" + key
238 }
239 return key
240}