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/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
|