ratatui_ruby 0.10.1 → 0.10.2

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/CHANGELOG.md +24 -0
  7. data/doc/concepts/application_architecture.md +2 -2
  8. data/doc/concepts/application_testing.md +1 -1
  9. data/doc/concepts/custom_widgets.md +2 -2
  10. data/doc/contributors/todo/align/api_completeness_audit-finished.md +375 -0
  11. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +206 -0
  12. data/doc/contributors/todo/align/terminal.md +647 -0
  13. data/doc/getting_started/quickstart.md +41 -41
  14. data/doc/images/app_cli_rich_moments.gif +0 -0
  15. data/examples/app_cli_rich_moments/README.md +81 -0
  16. data/examples/app_cli_rich_moments/app.rb +189 -0
  17. data/ext/ratatui_ruby/Cargo.lock +1 -1
  18. data/ext/ratatui_ruby/Cargo.toml +1 -1
  19. data/ext/ratatui_ruby/src/frame.rs +17 -4
  20. data/ext/ratatui_ruby/src/lib.rs +17 -3
  21. data/ext/ratatui_ruby/src/lib.rs.bak +286 -0
  22. data/ext/ratatui_ruby/src/rendering.rs +38 -25
  23. data/ext/ratatui_ruby/src/rendering.rs.bak +152 -0
  24. data/ext/ratatui_ruby/src/terminal.rs +245 -33
  25. data/ext/ratatui_ruby/src/terminal.rs.bak +381 -0
  26. data/ext/ratatui_ruby/src/terminal.rs.orig +409 -0
  27. data/ext/ratatui_ruby/src/widgets/barchart.rs +4 -3
  28. data/ext/ratatui_ruby/src/widgets/block.rs +4 -4
  29. data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
  30. data/ext/ratatui_ruby/src/widgets/canvas.rs +7 -4
  31. data/ext/ratatui_ruby/src/widgets/center.rs +3 -3
  32. data/ext/ratatui_ruby/src/widgets/chart.rs +4 -4
  33. data/ext/ratatui_ruby/src/widgets/clear.rs +6 -6
  34. data/ext/ratatui_ruby/src/widgets/cursor.rs +10 -7
  35. data/ext/ratatui_ruby/src/widgets/gauge.rs +4 -3
  36. data/ext/ratatui_ruby/src/widgets/layout.rs +3 -3
  37. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +4 -3
  38. data/ext/ratatui_ruby/src/widgets/list.rs +6 -9
  39. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -3
  40. data/ext/ratatui_ruby/src/widgets/paragraph.rs +5 -6
  41. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +4 -4
  42. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +8 -4
  43. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +10 -10
  44. data/ext/ratatui_ruby/src/widgets/sparkline.rs +4 -3
  45. data/ext/ratatui_ruby/src/widgets/table.rs +6 -6
  46. data/ext/ratatui_ruby/src/widgets/tabs.rs +4 -3
  47. data/lib/ratatui_ruby/labs/a11y.rb +173 -0
  48. data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
  49. data/lib/ratatui_ruby/labs.rb +47 -0
  50. data/lib/ratatui_ruby/layout/position.rb +26 -0
  51. data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
  52. data/lib/ratatui_ruby/terminal_lifecycle.rb +164 -6
  53. data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
  54. data/lib/ratatui_ruby/test_helper/terminal.rb +8 -1
  55. data/lib/ratatui_ruby/tui/core.rb +16 -0
  56. data/lib/ratatui_ruby/version.rb +1 -1
  57. data/lib/ratatui_ruby.rb +82 -3
  58. data/migrate_to_buffer.rb +145 -0
  59. data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
  60. data/sig/ratatui_ruby/labs.rbs +87 -0
  61. data/sig/ratatui_ruby/ratatui_ruby.rbs +12 -4
  62. data/sig/ratatui_ruby/terminal/viewport.rbs +19 -0
  63. data/sig/ratatui_ruby/terminal_lifecycle.rbs +13 -5
  64. data/sig/ratatui_ruby/tui/core.rbs +3 -0
  65. metadata +21 -2
  66. /data/doc/contributors/{future_work.md → todo/future_work.md} +0 -0
