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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/CHANGELOG.md +24 -0
- data/doc/concepts/application_architecture.md +2 -2
- data/doc/concepts/application_testing.md +1 -1
- data/doc/concepts/custom_widgets.md +2 -2
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +375 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +206 -0
- data/doc/contributors/todo/align/terminal.md +647 -0
- data/doc/getting_started/quickstart.md +41 -41
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/examples/app_cli_rich_moments/README.md +81 -0
- data/examples/app_cli_rich_moments/app.rb +189 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/frame.rs +17 -4
- data/ext/ratatui_ruby/src/lib.rs +17 -3
- data/ext/ratatui_ruby/src/lib.rs.bak +286 -0
- data/ext/ratatui_ruby/src/rendering.rs +38 -25
- data/ext/ratatui_ruby/src/rendering.rs.bak +152 -0
- data/ext/ratatui_ruby/src/terminal.rs +245 -33
- data/ext/ratatui_ruby/src/terminal.rs.bak +381 -0
- data/ext/ratatui_ruby/src/terminal.rs.orig +409 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/block.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +7 -4
- data/ext/ratatui_ruby/src/widgets/center.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/clear.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/cursor.rs +10 -7
- data/ext/ratatui_ruby/src/widgets/gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/layout.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -9
- data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +5 -6
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +8 -4
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +10 -10
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/table.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +4 -3
- data/lib/ratatui_ruby/labs/a11y.rb +173 -0
- data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
- data/lib/ratatui_ruby/labs.rb +47 -0
- data/lib/ratatui_ruby/layout/position.rb +26 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +164 -6
- data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +8 -1
- data/lib/ratatui_ruby/tui/core.rb +16 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +82 -3
- data/migrate_to_buffer.rb +145 -0
- data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
- data/sig/ratatui_ruby/labs.rbs +87 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +12 -4
- data/sig/ratatui_ruby/terminal/viewport.rbs +19 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +13 -5
- data/sig/ratatui_ruby/tui/core.rbs +3 -0
- metadata +21 -2
- /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](
|
|
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
|
|
224
|
-
|
|
225
|
-
| [Bar Chart](
|
|
226
|
-
| [Block](
|
|
227
|
-
| [Box](
|
|
228
|
-
| [Calendar](
|
|
229
|
-
| [Chart](
|
|
230
|
-
| [Gauge](
|
|
231
|
-
| [Layout Split](
|
|
232
|
-
| [Line Gauge](
|
|
233
|
-
| [List](
|
|
234
|
-
| [Map](
|
|
235
|
-
| [Popup](
|
|
236
|
-
| [Ratatui Logo](
|
|
237
|
-
| [Ratatui Mascot](
|
|
238
|
-
| [Rect](
|
|
239
|
-
| [Rich Text](
|
|
240
|
-
| [Scrollbar](
|
|
241
|
-
| [Scroll Text](
|
|
242
|
-
| [Sparkline](
|
|
243
|
-
| [Style Colors](
|
|
244
|
-
| [Table](
|
|
245
|
-
| [Tabs](
|
|
246
|
-
| [Text Width](
|
|
247
|
-
| [Canvas](
|
|
248
|
-
| [Cell](
|
|
249
|
-
| [Center](
|
|
250
|
-
| [Overlay](
|
|
251
|
-
| [Custom Render](
|
|
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
|
|
258
|
-
|
|
259
|
-
| [All Events](
|
|
260
|
-
| [Color Picker](
|
|
261
|
-
| [Debugging Showcase](
|
|
262
|
-
| [Login Form](
|
|
263
|
-
| [Stateful Interaction](
|
|
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
|
-
[](
|
|
267
|
+
[](../../examples/app_all_events/README.md)
|
|
268
268
|
|
|
269
269
|
#### Color Picker
|
|
270
270
|
|
|
271
|
-
[](
|
|
271
|
+
[](../../examples/app_color_picker/README.md)
|
|
272
272
|
|
|
273
273
|
#### Debugging Showcase
|
|
274
274
|
|
|
275
|
-
[](
|
|
275
|
+
[](../../examples/app_debugging_showcase/README.md)
|
|
276
276
|
|
|
277
277
|
#### Login Form
|
|
278
278
|
|
|
279
|
-
[](
|
|
279
|
+
[](../../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
|
+
[](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
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED
data/ext/ratatui_ruby/Cargo.toml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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")?;
|