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
|
@@ -5,11 +5,11 @@ use crate::errors::type_error_with_context;
|
|
|
5
5
|
use crate::rendering::render_node;
|
|
6
6
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
7
|
use ratatui::{
|
|
8
|
+
buffer::Buffer,
|
|
8
9
|
layout::{Constraint, Direction, Flex, Layout, Rect},
|
|
9
|
-
Frame,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
pub fn render(
|
|
12
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
13
|
let ruby = magnus::Ruby::get().unwrap();
|
|
14
14
|
let direction_sym: Symbol = node.funcall("direction", ())?;
|
|
15
15
|
let children_val: Value = node.funcall("children", ())?;
|
|
@@ -72,7 +72,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
72
72
|
let index = isize::try_from(i)
|
|
73
73
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
74
74
|
let child: Value = children_array.entry(index)?;
|
|
75
|
-
if let Err(e) = render_node(
|
|
75
|
+
if let Err(e) = render_node(buffer, chunks[i], child) {
|
|
76
76
|
eprintln!("Error rendering child {i}: {e:?}");
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -5,9 +5,10 @@ use crate::style::{parse_block, parse_style};
|
|
|
5
5
|
use crate::text::parse_span;
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, Value};
|
|
8
|
-
use ratatui::
|
|
8
|
+
use ratatui::buffer::Buffer;
|
|
9
|
+
use ratatui::{layout::Rect, widgets::LineGauge, widgets::Widget};
|
|
9
10
|
|
|
10
|
-
pub fn render(
|
|
11
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
12
|
let bump = Bump::new();
|
|
12
13
|
let ratio: f64 = node.funcall("ratio", ())?;
|
|
13
14
|
let label_val: Value = node.funcall("label", ())?;
|
|
@@ -52,7 +53,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
52
53
|
gauge = gauge.block(parse_block(block_val, &bump)?);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
gauge.render(area, buffer);
|
|
56
57
|
Ok(())
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -7,14 +7,14 @@ use crate::text::{parse_line, parse_span};
|
|
|
7
7
|
use crate::widgets::list_state::RubyListState;
|
|
8
8
|
use bumpalo::Bump;
|
|
9
9
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
10
|
+
use ratatui::buffer::Buffer;
|
|
10
11
|
use ratatui::{
|
|
11
12
|
layout::Rect,
|
|
12
13
|
text::Line,
|
|
13
|
-
widgets::{HighlightSpacing, List, ListItem, ListState},
|
|
14
|
-
Frame,
|
|
14
|
+
widgets::{HighlightSpacing, List, ListItem, ListState, StatefulWidget},
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
pub fn render(
|
|
17
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
18
18
|
let bump = Bump::new();
|
|
19
19
|
let ruby = magnus::Ruby::get().unwrap();
|
|
20
20
|
let items_val: Value = node.funcall("items", ())?;
|
|
@@ -107,7 +107,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
107
107
|
list = list.block(parse_block(block_val, &bump)?);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
StatefulWidget::render(list, area, buffer, &mut state);
|
|
111
111
|
Ok(())
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -116,7 +116,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
116
116
|
/// This function ignores `selected_index` and `offset` from the widget.
|
|
117
117
|
/// The State object is the single source of truth for selection and scroll position.
|
|
118
118
|
pub fn render_stateful(
|
|
119
|
-
|
|
119
|
+
buffer: &mut Buffer,
|
|
120
120
|
area: Rect,
|
|
121
121
|
node: Value,
|
|
122
122
|
state_wrapper: Value,
|
|
@@ -210,7 +210,7 @@ pub fn render_stateful(
|
|
|
210
210
|
// Borrow the inner ListState, render, and release the borrow immediately
|
|
211
211
|
{
|
|
212
212
|
let mut inner_state = state.borrow_mut();
|
|
213
|
-
|
|
213
|
+
StatefulWidget::render(list, area, buffer, &mut inner_state);
|
|
214
214
|
}
|
|
215
215
|
// Borrow is now released
|
|
216
216
|
|
|
@@ -299,7 +299,6 @@ mod tests {
|
|
|
299
299
|
state.select(Some(1));
|
|
300
300
|
|
|
301
301
|
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
302
|
-
use ratatui::widgets::StatefulWidget;
|
|
303
302
|
StatefulWidget::render(list, Rect::new(0, 0, 10, 2), &mut buf, &mut state);
|
|
304
303
|
|
|
305
304
|
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
@@ -328,7 +327,6 @@ mod tests {
|
|
|
328
327
|
state.select(Some(0));
|
|
329
328
|
|
|
330
329
|
let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
331
|
-
use ratatui::widgets::StatefulWidget;
|
|
332
330
|
StatefulWidget::render(
|
|
333
331
|
list_without_repeat,
|
|
334
332
|
Rect::new(0, 0, 10, 2),
|
|
@@ -370,7 +368,6 @@ mod tests {
|
|
|
370
368
|
state.select(Some(1));
|
|
371
369
|
|
|
372
370
|
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 4));
|
|
373
|
-
use ratatui::widgets::StatefulWidget;
|
|
374
371
|
StatefulWidget::render(list, Rect::new(0, 0, 15, 4), &mut buf, &mut state);
|
|
375
372
|
|
|
376
373
|
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
use crate::errors::type_error_with_context;
|
|
5
5
|
use crate::rendering::render_node;
|
|
6
6
|
use magnus::{prelude::*, Error, Value};
|
|
7
|
-
use ratatui::{layout::Rect
|
|
7
|
+
use ratatui::{buffer::Buffer, layout::Rect};
|
|
8
8
|
|
|
9
|
-
pub fn render(
|
|
9
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
10
10
|
let ruby = magnus::Ruby::get().unwrap();
|
|
11
11
|
let layers_val: Value = node.funcall("layers", ())?;
|
|
12
12
|
let layers_array = magnus::RArray::from_value(layers_val)
|
|
@@ -16,7 +16,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
16
16
|
let index = isize::try_from(i)
|
|
17
17
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
18
18
|
let layer: Value = layers_array.entry(index)?;
|
|
19
|
-
if let Err(e) = render_node(
|
|
19
|
+
if let Err(e) = render_node(buffer, area, layer) {
|
|
20
20
|
eprintln!("Error rendering overlay layer {i}: {e:?}");
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
5
|
use bumpalo::Bump;
|
|
6
6
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use ratatui::buffer::Buffer;
|
|
7
8
|
use ratatui::{
|
|
8
9
|
layout::{HorizontalAlignment, Rect},
|
|
9
|
-
widgets::{Paragraph, Wrap},
|
|
10
|
-
Frame,
|
|
10
|
+
widgets::{Paragraph, Widget, Wrap},
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
use crate::text::parse_text;
|
|
14
14
|
|
|
15
|
-
fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
|
|
15
|
+
pub fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
|
|
16
16
|
let text_val: Value = node.funcall("text", ())?;
|
|
17
17
|
let style_val: Value = node.funcall("style", ())?;
|
|
18
18
|
let block_val: Value = node.funcall("block", ())?;
|
|
@@ -50,10 +50,10 @@ fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
|
|
|
50
50
|
Ok(paragraph)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
pub fn render(
|
|
53
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
54
54
|
let bump = Bump::new();
|
|
55
55
|
let paragraph = create_paragraph(node, &bump)?;
|
|
56
|
-
|
|
56
|
+
Widget::render(paragraph, area, buffer);
|
|
57
57
|
Ok(())
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -78,7 +78,6 @@ mod tests {
|
|
|
78
78
|
fn test_paragraph_rendering() {
|
|
79
79
|
let p = Paragraph::new("test content").alignment(HorizontalAlignment::Center);
|
|
80
80
|
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
|
81
|
-
use ratatui::widgets::Widget;
|
|
82
81
|
p.render(Rect::new(0, 0, 20, 1), &mut buf);
|
|
83
82
|
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
84
83
|
assert!(content.contains("test content"));
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
use magnus::Value;
|
|
6
6
|
use ratatui::{
|
|
7
|
+
buffer::Buffer,
|
|
7
8
|
layout::Rect,
|
|
8
|
-
widgets::{RatatuiLogo, RatatuiLogoSize},
|
|
9
|
-
Frame,
|
|
9
|
+
widgets::{RatatuiLogo, RatatuiLogoSize, Widget},
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
pub fn render(
|
|
12
|
+
pub fn render(buffer: &mut Buffer, area: Rect, _node: Value) {
|
|
13
13
|
// RatatuiLogo does not support custom styling (it has fixed colors).
|
|
14
14
|
// It requires a size argument.
|
|
15
15
|
let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
|
|
16
|
-
|
|
16
|
+
widget.render(area, buffer);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
#[cfg(test)]
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
use crate::style::parse_block;
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, Value};
|
|
8
|
-
use ratatui::{
|
|
8
|
+
use ratatui::{
|
|
9
|
+
buffer::Buffer,
|
|
10
|
+
layout::Rect,
|
|
11
|
+
widgets::{RatatuiMascot, Widget},
|
|
12
|
+
};
|
|
9
13
|
|
|
10
|
-
pub fn render_ratatui_mascot(
|
|
14
|
+
pub fn render_ratatui_mascot(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
15
|
let block_val: Value = node.funcall("block", ())?;
|
|
12
16
|
|
|
13
17
|
let mut inner_area = area;
|
|
@@ -16,11 +20,11 @@ pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Resu
|
|
|
16
20
|
let bump = Bump::new();
|
|
17
21
|
let block = parse_block(block_val, &bump)?;
|
|
18
22
|
inner_area = block.inner(area);
|
|
19
|
-
|
|
23
|
+
block.render(area, buffer);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
let widget = RatatuiMascot::new();
|
|
23
|
-
|
|
27
|
+
widget.render(inner_area, buffer);
|
|
24
28
|
Ok(())
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -6,12 +6,12 @@ use crate::widgets::scrollbar_state::RubyScrollbarState;
|
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
8
8
|
use ratatui::{
|
|
9
|
+
buffer::Buffer,
|
|
9
10
|
layout::Rect,
|
|
10
|
-
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
|
|
11
|
-
Frame,
|
|
11
|
+
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget},
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
pub fn render(
|
|
14
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
15
|
let content_length: usize = node.funcall("content_length", ())?;
|
|
16
16
|
let position: usize = node.funcall("position", ())?;
|
|
17
17
|
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
@@ -79,13 +79,13 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if block_val.is_nil() {
|
|
82
|
-
|
|
82
|
+
StatefulWidget::render(scrollbar, area, buffer, &mut state);
|
|
83
83
|
} else {
|
|
84
84
|
let bump = Bump::new();
|
|
85
85
|
let block = parse_block(block_val, &bump)?;
|
|
86
86
|
let inner_area = block.inner(area);
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
block.render(area, buffer);
|
|
88
|
+
StatefulWidget::render(scrollbar, inner_area, buffer, &mut state);
|
|
89
89
|
}
|
|
90
90
|
Ok(())
|
|
91
91
|
}
|
|
@@ -95,7 +95,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
95
95
|
/// The State object is the single source of truth for position and `content_length`.
|
|
96
96
|
/// Widget properties (`position`, `content_length`) are ignored.
|
|
97
97
|
pub fn render_stateful(
|
|
98
|
-
|
|
98
|
+
buffer: &mut Buffer,
|
|
99
99
|
area: Rect,
|
|
100
100
|
node: Value,
|
|
101
101
|
state_wrapper: Value,
|
|
@@ -168,13 +168,13 @@ pub fn render_stateful(
|
|
|
168
168
|
{
|
|
169
169
|
let mut inner_state = state.borrow_mut();
|
|
170
170
|
if block_val.is_nil() {
|
|
171
|
-
|
|
171
|
+
StatefulWidget::render(scrollbar, area, buffer, &mut inner_state);
|
|
172
172
|
} else {
|
|
173
173
|
let bump = Bump::new();
|
|
174
174
|
let block = parse_block(block_val, &bump)?;
|
|
175
175
|
let inner_area = block.inner(area);
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
block.render(area, buffer);
|
|
177
|
+
StatefulWidget::render(scrollbar, inner_area, buffer, &mut inner_state);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
5
5
|
use bumpalo::Bump;
|
|
6
6
|
use magnus::{prelude::*, Error, RString, Value};
|
|
7
|
-
use ratatui::
|
|
7
|
+
use ratatui::buffer::Buffer;
|
|
8
|
+
use ratatui::{layout::Rect, widgets::RenderDirection, widgets::Sparkline, widgets::Widget};
|
|
8
9
|
|
|
9
|
-
pub fn render(
|
|
10
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
10
11
|
let bump = Bump::new();
|
|
11
12
|
let ruby = magnus::Ruby::get().unwrap();
|
|
12
13
|
let data_val: magnus::RArray = node.funcall("data", ())?;
|
|
@@ -72,7 +73,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
72
73
|
sparkline = sparkline.bar_set(parse_bar_set(bar_set_val, &bump)?);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
sparkline.render(area, buffer);
|
|
76
77
|
Ok(())
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -8,12 +8,12 @@ use crate::widgets::table_state::RubyTableState;
|
|
|
8
8
|
use bumpalo::Bump;
|
|
9
9
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
10
10
|
use ratatui::{
|
|
11
|
+
buffer::Buffer,
|
|
11
12
|
layout::{Constraint, Flex, Rect},
|
|
12
|
-
widgets::{Cell, HighlightSpacing, Row, Table, TableState},
|
|
13
|
-
Frame,
|
|
13
|
+
widgets::{Cell, HighlightSpacing, Row, StatefulWidget, Table, TableState},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
pub fn render(
|
|
16
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
17
17
|
let bump = Bump::new();
|
|
18
18
|
let ruby = magnus::Ruby::get().unwrap();
|
|
19
19
|
let header_val: Value = node.funcall("header", ())?;
|
|
@@ -123,7 +123,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
123
123
|
*state.offset_mut() = offset;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
StatefulWidget::render(table, area, buffer, &mut state);
|
|
127
127
|
Ok(())
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -132,7 +132,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
132
132
|
/// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
|
|
133
133
|
/// The State object is the single source of truth for selection and scroll position.
|
|
134
134
|
pub fn render_stateful(
|
|
135
|
-
|
|
135
|
+
buffer: &mut Buffer,
|
|
136
136
|
area: Rect,
|
|
137
137
|
node: Value,
|
|
138
138
|
state_wrapper: Value,
|
|
@@ -230,7 +230,7 @@ pub fn render_stateful(
|
|
|
230
230
|
// Borrow the inner TableState, render, and release the borrow immediately
|
|
231
231
|
{
|
|
232
232
|
let mut inner_state = state.borrow_mut();
|
|
233
|
-
|
|
233
|
+
StatefulWidget::render(table, area, buffer, &mut inner_state);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
Ok(())
|
|
@@ -6,12 +6,13 @@ use crate::style::parse_block;
|
|
|
6
6
|
use crate::text::{parse_line, parse_span};
|
|
7
7
|
use bumpalo::Bump;
|
|
8
8
|
use magnus::{prelude::*, Error, Value};
|
|
9
|
-
use ratatui::
|
|
9
|
+
use ratatui::buffer::Buffer;
|
|
10
|
+
use ratatui::{layout::Rect, text::Line, widgets::Tabs, widgets::Widget};
|
|
10
11
|
|
|
11
|
-
pub fn render(
|
|
12
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
12
13
|
let bump = Bump::new();
|
|
13
14
|
let tabs = create_tabs(node, &bump)?;
|
|
14
|
-
|
|
15
|
+
tabs.render(area, buffer);
|
|
15
16
|
Ok(())
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
require "rexml/document"
|
|
10
|
+
|
|
11
|
+
module RatatuiRuby
|
|
12
|
+
module Labs
|
|
13
|
+
# A11Y lab: exports widget tree as XML.
|
|
14
|
+
#
|
|
15
|
+
# Writes an XML representation of the widget tree to a temporary file
|
|
16
|
+
# every frame when enabled.
|
|
17
|
+
module A11y
|
|
18
|
+
# Path to the XML output file in the system temp directory.
|
|
19
|
+
OUTPUT_PATH = File.join(Dir.tmpdir, "ratatui_ruby_a11y.xml").freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Dumps the widget tree to XML (single widget for tree mode).
|
|
23
|
+
def dump_widget_tree(widget, _area = nil)
|
|
24
|
+
doc = REXML::Document.new
|
|
25
|
+
doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
|
|
26
|
+
doc.add(build_element(widget))
|
|
27
|
+
write_document(doc)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns startup message for users to see before TUI launches.
|
|
31
|
+
#
|
|
32
|
+
# Since stdout is captured during TUI rendering, users need to know
|
|
33
|
+
# where the XML file will be written before the app starts.
|
|
34
|
+
def startup_message
|
|
35
|
+
<<~MSG
|
|
36
|
+
A11Y Lab enabled! Widget tree will be written to:
|
|
37
|
+
#{OUTPUT_PATH}
|
|
38
|
+
|
|
39
|
+
Press Enter to launch the TUI...
|
|
40
|
+
MSG
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Dumps multiple widgets captured from Frame API mode.
|
|
44
|
+
def dump_widgets(widgets_with_areas)
|
|
45
|
+
Labs.warn_once!("Labs::A11y (RR_LABS=A11Y)")
|
|
46
|
+
|
|
47
|
+
# Reset counter each frame for stable IDs
|
|
48
|
+
@widget_id_counter = 0
|
|
49
|
+
|
|
50
|
+
doc = REXML::Document.new
|
|
51
|
+
doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
|
|
52
|
+
|
|
53
|
+
frame = REXML::Element.new("RatatuiFrame")
|
|
54
|
+
widgets_with_areas.each do |widget, area|
|
|
55
|
+
frame.add(build_element_with_area(widget, area))
|
|
56
|
+
end
|
|
57
|
+
doc.add(frame)
|
|
58
|
+
|
|
59
|
+
write_document(doc)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def write_document(doc)
|
|
63
|
+
output = +""
|
|
64
|
+
formatter = REXML::Formatters::Pretty.new(2)
|
|
65
|
+
formatter.compact = true
|
|
66
|
+
formatter.write(doc, output)
|
|
67
|
+
File.write(OUTPUT_PATH, output)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def build_element_with_area(widget, area)
|
|
71
|
+
class_name = widget.class.name&.split("::")&.last || "Unknown"
|
|
72
|
+
element = REXML::Element.new(class_name)
|
|
73
|
+
|
|
74
|
+
# Generate unique id for this widget
|
|
75
|
+
@widget_id_counter ||= 0
|
|
76
|
+
@widget_id_counter += 1
|
|
77
|
+
widget_id = "w#{@widget_id_counter}"
|
|
78
|
+
element.add_attribute("id", widget_id)
|
|
79
|
+
|
|
80
|
+
# Add area attributes
|
|
81
|
+
element.add_attribute("x", area.x.to_s)
|
|
82
|
+
element.add_attribute("y", area.y.to_s)
|
|
83
|
+
element.add_attribute("width", area.width.to_s)
|
|
84
|
+
element.add_attribute("height", area.height.to_s)
|
|
85
|
+
|
|
86
|
+
add_members(element, widget, parent_id: widget_id)
|
|
87
|
+
element
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private def build_element(node)
|
|
91
|
+
class_name = node.class.name&.split("::")&.last || "Unknown"
|
|
92
|
+
element = REXML::Element.new(class_name)
|
|
93
|
+
|
|
94
|
+
if node.respond_to?(:to_h) && node.respond_to?(:members)
|
|
95
|
+
add_members(element, node)
|
|
96
|
+
else
|
|
97
|
+
element.text = node.to_s
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
element
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private def add_members(element, node, parent_id: nil)
|
|
104
|
+
return unless node.respond_to?(:to_h) && node.respond_to?(:members)
|
|
105
|
+
|
|
106
|
+
node.to_h.each do |key, value|
|
|
107
|
+
# Skip nil and empty values entirely (no noise in output)
|
|
108
|
+
next if value.nil?
|
|
109
|
+
next if value.respond_to?(:empty?) && value.empty?
|
|
110
|
+
|
|
111
|
+
# Skip objects where all members are nil/empty (like default Style)
|
|
112
|
+
if value.respond_to?(:to_h) && value.respond_to?(:members)
|
|
113
|
+
attrs = value.to_h.compact
|
|
114
|
+
next if attrs.empty? || attrs.values.all? { |v| v.respond_to?(:empty?) && v.empty? }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Scalar values → XML attributes
|
|
118
|
+
# Exception: 'text' and 'content' accept Text (multi-line capable)
|
|
119
|
+
# Complex values → XML child elements
|
|
120
|
+
multiline_keys = %w[text content]
|
|
121
|
+
if scalar?(value) && !multiline_keys.include?(key.to_s)
|
|
122
|
+
element.add_attribute(key.to_s, value.to_s)
|
|
123
|
+
else
|
|
124
|
+
# Special handling for 'block' wrapper - add ARIA role and id/for
|
|
125
|
+
is_block_wrapper = key.to_s == "block" && parent_id
|
|
126
|
+
child = build_child_element(key, value, is_wrapper: is_block_wrapper, parent_id:)
|
|
127
|
+
element.add(child) if child
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def scalar?(value)
|
|
133
|
+
case value
|
|
134
|
+
when String, Symbol, Numeric, TrueClass, FalseClass
|
|
135
|
+
true
|
|
136
|
+
else
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private def build_child_element(key, value, is_wrapper: false, parent_id: nil)
|
|
142
|
+
element = REXML::Element.new(key.to_s)
|
|
143
|
+
|
|
144
|
+
# Add ARIA role and id/for association for block wrappers
|
|
145
|
+
if is_wrapper && parent_id
|
|
146
|
+
element.add_attribute("role", "group")
|
|
147
|
+
element.add_attribute("for", parent_id)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
case value
|
|
151
|
+
when Array
|
|
152
|
+
value.each { |item| element.add(build_element(item)) }
|
|
153
|
+
when Hash
|
|
154
|
+
# Plain Hash: serialize keys as attributes
|
|
155
|
+
value.each do |k, v|
|
|
156
|
+
next if v.nil?
|
|
157
|
+
next if v.respond_to?(:empty?) && v.empty?
|
|
158
|
+
element.add_attribute(k.to_s, v.to_s)
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
if value.respond_to?(:to_h) && value.respond_to?(:members)
|
|
162
|
+
add_members(element, value)
|
|
163
|
+
else
|
|
164
|
+
element.text = value.to_s
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
element
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
class Frame
|
|
10
|
+
# A11Y Lab Integration
|
|
11
|
+
#
|
|
12
|
+
# When the A11Y lab is enabled, we capture widgets as they are rendered
|
|
13
|
+
# and write the tree to XML when flush_a11y_capture is called.
|
|
14
|
+
module A11yCapture
|
|
15
|
+
# Intercepts render_widget to capture widgets for A11Y export.
|
|
16
|
+
# @param widget [widget] The widget being rendered
|
|
17
|
+
# @param area [Layout::Rect] The area to render into
|
|
18
|
+
def render_widget(widget, area)
|
|
19
|
+
if Labs.enabled?(:a11y)
|
|
20
|
+
widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
|
|
21
|
+
widgets << [widget, area]
|
|
22
|
+
end
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Intercepts render_stateful_widget to capture widgets for A11Y export.
|
|
27
|
+
# @param widget [widget] The widget being rendered
|
|
28
|
+
# @param area [Layout::Rect] The area to render into
|
|
29
|
+
# @param state [Object] The widget state
|
|
30
|
+
def render_stateful_widget(widget, area, state)
|
|
31
|
+
if Labs.enabled?(:a11y)
|
|
32
|
+
widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
|
|
33
|
+
widgets << [widget, area]
|
|
34
|
+
end
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called at end of draw block to flush captured widgets
|
|
39
|
+
def flush_a11y_capture
|
|
40
|
+
widgets = @a11y_widgets
|
|
41
|
+
return unless Labs.enabled?(:a11y) && widgets&.any?
|
|
42
|
+
|
|
43
|
+
Labs::A11y.dump_widgets(widgets)
|
|
44
|
+
@a11y_widgets = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
prepend A11yCapture
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
# Experimental lab features.
|
|
10
|
+
module Labs
|
|
11
|
+
@enabled_lab = nil #: Symbol?
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Returns whether the specified lab is enabled.
|
|
15
|
+
def enabled?(lab)
|
|
16
|
+
@enabled_lab == lab.to_sym.downcase
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Enables a lab programmatically.
|
|
20
|
+
def enable!(lab)
|
|
21
|
+
@enabled_lab = lab.to_sym.downcase
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Resets all labs (for testing only).
|
|
25
|
+
def reset!
|
|
26
|
+
@enabled_lab = nil
|
|
27
|
+
@warned = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Emits experimental warning once per session.
|
|
31
|
+
def warn_once!(feature_name)
|
|
32
|
+
return if @warned
|
|
33
|
+
|
|
34
|
+
RatatuiRuby.warn_experimental_feature(feature_name)
|
|
35
|
+
@warned = true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Auto-enable from environment variable
|
|
42
|
+
if (lab = ENV["RR_LABS"])
|
|
43
|
+
RatatuiRuby::Labs.enable!(lab)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require_relative "labs/a11y"
|
|
47
|
+
require_relative "labs/frame_a11y_capture"
|
|
@@ -50,6 +50,32 @@ module RatatuiRuby
|
|
|
50
50
|
def initialize(x: 0, y: 0)
|
|
51
51
|
super(x: Integer(x), y: Integer(y))
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Enables array destructuring for convenient coordinate extraction.
|
|
55
|
+
#
|
|
56
|
+
# Returns:: Array of [x, y] coordinates.
|
|
57
|
+
#
|
|
58
|
+
# === Example
|
|
59
|
+
#
|
|
60
|
+
#--
|
|
61
|
+
# SPDX-SnippetBegin
|
|
62
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
63
|
+
# SPDX-License-Identifier: MIT-0
|
|
64
|
+
#++
|
|
65
|
+
# pos = Position.new(x: 10, y: 5)
|
|
66
|
+
# x, y = pos # Uses deconstruct
|
|
67
|
+
# puts "Column: #{x}, Row: #{y}"
|
|
68
|
+
#--
|
|
69
|
+
# SPDX-SnippetEnd
|
|
70
|
+
#++
|
|
71
|
+
def deconstruct
|
|
72
|
+
[x, y]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Alias for implicit array conversion.
|
|
76
|
+
#
|
|
77
|
+
# Enables `x, y = position` syntax by making Position respond to +to_ary+.
|
|
78
|
+
alias to_ary deconstruct
|
|
53
79
|
end
|
|
54
80
|
end
|
|
55
81
|
end
|