@@ -214,69 +214,69 @@ end
214
214
 
215
215
  ## Examples
216
216
 
217
- These examples showcase the full power of **ratatui_ruby**. You can find their source code in the [examples directory](../examples).
217
+ These examples showcase the full power of **ratatui_ruby**. You can find their source code in the [examples directory](../../examples).
218
218
 
219
219
  ### Widget Demos
220
220
 
221
221
  Focused examples for individual widgets. Each demonstrates a single widget and its configuration options.
222
222
 
223
- | Widget | What it demonstrates |
224
- |--------|---------------------|
225
- | [Bar Chart](../examples/widget_barchart/app.rb) | Grouped bars, data visualization, custom bar styling |
226
- | [Block](../examples/widget_block/app.rb) | Borders, titles, padding, nested widgets |
227
- | [Box](../examples/widget_box/app.rb) | Block + Paragraph composition, text wrapping |
228
- | [Calendar](../examples/widget_calendar/app.rb) | Date highlighting, month display, event markers |
229
- | [Chart](../examples/widget_chart/app.rb) | Line/scatter plots, axes, legends, datasets |
230
- | [Gauge](../examples/widget_gauge/app.rb) | Progress bars, percentage display, unicode blocks |
231
- | [Layout Split](../examples/widget_layout_split/app.rb) | Constraint types, flex modes, responsive layouts |
232
- | [Line Gauge](../examples/widget_line_gauge/app.rb) | Horizontal progress, labels, thin-style gauges |
233
- | [List](../examples/widget_list/app.rb) | Selection, scrolling, highlight styles, rich text items |
234
- | [Map](../examples/widget_map/app.rb) | Canvas widget, world map rendering, coordinates |
235
- | [Popup](../examples/widget_popup/app.rb) | Clear widget, modal dialogs, overlay composition |
236
- | [Ratatui Logo](../examples/widget_ratatui_logo/app.rb) | Decorative branding widget |
237
- | [Ratatui Mascot](../examples/widget_ratatui_mascot/app.rb) | ASCII art Ferris mascot |
238
- | [Rect](../examples/widget_rect/app.rb) | Geometry helpers, area calculations, contains/intersection |
239
- | [Rich Text](../examples/widget_rich_text/app.rb) | Spans, lines, inline styling, mixed colors |
240
- | [Scrollbar](../examples/widget_scrollbar/app.rb) | Orientations, thumb/track styling, scroll state |
241
- | [Scroll Text](../examples/widget_scroll_text/app.rb) | Paragraph scrolling, viewport control, long content |
242
- | [Sparkline](../examples/widget_sparkline/app.rb) | Mini charts, time series, bar sets |
243
- | [Style Colors](../examples/widget_style_colors/app.rb) | Named colors, RGB, indexed 256-color palette |
244
- | [Table](../examples/widget_table/app.rb) | Row selection, column widths, per-cell styling |
245
- | [Tabs](../examples/widget_tabs/app.rb) | Tab navigation, highlighting, dividers |
246
- | [Text Width](../examples/widget_text_width/app.rb) | Unicode-aware width measurement, CJK support |
247
- | [Canvas](../examples/widget_canvas/app.rb) | Drawing shapes, markers, custom graphics |
248
- | [Cell](../examples/widget_cell/app.rb) | Buffer cell inspection, styling attributes |
249
- | [Center](../examples/widget_center/app.rb) | Centering content, horizontal/vertical alignment |
250
- | [Overlay](../examples/widget_overlay/app.rb) | Layering widgets, modal backgrounds |
251
- | [Custom Render](../examples/widget_render/app.rb) | Low-level Draw API, escape hatch for custom widgets |
223
+ | Widget | What it demonstrates |
224
+ | ------------------------------------------------------------- | ---------------------------------------------------------- |
225
+ | [Bar Chart](../../examples/widget_barchart/app.rb) | Grouped bars, data visualization, custom bar styling |
226
+ | [Block](../../examples/widget_block/app.rb) | Borders, titles, padding, nested widgets |
227
+ | [Box](../../examples/widget_box/app.rb) | Block + Paragraph composition, text wrapping |
228
+ | [Calendar](../../examples/widget_calendar/app.rb) | Date highlighting, month display, event markers |
229
+ | [Chart](../../examples/widget_chart/app.rb) | Line/scatter plots, axes, legends, datasets |
230
+ | [Gauge](../../examples/widget_gauge/app.rb) | Progress bars, percentage display, unicode blocks |
231
+ | [Layout Split](../../examples/widget_layout_split/app.rb) | Constraint types, flex modes, responsive layouts |
232
+ | [Line Gauge](../../examples/widget_line_gauge/app.rb) | Horizontal progress, labels, thin-style gauges |
233
+ | [List](../../examples/widget_list/app.rb) | Selection, scrolling, highlight styles, rich text items |
234
+ | [Map](../../examples/widget_map/app.rb) | Canvas widget, world map rendering, coordinates |
235
+ | [Popup](../../examples/widget_popup/app.rb) | Clear widget, modal dialogs, overlay composition |
236
+ | [Ratatui Logo](../../examples/widget_ratatui_logo/app.rb) | Decorative branding widget |
237
+ | [Ratatui Mascot](../../examples/widget_ratatui_mascot/app.rb) | ASCII art Ferris mascot |
238
+ | [Rect](../../examples/widget_rect/app.rb) | Geometry helpers, area calculations, contains/intersection |
239
+ | [Rich Text](../../examples/widget_rich_text/app.rb) | Spans, lines, inline styling, mixed colors |
240
+ | [Scrollbar](../../examples/widget_scrollbar/app.rb) | Orientations, thumb/track styling, scroll state |
241
+ | [Scroll Text](../../examples/widget_scroll_text/app.rb) | Paragraph scrolling, viewport control, long content |
242
+ | [Sparkline](../../examples/widget_sparkline/app.rb) | Mini charts, time series, bar sets |
243
+ | [Style Colors](../../examples/widget_style_colors/app.rb) | Named colors, RGB, indexed 256-color palette |
244
+ | [Table](../../examples/widget_table/app.rb) | Row selection, column widths, per-cell styling |
245
+ | [Tabs](../../examples/widget_tabs/app.rb) | Tab navigation, highlighting, dividers |
246
+ | [Text Width](../../examples/widget_text_width/app.rb) | Unicode-aware width measurement, CJK support |
247
+ | [Canvas](../../examples/widget_canvas/app.rb) | Drawing shapes, markers, custom graphics |
248
+ | [Cell](../../examples/widget_cell/app.rb) | Buffer cell inspection, styling attributes |
249
+ | [Center](../../examples/widget_center/app.rb) | Centering content, horizontal/vertical alignment |
250
+ | [Overlay](../../examples/widget_overlay/app.rb) | Layering widgets, modal backgrounds |
251
+ | [Custom Render](../../examples/widget_render/app.rb) | Low-level Draw API, escape hatch for custom widgets |
252
252
 
