ratatui_ruby 0.1.0
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 +7 -0
- data/.build.yml +34 -0
- data/.pre-commit-config.yaml +9 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +119 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +30 -0
- data/CONTRIBUTING.md +40 -0
- data/LICENSE +15 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT.txt +21 -0
- data/README.md +86 -0
- data/REUSE.toml +17 -0
- data/Rakefile +108 -0
- data/docs/application_testing.md +96 -0
- data/docs/contributors/design/ruby_frontend.md +100 -0
- data/docs/contributors/design/rust_backend.md +61 -0
- data/docs/contributors/design.md +11 -0
- data/docs/contributors/index.md +16 -0
- data/docs/images/examples-analytics.rb.png +0 -0
- data/docs/images/examples-box_demo.rb.png +0 -0
- data/docs/images/examples-dashboard.rb.png +0 -0
- data/docs/images/examples-login_form.rb.png +0 -0
- data/docs/images/examples-map_demo.rb.png +0 -0
- data/docs/images/examples-mouse_events.rb.png +0 -0
- data/docs/images/examples-scrollbar_demo.rb.png +0 -0
- data/docs/images/examples-stock_ticker.rb.png +0 -0
- data/docs/images/examples-system_monitor.rb.png +0 -0
- data/docs/index.md +18 -0
- data/docs/quickstart.md +126 -0
- data/examples/analytics.rb +87 -0
- data/examples/box_demo.rb +71 -0
- data/examples/dashboard.rb +72 -0
- data/examples/login_form.rb +114 -0
- data/examples/map_demo.rb +58 -0
- data/examples/mouse_events.rb +95 -0
- data/examples/scrollbar_demo.rb +75 -0
- data/examples/stock_ticker.rb +85 -0
- data/examples/system_monitor.rb +93 -0
- data/examples/test_analytics.rb +65 -0
- data/examples/test_box_demo.rb +38 -0
- data/examples/test_dashboard.rb +38 -0
- data/examples/test_login_form.rb +63 -0
- data/examples/test_map_demo.rb +100 -0
- data/examples/test_stock_ticker.rb +39 -0
- data/examples/test_system_monitor.rb +40 -0
- data/ext/ratatui_ruby/.cargo/config.toml +8 -0
- data/ext/ratatui_ruby/.gitignore +4 -0
- data/ext/ratatui_ruby/Cargo.lock +698 -0
- data/ext/ratatui_ruby/Cargo.toml +16 -0
- data/ext/ratatui_ruby/extconf.rb +12 -0
- data/ext/ratatui_ruby/src/events.rs +279 -0
- data/ext/ratatui_ruby/src/lib.rs +105 -0
- data/ext/ratatui_ruby/src/rendering.rs +31 -0
- data/ext/ratatui_ruby/src/style.rs +149 -0
- data/ext/ratatui_ruby/src/terminal.rs +131 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +73 -0
- data/ext/ratatui_ruby/src/widgets/block.rs +12 -0
- data/ext/ratatui_ruby/src/widgets/canvas.rs +146 -0
- data/ext/ratatui_ruby/src/widgets/center.rs +81 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +29 -0
- data/ext/ratatui_ruby/src/widgets/gauge.rs +50 -0
- data/ext/ratatui_ruby/src/widgets/layout.rs +82 -0
- data/ext/ratatui_ruby/src/widgets/linechart.rs +154 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +62 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +18 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +20 -0
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +56 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +68 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +59 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +117 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +51 -0
- data/lib/ratatui_ruby/output.rb +7 -0
- data/lib/ratatui_ruby/schema/bar_chart.rb +28 -0
- data/lib/ratatui_ruby/schema/block.rb +23 -0
- data/lib/ratatui_ruby/schema/canvas.rb +62 -0
- data/lib/ratatui_ruby/schema/center.rb +19 -0
- data/lib/ratatui_ruby/schema/constraint.rb +33 -0
- data/lib/ratatui_ruby/schema/cursor.rb +17 -0
- data/lib/ratatui_ruby/schema/gauge.rb +24 -0
- data/lib/ratatui_ruby/schema/layout.rb +22 -0
- data/lib/ratatui_ruby/schema/line_chart.rb +41 -0
- data/lib/ratatui_ruby/schema/list.rb +22 -0
- data/lib/ratatui_ruby/schema/overlay.rb +15 -0
- data/lib/ratatui_ruby/schema/paragraph.rb +37 -0
- data/lib/ratatui_ruby/schema/scrollbar.rb +33 -0
- data/lib/ratatui_ruby/schema/sparkline.rb +24 -0
- data/lib/ratatui_ruby/schema/style.rb +31 -0
- data/lib/ratatui_ruby/schema/table.rb +24 -0
- data/lib/ratatui_ruby/schema/tabs.rb +22 -0
- data/lib/ratatui_ruby/test_helper.rb +75 -0
- data/lib/ratatui_ruby/version.rb +10 -0
- data/lib/ratatui_ruby.rb +87 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +16 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +14 -0
- data/sig/ratatui_ruby/schema/block.rbs +11 -0
- data/sig/ratatui_ruby/schema/canvas.rbs +62 -0
- data/sig/ratatui_ruby/schema/center.rbs +11 -0
- data/sig/ratatui_ruby/schema/constraint.rbs +13 -0
- data/sig/ratatui_ruby/schema/cursor.rbs +10 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +13 -0
- data/sig/ratatui_ruby/schema/layout.rbs +11 -0
- data/sig/ratatui_ruby/schema/line_chart.rbs +20 -0
- data/sig/ratatui_ruby/schema/list.rbs +11 -0
- data/sig/ratatui_ruby/schema/overlay.rbs +9 -0
- data/sig/ratatui_ruby/schema/paragraph.rbs +11 -0
- data/sig/ratatui_ruby/schema/scrollbar.rbs +20 -0
- data/sig/ratatui_ruby/schema/sparkline.rbs +12 -0
- data/sig/ratatui_ruby/schema/style.rbs +13 -0
- data/sig/ratatui_ruby/schema/table.rbs +13 -0
- data/sig/ratatui_ruby/schema/tabs.rbs +11 -0
- data/sig/ratatui_ruby/test_helper.rbs +11 -0
- data/sig/ratatui_ruby/version.rbs +6 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +196 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_block, parse_color};
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::Rect,
|
|
8
|
+
style::{Color, Style},
|
|
9
|
+
symbols,
|
|
10
|
+
widgets::{Chart, Dataset},
|
|
11
|
+
Frame,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
16
|
+
let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
|
|
17
|
+
let y_labels_val: magnus::RArray = node.funcall("y_labels", ())?;
|
|
18
|
+
let y_bounds_val: magnus::RArray = node.funcall("y_bounds", ())?;
|
|
19
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
20
|
+
|
|
21
|
+
let mut datasets = Vec::new();
|
|
22
|
+
// We need to keep the data alive until the chart is rendered
|
|
23
|
+
let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
|
|
24
|
+
let mut name_storage: Vec<String> = Vec::new();
|
|
25
|
+
|
|
26
|
+
for i in 0..datasets_val.len() {
|
|
27
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
28
|
+
let name: String = ds_val.funcall("name", ())?;
|
|
29
|
+
let data_array: magnus::RArray = ds_val.funcall("data", ())?;
|
|
30
|
+
|
|
31
|
+
let mut points = Vec::new();
|
|
32
|
+
for j in 0..data_array.len() {
|
|
33
|
+
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
34
|
+
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
35
|
+
Error::new(magnus::exception::type_error(), "expected array for point")
|
|
36
|
+
})?;
|
|
37
|
+
let x_val: Value = point_array.entry(0)?;
|
|
38
|
+
let y_val: Value = point_array.entry(1)?;
|
|
39
|
+
|
|
40
|
+
let x: f64 = x_val.funcall("to_f", ())?;
|
|
41
|
+
let y: f64 = y_val.funcall("to_f", ())?;
|
|
42
|
+
points.push((x, y));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
data_storage.push(points);
|
|
46
|
+
name_storage.push(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for i in 0..data_storage.len() {
|
|
50
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
51
|
+
let color_val: Value = ds_val.funcall("color", ())?;
|
|
52
|
+
let color_str: String = color_val.funcall("to_s", ())?;
|
|
53
|
+
let color = parse_color(&color_str).unwrap_or(Color::White);
|
|
54
|
+
|
|
55
|
+
let ds = Dataset::default()
|
|
56
|
+
.name(name_storage[i].clone())
|
|
57
|
+
.marker(symbols::Marker::Braille)
|
|
58
|
+
.style(Style::default().fg(color))
|
|
59
|
+
.data(&data_storage[i]);
|
|
60
|
+
datasets.push(ds);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let mut x_labels = Vec::new();
|
|
64
|
+
for i in 0..x_labels_val.len() {
|
|
65
|
+
let label: String = x_labels_val.entry(i as isize)?;
|
|
66
|
+
x_labels.push(ratatui::text::Span::from(label));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let mut y_labels = Vec::new();
|
|
70
|
+
for i in 0..y_labels_val.len() {
|
|
71
|
+
let label: String = y_labels_val.entry(i as isize)?;
|
|
72
|
+
y_labels.push(ratatui::text::Span::from(label));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let y_bounds: [f64; 2] = [y_bounds_val.entry(0)?, y_bounds_val.entry(1)?];
|
|
76
|
+
|
|
77
|
+
// Calculate x_bounds based on datasets if possible
|
|
78
|
+
let mut min_x = 0.0;
|
|
79
|
+
let mut max_x = 0.0;
|
|
80
|
+
let mut first = true;
|
|
81
|
+
for ds_data in &data_storage {
|
|
82
|
+
for (x, _) in ds_data {
|
|
83
|
+
if first {
|
|
84
|
+
min_x = *x;
|
|
85
|
+
max_x = *x;
|
|
86
|
+
first = false;
|
|
87
|
+
} else {
|
|
88
|
+
if *x < min_x {
|
|
89
|
+
min_x = *x;
|
|
90
|
+
}
|
|
91
|
+
if *x > max_x {
|
|
92
|
+
max_x = *x;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Ensure there's some range
|
|
99
|
+
if min_x == max_x {
|
|
100
|
+
max_x = min_x + 1.0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let x_axis = ratatui::widgets::Axis::default()
|
|
104
|
+
.labels(x_labels)
|
|
105
|
+
.bounds([min_x, max_x]);
|
|
106
|
+
|
|
107
|
+
let y_axis = ratatui::widgets::Axis::default()
|
|
108
|
+
.labels(y_labels)
|
|
109
|
+
.bounds(y_bounds);
|
|
110
|
+
|
|
111
|
+
let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
|
112
|
+
|
|
113
|
+
if !block_val.is_nil() {
|
|
114
|
+
chart = chart.block(parse_block(block_val)?);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
frame.render_widget(chart, area);
|
|
118
|
+
Ok(())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[cfg(test)]
|
|
122
|
+
mod tests {
|
|
123
|
+
use super::*;
|
|
124
|
+
use ratatui::buffer::Buffer;
|
|
125
|
+
use ratatui::widgets::{Axis, Chart, Dataset, Widget};
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn test_linechart_rendering() {
|
|
129
|
+
let data = vec![(0.0, 0.0), (1.0, 1.0)];
|
|
130
|
+
let datasets = vec![Dataset::default().name("TestDS").data(&data)];
|
|
131
|
+
let chart = Chart::new(datasets)
|
|
132
|
+
.x_axis(
|
|
133
|
+
Axis::default()
|
|
134
|
+
.bounds([0.0, 1.0])
|
|
135
|
+
.labels(vec!["XMIN".into(), "XMAX".into()]),
|
|
136
|
+
)
|
|
137
|
+
.y_axis(
|
|
138
|
+
Axis::default()
|
|
139
|
+
.bounds([0.0, 1.0])
|
|
140
|
+
.labels(vec!["YMIN".into(), "YMAX".into()]),
|
|
141
|
+
);
|
|
142
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20)); // Larger buffer
|
|
143
|
+
chart.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
144
|
+
// Should have chart rendered (braille characters)
|
|
145
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
146
|
+
// Should have labels
|
|
147
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
148
|
+
assert!(content.contains("XMIN"));
|
|
149
|
+
assert!(content.contains("XMAX"));
|
|
150
|
+
assert!(content.contains("YMIN"));
|
|
151
|
+
assert!(content.contains("YMAX"));
|
|
152
|
+
assert!(content.contains("TestDS"));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::parse_block;
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::Rect,
|
|
8
|
+
widgets::{List, ListState},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let items_val: Value = node.funcall("items", ())?;
|
|
14
|
+
let items_array = magnus::RArray::from_value(items_val)
|
|
15
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array"))?;
|
|
16
|
+
let selected_index_val: Value = node.funcall("selected_index", ())?;
|
|
17
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
18
|
+
|
|
19
|
+
let mut items = Vec::new();
|
|
20
|
+
for i in 0..items_array.len() {
|
|
21
|
+
let item: String = items_array.entry(i as isize)?;
|
|
22
|
+
items.push(item);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let mut state = ListState::default();
|
|
26
|
+
if !selected_index_val.is_nil() {
|
|
27
|
+
let index: usize = selected_index_val.funcall("to_int", ())?;
|
|
28
|
+
state.select(Some(index));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let mut list = List::new(items).highlight_symbol(">> ");
|
|
32
|
+
|
|
33
|
+
if !block_val.is_nil() {
|
|
34
|
+
list = list.block(parse_block(block_val)?);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
frame.render_stateful_widget(list, area, &mut state);
|
|
38
|
+
Ok(())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
mod tests {
|
|
43
|
+
use super::*;
|
|
44
|
+
use ratatui::buffer::Buffer;
|
|
45
|
+
use ratatui::widgets::List;
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn test_list_rendering() {
|
|
49
|
+
let items = vec!["Item 1", "Item 2"];
|
|
50
|
+
let list = List::new(items).highlight_symbol(">> ");
|
|
51
|
+
let mut state = ListState::default();
|
|
52
|
+
state.select(Some(1));
|
|
53
|
+
|
|
54
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
55
|
+
use ratatui::widgets::StatefulWidget;
|
|
56
|
+
StatefulWidget::render(list, Rect::new(0, 0, 10, 2), &mut buf, &mut state);
|
|
57
|
+
|
|
58
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
59
|
+
assert!(content.contains("Item 1"));
|
|
60
|
+
assert!(content.contains(">> Item 2"));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
pub mod barchart;
|
|
5
|
+
pub mod block;
|
|
6
|
+
pub mod canvas;
|
|
7
|
+
pub mod center;
|
|
8
|
+
pub mod cursor;
|
|
9
|
+
pub mod gauge;
|
|
10
|
+
pub mod layout;
|
|
11
|
+
pub mod linechart;
|
|
12
|
+
pub mod list;
|
|
13
|
+
pub mod overlay;
|
|
14
|
+
pub mod paragraph;
|
|
15
|
+
pub mod scrollbar;
|
|
16
|
+
pub mod sparkline;
|
|
17
|
+
pub mod table;
|
|
18
|
+
pub mod tabs;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::rendering::render_node;
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{layout::Rect, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let layers_val: Value = node.funcall("layers", ())?;
|
|
10
|
+
let layers_array = magnus::RArray::from_value(layers_val)
|
|
11
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array for layers"))?;
|
|
12
|
+
|
|
13
|
+
for i in 0..layers_array.len() {
|
|
14
|
+
let layer: Value = layers_array.entry(i as isize)?;
|
|
15
|
+
if let Err(e) = render_node(frame, area, layer) {
|
|
16
|
+
eprintln!("Error rendering overlay layer {}: {:?}", i, e);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
Ok(())
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::{Alignment, Rect},
|
|
8
|
+
widgets::{Paragraph, Wrap},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let text: String = node.funcall("text", ())?;
|
|
14
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
15
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
16
|
+
let wrap: bool = node.funcall("wrap", ())?;
|
|
17
|
+
let align_sym: Symbol = node.funcall("align", ())?;
|
|
18
|
+
|
|
19
|
+
let style = parse_style(style_val)?;
|
|
20
|
+
let mut paragraph = Paragraph::new(text).style(style);
|
|
21
|
+
|
|
22
|
+
if !block_val.is_nil() {
|
|
23
|
+
paragraph = paragraph.block(parse_block(block_val)?);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if wrap {
|
|
27
|
+
paragraph = paragraph.wrap(Wrap { trim: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
match align_sym.to_string().as_str() {
|
|
31
|
+
"center" => paragraph = paragraph.alignment(Alignment::Center),
|
|
32
|
+
"right" => paragraph = paragraph.alignment(Alignment::Right),
|
|
33
|
+
_ => {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
frame.render_widget(paragraph, area);
|
|
37
|
+
Ok(())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[cfg(test)]
|
|
41
|
+
mod tests {
|
|
42
|
+
use super::*;
|
|
43
|
+
use ratatui::buffer::Buffer;
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn test_paragraph_rendering() {
|
|
47
|
+
let p = Paragraph::new("test content").alignment(Alignment::Center);
|
|
48
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
|
49
|
+
use ratatui::widgets::Widget;
|
|
50
|
+
p.render(Rect::new(0, 0, 20, 1), &mut buf);
|
|
51
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
52
|
+
assert!(content.contains("test content"));
|
|
53
|
+
// Check for centered alignment (should have leading spaces)
|
|
54
|
+
assert!(content.starts_with(' '));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::parse_block;
|
|
5
|
+
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::Rect,
|
|
8
|
+
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let content_length: usize = node.funcall("content_length", ())?;
|
|
14
|
+
let position: usize = node.funcall("position", ())?;
|
|
15
|
+
let orientation_sym: Symbol = node.funcall("orientation", ())?;
|
|
16
|
+
let thumb_symbol: String = node.funcall("thumb_symbol", ())?;
|
|
17
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
18
|
+
|
|
19
|
+
let mut state = ScrollbarState::new(content_length).position(position);
|
|
20
|
+
let mut scrollbar = Scrollbar::default().thumb_symbol(&thumb_symbol);
|
|
21
|
+
|
|
22
|
+
scrollbar = match orientation_sym.to_string().as_str() {
|
|
23
|
+
"horizontal" => scrollbar.orientation(ScrollbarOrientation::HorizontalBottom),
|
|
24
|
+
_ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if !block_val.is_nil() {
|
|
28
|
+
let block = parse_block(block_val)?;
|
|
29
|
+
let inner_area = block.inner(area);
|
|
30
|
+
frame.render_widget(block, area);
|
|
31
|
+
frame.render_stateful_widget(scrollbar, inner_area, &mut state);
|
|
32
|
+
} else {
|
|
33
|
+
frame.render_stateful_widget(scrollbar, area, &mut state);
|
|
34
|
+
}
|
|
35
|
+
Ok(())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[cfg(test)]
|
|
39
|
+
mod tests {
|
|
40
|
+
use super::*;
|
|
41
|
+
use ratatui::buffer::Buffer;
|
|
42
|
+
use ratatui::widgets::StatefulWidget;
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn test_scrollbar_vertical_render() {
|
|
46
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 5));
|
|
47
|
+
let mut state = ScrollbarState::new(10).position(2);
|
|
48
|
+
let scrollbar = Scrollbar::default().orientation(ScrollbarOrientation::VerticalRight);
|
|
49
|
+
|
|
50
|
+
// Note: Scrollbar is stateful
|
|
51
|
+
scrollbar.render(Rect::new(0, 0, 1, 5), &mut buf, &mut state);
|
|
52
|
+
|
|
53
|
+
// Vertical scrollbar should render something in the first column
|
|
54
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[test]
|
|
58
|
+
fn test_scrollbar_horizontal_render() {
|
|
59
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
|
60
|
+
let mut state = ScrollbarState::new(10).position(2);
|
|
61
|
+
let scrollbar = Scrollbar::default().orientation(ScrollbarOrientation::HorizontalBottom);
|
|
62
|
+
|
|
63
|
+
scrollbar.render(Rect::new(0, 0, 5, 1), &mut buf, &mut state);
|
|
64
|
+
|
|
65
|
+
// Horizontal scrollbar should render something in the first row
|
|
66
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{layout::Rect, widgets::Sparkline, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let data_val: magnus::RArray = node.funcall("data", ())?;
|
|
10
|
+
let max_val: Value = node.funcall("max", ())?;
|
|
11
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
12
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
13
|
+
|
|
14
|
+
let mut data_vec = Vec::new();
|
|
15
|
+
for i in 0..data_val.len() {
|
|
16
|
+
let val: u64 = data_val.entry(i as isize)?;
|
|
17
|
+
data_vec.push(val);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let mut sparkline = Sparkline::default().data(&data_vec);
|
|
21
|
+
|
|
22
|
+
if !max_val.is_nil() {
|
|
23
|
+
let max: u64 = u64::try_convert(max_val)?;
|
|
24
|
+
sparkline = sparkline.max(max);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if !style_val.is_nil() {
|
|
28
|
+
sparkline = sparkline.style(parse_style(style_val)?);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if !block_val.is_nil() {
|
|
32
|
+
sparkline = sparkline.block(parse_block(block_val)?);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
frame.render_widget(sparkline, area);
|
|
36
|
+
Ok(())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[cfg(test)]
|
|
40
|
+
mod tests {
|
|
41
|
+
use super::*;
|
|
42
|
+
use ratatui::buffer::Buffer;
|
|
43
|
+
use ratatui::widgets::{Sparkline, Widget};
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn test_sparkline_rendering() {
|
|
47
|
+
let data = vec![1, 2, 3, 4];
|
|
48
|
+
let sparkline = Sparkline::default().data(&data);
|
|
49
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
|
|
50
|
+
sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
|
|
51
|
+
// Should have sparkline rendered (non-space characters)
|
|
52
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
53
|
+
// In sparkline, higher values generally result in different bar symbols
|
|
54
|
+
// but verifying exact symbols might be fragile across ratatui versions.
|
|
55
|
+
// At least we know it should have rendered 4 bars for 4 data points.
|
|
56
|
+
let bars = buf.content().iter().filter(|c| c.symbol() != " ").count();
|
|
57
|
+
assert_eq!(bars, 4);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::{Constraint, Rect},
|
|
8
|
+
widgets::{Cell, Row, Table},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let header_val: Value = node.funcall("header", ())?;
|
|
14
|
+
let rows_val: Value = node.funcall("rows", ())?;
|
|
15
|
+
let rows_array = magnus::RArray::from_value(rows_val)
|
|
16
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array for rows"))?;
|
|
17
|
+
let widths_val: Value = node.funcall("widths", ())?;
|
|
18
|
+
let widths_array = magnus::RArray::from_value(widths_val)
|
|
19
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array for widths"))?;
|
|
20
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
21
|
+
|
|
22
|
+
let mut rows = Vec::new();
|
|
23
|
+
for i in 0..rows_array.len() {
|
|
24
|
+
let row_val: Value = rows_array.entry(i as isize)?;
|
|
25
|
+
let row_array = magnus::RArray::from_value(row_val)
|
|
26
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array for row"))?;
|
|
27
|
+
|
|
28
|
+
let mut cells = Vec::new();
|
|
29
|
+
for j in 0..row_array.len() {
|
|
30
|
+
let cell_val: Value = row_array.entry(j as isize)?;
|
|
31
|
+
let class = cell_val.class();
|
|
32
|
+
let class_name = unsafe { class.name() };
|
|
33
|
+
|
|
34
|
+
if class_name.as_ref() == "RatatuiRuby::Paragraph" {
|
|
35
|
+
let text: String = cell_val.funcall("text", ())?;
|
|
36
|
+
let style_val: Value = cell_val.funcall("style", ())?;
|
|
37
|
+
let cell_style = parse_style(style_val)?;
|
|
38
|
+
cells.push(Cell::from(text).style(cell_style));
|
|
39
|
+
} else if class_name.as_ref() == "RatatuiRuby::Style" {
|
|
40
|
+
cells.push(Cell::from("").style(parse_style(cell_val)?));
|
|
41
|
+
} else {
|
|
42
|
+
let cell_str: String = cell_val.funcall("to_s", ())?;
|
|
43
|
+
cells.push(Cell::from(cell_str));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
rows.push(Row::new(cells));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let mut constraints = Vec::new();
|
|
50
|
+
for i in 0..widths_array.len() {
|
|
51
|
+
let constraint_obj: Value = widths_array.entry(i as isize)?;
|
|
52
|
+
let type_sym: Symbol = constraint_obj.funcall("type", ())?;
|
|
53
|
+
let value: u16 = constraint_obj.funcall("value", ())?;
|
|
54
|
+
|
|
55
|
+
match type_sym.to_string().as_str() {
|
|
56
|
+
"length" => constraints.push(Constraint::Length(value)),
|
|
57
|
+
"percentage" => constraints.push(Constraint::Percentage(value)),
|
|
58
|
+
"min" => constraints.push(Constraint::Min(value)),
|
|
59
|
+
_ => {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let mut table = Table::new(rows, constraints);
|
|
64
|
+
|
|
65
|
+
if !header_val.is_nil() {
|
|
66
|
+
let header_array = magnus::RArray::from_value(header_val).ok_or_else(|| {
|
|
67
|
+
Error::new(magnus::exception::type_error(), "expected array for header")
|
|
68
|
+
})?;
|
|
69
|
+
let mut header_cells = Vec::new();
|
|
70
|
+
for i in 0..header_array.len() {
|
|
71
|
+
let cell_val: Value = header_array.entry(i as isize)?;
|
|
72
|
+
let class = cell_val.class();
|
|
73
|
+
let class_name = unsafe { class.name() };
|
|
74
|
+
|
|
75
|
+
if class_name.as_ref() == "RatatuiRuby::Paragraph" {
|
|
76
|
+
let text: String = cell_val.funcall("text", ())?;
|
|
77
|
+
let style_val: Value = cell_val.funcall("style", ())?;
|
|
78
|
+
let cell_style = parse_style(style_val)?;
|
|
79
|
+
header_cells.push(Cell::from(text).style(cell_style));
|
|
80
|
+
} else {
|
|
81
|
+
let cell_str: String = cell_val.funcall("to_s", ())?;
|
|
82
|
+
header_cells.push(Cell::from(cell_str));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
table = table.header(Row::new(header_cells));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if !block_val.is_nil() {
|
|
89
|
+
table = table.block(parse_block(block_val)?);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
frame.render_widget(table, area);
|
|
93
|
+
Ok(())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[cfg(test)]
|
|
97
|
+
mod tests {
|
|
98
|
+
use super::*;
|
|
99
|
+
use ratatui::buffer::Buffer;
|
|
100
|
+
use ratatui::widgets::{Row, Table, Widget};
|
|
101
|
+
|
|
102
|
+
#[test]
|
|
103
|
+
fn test_table_rendering() {
|
|
104
|
+
let rows = vec![Row::new(vec!["C1", "C2"])];
|
|
105
|
+
let table = Table::new(rows, [Constraint::Length(3), Constraint::Length(3)])
|
|
106
|
+
.header(Row::new(vec!["H1", "H2"]));
|
|
107
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
|
|
108
|
+
Widget::render(table, Rect::new(0, 0, 10, 2), &mut buf);
|
|
109
|
+
|
|
110
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
111
|
+
// Check for presence of header and row content
|
|
112
|
+
assert!(content.contains("H1"));
|
|
113
|
+
assert!(content.contains("H2"));
|
|
114
|
+
assert!(content.contains("C1"));
|
|
115
|
+
assert!(content.contains("C2"));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use crate::style::parse_block;
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{layout::Rect, text::Line, widgets::Tabs, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let titles_val: Value = node.funcall("titles", ())?;
|
|
10
|
+
let selected_index: usize = node.funcall("selected_index", ())?;
|
|
11
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
12
|
+
|
|
13
|
+
let titles_array = magnus::RArray::from_value(titles_val)
|
|
14
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array for titles"))?;
|
|
15
|
+
|
|
16
|
+
let mut titles = Vec::new();
|
|
17
|
+
for i in 0..titles_array.len() {
|
|
18
|
+
let title: String = titles_array.entry(i as isize)?;
|
|
19
|
+
titles.push(Line::from(title));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let mut tabs = Tabs::new(titles).select(selected_index);
|
|
23
|
+
|
|
24
|
+
if !block_val.is_nil() {
|
|
25
|
+
tabs = tabs.block(parse_block(block_val)?);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
frame.render_widget(tabs, area);
|
|
29
|
+
Ok(())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[cfg(test)]
|
|
33
|
+
mod tests {
|
|
34
|
+
use super::*;
|
|
35
|
+
use ratatui::buffer::Buffer;
|
|
36
|
+
use ratatui::text::Line;
|
|
37
|
+
use ratatui::widgets::{Tabs, Widget};
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn test_tabs_rendering() {
|
|
41
|
+
let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
|
|
42
|
+
let tabs = Tabs::new(titles).select(1).divider("|");
|
|
43
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
|
44
|
+
tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
|
|
45
|
+
// Should contain tab titles
|
|
46
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
47
|
+
assert!(content.contains("Tab1"));
|
|
48
|
+
assert!(content.contains("Tab2"));
|
|
49
|
+
assert!(content.contains('|'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
# A widget that displays numeric data as a bar chart.
|
|
8
|
+
#
|
|
9
|
+
# [data] A hash of { "Label" => value (Integer) }.
|
|
10
|
+
# [bar_width] The width of each bar in the chart.
|
|
11
|
+
# [bar_gap] The gap between bars.
|
|
12
|
+
# [max] Optional maximum value for the Y-axis.
|
|
13
|
+
# [style] Optional style for the bars.
|
|
14
|
+
# [block] Optional block widget to wrap the chart.
|
|
15
|
+
class BarChart < Data.define(:data, :bar_width, :bar_gap, :max, :style, :block)
|
|
16
|
+
# Creates a new BarChart widget.
|
|
17
|
+
#
|
|
18
|
+
# [data] A hash of { "Label" => value (Integer) }.
|
|
19
|
+
# [bar_width] The width of each bar in the chart.
|
|
20
|
+
# [bar_gap] The gap between bars.
|
|
21
|
+
# [max] Optional maximum value for the Y-axis.
|
|
22
|
+
# [style] Optional style for the bars.
|
|
23
|
+
# [block] Optional block widget to wrap the chart.
|
|
24
|
+
def initialize(data:, bar_width: 3, bar_gap: 1, max: nil, style: nil, block: nil)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
# A widget that wraps other widgets with a border and/or a title.
|
|
8
|
+
#
|
|
9
|
+
# [title] The title string to display on the border.
|
|
10
|
+
# [borders] An array of symbols representing which borders to display:
|
|
11
|
+
# [:top, :bottom, :left, :right, :all, :none]
|
|
12
|
+
# [border_color] The color of the border (e.g., "red", "#ff0000").
|
|
13
|
+
class Block < Data.define(:title, :borders, :border_color)
|
|
14
|
+
# Creates a new Block.
|
|
15
|
+
#
|
|
16
|
+
# [title] The title string to display on the border.
|
|
17
|
+
# [borders] An array of symbols representing which borders to display.
|
|
18
|
+
# [border_color] The color of the border.
|
|
19
|
+
def initialize(title: nil, borders: [:all], border_color: nil)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|