ratatui_ruby-tea 0.3.0 → 0.3.1
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/AGENTS.md +9 -5
- data/CHANGELOG.md +33 -0
- data/README.md +17 -0
- data/Rakefile +1 -1
- data/Steepfile +3 -3
- data/doc/contributors/design/commands_and_outlets.md +204 -0
- data/doc/contributors/kit-no-outlet.md +237 -0
- data/lib/ratatui_ruby/tea/command/cancellation_token.rb +135 -0
- data/lib/ratatui_ruby/tea/command/custom.rb +106 -0
- data/lib/ratatui_ruby/tea/command/outlet.rb +127 -0
- data/lib/ratatui_ruby/tea/command.rb +231 -9
- data/lib/ratatui_ruby/tea/router.rb +150 -82
- data/lib/ratatui_ruby/tea/runtime.rb +94 -50
- data/lib/ratatui_ruby/tea/version.rb +1 -1
- data/rbs_collection.lock.yaml +124 -0
- data/rbs_collection.yaml +15 -0
- data/sig/examples/verify_readme_usage/app.rbs +1 -1
- data/sig/examples/widget_command_system/app.rbs +1 -1
- data/sig/open3.rbs +17 -0
- data/sig/ratatui_ruby/tea/command.rbs +122 -6
- data/sig/ratatui_ruby/tea/router.rbs +110 -54
- data/sig/ratatui_ruby/tea/runtime.rbs +29 -11
- data/sig/ratatui_ruby/tea/shortcuts.rbs +18 -0
- data/sig/ratatui_ruby/tea/version.rbs +10 -0
- data/sig/ratatui_ruby/tea.rbs +19 -7
- data/tasks/steep.rake +11 -0
- metadata +14 -3
|
@@ -39,6 +39,27 @@ module RatatuiRuby
|
|
|
39
39
|
# UPDATE = from_router
|
|
40
40
|
# end
|
|
41
41
|
module Router
|
|
42
|
+
# Configuration for key handlers.
|
|
43
|
+
KeyHandlerConfig = Data.define(:handler, :action, :route, :guard) do
|
|
44
|
+
def initialize(handler: nil, action: nil, route: nil, guard: nil)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Configuration for scroll handlers (no coordinates).
|
|
50
|
+
ScrollHandlerConfig = Data.define(:handler, :action) do
|
|
51
|
+
def initialize(handler: nil, action: nil)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Configuration for click handlers (x, y coordinates).
|
|
57
|
+
ClickHandlerConfig = Data.define(:handler, :action) do
|
|
58
|
+
def initialize(handler: nil, action: nil)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
42
63
|
# :nodoc:
|
|
43
64
|
def self.included(base)
|
|
44
65
|
base.extend(ClassMethods)
|
|
@@ -90,11 +111,6 @@ module RatatuiRuby
|
|
|
90
111
|
@key_handlers = builder.handlers
|
|
91
112
|
end
|
|
92
113
|
|
|
93
|
-
# Returns the registered key handlers hash.
|
|
94
|
-
def key_handlers
|
|
95
|
-
@key_handlers ||= {}
|
|
96
|
-
end
|
|
97
|
-
|
|
98
114
|
# Declares mouse handlers in a block.
|
|
99
115
|
#
|
|
100
116
|
# === Example
|
|
@@ -106,12 +122,23 @@ module RatatuiRuby
|
|
|
106
122
|
def mousemap(&)
|
|
107
123
|
builder = MousemapBuilder.new
|
|
108
124
|
builder.instance_eval(&)
|
|
109
|
-
@
|
|
125
|
+
@scroll_handlers = builder.scroll_handlers
|
|
126
|
+
@click_handler = builder.click_handler
|
|
110
127
|
end
|
|
111
128
|
|
|
112
|
-
# Returns the registered
|
|
113
|
-
def
|
|
114
|
-
@
|
|
129
|
+
# Returns the registered key handlers hash.
|
|
130
|
+
private def key_handlers
|
|
131
|
+
@key_handlers ||= {}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns the registered scroll handlers hash.
|
|
135
|
+
private def scroll_handlers
|
|
136
|
+
@scroll_handlers ||= {}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns the registered click handler, if any.
|
|
140
|
+
private def click_handler
|
|
141
|
+
@click_handler
|
|
115
142
|
end
|
|
116
143
|
|
|
117
144
|
# Generates an UPDATE lambda from routes, keymap, and mousemap.
|
|
@@ -122,73 +149,106 @@ module RatatuiRuby
|
|
|
122
149
|
# 3. Handles mouse events via mousemap
|
|
123
150
|
# 4. Returns model unchanged for unhandled messages
|
|
124
151
|
def from_router
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
RouterUpdate.new(
|
|
153
|
+
routes:,
|
|
154
|
+
actions:,
|
|
155
|
+
key_handlers:,
|
|
156
|
+
scroll_handlers:,
|
|
157
|
+
click_handler:
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Internal UPDATE callable with proper typing.
|
|
163
|
+
class RouterUpdate # :nodoc:
|
|
164
|
+
def initialize(routes:, actions:, key_handlers:, scroll_handlers:, click_handler:)
|
|
165
|
+
@routes = routes
|
|
166
|
+
@actions = actions
|
|
167
|
+
@key_handlers = key_handlers
|
|
168
|
+
@scroll_handlers = scroll_handlers
|
|
169
|
+
@click_handler = click_handler
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Process message and return [model, command] tuple.
|
|
173
|
+
def call(message, model)
|
|
174
|
+
# 1. Try routing prefixed messages to child bags
|
|
175
|
+
@routes.each do |prefix, bag|
|
|
176
|
+
bag_update = bag.const_get(:UPDATE)
|
|
177
|
+
result = Tea.delegate(message, prefix, bag_update, model.public_send(prefix))
|
|
178
|
+
if result
|
|
179
|
+
new_bag_model, command = result
|
|
180
|
+
return [model.with(prefix => new_bag_model), command] #: [_DataModel, Command::execution?]
|
|
138
181
|
end
|
|
182
|
+
end
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
184
|
+
# 2. Try keymap handlers (message is an Event::Key)
|
|
185
|
+
if message.is_a?(RatatuiRuby::Event::Key)
|
|
186
|
+
@key_handlers.each do |key_name, config|
|
|
187
|
+
predicate = :"#{key_name}?"
|
|
188
|
+
next unless message.respond_to?(predicate) && message.public_send(predicate)
|
|
145
189
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
190
|
+
# Check guard if present
|
|
191
|
+
if (config.guard) && !config.guard.call(model)
|
|
192
|
+
next
|
|
193
|
+
end
|
|
150
194
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
# Get handler - either inline or from actions registry
|
|
196
|
+
handler = config.handler
|
|
197
|
+
if handler.nil? && config.action
|
|
198
|
+
handler = @actions[config.action]
|
|
199
|
+
end
|
|
200
|
+
next unless handler
|
|
201
|
+
|
|
202
|
+
command = handler.call
|
|
203
|
+
if command && config.route
|
|
204
|
+
command = Tea.route(command, config.route)
|
|
157
205
|
end
|
|
206
|
+
return [model, command] #: [_DataModel, Command::execution?]
|
|
158
207
|
end
|
|
208
|
+
end
|
|
159
209
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
210
|
+
# 3. Try mousemap handlers (message is an Event::Mouse)
|
|
211
|
+
if message.is_a?(RatatuiRuby::Event::Mouse)
|
|
212
|
+
# Scroll events (handler takes no arguments)
|
|
213
|
+
if message.scroll_up?
|
|
214
|
+
config = @scroll_handlers[:scroll_up]
|
|
215
|
+
if config
|
|
216
|
+
scroll_handler = config.handler
|
|
217
|
+
if scroll_handler.nil? && config.action
|
|
218
|
+
scroll_handler = @actions[config.action]
|
|
168
219
|
end
|
|
220
|
+
return [model, scroll_handler&.call] #: [_DataModel, Command::execution?]
|
|
169
221
|
end
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
222
|
+
end
|
|
223
|
+
if message.scroll_down?
|
|
224
|
+
config = @scroll_handlers[:scroll_down]
|
|
225
|
+
if config
|
|
226
|
+
scroll_handler = config.handler
|
|
227
|
+
if scroll_handler.nil? && config.action
|
|
228
|
+
scroll_handler = @actions[config.action]
|
|
175
229
|
end
|
|
230
|
+
return [model, scroll_handler&.call] #: [_DataModel, Command::execution?]
|
|
176
231
|
end
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
end
|
|
233
|
+
# Click events (handler takes x, y coordinates)
|
|
234
|
+
click_config = @click_handler
|
|
235
|
+
if message.down? && click_config
|
|
236
|
+
click_handler_proc = click_config.handler
|
|
237
|
+
if click_handler_proc.nil? && click_config.action
|
|
238
|
+
# Actions don't take coordinates, so just call without args
|
|
239
|
+
action_handler = @actions[click_config.action]
|
|
240
|
+
return [model, action_handler&.call] #: [_DataModel, Command::execution?]
|
|
241
|
+
elsif click_handler_proc
|
|
242
|
+
return [model, click_handler_proc.call(message.x, message.y)] #: [_DataModel, Command::execution?]
|
|
184
243
|
end
|
|
185
244
|
end
|
|
186
|
-
|
|
187
|
-
# 4. Unhandled - return model unchanged
|
|
188
|
-
[model, nil]
|
|
189
245
|
end
|
|
246
|
+
|
|
247
|
+
# 4. Unhandled - return model unchanged
|
|
248
|
+
[model, nil] #: [_DataModel, Command::execution?]
|
|
190
249
|
end
|
|
191
250
|
end
|
|
251
|
+
private_constant :RouterUpdate
|
|
192
252
|
|
|
193
253
|
# Builder for keymap DSL.
|
|
194
254
|
class KeymapBuilder
|
|
@@ -209,13 +269,13 @@ module RatatuiRuby
|
|
|
209
269
|
# [when/if/only/guard] Guard that runs if truthy (aliases).
|
|
210
270
|
# [unless/except/skip] Guard that runs if falsy (negative aliases).
|
|
211
271
|
def key(key_name, handler_or_action, route: nil, when: nil, if: nil, only: nil, guard: nil, unless: nil, except: nil, skip: nil)
|
|
212
|
-
|
|
272
|
+
handler = nil
|
|
273
|
+
action = nil
|
|
213
274
|
if handler_or_action.is_a?(Symbol)
|
|
214
|
-
|
|
275
|
+
action = handler_or_action
|
|
215
276
|
else
|
|
216
|
-
|
|
277
|
+
handler = handler_or_action
|
|
217
278
|
end
|
|
218
|
-
entry[:route] = route if route
|
|
219
279
|
|
|
220
280
|
guards = @guard_stack.dup
|
|
221
281
|
|
|
@@ -232,11 +292,16 @@ module RatatuiRuby
|
|
|
232
292
|
guards << -> (model) { !negative.call(model) }
|
|
233
293
|
end
|
|
234
294
|
|
|
235
|
-
if guards.any?
|
|
236
|
-
|
|
295
|
+
combined_guard = if guards.any?
|
|
296
|
+
-> (model) { guards.all? { |g| g.call(model) } }
|
|
237
297
|
end
|
|
238
298
|
|
|
239
|
-
@handlers[key_name.to_s] =
|
|
299
|
+
@handlers[key_name.to_s] = KeyHandlerConfig.new(
|
|
300
|
+
handler:,
|
|
301
|
+
action:,
|
|
302
|
+
route:,
|
|
303
|
+
guard: combined_guard
|
|
304
|
+
)
|
|
240
305
|
end
|
|
241
306
|
|
|
242
307
|
# Applies a guard to all keys in the block.
|
|
@@ -299,37 +364,40 @@ module RatatuiRuby
|
|
|
299
364
|
|
|
300
365
|
# Builder for mousemap DSL.
|
|
301
366
|
class MousemapBuilder
|
|
302
|
-
# Returns the registered handlers
|
|
303
|
-
attr_reader :
|
|
367
|
+
# Returns the registered scroll handlers (scroll_up, scroll_down).
|
|
368
|
+
attr_reader :scroll_handlers
|
|
369
|
+
|
|
370
|
+
# Returns the registered click handler.
|
|
371
|
+
attr_reader :click_handler
|
|
304
372
|
|
|
305
373
|
# :nodoc:
|
|
306
374
|
def initialize
|
|
307
|
-
@
|
|
375
|
+
@scroll_handlers = {}
|
|
376
|
+
@click_handler = nil
|
|
308
377
|
end
|
|
309
378
|
|
|
310
379
|
# Registers a click handler.
|
|
311
380
|
#
|
|
312
|
-
# [handler_or_action] Callable or Symbol (action name).
|
|
381
|
+
# [handler_or_action] Callable `^(Integer, Integer) -> Command` or Symbol (action name).
|
|
313
382
|
def click(handler_or_action)
|
|
314
|
-
|
|
383
|
+
if handler_or_action.is_a?(Symbol)
|
|
384
|
+
@click_handler = ClickHandlerConfig.new(action: handler_or_action)
|
|
385
|
+
else
|
|
386
|
+
@click_handler = ClickHandlerConfig.new(handler: handler_or_action)
|
|
387
|
+
end
|
|
315
388
|
end
|
|
316
389
|
|
|
317
390
|
# Registers a scroll handler.
|
|
318
391
|
#
|
|
319
392
|
# [direction] <tt>:up</tt> or <tt>:down</tt>.
|
|
320
|
-
# [handler_or_action] Callable or Symbol (action name).
|
|
393
|
+
# [handler_or_action] Callable `^() -> Command` or Symbol (action name).
|
|
321
394
|
def scroll(direction, handler_or_action)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
private def register(key, handler_or_action)
|
|
326
|
-
entry = {}
|
|
327
|
-
if handler_or_action.is_a?(Symbol)
|
|
328
|
-
entry[:action] = handler_or_action
|
|
395
|
+
config = if handler_or_action.is_a?(Symbol)
|
|
396
|
+
ScrollHandlerConfig.new(action: handler_or_action)
|
|
329
397
|
else
|
|
330
|
-
|
|
398
|
+
ScrollHandlerConfig.new(handler: handler_or_action)
|
|
331
399
|
end
|
|
332
|
-
@
|
|
400
|
+
@scroll_handlers[:"scroll_#{direction}"] = config
|
|
333
401
|
end
|
|
334
402
|
end
|
|
335
403
|
end
|
|
@@ -54,7 +54,8 @@ module RatatuiRuby
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
queue = Queue.new
|
|
57
|
-
pending_threads = []
|
|
57
|
+
pending_threads = [] #: Array[Thread]
|
|
58
|
+
active_commands = {} #: Hash[Command::_Command, active_entry]
|
|
58
59
|
|
|
59
60
|
catch(:quit) do
|
|
60
61
|
RatatuiRuby.run do |tui|
|
|
@@ -75,7 +76,7 @@ module RatatuiRuby
|
|
|
75
76
|
validate_ractor_shareable!(model, "model")
|
|
76
77
|
throw :quit if command.is_a?(Command::Exit)
|
|
77
78
|
|
|
78
|
-
thread = dispatch(command, queue) if command
|
|
79
|
+
thread = dispatch(command, queue, active_commands) if command
|
|
79
80
|
pending_threads << thread if thread
|
|
80
81
|
end
|
|
81
82
|
|
|
@@ -89,6 +90,9 @@ module RatatuiRuby
|
|
|
89
90
|
pending_threads.each(&:join)
|
|
90
91
|
pending_threads.clear
|
|
91
92
|
|
|
93
|
+
# Yield to ensure any final queue writes are visible
|
|
94
|
+
Thread.pass
|
|
95
|
+
|
|
92
96
|
# Process all pending queue items
|
|
93
97
|
until queue.empty?
|
|
94
98
|
begin
|
|
@@ -98,7 +102,7 @@ module RatatuiRuby
|
|
|
98
102
|
validate_ractor_shareable!(model, "model")
|
|
99
103
|
throw :quit if command.is_a?(Command::Exit)
|
|
100
104
|
|
|
101
|
-
thread = dispatch(command, queue) if command
|
|
105
|
+
thread = dispatch(command, queue, active_commands) if command
|
|
102
106
|
pending_threads << thread if thread
|
|
103
107
|
rescue ThreadError
|
|
104
108
|
break
|
|
@@ -116,7 +120,7 @@ module RatatuiRuby
|
|
|
116
120
|
validate_ractor_shareable!(model, "model")
|
|
117
121
|
throw :quit if command.is_a?(Command::Exit)
|
|
118
122
|
|
|
119
|
-
thread = dispatch(command, queue) if command
|
|
123
|
+
thread = dispatch(command, queue, active_commands) if command
|
|
120
124
|
pending_threads << thread if thread
|
|
121
125
|
rescue ThreadError
|
|
122
126
|
break
|
|
@@ -126,6 +130,30 @@ module RatatuiRuby
|
|
|
126
130
|
end
|
|
127
131
|
end
|
|
128
132
|
|
|
133
|
+
# Shutdown: signal all, wait grace periods, then kill
|
|
134
|
+
active_commands.each do |handle, entry|
|
|
135
|
+
entry[:token].cancel!
|
|
136
|
+
grace = handle.tea_cancellation_grace_period
|
|
137
|
+
if grace.finite?
|
|
138
|
+
deadline = Time.now + grace
|
|
139
|
+
sleep 0.02 while entry[:thread].alive? && Time.now < deadline
|
|
140
|
+
entry[:thread].kill if entry[:thread].alive?
|
|
141
|
+
else
|
|
142
|
+
entry[:thread].join
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Process any final messages from completed commands
|
|
147
|
+
until queue.empty?
|
|
148
|
+
begin
|
|
149
|
+
background_message = queue.pop(true)
|
|
150
|
+
result = update.call(background_message, model)
|
|
151
|
+
model, = normalize_update_result(result, model)
|
|
152
|
+
rescue ThreadError
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
129
157
|
model
|
|
130
158
|
end
|
|
131
159
|
|
|
@@ -140,26 +168,52 @@ module RatatuiRuby
|
|
|
140
168
|
"View returned nil. Return a widget, or use TUI#clear for an empty screen."
|
|
141
169
|
end
|
|
142
170
|
|
|
143
|
-
#
|
|
171
|
+
# Extracts [model, command] from update result.
|
|
144
172
|
#
|
|
145
|
-
#
|
|
173
|
+
# Uses is_a? checks for type narrowing. The result parameter is untyped
|
|
174
|
+
# because the method performs runtime type detection.
|
|
175
|
+
#
|
|
176
|
+
# @param result [Array, Command::execution, Object] The update result
|
|
177
|
+
# @param previous_model [Model] Fallback model if result is a command
|
|
178
|
+
# @return [Array(Object, Command::execution?)] The [model, command] tuple
|
|
146
179
|
private_class_method def self.normalize_update_result(result, previous_model)
|
|
147
|
-
|
|
148
|
-
return [previous_model,
|
|
180
|
+
# Case 0: Nil result - preserve previous model
|
|
181
|
+
return [previous_model, nil] if result.nil?
|
|
149
182
|
|
|
150
|
-
[
|
|
151
|
-
|
|
183
|
+
# Case 1: Already a [model, command] tuple
|
|
184
|
+
if result.is_a?(Array) && (result.size == 2)
|
|
185
|
+
model = result[0]
|
|
186
|
+
command = result[1]
|
|
187
|
+
# Verify the second element is a valid command (nil, built-in, or custom)
|
|
188
|
+
if command.nil? ||
|
|
189
|
+
command.class.name&.start_with?("RatatuiRuby::Tea::Command::") ||
|
|
190
|
+
(command.respond_to?(:tea_command?) && command.tea_command?)
|
|
152
191
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
return [model, command]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Case 2: Result is a Command - use previous model
|
|
197
|
+
if result.class.name&.start_with?("RatatuiRuby::Tea::Command::")
|
|
198
|
+
return [previous_model, result]
|
|
199
|
+
end
|
|
200
|
+
if result.respond_to?(:tea_command?) && result.tea_command?
|
|
201
|
+
return [previous_model, result]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Case 3: Result is the new model
|
|
205
|
+
[result, nil]
|
|
156
206
|
end
|
|
157
207
|
|
|
158
208
|
# Validates an object is Ractor-shareable (deeply frozen).
|
|
159
209
|
#
|
|
160
210
|
# Models and messages must be shareable for future Ractor support.
|
|
161
211
|
# Mutable objects cause race conditions. Freeze your data.
|
|
212
|
+
#
|
|
213
|
+
# Only enforced in debug mode (and tests). Production skips this check
|
|
214
|
+
# for performance; mutable objects will still cause bugs, but silently.
|
|
162
215
|
private_class_method def self.validate_ractor_shareable!(object, name)
|
|
216
|
+
return unless RatatuiRuby::Debug.enabled?
|
|
163
217
|
return if Ractor.shareable?(object)
|
|
164
218
|
|
|
165
219
|
raise RatatuiRuby::Error::Invariant,
|
|
@@ -170,47 +224,37 @@ module RatatuiRuby
|
|
|
170
224
|
#
|
|
171
225
|
# Spawns a background thread and pushes results to the message queue.
|
|
172
226
|
# See Command.system for message formats.
|
|
173
|
-
private_class_method def self.dispatch(command, queue)
|
|
227
|
+
private_class_method def self.dispatch(command, queue, active_commands = {})
|
|
174
228
|
case command
|
|
175
|
-
when Command::
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
queue << Ractor.make_shareable([command.tag, :stdout, line])
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
stderr_thread = Thread.new do
|
|
188
|
-
stderr.each_line do |line|
|
|
189
|
-
queue << Ractor.make_shareable([command.tag, :stderr, line])
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
stdout_thread.join
|
|
193
|
-
stderr_thread.join
|
|
194
|
-
status = wait_thr.value.exitstatus
|
|
195
|
-
queue << Ractor.make_shareable([command.tag, :complete, { status: }])
|
|
196
|
-
end
|
|
197
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
|
198
|
-
queue << Ractor.make_shareable([command.tag, :error, { message: e.message }])
|
|
199
|
-
end
|
|
229
|
+
when Command::Cancel
|
|
230
|
+
entry = active_commands[command.handle]
|
|
231
|
+
if entry && entry[:thread].alive?
|
|
232
|
+
entry[:token].cancel!
|
|
233
|
+
grace = command.handle.tea_cancellation_grace_period
|
|
234
|
+
if grace.finite?
|
|
235
|
+
deadline = Time.now + grace
|
|
236
|
+
sleep 0.02 while entry[:thread].alive? && Time.now < deadline
|
|
237
|
+
entry[:thread].kill if entry[:thread].alive?
|
|
200
238
|
else
|
|
201
|
-
|
|
202
|
-
message = [command.tag, { stdout:, stderr:, status: status.exitstatus }]
|
|
203
|
-
queue << Ractor.make_shareable(message)
|
|
239
|
+
entry[:thread].join
|
|
204
240
|
end
|
|
205
241
|
end
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
242
|
+
active_commands.delete(command.handle)
|
|
243
|
+
nil
|
|
244
|
+
else
|
|
245
|
+
# Custom command (responds to tea_command?)
|
|
246
|
+
if command.respond_to?(:tea_command?) && command.tea_command?
|
|
247
|
+
token = Command::CancellationToken.new
|
|
248
|
+
outlet = Command::Outlet.new(queue)
|
|
249
|
+
|
|
250
|
+
thread = Thread.new do
|
|
251
|
+
command.call(outlet, token)
|
|
252
|
+
rescue => e
|
|
253
|
+
queue << Command::Error.new(command:, exception: e)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
active_commands[command] = { thread:, token: }
|
|
257
|
+
thread
|
|
214
258
|
end
|
|
215
259
|
end
|
|
216
260
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
path: ".gem_rbs_collection"
|
|
7
|
+
gems:
|
|
8
|
+
- name: base64
|
|
9
|
+
version: 0.3.0
|
|
10
|
+
source:
|
|
11
|
+
type: rubygems
|
|
12
|
+
- name: bigdecimal
|
|
13
|
+
version: '0'
|
|
14
|
+
source:
|
|
15
|
+
type: stdlib
|
|
16
|
+
- name: csv
|
|
17
|
+
version: '0'
|
|
18
|
+
source:
|
|
19
|
+
type: stdlib
|
|
20
|
+
- name: dbm
|
|
21
|
+
version: '0'
|
|
22
|
+
source:
|
|
23
|
+
type: stdlib
|
|
24
|
+
- name: erb
|
|
25
|
+
version: '0'
|
|
26
|
+
source:
|
|
27
|
+
type: stdlib
|
|
28
|
+
- name: ffi
|
|
29
|
+
version: 1.17.3
|
|
30
|
+
source:
|
|
31
|
+
type: rubygems
|
|
32
|
+
- name: fileutils
|
|
33
|
+
version: '0'
|
|
34
|
+
source:
|
|
35
|
+
type: stdlib
|
|
36
|
+
- name: forwardable
|
|
37
|
+
version: '0'
|
|
38
|
+
source:
|
|
39
|
+
type: stdlib
|
|
40
|
+
- name: io-console
|
|
41
|
+
version: '0'
|
|
42
|
+
source:
|
|
43
|
+
type: stdlib
|
|
44
|
+
- name: json
|
|
45
|
+
version: '0'
|
|
46
|
+
source:
|
|
47
|
+
type: stdlib
|
|
48
|
+
- name: logger
|
|
49
|
+
version: '0'
|
|
50
|
+
source:
|
|
51
|
+
type: stdlib
|
|
52
|
+
- name: minitest
|
|
53
|
+
version: '0'
|
|
54
|
+
source:
|
|
55
|
+
type: stdlib
|
|
56
|
+
- name: monitor
|
|
57
|
+
version: '0'
|
|
58
|
+
source:
|
|
59
|
+
type: stdlib
|
|
60
|
+
- name: mutex_m
|
|
61
|
+
version: 0.3.0
|
|
62
|
+
source:
|
|
63
|
+
type: rubygems
|
|
64
|
+
- name: optparse
|
|
65
|
+
version: '0'
|
|
66
|
+
source:
|
|
67
|
+
type: stdlib
|
|
68
|
+
- name: pp
|
|
69
|
+
version: '0'
|
|
70
|
+
source:
|
|
71
|
+
type: stdlib
|
|
72
|
+
- name: prettyprint
|
|
73
|
+
version: '0'
|
|
74
|
+
source:
|
|
75
|
+
type: stdlib
|
|
76
|
+
- name: prism
|
|
77
|
+
version: 1.7.0
|
|
78
|
+
source:
|
|
79
|
+
type: rubygems
|
|
80
|
+
- name: pstore
|
|
81
|
+
version: '0'
|
|
82
|
+
source:
|
|
83
|
+
type: stdlib
|
|
84
|
+
- name: psych
|
|
85
|
+
version: '0'
|
|
86
|
+
source:
|
|
87
|
+
type: stdlib
|
|
88
|
+
- name: ratatui_ruby
|
|
89
|
+
version: 0.10.1
|
|
90
|
+
source:
|
|
91
|
+
type: rubygems
|
|
92
|
+
- name: ratatui_ruby-devtools
|
|
93
|
+
version: 0.1.0
|
|
94
|
+
source:
|
|
95
|
+
type: rubygems
|
|
96
|
+
- name: rbs
|
|
97
|
+
version: 3.10.2
|
|
98
|
+
source:
|
|
99
|
+
type: rubygems
|
|
100
|
+
- name: rdoc
|
|
101
|
+
version: '0'
|
|
102
|
+
source:
|
|
103
|
+
type: stdlib
|
|
104
|
+
- name: securerandom
|
|
105
|
+
version: '0'
|
|
106
|
+
source:
|
|
107
|
+
type: stdlib
|
|
108
|
+
- name: stringio
|
|
109
|
+
version: '0'
|
|
110
|
+
source:
|
|
111
|
+
type: stdlib
|
|
112
|
+
- name: strscan
|
|
113
|
+
version: '0'
|
|
114
|
+
source:
|
|
115
|
+
type: stdlib
|
|
116
|
+
- name: tsort
|
|
117
|
+
version: '0'
|
|
118
|
+
source:
|
|
119
|
+
type: stdlib
|
|
120
|
+
- name: uri
|
|
121
|
+
version: '0'
|
|
122
|
+
source:
|
|
123
|
+
type: stdlib
|
|
124
|
+
gemfile_lock_path: Gemfile.lock
|
data/rbs_collection.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
# RBS collection configuration
|
|
6
|
+
# See: https://github.com/ruby/rbs/blob/master/docs/collection.md
|
|
7
|
+
|
|
8
|
+
sources:
|
|
9
|
+
- type: stdlib
|
|
10
|
+
- type: rubygems
|
|
11
|
+
|
|
12
|
+
path: .gem_rbs_collection
|
|
13
|
+
|
|
14
|
+
gems:
|
|
15
|
+
- name: ratatui_ruby
|