bubbletea 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +241 -16
- data/bubbletea.gemspec +39 -0
- data/ext/bubbletea/extconf.rb +64 -0
- data/ext/bubbletea/extension.c +59 -0
- data/ext/bubbletea/extension.h +22 -0
- data/ext/bubbletea/program.c +251 -0
- data/go/bubbletea.go +98 -0
- data/go/go.mod +16 -0
- data/go/go.sum +15 -0
- data/go/input.go +158 -0
- data/go/keys.go +466 -0
- data/go/renderer.go +204 -0
- data/go/terminal.go +277 -0
- data/lib/bubbletea/commands.rb +126 -0
- data/lib/bubbletea/messages.rb +248 -0
- data/lib/bubbletea/model.rb +62 -0
- data/lib/bubbletea/runner.rb +376 -0
- data/lib/bubbletea/version.rb +1 -1
- data/lib/bubbletea.rb +12 -1
- metadata +43 -15
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -12
- data/sig/bubbletea.rbs +0 -4
data/go/keys.go
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
*/
|
|
6
|
+
import "C"
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"unicode/utf8"
|
|
11
|
+
"unsafe"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type KeyType int
|
|
15
|
+
|
|
16
|
+
const (
|
|
17
|
+
KeyNull KeyType = 0
|
|
18
|
+
KeyCtrlA KeyType = 1
|
|
19
|
+
KeyCtrlB KeyType = 2
|
|
20
|
+
KeyCtrlC KeyType = 3
|
|
21
|
+
KeyCtrlD KeyType = 4
|
|
22
|
+
KeyCtrlE KeyType = 5
|
|
23
|
+
KeyCtrlF KeyType = 6
|
|
24
|
+
KeyCtrlG KeyType = 7
|
|
25
|
+
KeyCtrlH KeyType = 8
|
|
26
|
+
KeyTab KeyType = 9
|
|
27
|
+
KeyCtrlJ KeyType = 10
|
|
28
|
+
KeyCtrlK KeyType = 11
|
|
29
|
+
KeyCtrlL KeyType = 12
|
|
30
|
+
KeyEnter KeyType = 13
|
|
31
|
+
KeyCtrlN KeyType = 14
|
|
32
|
+
KeyCtrlO KeyType = 15
|
|
33
|
+
KeyCtrlP KeyType = 16
|
|
34
|
+
KeyCtrlQ KeyType = 17
|
|
35
|
+
KeyCtrlR KeyType = 18
|
|
36
|
+
KeyCtrlS KeyType = 19
|
|
37
|
+
KeyCtrlT KeyType = 20
|
|
38
|
+
KeyCtrlU KeyType = 21
|
|
39
|
+
KeyCtrlV KeyType = 22
|
|
40
|
+
KeyCtrlW KeyType = 23
|
|
41
|
+
KeyCtrlX KeyType = 24
|
|
42
|
+
KeyCtrlY KeyType = 25
|
|
43
|
+
KeyCtrlZ KeyType = 26
|
|
44
|
+
KeyEsc KeyType = 27
|
|
45
|
+
KeyBackspace KeyType = 127
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const (
|
|
49
|
+
KeyRunes KeyType = -1
|
|
50
|
+
KeyUp KeyType = -2
|
|
51
|
+
KeyDown KeyType = -3
|
|
52
|
+
KeyRight KeyType = -4
|
|
53
|
+
KeyLeft KeyType = -5
|
|
54
|
+
KeyHome KeyType = -6
|
|
55
|
+
KeyEnd KeyType = -7
|
|
56
|
+
KeyPgUp KeyType = -8
|
|
57
|
+
KeyPgDown KeyType = -9
|
|
58
|
+
KeyDelete KeyType = -10
|
|
59
|
+
KeyInsert KeyType = -11
|
|
60
|
+
KeyF1 KeyType = -12
|
|
61
|
+
KeyF2 KeyType = -13
|
|
62
|
+
KeyF3 KeyType = -14
|
|
63
|
+
KeyF4 KeyType = -15
|
|
64
|
+
KeyF5 KeyType = -16
|
|
65
|
+
KeyF6 KeyType = -17
|
|
66
|
+
KeyF7 KeyType = -18
|
|
67
|
+
KeyF8 KeyType = -19
|
|
68
|
+
KeyF9 KeyType = -20
|
|
69
|
+
KeyF10 KeyType = -21
|
|
70
|
+
KeyF11 KeyType = -22
|
|
71
|
+
KeyF12 KeyType = -23
|
|
72
|
+
KeyShiftTab KeyType = -24
|
|
73
|
+
KeySpace KeyType = -25
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
type KeyEvent struct {
|
|
77
|
+
Type string `json:"type"` // "key"
|
|
78
|
+
KeyType int `json:"key_type"` // KeyType value
|
|
79
|
+
Runes []rune `json:"runes"` // Characters for KeyRunes
|
|
80
|
+
Alt bool `json:"alt"` // Alt modifier
|
|
81
|
+
Name string `json:"name"` // Human-readable name
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type MouseEvent struct {
|
|
85
|
+
Type string `json:"type"` // "mouse"
|
|
86
|
+
X int `json:"x"` // Column (0-based)
|
|
87
|
+
Y int `json:"y"` // Row (0-based)
|
|
88
|
+
Button int `json:"button"` // Button number
|
|
89
|
+
Action int `json:"action"` // 0=press, 1=release, 2=motion
|
|
90
|
+
Shift bool `json:"shift"`
|
|
91
|
+
Alt bool `json:"alt"`
|
|
92
|
+
Ctrl bool `json:"ctrl"`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ResizeEvent struct {
|
|
96
|
+
Type string `json:"type"` // "resize"
|
|
97
|
+
Width int `json:"width"`
|
|
98
|
+
Height int `json:"height"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type FocusEvent struct {
|
|
102
|
+
Type string `json:"type"` // "focus" or "blur"
|
|
103
|
+
Focus bool `json:"focus"` // true for focus, false for blur
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var keyNames = map[KeyType]string{
|
|
107
|
+
KeyNull: "ctrl+@",
|
|
108
|
+
KeyCtrlA: "ctrl+a",
|
|
109
|
+
KeyCtrlB: "ctrl+b",
|
|
110
|
+
KeyCtrlC: "ctrl+c",
|
|
111
|
+
KeyCtrlD: "ctrl+d",
|
|
112
|
+
KeyCtrlE: "ctrl+e",
|
|
113
|
+
KeyCtrlF: "ctrl+f",
|
|
114
|
+
KeyCtrlG: "ctrl+g",
|
|
115
|
+
KeyCtrlH: "ctrl+h",
|
|
116
|
+
KeyTab: "tab",
|
|
117
|
+
KeyCtrlJ: "ctrl+j",
|
|
118
|
+
KeyCtrlK: "ctrl+k",
|
|
119
|
+
KeyCtrlL: "ctrl+l",
|
|
120
|
+
KeyEnter: "enter",
|
|
121
|
+
KeyCtrlN: "ctrl+n",
|
|
122
|
+
KeyCtrlO: "ctrl+o",
|
|
123
|
+
KeyCtrlP: "ctrl+p",
|
|
124
|
+
KeyCtrlQ: "ctrl+q",
|
|
125
|
+
KeyCtrlR: "ctrl+r",
|
|
126
|
+
KeyCtrlS: "ctrl+s",
|
|
127
|
+
KeyCtrlT: "ctrl+t",
|
|
128
|
+
KeyCtrlU: "ctrl+u",
|
|
129
|
+
KeyCtrlV: "ctrl+v",
|
|
130
|
+
KeyCtrlW: "ctrl+w",
|
|
131
|
+
KeyCtrlX: "ctrl+x",
|
|
132
|
+
KeyCtrlY: "ctrl+y",
|
|
133
|
+
KeyCtrlZ: "ctrl+z",
|
|
134
|
+
KeyEsc: "esc",
|
|
135
|
+
KeyBackspace: "backspace",
|
|
136
|
+
KeyRunes: "runes",
|
|
137
|
+
KeyUp: "up",
|
|
138
|
+
KeyDown: "down",
|
|
139
|
+
KeyRight: "right",
|
|
140
|
+
KeyLeft: "left",
|
|
141
|
+
KeyHome: "home",
|
|
142
|
+
KeyEnd: "end",
|
|
143
|
+
KeyPgUp: "pgup",
|
|
144
|
+
KeyPgDown: "pgdown",
|
|
145
|
+
KeyDelete: "delete",
|
|
146
|
+
KeyInsert: "insert",
|
|
147
|
+
KeyF1: "f1",
|
|
148
|
+
KeyF2: "f2",
|
|
149
|
+
KeyF3: "f3",
|
|
150
|
+
KeyF4: "f4",
|
|
151
|
+
KeyF5: "f5",
|
|
152
|
+
KeyF6: "f6",
|
|
153
|
+
KeyF7: "f7",
|
|
154
|
+
KeyF8: "f8",
|
|
155
|
+
KeyF9: "f9",
|
|
156
|
+
KeyF10: "f10",
|
|
157
|
+
KeyF11: "f11",
|
|
158
|
+
KeyF12: "f12",
|
|
159
|
+
KeyShiftTab: "shift+tab",
|
|
160
|
+
KeySpace: "space",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var escapeSequences = map[string]KeyType{
|
|
164
|
+
"\x1b[A": KeyUp,
|
|
165
|
+
"\x1b[B": KeyDown,
|
|
166
|
+
"\x1b[C": KeyRight,
|
|
167
|
+
"\x1b[D": KeyLeft,
|
|
168
|
+
"\x1b[H": KeyHome,
|
|
169
|
+
"\x1b[F": KeyEnd,
|
|
170
|
+
"\x1b[1~": KeyHome,
|
|
171
|
+
"\x1b[4~": KeyEnd,
|
|
172
|
+
"\x1b[5~": KeyPgUp,
|
|
173
|
+
"\x1b[6~": KeyPgDown,
|
|
174
|
+
"\x1b[2~": KeyInsert,
|
|
175
|
+
"\x1b[3~": KeyDelete,
|
|
176
|
+
"\x1bOP": KeyF1,
|
|
177
|
+
"\x1bOQ": KeyF2,
|
|
178
|
+
"\x1bOR": KeyF3,
|
|
179
|
+
"\x1bOS": KeyF4,
|
|
180
|
+
"\x1b[15~": KeyF5,
|
|
181
|
+
"\x1b[17~": KeyF6,
|
|
182
|
+
"\x1b[18~": KeyF7,
|
|
183
|
+
"\x1b[19~": KeyF8,
|
|
184
|
+
"\x1b[20~": KeyF9,
|
|
185
|
+
"\x1b[21~": KeyF10,
|
|
186
|
+
"\x1b[23~": KeyF11,
|
|
187
|
+
"\x1b[24~": KeyF12,
|
|
188
|
+
"\x1b[Z": KeyShiftTab,
|
|
189
|
+
"\x1b[1;2A": KeyType(-100), // Shift+Up (placeholder)
|
|
190
|
+
"\x1b[1;2B": KeyType(-101), // Shift+Down
|
|
191
|
+
"\x1b[1;2C": KeyType(-102), // Shift+Right
|
|
192
|
+
"\x1b[1;2D": KeyType(-103), // Shift+Left
|
|
193
|
+
"\x1b[1;5A": KeyType(-104), // Ctrl+Up
|
|
194
|
+
"\x1b[1;5B": KeyType(-105), // Ctrl+Down
|
|
195
|
+
"\x1b[1;5C": KeyType(-106), // Ctrl+Right
|
|
196
|
+
"\x1b[1;5D": KeyType(-107), // Ctrl+Left
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func ParseInput(data []byte) (int, string) {
|
|
200
|
+
if len(data) == 0 {
|
|
201
|
+
return 0, ""
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if len(data) >= 3 {
|
|
205
|
+
if data[0] == 0x1b && data[1] == '[' {
|
|
206
|
+
if data[2] == 'I' {
|
|
207
|
+
// Focus gained
|
|
208
|
+
event := FocusEvent{Type: "focus", Focus: true}
|
|
209
|
+
jsonBytes, _ := json.Marshal(event)
|
|
210
|
+
|
|
211
|
+
return 3, string(jsonBytes)
|
|
212
|
+
}
|
|
213
|
+
if data[2] == 'O' {
|
|
214
|
+
// Focus lost
|
|
215
|
+
event := FocusEvent{Type: "blur", Focus: false}
|
|
216
|
+
jsonBytes, _ := json.Marshal(event)
|
|
217
|
+
|
|
218
|
+
return 3, string(jsonBytes)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for mouse events (SGR format: ESC [ < ... M or m)
|
|
224
|
+
if len(data) >= 6 && data[0] == 0x1b && data[1] == '[' && data[2] == '<' {
|
|
225
|
+
consumed, mouseEvent := parseMouseSGR(data)
|
|
226
|
+
if consumed > 0 {
|
|
227
|
+
jsonBytes, _ := json.Marshal(mouseEvent)
|
|
228
|
+
return consumed, string(jsonBytes)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if data[0] == 0x1b && len(data) > 1 {
|
|
233
|
+
for seq, keyType := range escapeSequences {
|
|
234
|
+
if len(data) >= len(seq) && string(data[:len(seq)]) == seq {
|
|
235
|
+
name := keyNames[keyType]
|
|
236
|
+
|
|
237
|
+
if name == "" {
|
|
238
|
+
name = "unknown"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
event := KeyEvent{
|
|
242
|
+
Type: "key",
|
|
243
|
+
KeyType: int(keyType),
|
|
244
|
+
Runes: nil,
|
|
245
|
+
Alt: false,
|
|
246
|
+
Name: name,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
jsonBytes, _ := json.Marshal(event)
|
|
250
|
+
|
|
251
|
+
return len(seq), string(jsonBytes)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Alt + key (ESC followed by a character)
|
|
256
|
+
if len(data) >= 2 && data[1] >= 32 && data[1] < 127 {
|
|
257
|
+
r := rune(data[1])
|
|
258
|
+
|
|
259
|
+
event := KeyEvent{
|
|
260
|
+
Type: "key",
|
|
261
|
+
KeyType: int(KeyRunes),
|
|
262
|
+
Runes: []rune{r},
|
|
263
|
+
Alt: true,
|
|
264
|
+
Name: "alt+" + string(r),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
jsonBytes, _ := json.Marshal(event)
|
|
268
|
+
|
|
269
|
+
return 2, string(jsonBytes)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
event := KeyEvent{
|
|
273
|
+
Type: "key",
|
|
274
|
+
KeyType: int(KeyEsc),
|
|
275
|
+
Runes: nil,
|
|
276
|
+
Alt: false,
|
|
277
|
+
Name: "esc",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
jsonBytes, _ := json.Marshal(event)
|
|
281
|
+
|
|
282
|
+
return 1, string(jsonBytes)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Control characters (0-31, 127)
|
|
286
|
+
if data[0] < 32 || data[0] == 127 {
|
|
287
|
+
keyType := KeyType(data[0])
|
|
288
|
+
name := keyNames[keyType]
|
|
289
|
+
|
|
290
|
+
if name == "" {
|
|
291
|
+
name = "ctrl+?"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
event := KeyEvent{
|
|
295
|
+
Type: "key",
|
|
296
|
+
KeyType: int(keyType),
|
|
297
|
+
Runes: nil,
|
|
298
|
+
Alt: false,
|
|
299
|
+
Name: name,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
jsonBytes, _ := json.Marshal(event)
|
|
303
|
+
|
|
304
|
+
return 1, string(jsonBytes)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if data[0] == ' ' {
|
|
308
|
+
event := KeyEvent{
|
|
309
|
+
Type: "key",
|
|
310
|
+
KeyType: int(KeySpace),
|
|
311
|
+
Runes: []rune{' '},
|
|
312
|
+
Alt: false,
|
|
313
|
+
Name: "space",
|
|
314
|
+
}
|
|
315
|
+
jsonBytes, _ := json.Marshal(event)
|
|
316
|
+
return 1, string(jsonBytes)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Regular character (UTF-8)
|
|
320
|
+
r, size := utf8.DecodeRune(data)
|
|
321
|
+
|
|
322
|
+
if r == utf8.RuneError && size == 1 {
|
|
323
|
+
return 1, "" // Invalid UTF-8, skip byte
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
event := KeyEvent{
|
|
327
|
+
Type: "key",
|
|
328
|
+
KeyType: int(KeyRunes),
|
|
329
|
+
Runes: []rune{r},
|
|
330
|
+
Alt: false,
|
|
331
|
+
Name: string(r),
|
|
332
|
+
}
|
|
333
|
+
jsonBytes, _ := json.Marshal(event)
|
|
334
|
+
return size, string(jsonBytes)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// parseMouseSGR parses SGR mouse format: ESC [ < Cb ; Cx ; Cy M/m
|
|
338
|
+
func parseMouseSGR(data []byte) (int, MouseEvent) {
|
|
339
|
+
// Format: \x1b[<button;x;y(M|m)
|
|
340
|
+
// M = press/motion, m = release
|
|
341
|
+
if len(data) < 6 || data[0] != 0x1b || data[1] != '[' || data[2] != '<' {
|
|
342
|
+
return 0, MouseEvent{}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Find the terminating M or m
|
|
346
|
+
endIndex := -1
|
|
347
|
+
|
|
348
|
+
for i := 3; i < len(data) && i < 32; i++ {
|
|
349
|
+
if data[i] == 'M' || data[i] == 'm' {
|
|
350
|
+
endIndex = i
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if endIndex == -1 {
|
|
356
|
+
return 0, MouseEvent{} // Incomplete sequence
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
params := string(data[3:endIndex])
|
|
360
|
+
var button, x, y int
|
|
361
|
+
n, err := parseInts(params, &button, &x, &y)
|
|
362
|
+
|
|
363
|
+
if err != nil || n != 3 {
|
|
364
|
+
return 0, MouseEvent{}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
shift := (button & 4) != 0
|
|
368
|
+
alt := (button & 8) != 0
|
|
369
|
+
ctrl := (button & 16) != 0
|
|
370
|
+
motion := (button & 32) != 0
|
|
371
|
+
buttonNum := button & 3
|
|
372
|
+
|
|
373
|
+
action := 0 // press
|
|
374
|
+
|
|
375
|
+
if data[endIndex] == 'm' {
|
|
376
|
+
action = 1 // release
|
|
377
|
+
} else if motion {
|
|
378
|
+
action = 2 // motion
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (button & 64) != 0 {
|
|
382
|
+
if buttonNum == 0 {
|
|
383
|
+
buttonNum = 4 // wheel up
|
|
384
|
+
} else if buttonNum == 1 {
|
|
385
|
+
buttonNum = 5 // wheel down
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return endIndex + 1, MouseEvent{
|
|
390
|
+
Type: "mouse",
|
|
391
|
+
X: x - 1,
|
|
392
|
+
Y: y - 1,
|
|
393
|
+
Button: buttonNum,
|
|
394
|
+
Action: action,
|
|
395
|
+
Shift: shift,
|
|
396
|
+
Alt: alt,
|
|
397
|
+
Ctrl: ctrl,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// parseInts parses semicolon-separated integers
|
|
402
|
+
func parseInts(s string, vals ...*int) (int, error) {
|
|
403
|
+
count := 0
|
|
404
|
+
start := 0
|
|
405
|
+
valIndex := 0
|
|
406
|
+
|
|
407
|
+
for i := 0; i <= len(s) && valIndex < len(vals); i++ {
|
|
408
|
+
if i == len(s) || s[i] == ';' {
|
|
409
|
+
if i > start {
|
|
410
|
+
num := 0
|
|
411
|
+
|
|
412
|
+
for j := start; j < i; j++ {
|
|
413
|
+
if s[j] < '0' || s[j] > '9' {
|
|
414
|
+
return count, nil
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
num = num*10 + int(s[j]-'0')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
*vals[valIndex] = num
|
|
421
|
+
count++
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
valIndex++
|
|
425
|
+
start = i + 1
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return count, nil
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//export tea_parse_input
|
|
432
|
+
func tea_parse_input(data *C.char, dataLength C.int) *C.char {
|
|
433
|
+
if dataLength <= 0 {
|
|
434
|
+
return C.CString("")
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
goData := C.GoBytes(unsafe.Pointer(data), dataLength)
|
|
438
|
+
|
|
439
|
+
_, jsonEvent := ParseInput(goData)
|
|
440
|
+
return C.CString(jsonEvent)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
//export tea_parse_input_with_consumed
|
|
444
|
+
func tea_parse_input_with_consumed(data *C.char, dataLength C.int, consumed *C.int) *C.char {
|
|
445
|
+
if dataLength <= 0 {
|
|
446
|
+
*consumed = 0
|
|
447
|
+
return C.CString("")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
goData := C.GoBytes(unsafe.Pointer(data), dataLength)
|
|
451
|
+
|
|
452
|
+
bytesConsumed, jsonEvent := ParseInput(goData)
|
|
453
|
+
*consumed = C.int(bytesConsumed)
|
|
454
|
+
return C.CString(jsonEvent)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
//export tea_get_key_name
|
|
458
|
+
func tea_get_key_name(keyType C.int) *C.char {
|
|
459
|
+
name, exists := keyNames[KeyType(keyType)]
|
|
460
|
+
|
|
461
|
+
if !exists {
|
|
462
|
+
return C.CString("")
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return C.CString(name)
|
|
466
|
+
}
|
data/go/renderer.go
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
*/
|
|
6
|
+
import "C"
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"os"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"unsafe"
|
|
13
|
+
"github.com/charmbracelet/x/ansi"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type Renderer struct {
|
|
17
|
+
mu sync.Mutex
|
|
18
|
+
lastRender string
|
|
19
|
+
lastLines []string
|
|
20
|
+
linesRendered int
|
|
21
|
+
width int
|
|
22
|
+
height int
|
|
23
|
+
altScreen bool
|
|
24
|
+
cursorHidden bool
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var (
|
|
28
|
+
renderers = make(map[uint64]*Renderer)
|
|
29
|
+
renderersMu sync.RWMutex
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
func getRenderer(id uint64) *Renderer {
|
|
33
|
+
renderersMu.RLock()
|
|
34
|
+
defer renderersMu.RUnlock()
|
|
35
|
+
return renderers[id]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//export tea_renderer_new
|
|
39
|
+
func tea_renderer_new(programID C.ulonglong) C.ulonglong {
|
|
40
|
+
renderer := &Renderer{}
|
|
41
|
+
|
|
42
|
+
renderersMu.Lock()
|
|
43
|
+
id := getNextID()
|
|
44
|
+
renderers[id] = renderer
|
|
45
|
+
renderersMu.Unlock()
|
|
46
|
+
|
|
47
|
+
state := getProgram(uint64(programID))
|
|
48
|
+
|
|
49
|
+
if state != nil {
|
|
50
|
+
state.width = 80
|
|
51
|
+
state.height = 24
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return C.ulonglong(id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//export tea_renderer_free
|
|
58
|
+
func tea_renderer_free(id C.ulonglong) {
|
|
59
|
+
renderersMu.Lock()
|
|
60
|
+
delete(renderers, uint64(id))
|
|
61
|
+
renderersMu.Unlock()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//export tea_renderer_set_size
|
|
65
|
+
func tea_renderer_set_size(id C.ulonglong, width C.int, height C.int) {
|
|
66
|
+
renderer := getRenderer(uint64(id))
|
|
67
|
+
|
|
68
|
+
if renderer == nil {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
renderer.mu.Lock()
|
|
73
|
+
renderer.width = int(width)
|
|
74
|
+
renderer.height = int(height)
|
|
75
|
+
renderer.mu.Unlock()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//export tea_renderer_set_alt_screen
|
|
79
|
+
func tea_renderer_set_alt_screen(id C.ulonglong, enabled C.int) {
|
|
80
|
+
renderer := getRenderer(uint64(id))
|
|
81
|
+
if renderer == nil {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
renderer.mu.Lock()
|
|
86
|
+
renderer.altScreen = enabled != 0
|
|
87
|
+
renderer.mu.Unlock()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//export tea_renderer_render
|
|
91
|
+
func tea_renderer_render(id C.ulonglong, view *C.char) {
|
|
92
|
+
renderer := getRenderer(uint64(id))
|
|
93
|
+
|
|
94
|
+
if renderer == nil {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
renderer.mu.Lock()
|
|
99
|
+
defer renderer.mu.Unlock()
|
|
100
|
+
|
|
101
|
+
viewString := C.GoString(view)
|
|
102
|
+
|
|
103
|
+
if viewString == renderer.lastRender {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var buffer strings.Builder
|
|
108
|
+
newLines := strings.Split(viewString, "\n")
|
|
109
|
+
|
|
110
|
+
if renderer.height > 0 && len(newLines) > renderer.height {
|
|
111
|
+
newLines = newLines[len(newLines)-renderer.height:]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if renderer.altScreen {
|
|
115
|
+
buffer.WriteString(ansi.CursorHomePosition)
|
|
116
|
+
|
|
117
|
+
for i, line := range newLines {
|
|
118
|
+
if renderer.width > 0 && ansi.StringWidth(line) > renderer.width {
|
|
119
|
+
line = ansi.Truncate(line, renderer.width, "")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
buffer.WriteString(line)
|
|
123
|
+
buffer.WriteString(ansi.EraseLine(0))
|
|
124
|
+
|
|
125
|
+
if i < len(newLines) - 1 {
|
|
126
|
+
buffer.WriteString("\r\n")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if len(newLines) < renderer.linesRendered {
|
|
131
|
+
for i := len(newLines); i < renderer.linesRendered; i++ {
|
|
132
|
+
buffer.WriteString("\r\n")
|
|
133
|
+
buffer.WriteString(ansi.EraseLine(2))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
if renderer.linesRendered > 1 {
|
|
138
|
+
buffer.WriteString(ansi.CursorUp(renderer.linesRendered - 1))
|
|
139
|
+
}
|
|
140
|
+
buffer.WriteString("\r")
|
|
141
|
+
|
|
142
|
+
for i, line := range newLines {
|
|
143
|
+
if renderer.width > 0 && ansi.StringWidth(line) > renderer.width {
|
|
144
|
+
line = ansi.Truncate(line, renderer.width, "")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
buffer.WriteString(line)
|
|
148
|
+
buffer.WriteString(ansi.EraseLine(0))
|
|
149
|
+
|
|
150
|
+
if i < len(newLines) - 1 {
|
|
151
|
+
buffer.WriteString("\r\n")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if len(newLines) < renderer.linesRendered {
|
|
156
|
+
for i := len(newLines); i < renderer.linesRendered; i++ {
|
|
157
|
+
buffer.WriteString("\r\n")
|
|
158
|
+
buffer.WriteString(ansi.EraseLine(2))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
buffer.WriteString(ansi.CursorUp(renderer.linesRendered - len(newLines)))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
buffer.WriteString("\r")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
os.Stdout.WriteString(buffer.String())
|
|
168
|
+
|
|
169
|
+
renderer.lastRender = viewString
|
|
170
|
+
renderer.lastLines = newLines
|
|
171
|
+
renderer.linesRendered = len(newLines)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//export tea_renderer_clear
|
|
175
|
+
func tea_renderer_clear(id C.ulonglong) {
|
|
176
|
+
renderer := getRenderer(uint64(id))
|
|
177
|
+
|
|
178
|
+
if renderer == nil {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
renderer.mu.Lock()
|
|
183
|
+
defer renderer.mu.Unlock()
|
|
184
|
+
|
|
185
|
+
os.Stdout.WriteString(ansi.EraseEntireScreen)
|
|
186
|
+
os.Stdout.WriteString(ansi.CursorHomePosition)
|
|
187
|
+
|
|
188
|
+
renderer.lastRender = ""
|
|
189
|
+
renderer.lastLines = nil
|
|
190
|
+
renderer.linesRendered = 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//export tea_string_width
|
|
194
|
+
func tea_string_width(s *C.char) C.int {
|
|
195
|
+
return C.int(ansi.StringWidth(C.GoString(s)))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//export tea_truncate_string
|
|
199
|
+
func tea_truncate_string(s *C.char, width C.int) *C.char {
|
|
200
|
+
result := ansi.Truncate(C.GoString(s), int(width), "")
|
|
201
|
+
return C.CString(result)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var _ = unsafe.Pointer(nil)
|