bubbletea 0.1.0-arm-linux-gnu
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- 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/build/linux_arm/libbubbletea.a +0 -0
- data/go/build/linux_arm/libbubbletea.h +147 -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/3.2/bubbletea.so +0 -0
- data/lib/bubbletea/3.3/bubbletea.so +0 -0
- data/lib/bubbletea/3.4/bubbletea.so +0 -0
- data/lib/bubbletea/4.0/bubbletea.so +0 -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 +5 -0
- data/lib/bubbletea.rb +19 -0
- metadata +88 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bubbletea
|
|
4
|
+
# Runner manages the event loop and coordinates between the model and the terminal
|
|
5
|
+
class Runner
|
|
6
|
+
attr_reader :options
|
|
7
|
+
|
|
8
|
+
DEFAULT_OPTIONS = {
|
|
9
|
+
alt_screen: false,
|
|
10
|
+
mouse_cell_motion: false,
|
|
11
|
+
mouse_all_motion: false,
|
|
12
|
+
bracketed_paste: false,
|
|
13
|
+
report_focus: false,
|
|
14
|
+
fps: 60,
|
|
15
|
+
input_timeout: 10,
|
|
16
|
+
without_renderer: false,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(model, **options)
|
|
20
|
+
@model = model
|
|
21
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
22
|
+
@program = Program.new
|
|
23
|
+
@renderer_id = nil
|
|
24
|
+
@running = false
|
|
25
|
+
@pending_ticks = []
|
|
26
|
+
@width = 80
|
|
27
|
+
@height = 24
|
|
28
|
+
@resize_pending = false
|
|
29
|
+
@previous_winch_handler = nil
|
|
30
|
+
@in_alt_screen = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
setup_terminal
|
|
35
|
+
@renderer_id = @program.create_renderer unless @options[:without_renderer]
|
|
36
|
+
|
|
37
|
+
update_terminal_size
|
|
38
|
+
@running = true
|
|
39
|
+
|
|
40
|
+
new_model, command = @model.init
|
|
41
|
+
@model = new_model if new_model
|
|
42
|
+
process_command(command)
|
|
43
|
+
|
|
44
|
+
return unless @running
|
|
45
|
+
|
|
46
|
+
handle_message(WindowSizeMessage.new(width: @width, height: @height))
|
|
47
|
+
|
|
48
|
+
render
|
|
49
|
+
run_loop
|
|
50
|
+
render
|
|
51
|
+
ensure
|
|
52
|
+
cleanup_terminal
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def send(message)
|
|
56
|
+
@pending_messages ||= []
|
|
57
|
+
@pending_messages << message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def setup_terminal
|
|
63
|
+
@program.enter_raw_mode
|
|
64
|
+
@program.hide_cursor
|
|
65
|
+
@program.start_input_reader
|
|
66
|
+
|
|
67
|
+
if @options[:alt_screen]
|
|
68
|
+
@program.enter_alt_screen
|
|
69
|
+
@program.renderer_set_alt_screen(@renderer_id, true) if @renderer_id
|
|
70
|
+
@in_alt_screen = true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@program.enable_mouse_cell_motion if @options[:mouse_cell_motion]
|
|
74
|
+
@program.enable_mouse_all_motion if @options[:mouse_all_motion]
|
|
75
|
+
@program.enable_bracketed_paste if @options[:bracketed_paste]
|
|
76
|
+
@program.enable_report_focus if @options[:report_focus]
|
|
77
|
+
|
|
78
|
+
setup_resize_handler
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cleanup_terminal
|
|
82
|
+
restore_resize_handler
|
|
83
|
+
|
|
84
|
+
@program.disable_mouse if @options[:mouse_cell_motion] || @options[:mouse_all_motion]
|
|
85
|
+
@program.disable_bracketed_paste if @options[:bracketed_paste]
|
|
86
|
+
@program.disable_report_focus if @options[:report_focus]
|
|
87
|
+
|
|
88
|
+
if @in_alt_screen
|
|
89
|
+
@program.exit_alt_screen
|
|
90
|
+
else
|
|
91
|
+
print "\r\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@program.show_cursor
|
|
95
|
+
@program.stop_input_reader
|
|
96
|
+
@program.exit_raw_mode
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def setup_resize_handler
|
|
100
|
+
@previous_winch_handler = Signal.trap("WINCH") do
|
|
101
|
+
@resize_pending = true
|
|
102
|
+
@previous_winch_handler.call if @previous_winch_handler.is_a?(Proc)
|
|
103
|
+
end
|
|
104
|
+
rescue ArgumentError
|
|
105
|
+
# SIGWINCH not supported on this platform
|
|
106
|
+
@previous_winch_handler = nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def restore_resize_handler
|
|
110
|
+
if @previous_winch_handler
|
|
111
|
+
Signal.trap("WINCH", @previous_winch_handler)
|
|
112
|
+
else
|
|
113
|
+
Signal.trap("WINCH", "DEFAULT")
|
|
114
|
+
end
|
|
115
|
+
rescue ArgumentError
|
|
116
|
+
# SIGWINCH not supported on this platform
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def update_terminal_size
|
|
120
|
+
size = @program.terminal_size
|
|
121
|
+
return unless size
|
|
122
|
+
|
|
123
|
+
@width, @height = size
|
|
124
|
+
@program.renderer_set_size(@renderer_id, @width, @height) if @renderer_id
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def run_loop
|
|
128
|
+
frame_duration = 1.0 / @options[:fps]
|
|
129
|
+
last_frame = Time.now
|
|
130
|
+
|
|
131
|
+
while @running
|
|
132
|
+
check_resize
|
|
133
|
+
process_pending_messages
|
|
134
|
+
|
|
135
|
+
event = @program.poll_event(@options[:input_timeout])
|
|
136
|
+
|
|
137
|
+
if event
|
|
138
|
+
message = Bubbletea.parse_event(event)
|
|
139
|
+
handle_message(message) if message
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
process_ticks
|
|
143
|
+
|
|
144
|
+
now = Time.now
|
|
145
|
+
|
|
146
|
+
if now - last_frame >= frame_duration
|
|
147
|
+
render
|
|
148
|
+
last_frame = now
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def check_resize
|
|
154
|
+
return unless @resize_pending
|
|
155
|
+
|
|
156
|
+
@resize_pending = false
|
|
157
|
+
|
|
158
|
+
size = @program.terminal_size
|
|
159
|
+
return unless size
|
|
160
|
+
|
|
161
|
+
new_width, new_height = size
|
|
162
|
+
|
|
163
|
+
return if new_width == @width && new_height == @height
|
|
164
|
+
|
|
165
|
+
@width = new_width
|
|
166
|
+
@height = new_height
|
|
167
|
+
|
|
168
|
+
@program.renderer_set_size(@renderer_id, @width, @height) if @renderer_id
|
|
169
|
+
|
|
170
|
+
handle_message(WindowSizeMessage.new(width: @width, height: @height))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def process_pending_messages
|
|
174
|
+
return unless @pending_messages&.any?
|
|
175
|
+
|
|
176
|
+
messages = @pending_messages
|
|
177
|
+
@pending_messages = []
|
|
178
|
+
|
|
179
|
+
messages.each { |message| handle_message(message) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def handle_message(message)
|
|
183
|
+
return unless @running
|
|
184
|
+
|
|
185
|
+
new_model, command = @model.update(message)
|
|
186
|
+
@model = new_model if new_model
|
|
187
|
+
process_command(command)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def process_command(command)
|
|
191
|
+
return if command.nil?
|
|
192
|
+
|
|
193
|
+
case command
|
|
194
|
+
when QuitCommand
|
|
195
|
+
@running = false
|
|
196
|
+
|
|
197
|
+
when BatchCommand
|
|
198
|
+
Thread.new do
|
|
199
|
+
execute_batch_sync(command.commands)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
when SequenceCommand
|
|
203
|
+
Thread.new do
|
|
204
|
+
execute_sequence_sync(command.commands)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
when TickCommand
|
|
208
|
+
schedule_tick(command)
|
|
209
|
+
|
|
210
|
+
when SendMessage
|
|
211
|
+
if command.delay.positive?
|
|
212
|
+
schedule_delayed_message(command)
|
|
213
|
+
else
|
|
214
|
+
handle_message(command.message)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
when EnterAltScreenCommand
|
|
218
|
+
@program.enter_alt_screen
|
|
219
|
+
@program.renderer_set_alt_screen(@renderer_id, true) if @renderer_id
|
|
220
|
+
@in_alt_screen = true
|
|
221
|
+
|
|
222
|
+
when ExitAltScreenCommand
|
|
223
|
+
@program.exit_alt_screen
|
|
224
|
+
@program.renderer_set_alt_screen(@renderer_id, false) if @renderer_id
|
|
225
|
+
@in_alt_screen = false
|
|
226
|
+
|
|
227
|
+
when SetWindowTitleCommand
|
|
228
|
+
Bubbletea._set_window_title(command.title)
|
|
229
|
+
|
|
230
|
+
when PutsCommand
|
|
231
|
+
warn "\r#{command.text}\r"
|
|
232
|
+
|
|
233
|
+
when SuspendCommand
|
|
234
|
+
suspend_process
|
|
235
|
+
|
|
236
|
+
when Proc
|
|
237
|
+
Thread.new do
|
|
238
|
+
result = command.call
|
|
239
|
+
next unless result
|
|
240
|
+
|
|
241
|
+
if result.is_a?(Message)
|
|
242
|
+
send(result)
|
|
243
|
+
else
|
|
244
|
+
process_command(result)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def execute_command_sync(command)
|
|
251
|
+
return if command.nil?
|
|
252
|
+
|
|
253
|
+
case command
|
|
254
|
+
when QuitCommand
|
|
255
|
+
@running = false
|
|
256
|
+
|
|
257
|
+
when BatchCommand
|
|
258
|
+
execute_batch_sync(command.commands)
|
|
259
|
+
|
|
260
|
+
when SequenceCommand
|
|
261
|
+
execute_sequence_sync(command.commands)
|
|
262
|
+
|
|
263
|
+
when TickCommand
|
|
264
|
+
schedule_tick(command)
|
|
265
|
+
|
|
266
|
+
when SendMessage
|
|
267
|
+
sleep(command.delay) if command.delay.positive?
|
|
268
|
+
handle_message(command.message)
|
|
269
|
+
|
|
270
|
+
when EnterAltScreenCommand
|
|
271
|
+
@program.enter_alt_screen
|
|
272
|
+
@program.renderer_set_alt_screen(@renderer_id, true) if @renderer_id
|
|
273
|
+
@in_alt_screen = true
|
|
274
|
+
|
|
275
|
+
when ExitAltScreenCommand
|
|
276
|
+
@program.exit_alt_screen
|
|
277
|
+
@program.renderer_set_alt_screen(@renderer_id, false) if @renderer_id
|
|
278
|
+
@in_alt_screen = false
|
|
279
|
+
|
|
280
|
+
when SetWindowTitleCommand
|
|
281
|
+
Bubbletea._set_window_title(command.title)
|
|
282
|
+
|
|
283
|
+
when PutsCommand
|
|
284
|
+
warn "\r#{command.text}\r"
|
|
285
|
+
|
|
286
|
+
when SuspendCommand
|
|
287
|
+
suspend_process
|
|
288
|
+
|
|
289
|
+
when Proc
|
|
290
|
+
result = command.call
|
|
291
|
+
return unless result
|
|
292
|
+
|
|
293
|
+
if result.is_a?(Message)
|
|
294
|
+
send(result)
|
|
295
|
+
else
|
|
296
|
+
execute_command_sync(result)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def execute_sequence_sync(commands)
|
|
302
|
+
commands.each do |cmd|
|
|
303
|
+
break unless @running
|
|
304
|
+
|
|
305
|
+
execute_command_sync(cmd)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def execute_batch_sync(commands)
|
|
310
|
+
threads = commands.map do |cmd|
|
|
311
|
+
Thread.new { execute_command_sync(cmd) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
threads.each(&:join)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def schedule_tick(tick_command)
|
|
318
|
+
@pending_ticks << {
|
|
319
|
+
at: Time.now + tick_command.duration,
|
|
320
|
+
callback: tick_command.callback,
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def schedule_delayed_message(send_command)
|
|
325
|
+
@pending_ticks << {
|
|
326
|
+
at: Time.now + send_command.delay,
|
|
327
|
+
message: send_command.message,
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def process_ticks
|
|
332
|
+
now = Time.now
|
|
333
|
+
ready, @pending_ticks = @pending_ticks.partition { |tick| tick[:at] <= now }
|
|
334
|
+
|
|
335
|
+
ready.each do |tick|
|
|
336
|
+
if tick[:callback]
|
|
337
|
+
result = tick[:callback].call
|
|
338
|
+
handle_message(result) if result
|
|
339
|
+
elsif tick[:message]
|
|
340
|
+
handle_message(tick[:message])
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def suspend_process
|
|
346
|
+
@program.disable_mouse if @options[:mouse_cell_motion] || @options[:mouse_all_motion]
|
|
347
|
+
@program.show_cursor
|
|
348
|
+
@program.stop_input_reader
|
|
349
|
+
@program.exit_raw_mode
|
|
350
|
+
|
|
351
|
+
Process.kill("TSTP", Process.pid)
|
|
352
|
+
|
|
353
|
+
# When we get here, we've been resumed (SIGCONT was received)
|
|
354
|
+
@program.enter_raw_mode
|
|
355
|
+
@program.hide_cursor
|
|
356
|
+
@program.start_input_reader
|
|
357
|
+
@program.enable_mouse_cell_motion if @options[:mouse_cell_motion]
|
|
358
|
+
@program.enable_mouse_all_motion if @options[:mouse_all_motion]
|
|
359
|
+
|
|
360
|
+
handle_message(ResumeMessage.new)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def render
|
|
364
|
+
return if @options[:without_renderer]
|
|
365
|
+
return unless @renderer_id
|
|
366
|
+
|
|
367
|
+
view = @model.view
|
|
368
|
+
@program.render(@renderer_id, view)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def self.run(model, **options)
|
|
373
|
+
runner = Runner.new(model, **options)
|
|
374
|
+
runner.run
|
|
375
|
+
end
|
|
376
|
+
end
|
data/lib/bubbletea.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "bubbletea/version"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
major, minor, _patch = RUBY_VERSION.split(".") #: [String, String, String]
|
|
7
|
+
require_relative "bubbletea/#{major}.#{minor}/bubbletea"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
require_relative "bubbletea/bubbletea"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "bubbletea/messages"
|
|
13
|
+
require_relative "bubbletea/commands"
|
|
14
|
+
require_relative "bubbletea/model"
|
|
15
|
+
require_relative "bubbletea/runner"
|
|
16
|
+
|
|
17
|
+
module Bubbletea
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bubbletea
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: arm-linux-gnu
|
|
6
|
+
authors:
|
|
7
|
+
- Marco Roth
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lipgloss
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
description: Build beautiful, interactive terminal applications using the Elm Architecture
|
|
27
|
+
in Ruby.
|
|
28
|
+
email:
|
|
29
|
+
- marco.roth@intergga.ch
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- bubbletea.gemspec
|
|
37
|
+
- ext/bubbletea/extconf.rb
|
|
38
|
+
- ext/bubbletea/extension.c
|
|
39
|
+
- ext/bubbletea/extension.h
|
|
40
|
+
- ext/bubbletea/program.c
|
|
41
|
+
- go/bubbletea.go
|
|
42
|
+
- go/build/linux_arm/libbubbletea.a
|
|
43
|
+
- go/build/linux_arm/libbubbletea.h
|
|
44
|
+
- go/go.mod
|
|
45
|
+
- go/go.sum
|
|
46
|
+
- go/input.go
|
|
47
|
+
- go/keys.go
|
|
48
|
+
- go/renderer.go
|
|
49
|
+
- go/terminal.go
|
|
50
|
+
- lib/bubbletea.rb
|
|
51
|
+
- lib/bubbletea/3.2/bubbletea.so
|
|
52
|
+
- lib/bubbletea/3.3/bubbletea.so
|
|
53
|
+
- lib/bubbletea/3.4/bubbletea.so
|
|
54
|
+
- lib/bubbletea/4.0/bubbletea.so
|
|
55
|
+
- lib/bubbletea/commands.rb
|
|
56
|
+
- lib/bubbletea/messages.rb
|
|
57
|
+
- lib/bubbletea/model.rb
|
|
58
|
+
- lib/bubbletea/runner.rb
|
|
59
|
+
- lib/bubbletea/version.rb
|
|
60
|
+
homepage: https://github.com/marcoroth/bubbletea-ruby
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/marcoroth/bubbletea-ruby
|
|
65
|
+
source_code_uri: https://github.com/marcoroth/bubbletea-ruby
|
|
66
|
+
changelog_uri: https://github.com/marcoroth/bubbletea-ruby/releases
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.2'
|
|
76
|
+
- - "<"
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 4.1.dev
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 3.3.22
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 4.0.3
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Ruby wrapper for Charm's bubbletea. A powerful TUI framework.
|
|
88
|
+
test_files: []
|