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.
@@ -0,0 +1,251 @@
1
+ #include "extension.h"
2
+
3
+ static void program_free(void *pointer) {
4
+ bubbletea_program_t *program = (bubbletea_program_t *)pointer;
5
+
6
+ if (program->handle != 0) {
7
+ tea_free_program(program->handle);
8
+ }
9
+
10
+ xfree(program);
11
+ }
12
+
13
+ static size_t program_memsize(const void *pointer) {
14
+ return sizeof(bubbletea_program_t);
15
+ }
16
+
17
+ const rb_data_type_t program_type = {
18
+ .wrap_struct_name = "Bubbletea::Program",
19
+ .function = {
20
+ .dmark = NULL,
21
+ .dfree = program_free,
22
+ .dsize = program_memsize,
23
+ },
24
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY
25
+ };
26
+
27
+ static VALUE program_alloc(VALUE klass) {
28
+ bubbletea_program_t *program = ALLOC(bubbletea_program_t);
29
+ program->handle = tea_new_program();
30
+ return TypedData_Wrap_Struct(klass, &program_type, program);
31
+ }
32
+
33
+ static VALUE program_initialize(VALUE self) {
34
+ GET_PROGRAM(self, program);
35
+
36
+ tea_terminal_init(program->handle);
37
+
38
+ return self;
39
+ }
40
+
41
+ /* Terminal control methods */
42
+
43
+ static VALUE program_enter_raw_mode(VALUE self) {
44
+ GET_PROGRAM(self, program);
45
+ return tea_terminal_enter_raw_mode(program->handle) == 0 ? Qtrue : Qfalse;
46
+ }
47
+
48
+ static VALUE program_exit_raw_mode(VALUE self) {
49
+ GET_PROGRAM(self, program);
50
+ return tea_terminal_exit_raw_mode(program->handle) == 0 ? Qtrue : Qfalse;
51
+ }
52
+
53
+ static VALUE program_enter_alt_screen(VALUE self) {
54
+ GET_PROGRAM(self, program);
55
+ tea_terminal_enter_alt_screen(program->handle);
56
+ return Qnil;
57
+ }
58
+
59
+ static VALUE program_exit_alt_screen(VALUE self) {
60
+ GET_PROGRAM(self, program);
61
+ tea_terminal_exit_alt_screen(program->handle);
62
+ return Qnil;
63
+ }
64
+
65
+ static VALUE program_hide_cursor(VALUE self) {
66
+ GET_PROGRAM(self, program);
67
+ tea_terminal_hide_cursor(program->handle);
68
+ return Qnil;
69
+ }
70
+
71
+ static VALUE program_show_cursor(VALUE self) {
72
+ GET_PROGRAM(self, program);
73
+ tea_terminal_show_cursor(program->handle);
74
+ return Qnil;
75
+ }
76
+
77
+ static VALUE program_enable_mouse_cell_motion(VALUE self) {
78
+ GET_PROGRAM(self, program);
79
+ tea_terminal_enable_mouse_cell_motion(program->handle);
80
+ return Qnil;
81
+ }
82
+
83
+ static VALUE program_enable_mouse_all_motion(VALUE self) {
84
+ GET_PROGRAM(self, program);
85
+ tea_terminal_enable_mouse_all_motion(program->handle);
86
+ return Qnil;
87
+ }
88
+
89
+ static VALUE program_disable_mouse(VALUE self) {
90
+ GET_PROGRAM(self, program);
91
+ tea_terminal_disable_mouse(program->handle);
92
+ return Qnil;
93
+ }
94
+
95
+ static VALUE program_enable_bracketed_paste(VALUE self) {
96
+ GET_PROGRAM(self, program);
97
+ tea_terminal_enable_bracketed_paste(program->handle);
98
+ return Qnil;
99
+ }
100
+
101
+ static VALUE program_disable_bracketed_paste(VALUE self) {
102
+ GET_PROGRAM(self, program);
103
+ tea_terminal_disable_bracketed_paste(program->handle);
104
+ return Qnil;
105
+ }
106
+
107
+ static VALUE program_enable_report_focus(VALUE self) {
108
+ GET_PROGRAM(self, program);
109
+ tea_terminal_enable_report_focus(program->handle);
110
+ return Qnil;
111
+ }
112
+
113
+ static VALUE program_disable_report_focus(VALUE self) {
114
+ GET_PROGRAM(self, program);
115
+ tea_terminal_disable_report_focus(program->handle);
116
+ return Qnil;
117
+ }
118
+
119
+ static VALUE program_terminal_size(VALUE self) {
120
+ GET_PROGRAM(self, program);
121
+ int width, height;
122
+
123
+ if (tea_terminal_get_size(program->handle, &width, &height) == 0) {
124
+ return rb_ary_new_from_args(2, INT2NUM(width), INT2NUM(height));
125
+ }
126
+
127
+ return Qnil;
128
+ }
129
+
130
+ /* Input methods */
131
+
132
+ static VALUE program_start_input_reader(VALUE self) {
133
+ GET_PROGRAM(self, program);
134
+ return tea_input_start_reader(program->handle) == 0 ? Qtrue : Qfalse;
135
+ }
136
+
137
+ static VALUE program_stop_input_reader(VALUE self) {
138
+ GET_PROGRAM(self, program);
139
+ tea_input_stop_reader(program->handle);
140
+ return Qnil;
141
+ }
142
+
143
+ static VALUE program_read_raw_input(VALUE self, VALUE timeout_ms) {
144
+ GET_PROGRAM(self, program);
145
+
146
+ char buffer[256];
147
+ int bytes_read = tea_input_read_raw(program->handle, buffer, sizeof(buffer), NUM2INT(timeout_ms));
148
+
149
+ if (bytes_read > 0) {
150
+ return rb_str_new(buffer, bytes_read);
151
+ } else if (bytes_read == 0) {
152
+ return Qnil; // Timeout
153
+ } else {
154
+ return Qnil; // Error
155
+ }
156
+ }
157
+
158
+ static VALUE program_poll_event(VALUE self, VALUE timeout_ms) {
159
+ GET_PROGRAM(self, program);
160
+
161
+ char buffer[256];
162
+ int bytes_read = tea_input_read_raw(program->handle, buffer, sizeof(buffer), NUM2INT(timeout_ms));
163
+
164
+ if (bytes_read <= 0) {
165
+ return Qnil; // Timeout or error
166
+ }
167
+
168
+ int consumed;
169
+ char *json = tea_parse_input_with_consumed(buffer, bytes_read, &consumed);
170
+
171
+ if (json == NULL || json[0] == '\0') {
172
+ tea_free(json);
173
+ return Qnil;
174
+ }
175
+
176
+ VALUE rb_json = rb_utf8_str_new_cstr(json);
177
+ tea_free(json);
178
+
179
+ VALUE rb_json_module = rb_const_get(rb_cObject, rb_intern("JSON"));
180
+ VALUE rb_hash = rb_funcall(rb_json_module, rb_intern("parse"), 1, rb_json);
181
+
182
+ return rb_hash;
183
+ }
184
+
185
+ /* Renderer methods */
186
+
187
+ static VALUE program_create_renderer(VALUE self) {
188
+ GET_PROGRAM(self, program);
189
+ unsigned long long renderer_id = tea_renderer_new(program->handle);
190
+ return ULL2NUM(renderer_id);
191
+ }
192
+
193
+ static VALUE program_render(VALUE self, VALUE renderer_id, VALUE view) {
194
+ Check_Type(view, T_STRING);
195
+ tea_renderer_render(NUM2ULL(renderer_id), StringValueCStr(view));
196
+ return Qnil;
197
+ }
198
+
199
+ static VALUE program_renderer_set_size(VALUE self, VALUE renderer_id, VALUE width, VALUE height) {
200
+ tea_renderer_set_size(NUM2ULL(renderer_id), NUM2INT(width), NUM2INT(height));
201
+ return Qnil;
202
+ }
203
+
204
+ static VALUE program_renderer_set_alt_screen(VALUE self, VALUE renderer_id, VALUE enabled) {
205
+ tea_renderer_set_alt_screen(NUM2ULL(renderer_id), RTEST(enabled) ? 1 : 0);
206
+ return Qnil;
207
+ }
208
+
209
+ static VALUE program_renderer_clear(VALUE self, VALUE renderer_id) {
210
+ tea_renderer_clear(NUM2ULL(renderer_id));
211
+ return Qnil;
212
+ }
213
+
214
+ static VALUE program_string_width(VALUE self, VALUE str) {
215
+ Check_Type(str, T_STRING);
216
+ return INT2NUM(tea_string_width(StringValueCStr(str)));
217
+ }
218
+
219
+ void Init_bubbletea_program(void) {
220
+ cProgram = rb_define_class_under(mBubbletea, "Program", rb_cObject);
221
+
222
+ rb_define_alloc_func(cProgram, program_alloc);
223
+ rb_define_method(cProgram, "initialize", program_initialize, 0);
224
+
225
+ rb_define_method(cProgram, "enter_raw_mode", program_enter_raw_mode, 0);
226
+ rb_define_method(cProgram, "exit_raw_mode", program_exit_raw_mode, 0);
227
+ rb_define_method(cProgram, "enter_alt_screen", program_enter_alt_screen, 0);
228
+ rb_define_method(cProgram, "exit_alt_screen", program_exit_alt_screen, 0);
229
+ rb_define_method(cProgram, "hide_cursor", program_hide_cursor, 0);
230
+ rb_define_method(cProgram, "show_cursor", program_show_cursor, 0);
231
+ rb_define_method(cProgram, "enable_mouse_cell_motion", program_enable_mouse_cell_motion, 0);
232
+ rb_define_method(cProgram, "enable_mouse_all_motion", program_enable_mouse_all_motion, 0);
233
+ rb_define_method(cProgram, "disable_mouse", program_disable_mouse, 0);
234
+ rb_define_method(cProgram, "enable_bracketed_paste", program_enable_bracketed_paste, 0);
235
+ rb_define_method(cProgram, "disable_bracketed_paste", program_disable_bracketed_paste, 0);
236
+ rb_define_method(cProgram, "enable_report_focus", program_enable_report_focus, 0);
237
+ rb_define_method(cProgram, "disable_report_focus", program_disable_report_focus, 0);
238
+ rb_define_method(cProgram, "terminal_size", program_terminal_size, 0);
239
+
240
+ rb_define_method(cProgram, "start_input_reader", program_start_input_reader, 0);
241
+ rb_define_method(cProgram, "stop_input_reader", program_stop_input_reader, 0);
242
+ rb_define_method(cProgram, "read_raw_input", program_read_raw_input, 1);
243
+ rb_define_method(cProgram, "poll_event", program_poll_event, 1);
244
+
245
+ rb_define_method(cProgram, "create_renderer", program_create_renderer, 0);
246
+ rb_define_method(cProgram, "render", program_render, 2);
247
+ rb_define_method(cProgram, "renderer_set_size", program_renderer_set_size, 3);
248
+ rb_define_method(cProgram, "renderer_set_alt_screen", program_renderer_set_alt_screen, 2);
249
+ rb_define_method(cProgram, "renderer_clear", program_renderer_clear, 1);
250
+ rb_define_method(cProgram, "string_width", program_string_width, 1);
251
+ }
data/go/bubbletea.go ADDED
@@ -0,0 +1,98 @@
1
+ package main
2
+
3
+ /*
4
+ #include <stdlib.h>
5
+ */
6
+ import "C"
7
+
8
+ import (
9
+ "runtime/debug"
10
+ "sync"
11
+ "unsafe"
12
+ )
13
+
14
+ var (
15
+ nextID uint64 = 1
16
+ nextIDMu sync.Mutex
17
+ )
18
+
19
+ func getNextID() uint64 {
20
+ nextIDMu.Lock()
21
+ defer nextIDMu.Unlock()
22
+ id := nextID
23
+ nextID++
24
+ return id
25
+ }
26
+
27
+ var (
28
+ programs = make(map[uint64]*ProgramState)
29
+ programsMu sync.RWMutex
30
+ )
31
+
32
+ type ProgramState struct {
33
+ terminal *Terminal
34
+ input *InputReader
35
+ width int
36
+ height int
37
+ }
38
+
39
+ func getProgram(id uint64) *ProgramState {
40
+ programsMu.RLock()
41
+ defer programsMu.RUnlock()
42
+ return programs[id]
43
+ }
44
+
45
+ //export tea_free
46
+ func tea_free(pointer *C.char) {
47
+ C.free(unsafe.Pointer(pointer))
48
+ }
49
+
50
+ //export tea_new_program
51
+ func tea_new_program() C.ulonglong {
52
+ state := &ProgramState{}
53
+
54
+ programsMu.Lock()
55
+ id := getNextID()
56
+ programs[id] = state
57
+ programsMu.Unlock()
58
+
59
+ return C.ulonglong(id)
60
+ }
61
+
62
+ //export tea_free_program
63
+ func tea_free_program(id C.ulonglong) {
64
+ programsMu.Lock()
65
+ defer programsMu.Unlock()
66
+
67
+ state := programs[uint64(id)]
68
+ if state != nil {
69
+ if state.input != nil {
70
+ state.input.Stop()
71
+ }
72
+
73
+ if state.terminal != nil {
74
+ state.terminal.Restore()
75
+ }
76
+ }
77
+
78
+ delete(programs, uint64(id))
79
+ }
80
+
81
+ //export tea_upstream_version
82
+ func tea_upstream_version() *C.char {
83
+ info, ok := debug.ReadBuildInfo()
84
+
85
+ if !ok {
86
+ return C.CString("unknown")
87
+ }
88
+
89
+ for _, dep := range info.Deps {
90
+ if dep.Path == "github.com/charmbracelet/x/ansi" {
91
+ return C.CString(dep.Version)
92
+ }
93
+ }
94
+
95
+ return C.CString("unknown")
96
+ }
97
+
98
+ func main() {}
data/go/go.mod ADDED
@@ -0,0 +1,16 @@
1
+ module github.com/marcoroth/bubbletea-ruby/go
2
+
3
+ go 1.23.0
4
+
5
+ require (
6
+ github.com/charmbracelet/x/ansi v0.8.0
7
+ github.com/charmbracelet/x/term v0.2.1
8
+ github.com/muesli/cancelreader v0.2.2
9
+ )
10
+
11
+ require (
12
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
13
+ github.com/mattn/go-runewidth v0.0.16 // indirect
14
+ github.com/rivo/uniseg v0.4.7 // indirect
15
+ golang.org/x/sys v0.30.0 // indirect
16
+ )
data/go/go.sum ADDED
@@ -0,0 +1,15 @@
1
+ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
2
+ github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
3
+ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
4
+ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
5
+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
6
+ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
7
+ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
8
+ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
9
+ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
10
+ github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
11
+ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
12
+ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
13
+ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
14
+ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
15
+ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
data/go/input.go ADDED
@@ -0,0 +1,158 @@
1
+ package main
2
+
3
+ /*
4
+ #include <stdlib.h>
5
+ */
6
+ import "C"
7
+
8
+ import (
9
+ "context"
10
+ "os"
11
+ "sync"
12
+ "time"
13
+ "unsafe"
14
+ "github.com/muesli/cancelreader"
15
+ )
16
+
17
+ type InputReader struct {
18
+ cancelReader cancelreader.CancelReader
19
+ ctx context.Context
20
+ cancel context.CancelFunc
21
+ events chan []byte
22
+ mu sync.Mutex
23
+ running bool
24
+ }
25
+
26
+ func NewInputReader() (*InputReader, error) {
27
+ reader, err := cancelreader.NewReader(os.Stdin)
28
+
29
+ if err != nil {
30
+ return nil, err
31
+ }
32
+
33
+ ctx, cancel := context.WithCancel(context.Background())
34
+
35
+ return &InputReader{
36
+ cancelReader: reader,
37
+ ctx: ctx,
38
+ cancel: cancel,
39
+ events: make(chan []byte, 100),
40
+ }, nil
41
+ }
42
+
43
+ func (reader *InputReader) Start() {
44
+ reader.mu.Lock()
45
+
46
+ if reader.running {
47
+ reader.mu.Unlock()
48
+ return
49
+ }
50
+
51
+ reader.running = true
52
+ reader.mu.Unlock()
53
+
54
+ go reader.readLoop()
55
+ }
56
+
57
+ func (reader *InputReader) Stop() {
58
+ reader.mu.Lock()
59
+ defer reader.mu.Unlock()
60
+
61
+ if !reader.running {
62
+ return
63
+ }
64
+
65
+ reader.running = false
66
+ reader.cancel()
67
+ reader.cancelReader.Cancel()
68
+ reader.cancelReader.Close()
69
+ }
70
+
71
+ func (reader *InputReader) readLoop() {
72
+ var buf [256]byte
73
+
74
+ for {
75
+ select {
76
+ case <-reader.ctx.Done():
77
+ return
78
+ default:
79
+ }
80
+
81
+ n, err := reader.cancelReader.Read(buf[:])
82
+ if err != nil {
83
+ return
84
+ }
85
+
86
+ if n > 0 {
87
+ data := make([]byte, n)
88
+ copy(data, buf[:n])
89
+
90
+ select {
91
+ case reader.events <- data:
92
+ case <-reader.ctx.Done():
93
+ return
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ //export tea_input_start_reader
100
+ func tea_input_start_reader(programID C.ulonglong) C.int {
101
+ state := getProgram(uint64(programID))
102
+ if state == nil {
103
+ return -1
104
+ }
105
+
106
+ if state.input != nil {
107
+ return 0
108
+ }
109
+
110
+ reader, err := NewInputReader()
111
+ if err != nil {
112
+ return -1
113
+ }
114
+
115
+ state.input = reader
116
+ reader.Start()
117
+
118
+ return 0
119
+ }
120
+
121
+ //export tea_input_stop_reader
122
+ func tea_input_stop_reader(programID C.ulonglong) {
123
+ state := getProgram(uint64(programID))
124
+ if state == nil || state.input == nil {
125
+ return
126
+ }
127
+
128
+ state.input.Stop()
129
+ state.input = nil
130
+ }
131
+
132
+ //export tea_input_read_raw
133
+ func tea_input_read_raw(programID C.ulonglong, buffer *C.char, bufferSize C.int, timeoutMs C.int) C.int {
134
+ state := getProgram(uint64(programID))
135
+
136
+ if state == nil || state.input == nil {
137
+ return -1
138
+ }
139
+
140
+ timeout := time.Duration(timeoutMs) * time.Millisecond
141
+
142
+ select {
143
+ case data := <-state.input.events:
144
+ copyLength := len(data)
145
+
146
+ if copyLength > int(bufferSize) {
147
+ copyLength = int(bufferSize)
148
+ }
149
+
150
+ cBuffer := (*[1 << 20]byte)(unsafe.Pointer(buffer))[:copyLength:copyLength]
151
+ copy(cBuffer, data[:copyLength])
152
+
153
+ return C.int(copyLength)
154
+
155
+ case <-time.After(timeout):
156
+ return 0
157
+ }
158
+ }