bubbletea 0.1.0-x86_64-linux-musl

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,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bubbletea
4
+ VERSION = "0.1.0"
5
+ 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: x86_64-linux-musl
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_amd64/libbubbletea.a
43
+ - go/build/linux_amd64/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: []