253
253
  ### Sample Applications
254
254
 
255
255
  These larger examples combine widgets into complete applications, demonstrating real-world TUI patterns and architectures.
256
256
 
257
- | Application | Architecture | What you'll learn |
258
- |-------------|--------------|-------------------|
259
- | [All Events](../examples/app_all_events/app.rb) | Model-View-Update | Event handling, unidirectional data flow, scalable structure |
260
- | [Color Picker](../examples/app_color_picker/app.rb) | Component-Based | Hit testing, modal dialogs, encapsulated state |
261
- | [Debugging Showcase](../examples/app_debugging_showcase/app.rb) | Simple Loop | Remote debugging, Rust backtraces, improved error messages |
262
- | [Login Form](../examples/app_login_form/app.rb) | Overlay + Center | Modal forms, cursor positioning, text input |
263
- | [Stateful Interaction](../examples/app_stateful_interaction/app.rb) | State Objects | ListState/TableState, offset read-back, mouse click-to-row |
257
+ | Application | Architecture | What you'll learn |
258
+ | ---------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------ |
259
+ | [All Events](../../examples/app_all_events/app.rb) | Model-View-Update | Event handling, unidirectional data flow, scalable structure |
260
+ | [Color Picker](../../examples/app_color_picker/app.rb) | Component-Based | Hit testing, modal dialogs, encapsulated state |
261
+ | [Debugging Showcase](../../examples/app_debugging_showcase/app.rb) | Simple Loop | Remote debugging, Rust backtraces, improved error messages |
262
+ | [Login Form](../../examples/app_login_form/app.rb) | Overlay + Center | Modal forms, cursor positioning, text input |
263
+ | [Stateful Interaction](../../examples/app_stateful_interaction/app.rb) | State Objects | ListState/TableState, offset read-back, mouse click-to-row |
264
264
 
