r2ui 0.1.0 → 0.2.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/CHANGELOG.md +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- data/lib/r2ui/terminal.rb +0 -53
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2UI
|
|
4
|
+
module Compat
|
|
5
|
+
module Tea
|
|
6
|
+
# Pure-Ruby port of bubbletea 0.1.4's Go input parser (go/keys.go:
|
|
7
|
+
# ParseInput, parseMouseSGR, parseInts, keyNames, tea_get_key_name).
|
|
8
|
+
# Each event is the Hash that JSON.parse returns for Go's json.Marshal
|
|
9
|
+
# of the event struct, so Bubbletea.parse_event consumes it unchanged.
|
|
10
|
+
module Input
|
|
11
|
+
KEY_RUNES = -1
|
|
12
|
+
KEY_ESC = 27
|
|
13
|
+
KEY_SPACE = -25
|
|
14
|
+
|
|
15
|
+
KEY_NAMES = {
|
|
16
|
+
0 => "ctrl+@", 1 => "ctrl+a", 2 => "ctrl+b", 3 => "ctrl+c", 4 => "ctrl+d",
|
|
17
|
+
5 => "ctrl+e", 6 => "ctrl+f", 7 => "ctrl+g", 8 => "ctrl+h", 9 => "tab",
|
|
18
|
+
10 => "ctrl+j", 11 => "ctrl+k", 12 => "ctrl+l", 13 => "enter", 14 => "ctrl+n",
|
|
19
|
+
15 => "ctrl+o", 16 => "ctrl+p", 17 => "ctrl+q", 18 => "ctrl+r", 19 => "ctrl+s",
|
|
20
|
+
20 => "ctrl+t", 21 => "ctrl+u", 22 => "ctrl+v", 23 => "ctrl+w", 24 => "ctrl+x",
|
|
21
|
+
25 => "ctrl+y", 26 => "ctrl+z", 27 => "esc", 127 => "backspace",
|
|
22
|
+
-1 => "runes", -2 => "up", -3 => "down", -4 => "right", -5 => "left",
|
|
23
|
+
-6 => "home", -7 => "end", -8 => "pgup", -9 => "pgdown", -10 => "delete",
|
|
24
|
+
-11 => "insert", -12 => "f1", -13 => "f2", -14 => "f3", -15 => "f4",
|
|
25
|
+
-16 => "f5", -17 => "f6", -18 => "f7", -19 => "f8", -20 => "f9",
|
|
26
|
+
-21 => "f10", -22 => "f11", -23 => "f12", -24 => "shift+tab", -25 => "space"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# No sequence is a prefix of another, so Go's random map iteration
|
|
30
|
+
# order never changes the result.
|
|
31
|
+
ESCAPE_SEQUENCES = {
|
|
32
|
+
"\e[A" => -2, "\e[B" => -3, "\e[C" => -4, "\e[D" => -5,
|
|
33
|
+
"\e[H" => -6, "\e[F" => -7, "\e[1~" => -6, "\e[4~" => -7,
|
|
34
|
+
"\e[5~" => -8, "\e[6~" => -9, "\e[2~" => -11, "\e[3~" => -10,
|
|
35
|
+
"\eOP" => -12, "\eOQ" => -13, "\eOR" => -14, "\eOS" => -15,
|
|
36
|
+
"\e[15~" => -16, "\e[17~" => -17, "\e[18~" => -18, "\e[19~" => -19,
|
|
37
|
+
"\e[20~" => -20, "\e[21~" => -21, "\e[23~" => -22, "\e[24~" => -23,
|
|
38
|
+
"\e[Z" => -24,
|
|
39
|
+
# Upstream placeholders for shift/ctrl arrows; they have no name.
|
|
40
|
+
"\e[1;2A" => -100, "\e[1;2B" => -101, "\e[1;2C" => -102, "\e[1;2D" => -103,
|
|
41
|
+
"\e[1;5A" => -104, "\e[1;5B" => -105, "\e[1;5C" => -106, "\e[1;5D" => -107
|
|
42
|
+
}.each_with_object({}) { |(k, v), h| h[k.b.freeze] = v }.freeze
|
|
43
|
+
|
|
44
|
+
PASTE_START = "\e[200~".b.freeze
|
|
45
|
+
PASTE_END = "\e[201~".b.freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Go tea_get_key_name: "" for unknown types.
|
|
50
|
+
def key_name(key_type)
|
|
51
|
+
KEY_NAMES.fetch(key_type, "")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Go ParseInput: returns [consumed, event_hash_or_nil].
|
|
55
|
+
def parse(bytes)
|
|
56
|
+
data = bytes.b
|
|
57
|
+
return [0, nil] if data.empty?
|
|
58
|
+
|
|
59
|
+
b0 = data.getbyte(0)
|
|
60
|
+
b1 = data.getbyte(1)
|
|
61
|
+
|
|
62
|
+
if data.bytesize >= 3 && b0 == 0x1b && b1 == 0x5b
|
|
63
|
+
b2 = data.getbyte(2)
|
|
64
|
+
return [3, { "type" => "focus", "focus" => true }] if b2 == 0x49 # I
|
|
65
|
+
return [3, { "type" => "blur", "focus" => false }] if b2 == 0x4f # O
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if data.bytesize >= 6 && b0 == 0x1b && b1 == 0x5b && data.getbyte(2) == 0x3c
|
|
69
|
+
consumed, mouse = parse_mouse_sgr(data)
|
|
70
|
+
return [consumed, mouse] if consumed.positive?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if b0 == 0x1b && data.bytesize > 1
|
|
74
|
+
ESCAPE_SEQUENCES.each do |seq, key_type|
|
|
75
|
+
next unless data.start_with?(seq)
|
|
76
|
+
|
|
77
|
+
name = KEY_NAMES.fetch(key_type, "")
|
|
78
|
+
name = "unknown" if name.empty?
|
|
79
|
+
return [seq.bytesize, key_event(key_type, nil, false, name)]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if b1 >= 32 && b1 < 127
|
|
83
|
+
char = b1.chr
|
|
84
|
+
return [2, key_event(KEY_RUNES, [b1], true, "alt+#{char}")]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return [1, key_event(KEY_ESC, nil, false, "esc")]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if b0 < 32 || b0 == 127
|
|
91
|
+
name = KEY_NAMES.fetch(b0, "")
|
|
92
|
+
name = "ctrl+?" if name.empty?
|
|
93
|
+
return [1, key_event(b0, nil, false, name)]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return [1, key_event(KEY_SPACE, [32], false, "space")] if b0 == 0x20
|
|
97
|
+
|
|
98
|
+
size = utf8_size(data)
|
|
99
|
+
return [1, nil] unless size
|
|
100
|
+
|
|
101
|
+
char = data.byteslice(0, size).force_encoding(Encoding::UTF_8)
|
|
102
|
+
[size, key_event(KEY_RUNES, [char.ord], false, char)]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Decodes every event in one complete input, in order, plus bracketed
|
|
106
|
+
# paste. A paste with no end marker takes the rest of the input.
|
|
107
|
+
def parse_all(bytes)
|
|
108
|
+
decoder = Decoder.new
|
|
109
|
+
decoder.feed(bytes) + decoder.flush
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Decodes a stream of read chunks. An open bracketed paste, or a
|
|
113
|
+
# paste start marker cut off at the end of a chunk, is held until the
|
|
114
|
+
# following chunks complete it, so a paste of any length arrives as
|
|
115
|
+
# one paste event.
|
|
116
|
+
class Decoder
|
|
117
|
+
# Shortest chunk tail held as a possible split start marker. A lone
|
|
118
|
+
# "\e" or "\e[" is a real key press (esc, alt+[) and is never held.
|
|
119
|
+
MIN_HELD_PREFIX = 3
|
|
120
|
+
|
|
121
|
+
def initialize
|
|
122
|
+
@pending = "".b
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Events completed by this chunk; held bytes wait for the next one.
|
|
126
|
+
def feed(bytes)
|
|
127
|
+
data = @pending + bytes.b
|
|
128
|
+
@pending = "".b
|
|
129
|
+
events = []
|
|
130
|
+
pos = 0
|
|
131
|
+
while pos < data.bytesize
|
|
132
|
+
rest = data.byteslice(pos, data.bytesize - pos)
|
|
133
|
+
if rest.start_with?(PASTE_START)
|
|
134
|
+
body = pos + PASTE_START.bytesize
|
|
135
|
+
stop = data.index(PASTE_END, body)
|
|
136
|
+
unless stop
|
|
137
|
+
@pending = rest
|
|
138
|
+
break
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
events << Input.paste_event(data.byteslice(body, stop - body))
|
|
142
|
+
pos = stop + PASTE_END.bytesize
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if rest.bytesize >= MIN_HELD_PREFIX && rest.bytesize < PASTE_START.bytesize &&
|
|
147
|
+
PASTE_START.start_with?(rest)
|
|
148
|
+
@pending = rest
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
consumed, event = Input.parse(rest)
|
|
153
|
+
break if consumed <= 0
|
|
154
|
+
|
|
155
|
+
events << event if event
|
|
156
|
+
pos += consumed
|
|
157
|
+
end
|
|
158
|
+
events
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Events for whatever is held, as if the input ended here: an open
|
|
162
|
+
# paste becomes a paste of what arrived, a partial marker plain keys.
|
|
163
|
+
def flush
|
|
164
|
+
data = @pending
|
|
165
|
+
@pending = "".b
|
|
166
|
+
return [] if data.empty?
|
|
167
|
+
return [Input.paste_event(data.byteslice(PASTE_START.bytesize, data.bytesize))] if data.start_with?(PASTE_START)
|
|
168
|
+
|
|
169
|
+
events = []
|
|
170
|
+
pos = 0
|
|
171
|
+
while pos < data.bytesize
|
|
172
|
+
consumed, event = Input.parse(data.byteslice(pos, data.bytesize - pos))
|
|
173
|
+
events << event if event
|
|
174
|
+
pos += consumed
|
|
175
|
+
end
|
|
176
|
+
events
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def key_event(key_type, runes, alt, name)
|
|
181
|
+
{ "type" => "key", "key_type" => key_type, "runes" => runes, "alt" => alt, "name" => name }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def paste_event(bytes)
|
|
185
|
+
text = bytes.dup.force_encoding(Encoding::UTF_8).scrub
|
|
186
|
+
{ "type" => "key", "key_type" => KEY_RUNES, "runes" => text.codepoints,
|
|
187
|
+
"alt" => false, "name" => "[#{text}]", "paste" => true }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Go parseMouseSGR: ESC [ < Cb ; Cx ; Cy (M|m). Returns [0, nil] on failure.
|
|
191
|
+
def parse_mouse_sgr(data)
|
|
192
|
+
end_index = nil
|
|
193
|
+
i = 3
|
|
194
|
+
while i < data.bytesize && i < 32
|
|
195
|
+
byte = data.getbyte(i)
|
|
196
|
+
if byte == 0x4d || byte == 0x6d # M / m
|
|
197
|
+
end_index = i
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
i += 1
|
|
201
|
+
end
|
|
202
|
+
return [0, nil] unless end_index
|
|
203
|
+
|
|
204
|
+
count, (button, x, y) = parse_ints(data.byteslice(3, end_index - 3), 3)
|
|
205
|
+
return [0, nil] unless count == 3
|
|
206
|
+
|
|
207
|
+
button_num = button & 3
|
|
208
|
+
action = if data.getbyte(end_index) == 0x6d then 1
|
|
209
|
+
elsif button.anybits?(32) then 2
|
|
210
|
+
else 0
|
|
211
|
+
end
|
|
212
|
+
if button.anybits?(64)
|
|
213
|
+
if button_num.zero? then button_num = 4
|
|
214
|
+
elsif button_num == 1 then button_num = 5
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
[end_index + 1, {
|
|
219
|
+
"type" => "mouse", "x" => x - 1, "y" => y - 1, "button" => button_num, "action" => action,
|
|
220
|
+
"shift" => button.anybits?(4), "alt" => button.anybits?(8), "ctrl" => button.anybits?(16)
|
|
221
|
+
}]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Go parseInts: empty fields are skipped (slot left 0, not counted);
|
|
225
|
+
# a non-digit stops parsing and returns the count so far.
|
|
226
|
+
def parse_ints(str, slots)
|
|
227
|
+
vals = Array.new(slots, 0)
|
|
228
|
+
count = 0
|
|
229
|
+
start = 0
|
|
230
|
+
index = 0
|
|
231
|
+
i = 0
|
|
232
|
+
while i <= str.bytesize && index < slots
|
|
233
|
+
if i == str.bytesize || str.getbyte(i) == 0x3b
|
|
234
|
+
if i > start
|
|
235
|
+
num = 0
|
|
236
|
+
(start...i).each do |j|
|
|
237
|
+
d = str.getbyte(j)
|
|
238
|
+
return [count, vals] if d < 0x30 || d > 0x39
|
|
239
|
+
|
|
240
|
+
num = (num * 10) + (d - 0x30)
|
|
241
|
+
end
|
|
242
|
+
vals[index] = num
|
|
243
|
+
count += 1
|
|
244
|
+
end
|
|
245
|
+
index += 1
|
|
246
|
+
start = i + 1
|
|
247
|
+
end
|
|
248
|
+
i += 1
|
|
249
|
+
end
|
|
250
|
+
[count, vals]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Byte length of the valid UTF-8 rune at the start of data, or nil
|
|
254
|
+
# where Go's utf8.DecodeRune returns (RuneError, 1).
|
|
255
|
+
def utf8_size(data)
|
|
256
|
+
b0 = data.getbyte(0)
|
|
257
|
+
len = if b0 < 0x80 then 1
|
|
258
|
+
elsif b0 >= 0xc2 && b0 <= 0xdf then 2
|
|
259
|
+
elsif b0 >= 0xe0 && b0 <= 0xef then 3
|
|
260
|
+
elsif b0 >= 0xf0 && b0 <= 0xf4 then 4
|
|
261
|
+
end
|
|
262
|
+
return nil unless len && data.bytesize >= len
|
|
263
|
+
|
|
264
|
+
data.byteslice(0, len).force_encoding(Encoding::UTF_8).valid_encoding? ? len : nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2UI
|
|
4
|
+
module Compat
|
|
5
|
+
module Tea
|
|
6
|
+
# Upstream's InputReader (go/input.go): a background reader that pushes raw chunks of up to 256
|
|
7
|
+
# bytes onto a queue. Go reads through cancelreader so that a stopped reader no longer consumes
|
|
8
|
+
# stdin; here the thread waits with IO.select on a short timeout and checks a stop flag, and
|
|
9
|
+
# only reads once the input is readable. EOF or a read error ends the thread.
|
|
10
|
+
class InputReader
|
|
11
|
+
CHUNK = 256
|
|
12
|
+
WAIT = 0.02
|
|
13
|
+
|
|
14
|
+
attr_reader :queue
|
|
15
|
+
|
|
16
|
+
def initialize(io)
|
|
17
|
+
@io = io
|
|
18
|
+
@queue = Thread::Queue.new
|
|
19
|
+
@stop = false
|
|
20
|
+
@thread = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start
|
|
24
|
+
@thread ||= Thread.new do
|
|
25
|
+
Thread.current.report_on_exception = false
|
|
26
|
+
Thread.current.name = "bubbletea input reader"
|
|
27
|
+
read_loop
|
|
28
|
+
end
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def running? = !@thread.nil? && !@stop
|
|
33
|
+
|
|
34
|
+
# Stop consuming input. With wait, joins the thread (it notices the flag within WAIT seconds);
|
|
35
|
+
# a thread stuck in a read after all is killed rather than left to steal a child's input.
|
|
36
|
+
def stop(wait: true)
|
|
37
|
+
@stop = true
|
|
38
|
+
thread = @thread
|
|
39
|
+
return unless thread && wait && thread != Thread.current
|
|
40
|
+
|
|
41
|
+
thread.kill unless thread.join(WAIT * 10)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# One chunk, or nil when none arrives within timeout_ms.
|
|
46
|
+
def pop(timeout_ms)
|
|
47
|
+
timeout = timeout_ms.to_i <= 0 ? 0 : timeout_ms / 1000.0
|
|
48
|
+
@queue.pop(timeout: timeout)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def read_loop
|
|
54
|
+
until @stop
|
|
55
|
+
ready = IO.select([@io], nil, nil, WAIT)
|
|
56
|
+
next unless ready && !@stop
|
|
57
|
+
|
|
58
|
+
@queue << @io.readpartial(CHUNK)
|
|
59
|
+
end
|
|
60
|
+
rescue EOFError, IOError, SystemCallError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copied unchanged from bubbletea 0.1.4 (https://github.com/marcoroth/bubbletea-ruby).
|
|
4
|
+
# Copyright (c) 2025 Marco Roth. MIT License; see LICENSE-bubbletea.txt in this directory.
|
|
5
|
+
|
|
6
|
+
module Bubbletea
|
|
7
|
+
class Message
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class KeyMessage < Message
|
|
11
|
+
KEY_NULL = 0
|
|
12
|
+
KEY_CTRL_A = 1
|
|
13
|
+
KEY_CTRL_B = 2
|
|
14
|
+
KEY_CTRL_C = 3
|
|
15
|
+
KEY_CTRL_D = 4
|
|
16
|
+
KEY_CTRL_E = 5
|
|
17
|
+
KEY_CTRL_F = 6
|
|
18
|
+
KEY_CTRL_G = 7
|
|
19
|
+
KEY_CTRL_H = 8
|
|
20
|
+
KEY_TAB = 9
|
|
21
|
+
KEY_CTRL_J = 10
|
|
22
|
+
KEY_CTRL_K = 11
|
|
23
|
+
KEY_CTRL_L = 12
|
|
24
|
+
KEY_ENTER = 13
|
|
25
|
+
KEY_CTRL_N = 14
|
|
26
|
+
KEY_CTRL_O = 15
|
|
27
|
+
KEY_CTRL_P = 16
|
|
28
|
+
KEY_CTRL_Q = 17
|
|
29
|
+
KEY_CTRL_R = 18
|
|
30
|
+
KEY_CTRL_S = 19
|
|
31
|
+
KEY_CTRL_T = 20
|
|
32
|
+
KEY_CTRL_U = 21
|
|
33
|
+
KEY_CTRL_V = 22
|
|
34
|
+
KEY_CTRL_W = 23
|
|
35
|
+
KEY_CTRL_X = 24
|
|
36
|
+
KEY_CTRL_Y = 25
|
|
37
|
+
KEY_CTRL_Z = 26
|
|
38
|
+
KEY_ESC = 27
|
|
39
|
+
KEY_BACKSPACE = 127
|
|
40
|
+
|
|
41
|
+
KEY_RUNES = -1
|
|
42
|
+
KEY_UP = -2
|
|
43
|
+
KEY_DOWN = -3
|
|
44
|
+
KEY_RIGHT = -4
|
|
45
|
+
KEY_LEFT = -5
|
|
46
|
+
KEY_HOME = -6
|
|
47
|
+
KEY_END = -7
|
|
48
|
+
KEY_PGUP = -8
|
|
49
|
+
KEY_PGDOWN = -9
|
|
50
|
+
KEY_DELETE = -10
|
|
51
|
+
KEY_INSERT = -11
|
|
52
|
+
KEY_F1 = -12
|
|
53
|
+
KEY_F2 = -13
|
|
54
|
+
KEY_F3 = -14
|
|
55
|
+
KEY_F4 = -15
|
|
56
|
+
KEY_F5 = -16
|
|
57
|
+
KEY_F6 = -17
|
|
58
|
+
KEY_F7 = -18
|
|
59
|
+
KEY_F8 = -19
|
|
60
|
+
KEY_F9 = -20
|
|
61
|
+
KEY_F10 = -21
|
|
62
|
+
KEY_F11 = -22
|
|
63
|
+
KEY_F12 = -23
|
|
64
|
+
KEY_SHIFT_TAB = -24
|
|
65
|
+
KEY_SPACE = -25
|
|
66
|
+
|
|
67
|
+
attr_reader :key_type, :runes, :alt, :name
|
|
68
|
+
|
|
69
|
+
def initialize(key_type:, runes: [], alt: false, name: nil)
|
|
70
|
+
super()
|
|
71
|
+
|
|
72
|
+
@key_type = key_type
|
|
73
|
+
@runes = runes.is_a?(Array) ? runes : []
|
|
74
|
+
@alt = alt
|
|
75
|
+
@name = name || lookup_key_name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def lookup_key_name
|
|
81
|
+
base_name = if @key_type == KEY_RUNES && char
|
|
82
|
+
char
|
|
83
|
+
else
|
|
84
|
+
go_name = Bubbletea.get_key_name(@key_type)
|
|
85
|
+
go_name.empty? ? "unknown" : go_name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@alt ? "alt+#{base_name}" : base_name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
public
|
|
92
|
+
|
|
93
|
+
def to_s
|
|
94
|
+
@name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def char
|
|
98
|
+
@runes.pack("U*") if @runes.any?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ctrl?
|
|
102
|
+
@key_type.between?(0, 31)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def runes?
|
|
106
|
+
@key_type == KEY_RUNES
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def space?
|
|
110
|
+
@key_type == KEY_SPACE
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def enter?
|
|
114
|
+
@key_type == KEY_ENTER
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def backspace?
|
|
118
|
+
@key_type == KEY_BACKSPACE
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def tab?
|
|
122
|
+
@key_type == KEY_TAB
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def esc?
|
|
126
|
+
@key_type == KEY_ESC
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def up?
|
|
130
|
+
@key_type == KEY_UP
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def down?
|
|
134
|
+
@key_type == KEY_DOWN
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def left?
|
|
138
|
+
@key_type == KEY_LEFT
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def right?
|
|
142
|
+
@key_type == KEY_RIGHT
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class MouseMessage < Message
|
|
147
|
+
BUTTON_NONE = 0
|
|
148
|
+
BUTTON_LEFT = 1
|
|
149
|
+
BUTTON_MIDDLE = 2
|
|
150
|
+
BUTTON_RIGHT = 3
|
|
151
|
+
BUTTON_WHEEL_UP = 4
|
|
152
|
+
BUTTON_WHEEL_DOWN = 5
|
|
153
|
+
|
|
154
|
+
ACTION_PRESS = 0
|
|
155
|
+
ACTION_RELEASE = 1
|
|
156
|
+
ACTION_MOTION = 2
|
|
157
|
+
|
|
158
|
+
attr_reader :x, :y, :button, :action, :shift, :alt, :ctrl
|
|
159
|
+
|
|
160
|
+
def initialize(x:, y:, button:, action:, shift: false, alt: false, ctrl: false)
|
|
161
|
+
super()
|
|
162
|
+
|
|
163
|
+
@x = x
|
|
164
|
+
@y = y
|
|
165
|
+
@button = button
|
|
166
|
+
@action = action
|
|
167
|
+
@shift = shift
|
|
168
|
+
@alt = alt
|
|
169
|
+
@ctrl = ctrl
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def press?
|
|
173
|
+
@action == ACTION_PRESS
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def release?
|
|
177
|
+
@action == ACTION_RELEASE
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def motion?
|
|
181
|
+
@action == ACTION_MOTION
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def wheel?
|
|
185
|
+
@button >= BUTTON_WHEEL_UP
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def left?
|
|
189
|
+
@button == BUTTON_LEFT
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def right?
|
|
193
|
+
@button == BUTTON_RIGHT
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def middle?
|
|
197
|
+
@button == BUTTON_MIDDLE
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
class WindowSizeMessage < Message
|
|
202
|
+
attr_reader :width, :height
|
|
203
|
+
|
|
204
|
+
def initialize(width:, height:)
|
|
205
|
+
super()
|
|
206
|
+
|
|
207
|
+
@width = width
|
|
208
|
+
@height = height
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class FocusMessage < Message
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class BlurMessage < Message
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
class QuitMessage < Message
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
class ResumeMessage < Message
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.parse_event(hash)
|
|
225
|
+
return nil if hash.nil?
|
|
226
|
+
|
|
227
|
+
case hash["type"]
|
|
228
|
+
when "key"
|
|
229
|
+
KeyMessage.new(
|
|
230
|
+
key_type: hash["key_type"],
|
|
231
|
+
runes: hash["runes"] || [],
|
|
232
|
+
alt: hash["alt"] || false,
|
|
233
|
+
name: hash["name"]
|
|
234
|
+
)
|
|
235
|
+
when "mouse"
|
|
236
|
+
MouseMessage.new(
|
|
237
|
+
x: hash["x"],
|
|
238
|
+
y: hash["y"],
|
|
239
|
+
button: hash["button"],
|
|
240
|
+
action: hash["action"],
|
|
241
|
+
shift: hash["shift"] || false,
|
|
242
|
+
alt: hash["alt"] || false,
|
|
243
|
+
ctrl: hash["ctrl"] || false
|
|
244
|
+
)
|
|
245
|
+
when "focus"
|
|
246
|
+
FocusMessage.new
|
|
247
|
+
when "blur"
|
|
248
|
+
BlurMessage.new
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copied unchanged from bubbletea 0.1.4 (https://github.com/marcoroth/bubbletea-ruby).
|
|
4
|
+
# Copyright (c) 2025 Marco Roth. MIT License; see LICENSE-bubbletea.txt in this directory.
|
|
5
|
+
|
|
6
|
+
module Bubbletea
|
|
7
|
+
# Model module that provides the Elm Architecture interface.
|
|
8
|
+
# Include this module in your model class and implement:
|
|
9
|
+
# - init: Returns [model, command] - initial state and optional command
|
|
10
|
+
# - update(message): Returns [model, command] - new state and optional command
|
|
11
|
+
# - view: Returns String - the current view to render
|
|
12
|
+
#
|
|
13
|
+
# Example:
|
|
14
|
+
# class Counter
|
|
15
|
+
# include Bubbletea::Model
|
|
16
|
+
#
|
|
17
|
+
# attr_reader :count
|
|
18
|
+
#
|
|
19
|
+
# def initialize
|
|
20
|
+
# @count = 0
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def init
|
|
24
|
+
# [self, nil]
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def update(message)
|
|
28
|
+
# case message
|
|
29
|
+
# when Bubbletea::KeyMessage
|
|
30
|
+
# case message.to_s
|
|
31
|
+
# when "q", "ctrl+c"
|
|
32
|
+
# [self, Bubbletea.quit]
|
|
33
|
+
# when "up", "k"
|
|
34
|
+
# @count += 1
|
|
35
|
+
# [self, nil]
|
|
36
|
+
# when "down", "j"
|
|
37
|
+
# @count -= 1
|
|
38
|
+
# [self, nil]
|
|
39
|
+
# else
|
|
40
|
+
# [self, nil]
|
|
41
|
+
# end
|
|
42
|
+
# else
|
|
43
|
+
# [self, nil]
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# def view
|
|
48
|
+
# "Count: #{@count}\n\nPress up/down to change, q to quit"
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
module Model
|
|
53
|
+
def init
|
|
54
|
+
[self, nil]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def update(_message)
|
|
58
|
+
[self, nil]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def view
|
|
62
|
+
""
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|