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/terminal.go ADDED
@@ -0,0 +1,277 @@
1
+ package main
2
+
3
+ /*
4
+ #include <stdlib.h>
5
+ */
6
+ import "C"
7
+
8
+ import (
9
+ "os"
10
+ "github.com/charmbracelet/x/ansi"
11
+ "github.com/charmbracelet/x/term"
12
+ )
13
+
14
+ type Terminal struct {
15
+ input *os.File
16
+ output *os.File
17
+ previousState *term.State
18
+ rawMode bool
19
+ altScreen bool
20
+ cursorHidden bool
21
+ mouseEnabled bool
22
+ }
23
+
24
+ //export tea_terminal_init
25
+ func tea_terminal_init(programID C.ulonglong) C.int {
26
+ state := getProgram(uint64(programID))
27
+ if state == nil {
28
+ return -1
29
+ }
30
+
31
+ state.terminal = &Terminal{
32
+ input: os.Stdin,
33
+ output: os.Stdout,
34
+ }
35
+
36
+ return 0
37
+ }
38
+
39
+ //export tea_terminal_enter_raw_mode
40
+ func tea_terminal_enter_raw_mode(programID C.ulonglong) C.int {
41
+ state := getProgram(uint64(programID))
42
+ if state == nil {
43
+ return -1
44
+ }
45
+
46
+ if state.terminal == nil {
47
+ state.terminal = &Terminal{
48
+ input: os.Stdin,
49
+ output: os.Stdout,
50
+ }
51
+ }
52
+
53
+ if state.terminal.rawMode {
54
+ return 0
55
+ }
56
+
57
+ oldState, err := term.MakeRaw(os.Stdin.Fd())
58
+ if err != nil {
59
+ return -1
60
+ }
61
+
62
+ state.terminal.previousState = oldState
63
+ state.terminal.rawMode = true
64
+
65
+ return 0
66
+ }
67
+
68
+ //export tea_terminal_exit_raw_mode
69
+ func tea_terminal_exit_raw_mode(programID C.ulonglong) C.int {
70
+ state := getProgram(uint64(programID))
71
+ if state == nil || state.terminal == nil {
72
+ return -1
73
+ }
74
+
75
+ if !state.terminal.rawMode {
76
+ return 0
77
+ }
78
+
79
+ if state.terminal.previousState != nil {
80
+ if err := term.Restore(os.Stdin.Fd(), state.terminal.previousState); err != nil {
81
+ return -1
82
+ }
83
+ }
84
+
85
+ state.terminal.rawMode = false
86
+ return 0
87
+ }
88
+
89
+ func (t *Terminal) Restore() {
90
+ if t.previousState != nil {
91
+ term.Restore(os.Stdin.Fd(), t.previousState)
92
+ }
93
+ }
94
+
95
+ //export tea_terminal_enter_alt_screen
96
+ func tea_terminal_enter_alt_screen(programID C.ulonglong) {
97
+ state := getProgram(uint64(programID))
98
+
99
+ if state == nil || state.terminal == nil {
100
+ return
101
+ }
102
+
103
+ if state.terminal.altScreen {
104
+ return
105
+ }
106
+
107
+ os.Stdout.WriteString(ansi.SetAltScreenBufferMode)
108
+ os.Stdout.WriteString(ansi.EraseEntireScreen)
109
+ os.Stdout.WriteString(ansi.CursorHomePosition)
110
+
111
+ state.terminal.altScreen = true
112
+ }
113
+
114
+ //export tea_terminal_exit_alt_screen
115
+ func tea_terminal_exit_alt_screen(programID C.ulonglong) {
116
+ state := getProgram(uint64(programID))
117
+
118
+ if state == nil || state.terminal == nil {
119
+ return
120
+ }
121
+
122
+ if !state.terminal.altScreen {
123
+ return
124
+ }
125
+
126
+ os.Stdout.WriteString(ansi.ResetAltScreenBufferMode)
127
+
128
+ state.terminal.altScreen = false
129
+ }
130
+
131
+ //export tea_terminal_hide_cursor
132
+ func tea_terminal_hide_cursor(programID C.ulonglong) {
133
+ state := getProgram(uint64(programID))
134
+ if state == nil || state.terminal == nil {
135
+ return
136
+ }
137
+
138
+ if state.terminal.cursorHidden {
139
+ return
140
+ }
141
+
142
+ os.Stdout.WriteString(ansi.HideCursor)
143
+ state.terminal.cursorHidden = true
144
+ }
145
+
146
+ //export tea_terminal_show_cursor
147
+ func tea_terminal_show_cursor(programID C.ulonglong) {
148
+ state := getProgram(uint64(programID))
149
+ if state == nil || state.terminal == nil {
150
+ return
151
+ }
152
+
153
+ if !state.terminal.cursorHidden {
154
+ return
155
+ }
156
+
157
+ os.Stdout.WriteString(ansi.ShowCursor)
158
+ state.terminal.cursorHidden = false
159
+ }
160
+
161
+ //export tea_terminal_enable_mouse_cell_motion
162
+ func tea_terminal_enable_mouse_cell_motion(programID C.ulonglong) {
163
+ state := getProgram(uint64(programID))
164
+
165
+ if state == nil || state.terminal == nil {
166
+ return
167
+ }
168
+
169
+ os.Stdout.WriteString(ansi.SetButtonEventMouseMode)
170
+ os.Stdout.WriteString(ansi.SetSgrExtMouseMode)
171
+
172
+ state.terminal.mouseEnabled = true
173
+ }
174
+
175
+ //export tea_terminal_enable_mouse_all_motion
176
+ func tea_terminal_enable_mouse_all_motion(programID C.ulonglong) {
177
+ state := getProgram(uint64(programID))
178
+
179
+ if state == nil || state.terminal == nil {
180
+ return
181
+ }
182
+
183
+ os.Stdout.WriteString(ansi.SetAnyEventMouseMode)
184
+ os.Stdout.WriteString(ansi.SetSgrExtMouseMode)
185
+
186
+ state.terminal.mouseEnabled = true
187
+ }
188
+
189
+ //export tea_terminal_disable_mouse
190
+ func tea_terminal_disable_mouse(programID C.ulonglong) {
191
+ state := getProgram(uint64(programID))
192
+
193
+ if state == nil || state.terminal == nil {
194
+ return
195
+ }
196
+
197
+ if !state.terminal.mouseEnabled {
198
+ return
199
+ }
200
+
201
+ os.Stdout.WriteString(ansi.ResetButtonEventMouseMode)
202
+ os.Stdout.WriteString(ansi.ResetAnyEventMouseMode)
203
+ os.Stdout.WriteString(ansi.ResetSgrExtMouseMode)
204
+
205
+ state.terminal.mouseEnabled = false
206
+ }
207
+
208
+ //export tea_terminal_enable_bracketed_paste
209
+ func tea_terminal_enable_bracketed_paste(programID C.ulonglong) {
210
+ os.Stdout.WriteString(ansi.SetBracketedPasteMode)
211
+ }
212
+
213
+ //export tea_terminal_disable_bracketed_paste
214
+ func tea_terminal_disable_bracketed_paste(programID C.ulonglong) {
215
+ os.Stdout.WriteString(ansi.ResetBracketedPasteMode)
216
+ }
217
+
218
+ //export tea_terminal_enable_report_focus
219
+ func tea_terminal_enable_report_focus(programID C.ulonglong) {
220
+ os.Stdout.WriteString(ansi.SetFocusEventMode)
221
+ }
222
+
223
+ //export tea_terminal_disable_report_focus
224
+ func tea_terminal_disable_report_focus(programID C.ulonglong) {
225
+ os.Stdout.WriteString(ansi.ResetFocusEventMode)
226
+ }
227
+
228
+ //export tea_terminal_get_size
229
+ func tea_terminal_get_size(programID C.ulonglong, widthOut *C.int, heightOut *C.int) C.int {
230
+ w, h, err := term.GetSize(os.Stdout.Fd())
231
+
232
+ if err != nil {
233
+ return -1
234
+ }
235
+
236
+ *widthOut = C.int(w)
237
+ *heightOut = C.int(h)
238
+
239
+ state := getProgram(uint64(programID))
240
+
241
+ if state != nil {
242
+ state.width = w
243
+ state.height = h
244
+ }
245
+
246
+ return 0
247
+ }
248
+
249
+ //export tea_terminal_set_window_title
250
+ func tea_terminal_set_window_title(title *C.char) {
251
+ os.Stdout.WriteString(ansi.SetWindowTitle(C.GoString(title)))
252
+ }
253
+
254
+ //export tea_terminal_is_tty
255
+ func tea_terminal_is_tty() C.int {
256
+ if term.IsTerminal(os.Stdin.Fd()) {
257
+ return 1
258
+ }
259
+
260
+ return 0
261
+ }
262
+
263
+ //export tea_terminal_clear_screen
264
+ func tea_terminal_clear_screen() {
265
+ os.Stdout.WriteString(ansi.EraseEntireScreen)
266
+ os.Stdout.WriteString(ansi.CursorHomePosition)
267
+ }
268
+
269
+ //export tea_terminal_erase_line
270
+ func tea_terminal_erase_line() {
271
+ os.Stdout.WriteString(ansi.EraseLine(2)) // 2 = erase entire line
272
+ }
273
+
274
+ //export tea_terminal_cursor_home
275
+ func tea_terminal_cursor_home() {
276
+ os.Stdout.WriteString(ansi.CursorHomePosition)
277
+ }
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bubbletea
4
+ class Command
5
+ end
6
+
7
+ class QuitCommand < Command
8
+ end
9
+
10
+ class BatchCommand < Command
11
+ attr_reader :commands
12
+
13
+ def initialize(commands)
14
+ super()
15
+
16
+ @commands = commands
17
+ end
18
+ end
19
+
20
+ class TickCommand < Command
21
+ attr_reader :duration, :callback
22
+
23
+ def initialize(duration, &callback)
24
+ super()
25
+
26
+ @duration = duration
27
+ @callback = callback
28
+ end
29
+ end
30
+
31
+ class SendMessage < Command
32
+ attr_reader :message, :delay
33
+
34
+ def initialize(message, delay: 0)
35
+ super()
36
+
37
+ @message = message
38
+ @delay = delay
39
+ end
40
+ end
41
+
42
+ class SequenceCommand < Command
43
+ attr_reader :commands
44
+
45
+ def initialize(commands)
46
+ super()
47
+
48
+ @commands = commands
49
+ end
50
+ end
51
+
52
+ class EnterAltScreenCommand < Command
53
+ end
54
+
55
+ class ExitAltScreenCommand < Command
56
+ end
57
+
58
+ class SetWindowTitleCommand < Command
59
+ attr_reader :title
60
+
61
+ def initialize(title)
62
+ super()
63
+
64
+ @title = title
65
+ end
66
+ end
67
+
68
+ class PutsCommand < Command
69
+ attr_reader :text
70
+
71
+ def initialize(text)
72
+ super()
73
+
74
+ @text = text
75
+ end
76
+ end
77
+
78
+ class SuspendCommand < Command
79
+ end
80
+
81
+ class << self
82
+ def quit
83
+ QuitCommand.new
84
+ end
85
+
86
+ def batch(*commands)
87
+ BatchCommand.new(commands.flatten.compact)
88
+ end
89
+
90
+ def tick(duration, &)
91
+ TickCommand.new(duration, &)
92
+ end
93
+
94
+ def send_message(message, delay: 0)
95
+ SendMessage.new(message, delay: delay)
96
+ end
97
+
98
+ def sequence(*commands)
99
+ SequenceCommand.new(commands.flatten.compact)
100
+ end
101
+
102
+ def none
103
+ nil
104
+ end
105
+
106
+ def enter_alt_screen
107
+ EnterAltScreenCommand.new
108
+ end
109
+
110
+ def exit_alt_screen
111
+ ExitAltScreenCommand.new
112
+ end
113
+
114
+ def set_window_title(title) # rubocop:disable Naming/AccessorMethodName
115
+ SetWindowTitleCommand.new(title)
116
+ end
117
+
118
+ def puts(text)
119
+ PutsCommand.new(text)
120
+ end
121
+
122
+ def suspend
123
+ SuspendCommand.new
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bubbletea
4
+ class Message
5
+ end
6
+
7
+ class KeyMessage < Message
8
+ KEY_NULL = 0
9
+ KEY_CTRL_A = 1
10
+ KEY_CTRL_B = 2
11
+ KEY_CTRL_C = 3
12
+ KEY_CTRL_D = 4
13
+ KEY_CTRL_E = 5
14
+ KEY_CTRL_F = 6
15
+ KEY_CTRL_G = 7
16
+ KEY_CTRL_H = 8
17
+ KEY_TAB = 9
18
+ KEY_CTRL_J = 10
19
+ KEY_CTRL_K = 11
20
+ KEY_CTRL_L = 12
21
+ KEY_ENTER = 13
22
+ KEY_CTRL_N = 14
23
+ KEY_CTRL_O = 15
24
+ KEY_CTRL_P = 16
25
+ KEY_CTRL_Q = 17
26
+ KEY_CTRL_R = 18
27
+ KEY_CTRL_S = 19
28
+ KEY_CTRL_T = 20
29
+ KEY_CTRL_U = 21
30
+ KEY_CTRL_V = 22
31
+ KEY_CTRL_W = 23
32
+ KEY_CTRL_X = 24
33
+ KEY_CTRL_Y = 25
34
+ KEY_CTRL_Z = 26
35
+ KEY_ESC = 27
36
+ KEY_BACKSPACE = 127
37
+
38
+ KEY_RUNES = -1
39
+ KEY_UP = -2
40
+ KEY_DOWN = -3
41
+ KEY_RIGHT = -4
42
+ KEY_LEFT = -5
43
+ KEY_HOME = -6
44
+ KEY_END = -7
45
+ KEY_PGUP = -8
46
+ KEY_PGDOWN = -9
47
+ KEY_DELETE = -10
48
+ KEY_INSERT = -11
49
+ KEY_F1 = -12
50
+ KEY_F2 = -13
51
+ KEY_F3 = -14
52
+ KEY_F4 = -15
53
+ KEY_F5 = -16
54
+ KEY_F6 = -17
55
+ KEY_F7 = -18
56
+ KEY_F8 = -19
57
+ KEY_F9 = -20
58
+ KEY_F10 = -21
59
+ KEY_F11 = -22
60
+ KEY_F12 = -23
61
+ KEY_SHIFT_TAB = -24
62
+ KEY_SPACE = -25
63
+
64
+ attr_reader :key_type, :runes, :alt, :name
65
+
66
+ def initialize(key_type:, runes: [], alt: false, name: nil)
67
+ super()
68
+
69
+ @key_type = key_type
70
+ @runes = runes.is_a?(Array) ? runes : []
71
+ @alt = alt
72
+ @name = name || lookup_key_name
73
+ end
74
+
75
+ private
76
+
77
+ def lookup_key_name
78
+ base_name = if @key_type == KEY_RUNES && char
79
+ char
80
+ else
81
+ go_name = Bubbletea.get_key_name(@key_type)
82
+ go_name.empty? ? "unknown" : go_name
83
+ end
84
+
85
+ @alt ? "alt+#{base_name}" : base_name
86
+ end
87
+
88
+ public
89
+
90
+ def to_s
91
+ @name
92
+ end
93
+
94
+ def char
95
+ @runes.pack("U*") if @runes.any?
96
+ end
97
+
98
+ def ctrl?
99
+ @key_type.between?(0, 31)
100
+ end
101
+
102
+ def runes?
103
+ @key_type == KEY_RUNES
104
+ end
105
+
106
+ def space?
107
+ @key_type == KEY_SPACE
108
+ end
109
+
110
+ def enter?
111
+ @key_type == KEY_ENTER
112
+ end
113
+
114
+ def backspace?
115
+ @key_type == KEY_BACKSPACE
116
+ end
117
+
118
+ def tab?
119
+ @key_type == KEY_TAB
120
+ end
121
+
122
+ def esc?
123
+ @key_type == KEY_ESC
124
+ end
125
+
126
+ def up?
127
+ @key_type == KEY_UP
128
+ end
129
+
130
+ def down?
131
+ @key_type == KEY_DOWN
132
+ end
133
+
134
+ def left?
135
+ @key_type == KEY_LEFT
136
+ end
137
+
138
+ def right?
139
+ @key_type == KEY_RIGHT
140
+ end
141
+ end
142
+
143
+ class MouseMessage < Message
144
+ BUTTON_NONE = 0
145
+ BUTTON_LEFT = 1
146
+ BUTTON_MIDDLE = 2
147
+ BUTTON_RIGHT = 3
148
+ BUTTON_WHEEL_UP = 4
149
+ BUTTON_WHEEL_DOWN = 5
150
+
151
+ ACTION_PRESS = 0
152
+ ACTION_RELEASE = 1
153
+ ACTION_MOTION = 2
154
+
155
+ attr_reader :x, :y, :button, :action, :shift, :alt, :ctrl
156
+
157
+ def initialize(x:, y:, button:, action:, shift: false, alt: false, ctrl: false)
158
+ super()
159
+
160
+ @x = x
161
+ @y = y
162
+ @button = button
163
+ @action = action
164
+ @shift = shift
165
+ @alt = alt
166
+ @ctrl = ctrl
167
+ end
168
+
169
+ def press?
170
+ @action == ACTION_PRESS
171
+ end
172
+
173
+ def release?
174
+ @action == ACTION_RELEASE
175
+ end
176
+
177
+ def motion?
178
+ @action == ACTION_MOTION
179
+ end
180
+
181
+ def wheel?
182
+ @button >= BUTTON_WHEEL_UP
183
+ end
184
+
185
+ def left?
186
+ @button == BUTTON_LEFT
187
+ end
188
+
189
+ def right?
190
+ @button == BUTTON_RIGHT
191
+ end
192
+
193
+ def middle?
194
+ @button == BUTTON_MIDDLE
195
+ end
196
+ end
197
+
198
+ class WindowSizeMessage < Message
199
+ attr_reader :width, :height
200
+
201
+ def initialize(width:, height:)
202
+ super()
203
+
204
+ @width = width
205
+ @height = height
206
+ end
207
+ end
208
+
209
+ class FocusMessage < Message
210
+ end
211
+
212
+ class BlurMessage < Message
213
+ end
214
+
215
+ class QuitMessage < Message
216
+ end
217
+
218
+ class ResumeMessage < Message
219
+ end
220
+
221
+ def self.parse_event(hash)
222
+ return nil if hash.nil?
223
+
224
+ case hash["type"]
225
+ when "key"
226
+ KeyMessage.new(
227
+ key_type: hash["key_type"],
228
+ runes: hash["runes"] || [],
229
+ alt: hash["alt"] || false,
230
+ name: hash["name"]
231
+ )
232
+ when "mouse"
233
+ MouseMessage.new(
234
+ x: hash["x"],
235
+ y: hash["y"],
236
+ button: hash["button"],
237
+ action: hash["action"],
238
+ shift: hash["shift"] || false,
239
+ alt: hash["alt"] || false,
240
+ ctrl: hash["ctrl"] || false
241
+ )
242
+ when "focus"
243
+ FocusMessage.new
244
+ when "blur"
245
+ BlurMessage.new
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bubbletea
4
+ # Model module that provides the Elm Architecture interface.
5
+ # Include this module in your model class and implement:
6
+ # - init: Returns [model, command] - initial state and optional command
7
+ # - update(message): Returns [model, command] - new state and optional command
8
+ # - view: Returns String - the current view to render
9
+ #
10
+ # Example:
11
+ # class Counter
12
+ # include Bubbletea::Model
13
+ #
14
+ # attr_reader :count
15
+ #
16
+ # def initialize
17
+ # @count = 0
18
+ # end
19
+ #
20
+ # def init
21
+ # [self, nil]
22
+ # end
23
+ #
24
+ # def update(message)
25
+ # case message
26
+ # when Bubbletea::KeyMessage
27
+ # case message.to_s
28
+ # when "q", "ctrl+c"
29
+ # [self, Bubbletea.quit]
30
+ # when "up", "k"
31
+ # @count += 1
32
+ # [self, nil]
33
+ # when "down", "j"
34
+ # @count -= 1
35
+ # [self, nil]
36
+ # else
37
+ # [self, nil]
38
+ # end
39
+ # else
40
+ # [self, nil]
41
+ # end
42
+ # end
43
+ #
44
+ # def view
45
+ # "Count: #{@count}\n\nPress up/down to change, q to quit"
46
+ # end
47
+ # end
48
+ #
49
+ module Model
50
+ def init
51
+ [self, nil]
52
+ end
53
+
54
+ def update(_message)
55
+ [self, nil]
56
+ end
57
+
58
+ def view
59
+ ""
60
+ end
61
+ end
62
+ end