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.
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)