265
265
  #### All Events
266
266
 
267
- [![all_events](../images/app_all_events.png)](../examples/app_all_events/README.md)
267
+ [![all_events](../images/app_all_events.png)](../../examples/app_all_events/README.md)
268
268
 
269
269
  #### Color Picker
270
270
 
271
- [![color_picker](../images/app_color_picker.png)](../examples/app_color_picker/README.md)
271
+ [![color_picker](../images/app_color_picker.png)](../../examples/app_color_picker/README.md)
272
272
 
273
273
  #### Debugging Showcase
274
274
 
275
- [![debugging_showcase](../images/app_debugging_showcase.gif)](../examples/app_debugging_showcase/README.md)
275
+ [![debugging_showcase](../images/app_debugging_showcase.gif)](../../examples/app_debugging_showcase/README.md)
276
276
 
277
277
  #### Login Form
278
278
 
279
- [![login_form](../images/app_login_form.png)](../examples/app_login_form/README.md)
279
+ [![login_form](../images/app_login_form.png)](../../examples/app_login_form/README.md)
280
280
 
281
281
 
282
282
  ## Next Steps
Binary file
@@ -0,0 +1,81 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # CLI Rich Moments Example
7
+
8
+ [![CLI Rich Moments](../../doc/images/app_cli_rich_moments.gif)](app.rb)
9
+
10
+ Demonstrates inline viewport usage for CLI tools that need brief moments of rich interactivity without full-screen commitment.
11
+
12
+ ## Context
13
+
14
+ CLI applications often need moments of richness—a spinner while connecting, a quick menu selection, or a brief editor. But committing to a full-screen TUI feels wrong when 90% of the app is traditional CLI output.
15
+
16
+ ## Problem
17
+
18
+ Standard full-screen TUIs (alternate screen) erase themselves on exit. Your carefully formatted CLI output disappears. Users lose their history. The terminal scrollback becomes useless.
19
+
20
+ ## Solution
21
+
22
+ This example shows how **inline viewports** solve this problem. Inline regions persist in terminal scrollback after exit. You can mix inline and fullscreen viewports across multiple `RatatuiRuby.run` calls in a single application flow.
23
+
24
+ ## Flow
25
+
26
+ The application demonstrates four distinct phases:
27
+
28
+ 1. **1-line inline**: Braille spinner (⠋ ⠙ ⠹ etc.) + "Connecting..." status
29
+ 2. **5-line inline**: Radio button menu with up/down/enter navigation
30
+ 3. **Fullscreen**: Configuration editor showing legitimate use of alternate screen
31
+ 4. **1-line inline**: Braille spinner + "Saving..." confirmation
32
+
33
+ After exit, all inline outputs remain visible in terminal history. The fullscreen portion disappears cleanly.
34
+
35
+ ## Running
36
+
37
+ <!-- SPDX-SnippetBegin -->
38
+ <!--
39
+ SPDX-FileCopyrightText: 2026 Kerrick Long
40
+ SPDX-License-Identifier: MIT-0
41
+ -->
42
+ ```bash
43
+ cd examples/app_cli_rich_moments
44
+ ruby app.rb
45
+ ```
46
+ <!-- SPDX-SnippetEnd -->
47
+
48
+ ## Architecture
49
+
50
+ Each phase is a separate `RatatuiRuby.run` block with its own viewport configuration. State (like menu selection) must be passed between phases via instance variables.
51
+
52
+ **Spinner phases**:
53
+ - Use `viewport: :inline, height: 1`
54
+ - Animate Braille frames with short delays
55
+ - No input handling needed
56
+
57
+ **Menu phase**:
58
+ - Uses `viewport: :inline, height: 5`
59
+ - Handles up/down arrow keys for selection
60
+ - Returns selected choice for next phase
61
+
62
+ **Editor phase**:
63
+ - Uses default fullscreen viewport
64
+ - Demonstrates when alternate screen is appropriate
65
+ - Full 80×24 available
66
+
67
+ ## Key Concepts
68
+
69
+ - **Viewport independence**: Each `run` block can use different viewport modes
70
+ - **Scrollback persistence**: Inline content remains after exit
71
+ - **State management**: Pass data between phases via instance variables
72
+ - **Appropriate contexts**: Inline for brief moments, fullscreen for sustained interaction
73
+
74
+ ## Learning Outcomes
75
+
76
+ After studying this example, you'll understand:
77
+
78
+ - When to use inline vs fullscreen viewports
79
+ - How to mix viewport modes in a single application
80
+ - Why inline viewports improve CLI UX for transient interactions
81
+ - How to manage state across multiple `run` blocks
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
+ require "ratatui_ruby"
10
+
11
+ class AppCliRichMoments
12
+ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
13
+ MENU_OPTIONS = [
14
+ "Development Environment",
15
+ "Staging Environment",
16
+ "Production Environment",
17
+ ].freeze
18
+
19
+ def initialize
20
+ @selected_index = 0
21
+ @choice = nil
22
+ @tui = RatatuiRuby::TUI.new
23
+ end
24
+
25
+ def run
26
+ phase_connecting
27
+ @choice = phase_menu
28
+ phase_editor
29
+ phase_saving
30
+ end
31
+
32
+ private def phase_connecting
33
+ RatatuiRuby.run(viewport: :inline, height: 1) do
34
+ 10.times do |i|
35
+ render_spinner(SPINNER_FRAMES[i % SPINNER_FRAMES.length], "Connecting to server...")
36
+ sleep 0.1
37
+ end
38
+
39
+ # Insert success message above viewport into scrollback
40
+ status = "✓ Connected to server"
41
+ @tui.insert_before(1, @tui.paragraph(text: status, style: @tui.style(fg: :green)))
42
+ end
43
+ end
44
+
45
+ private def phase_menu
46
+ RatatuiRuby.run(viewport: :inline, height: 5) do
47
+ loop do
48
+ render_menu
49
+ case handle_menu_input
50
+ when :quit, :select
51
+ # Position cursor after viewport for next phase
52
+ area = @tui.viewport_area
53
+
54
+ # # If you wanted to remove the menu from scrollback, you could do:
55
+ # @tui.draw { |frame| frame.render_widget(@tui.clear, frame.area) }
56
+ # # And move the cursor to avoid extra blank space.
57
+ # RatatuiRuby.cursor_position = [0, area.y]
58
+
59
+ # But instead, we'll leave it in scrollback for reference.
60
+ # Move the cursor to avoid overwriting it.
61
+ RatatuiRuby.cursor_position = [0, area.y + area.height]
62
+
63
+ return MENU_OPTIONS[@selected_index]
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ private def phase_editor
70
+ RatatuiRuby.run do # Fullscreen by default
71
+ loop do
72
+ render_editor
73
+ break if handle_editor_input == :quit
74
+ end
75
+ end
76
+ end
77
+
78
+ private def phase_saving
79
+ RatatuiRuby.run(viewport: :inline, height: 1) do
80
+ 10.times do |i|
81
+ render_spinner(SPINNER_FRAMES[i % SPINNER_FRAMES.length], "Saving configuration...")
82
+ sleep 0.1
83
+ end
84
+
85
+ status = "✓ Configuration saved to #{@choice.downcase.gsub(' ', '_')}.yml"
86
+ @tui.insert_before(1, @tui.paragraph(text: status, style: @tui.style(fg: :green)))
87
+ end
88
+ end
89
+
90
+ private def render_spinner(frame, message)
91
+ @tui.draw do |f|
92
+ text = "#{frame} #{message}"
93
+ widget = @tui.paragraph(text:, style: @tui.style(fg: :cyan))
94
+ f.render_widget(widget, f.area)
95
+ end
96
+ end
97
+
98
+ private def render_menu
99
+ @tui.draw do |f|
100
+ lines = MENU_OPTIONS.map.with_index do |option, idx|
101
+ prefix = (idx == @selected_index) ? "→ " : " "
102
+ style = (idx == @selected_index) ? @tui.style(fg: :cyan, modifiers: [:bold]) : @tui.style(fg: :white)
103
+ @tui.text_line(spans: [@tui.text_span(content: "#{prefix}#{option}", style:)])
104
+ end
105
+
106
+ widget = @tui.paragraph(
107
+ text: lines,
108
+ block: @tui.block(borders: :all, title: "Select Environment")
109
+ )
110
+ f.render_widget(widget, f.area)
111
+ end
112
+ end
113
+
114
+ private def render_editor
115
+ @tui.draw do |f|
116
+ areas = @tui.layout_split(
117
+ f.area,
118
+ direction: :vertical,
119
+ constraints: [
120
+ @tui.constraint_fill(1),
121
+ @tui.constraint_length(3),
122
+ ]
123
+ )
124
+
125
+ # Main content area
126
+ content_text = [
127
+ "Editing: #{@choice}",
128
+ "",
129
+ "# Database Configuration",
130
+ "database:",
131
+ " adapter: postgresql",
132
+ " host: db.example.com",
133
+ " port: 5432",
134
+ "",
135
+ "# Cache Configuration",
136
+ "cache:",
137
+ " provider: redis",
138
+ " ttl: 3600",
139
+ ].join("\n")
140
+
141
+ content = @tui.paragraph(
142
+ text: content_text,
143
+ block: @tui.block(borders: :all, title: "Configuration Editor"),
144
+ style: @tui.style(fg: :yellow)
145
+ )
146
+ f.render_widget(content, areas[0])
147
+
148
+ # Help panel
149
+ help_text = "q: Save and Exit | Ctrl+C: Cancel"
150
+ help = @tui.paragraph(
151
+ text: help_text,
152
+ block: @tui.block(borders: :all),
153
+ style: @tui.style(fg: :dark_gray),
154
+ alignment: :center
155
+ )
156
+ f.render_widget(help, areas[1])
157
+ end
158
+ end
159
+
160
+ private def handle_menu_input
161
+ case @tui.poll_event
162
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
163
+ :quit
164
+ in { type: :key, code: "enter" }
165
+ :select
166
+ in { type: :key, code: "up" }
167
+ @selected_index = (@selected_index - 1) % MENU_OPTIONS.length
168
+ nil
169
+ in { type: :key, code: "down" }
170
+ @selected_index = (@selected_index + 1) % MENU_OPTIONS.length
171
+ nil
172
+ else
173
+ nil
174
+ end
175
+ end
176
+
177
+ private def handle_editor_input
178
+ case @tui.poll_event
179
+ in { type: :key, code: "q" }
180
+ :quit # "and save," presumably
181
+ in { type: :key, code: "c", modifiers: ["ctrl"] }
182
+ :quit
183
+ else
184
+ nil
185
+ end
186
+ end
187
+ end
188
+
189
+ AppCliRichMoments.new.run if __FILE__ == $PROGRAM_NAME
@@ -1059,7 +1059,7 @@ dependencies = [
1059
1059
 
1060
1060
  [[package]]
1061
1061
  name = "ratatui_ruby"
1062
- version = "0.10.1"
1062
+ version = "0.10.2"
1063
1063
  dependencies = [
1064
1064
  "bumpalo",
1065
1065
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.10.1"
6
+ version = "0.10.2"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -151,8 +151,18 @@ impl RubyFrame {
151
151
  // 4. ensure_active() above guarantees we're still in the callback
152
152
  let frame = unsafe { (*self.inner.get()).as_mut() };
153
153
 
154
+ // Special case: Cursor widget requires Frame.set_cursor_position
155
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
156
+ let widget_class = unsafe { widget.class().name() }.into_owned();
157
+ if widget_class == "RatatuiRuby::Widgets::Cursor" {
158
+ let cursor_x: u16 = widget.funcall("x", ())?;
159
+ let cursor_y: u16 = widget.funcall("y", ())?;
160
+ frame.set_cursor_position((rect.x + cursor_x, rect.y + cursor_y));
161
+ return Ok(());
162
+ }
163
+
154
164
  // Delegate to the existing render_node function
155
- rendering::render_node(frame, rect, widget)
165
+ rendering::render_node(frame.buffer_mut(), rect, widget)
156
166
  }
157
167
 
158
168
  /// Renders a stateful widget at the specified area.
@@ -192,13 +202,16 @@ impl RubyFrame {
192
202
 
193
203
  match (widget_class.as_str(), state_class.as_str()) {
194
204
  ("RatatuiRuby::Widgets::List", "RatatuiRuby::ListState") => {
195
- widgets::list::render_stateful(frame, rect, widget, state)
205
+ let buffer = frame.buffer_mut();
206
+ widgets::list::render_stateful(buffer, rect, widget, state)
196
207
  }
197
208
  ("RatatuiRuby::Widgets::Table", "RatatuiRuby::TableState") => {
198
- widgets::table::render_stateful(frame, rect, widget, state)
209
+ let buffer = frame.buffer_mut();
210
+ widgets::table::render_stateful(buffer, rect, widget, state)
199
211
  }
200
212
  ("RatatuiRuby::Widgets::Scrollbar", "RatatuiRuby::ScrollbarState") => {
201
- widgets::scrollbar::render_stateful(frame, rect, widget, state)
213
+ let buffer = frame.buffer_mut();
214
+ widgets::scrollbar::render_stateful(buffer, rect, widget, state)
202
215
  }
203
216
  _ => Err(Error::new(
204
217
  ruby.exception_arg_error(),
@@ -83,7 +83,8 @@ fn draw(args: &[Value]) -> Result<(), Error> {
83
83
  active.store(false, std::sync::atomic::Ordering::Relaxed);
84
84
  } else if let Some(tree_value) = tree {
85
85
  // Legacy API: render tree to full area
86
- if let Err(e) = rendering::render_node(f, f.area(), tree_value) {
86
+ let area = f.area();
87
+ if let Err(e) = rendering::render_node(f.buffer_mut(), area, tree_value) {
87
88
  render_error = Some(e);
88
89
  }
89
90
  }
@@ -159,7 +160,7 @@ fn init() -> Result<(), Error> {
159
160
  let ruby = magnus::Ruby::get().unwrap();
160
161
  let m = ruby.define_module("RatatuiRuby")?;
161
162
 
162
- m.define_module_function("_init_terminal", function!(init_terminal, 2))?;
163
+ m.define_module_function("_init_terminal", function!(init_terminal, 4))?;
163
164
  m.define_module_function("_restore_terminal", function!(restore_terminal, 0))?;
164
165
  m.define_module_function("_draw", function!(draw, -1))?;
165
166
  m.define_module_function(
@@ -193,7 +194,7 @@ fn init() -> Result<(), Error> {
193
194
  // Test backend helpers
194
195
  m.define_module_function(
195
196
  "_init_test_terminal",
196
- function!(terminal::init_test_terminal, 2),
197
+ function!(terminal::init_test_terminal, 4),
197
198
  )?;
198
199
  m.define_module_function(
199
200
  "get_buffer_content",
@@ -203,12 +204,25 @@ fn init() -> Result<(), Error> {
203
204
  "get_cursor_position",
204
205
  function!(terminal::get_cursor_position, 0),
205
206
  )?;
207
+ m.define_module_function(
208
+ "set_cursor_position",
209
+ function!(terminal::set_cursor_position, 2),
210
+ )?;
206
211
  m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
207
212
  m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
208
213
  m.define_module_function(
209
214
  "_get_terminal_area",
210
215
  function!(terminal::get_terminal_area, 0),
211
216
  )?;
217
+ m.define_module_function(
218
+ "_get_terminal_size",
219
+ function!(terminal::get_terminal_size, 0),
220
+ )?;
221
+ m.define_module_function("_insert_before", function!(terminal::insert_before, 2))?;
222
+ m.define_module_function(
223
+ "_get_viewport_type",
224
+ function!(terminal::get_viewport_type, 0),
225
+ )?;
212
226
 
213
227
  // Register Layout.split on the Layout::Layout class (inside the Layout module)
214
228
  let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;