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.
@@ -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
- @mouse_handlers = builder.handlers
125
+ @scroll_handlers = builder.scroll_handlers
126
+ @click_handler = builder.click_handler
110
127
  end
111
128
 
112
- # Returns the registered mouse handlers hash.
113
- def mouse_handlers
114
- @mouse_handlers ||= {}
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
- my_routes = routes
126
- my_actions = actions
127
- my_key_handlers = key_handlers
128
- my_mouse_handlers = mouse_handlers
129
-
130
- lambda do |message, model|
131
- # 1. Try routing prefixed messages to children
132
- my_routes.each do |prefix, child|
133
- result = Tea.delegate(message, prefix, child::UPDATE, model.public_send(prefix))
134
- if result
135
- new_child, command = result
136
- return [model.with(prefix => new_child), command]
137
- end
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
- # 2. Try keymap handlers (message is an Event)
141
- if message.respond_to?(:key?) && message.key?
142
- my_key_handlers.each do |key_name, config|
143
- predicate = :"#{key_name}?"
144
- next unless message.respond_to?(predicate) && message.public_send(predicate)
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
- # Check guard if present
147
- if (config[:guard]) && !config[:guard].call(model)
148
- next
149
- end
190
+ # Check guard if present
191
+ if (config.guard) && !config.guard.call(model)
192
+ next
193
+ end
150
194
 
151
- handler = config[:handler] || my_actions[config[:action]]
152
- command = handler.call
153
- if config[:route]
154
- command = Tea.route(command, config[:route])
155
- end
156
- return [model, command]
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
- # 3. Try mousemap handlers
161
- if message.respond_to?(:mouse?) && message.mouse?
162
- # Scroll events
163
- if message.respond_to?(:scroll_up?) && message.scroll_up?
164
- config = my_mouse_handlers[:scroll_up]
165
- if config
166
- handler = config[:handler] || my_actions[config[:action]]
167
- return [model, handler.call]
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
- if message.respond_to?(:scroll_down?) && message.scroll_down?
171
- config = my_mouse_handlers[:scroll_down]
172
- if config
173
- handler = config[:handler] || my_actions[config[:action]]
174
- return [model, handler.call]
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
- # Click events
178
- if message.respond_to?(:click?) && message.click?
179
- config = my_mouse_handlers[:click]
180
- if config
181
- handler = config[:handler] || my_actions[config[:action]]
182
- return [model, handler.call(message.x, message.y)]
183
- end
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
- entry = {}
272
+ handler = nil
273
+ action = nil
213
274
  if handler_or_action.is_a?(Symbol)
214
- entry[:action] = handler_or_action
275
+ action = handler_or_action
215
276
  else
216
- entry[:handler] = handler_or_action
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
- entry[:guard] = -> (model) { guards.all? { |g| g.call(model) } }
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] = entry
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 hash.
303
- attr_reader :handlers
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
- @handlers = {}
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
- register(:click, handler_or_action)
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
- register(:"scroll_#{direction}", handler_or_action)
323
- end
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
- entry[:handler] = handler_or_action
398
+ ScrollHandlerConfig.new(handler: handler_or_action)
331
399
  end
332
- @handlers[key] = entry
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
- # Detects whether +result+ is a +[model, command]+ tuple, a plain model, or a Command alone.
171
+ # Extracts [model, command] from update result.
144
172
  #
145
- # Returns +[model, command]+ in all cases.
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
- return result if result.is_a?(Array) && result.size == 2 && valid_command?(result[1])
148
- return [previous_model, result] if valid_command?(result)
180
+ # Case 0: Nil result - preserve previous model
181
+ return [previous_model, nil] if result.nil?
149
182
 
150
- [result, nil]
151
- end
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
- # Returns +true+ if +value+ is a valid command (+nil+ or a +Command+ type).
154
- private_class_method def self.valid_command?(value)
155
- value.nil? || value.class.name&.start_with?("RatatuiRuby::Tea::Command::")
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::System
176
- Thread.new do
177
- require "open3"
178
- if command.stream?
179
- begin
180
- Open3.popen3(command.command) do |stdin, stdout, stderr, wait_thr|
181
- stdin.close
182
- stdout_thread = Thread.new do
183
- stdout.each_line do |line|
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
- stdout, stderr, status = Open3.capture3(command.command)
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
- when Command::Mapped
207
- inner_queue = Queue.new
208
- inner_thread = dispatch(command.inner_command, inner_queue)
209
- Thread.new do
210
- inner_thread&.join
211
- inner_message = inner_queue.pop
212
- transformed = command.mapper.call(inner_message)
213
- queue << Ractor.make_shareable(transformed)
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
@@ -9,6 +9,6 @@ module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
9
9
  module Tea
10
10
  # The version of this gem.
11
11
  # See https://semver.org/spec/v2.0.0.html
12
- VERSION = "0.3.0"
12
+ VERSION = "0.3.1"
13
13
  end
14
14
  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
@@ -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