ratatui_ruby 0.1.0 → 0.3.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 +4 -4
- data/.builds/ruby-3.2.yml +52 -0
- data/.builds/ruby-3.3.yml +52 -0
- data/.builds/ruby-3.4.yml +52 -0
- data/.builds/ruby-4.0.0.yml +53 -0
- data/.pre-commit-config.yaml +9 -2
- data/AGENTS.md +53 -5
- data/CHANGELOG.md +51 -1
- data/README.md +38 -18
- data/REUSE.toml +5 -0
- data/Rakefile +3 -100
- data/{docs → doc}/contributors/index.md +2 -1
- data/doc/custom.css +8 -0
- data/doc/images/examples-calendar_demo.rb.png +0 -0
- data/doc/images/examples-chart_demo.rb.png +0 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-list_styles.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-stock_ticker.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/{docs → doc}/index.md +1 -1
- data/{docs → doc}/quickstart.md +81 -11
- data/examples/analytics.rb +2 -1
- data/examples/calendar_demo.rb +55 -0
- data/examples/chart_demo.rb +84 -0
- data/examples/custom_widget.rb +43 -0
- data/examples/list_styles.rb +66 -0
- data/examples/login_form.rb +2 -1
- data/examples/popup_demo.rb +105 -0
- data/examples/quickstart_dsl.rb +30 -0
- data/examples/quickstart_lifecycle.rb +40 -0
- data/examples/readme_usage.rb +21 -0
- data/examples/scroll_text.rb +74 -0
- data/examples/stock_ticker.rb +13 -5
- data/examples/system_monitor.rb +2 -1
- data/examples/table_select.rb +70 -0
- data/examples/test_calendar_demo.rb +66 -0
- data/examples/test_list_styles.rb +61 -0
- data/examples/test_popup_demo.rb +62 -0
- data/examples/test_scroll_text.rb +130 -0
- data/examples/test_table_select.rb +37 -0
- data/ext/ratatui_ruby/.cargo/config.toml +5 -0
- data/ext/ratatui_ruby/Cargo.lock +260 -50
- data/ext/ratatui_ruby/Cargo.toml +5 -4
- data/ext/ratatui_ruby/extconf.rb +1 -1
- data/ext/ratatui_ruby/src/buffer.rs +54 -0
- data/ext/ratatui_ruby/src/events.rs +115 -107
- data/ext/ratatui_ruby/src/lib.rs +15 -6
- data/ext/ratatui_ruby/src/rendering.rs +18 -1
- data/ext/ratatui_ruby/src/style.rs +2 -1
- data/ext/ratatui_ruby/src/terminal.rs +27 -24
- data/ext/ratatui_ruby/src/widgets/calendar.rs +82 -0
- data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
- data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
- data/ext/ratatui_ruby/src/widgets/chart.rs +260 -0
- data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/list.rs +44 -5
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -1
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
- data/lib/ratatui_ruby/dsl.rb +64 -0
- data/lib/ratatui_ruby/schema/calendar.rb +26 -0
- data/lib/ratatui_ruby/schema/chart.rb +81 -0
- data/lib/ratatui_ruby/schema/clear.rb +83 -0
- data/lib/ratatui_ruby/schema/list.rb +8 -2
- data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
- data/lib/ratatui_ruby/schema/rect.rb +24 -0
- data/lib/ratatui_ruby/schema/table.rb +8 -2
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +24 -2
- data/mise.toml +8 -0
- data/sig/ratatui_ruby/buffer.rbs +11 -0
- data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
- data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
- data/sig/ratatui_ruby/schema/list.rbs +4 -1
- data/sig/ratatui_ruby/schema/rect.rbs +14 -0
- data/tasks/bump/cargo_lockfile.rb +19 -0
- data/tasks/bump/changelog.rb +37 -0
- data/tasks/bump/comparison_links.rb +41 -0
- data/tasks/bump/header.rb +30 -0
- data/tasks/bump/history.rb +30 -0
- data/tasks/bump/manifest.rb +31 -0
- data/tasks/bump/ruby_gem.rb +35 -0
- data/tasks/bump/sem_ver.rb +34 -0
- data/tasks/bump/unreleased_section.rb +38 -0
- data/tasks/bump.rake +49 -0
- data/tasks/doc.rake +25 -0
- data/tasks/extension.rake +12 -0
- data/tasks/lint.rake +49 -0
- data/tasks/rdoc_config.rb +15 -0
- data/tasks/resources/build.yml.erb +65 -0
- data/tasks/resources/index.html.erb +38 -0
- data/tasks/resources/rubies.yml +7 -0
- data/tasks/sourcehut.rake +38 -0
- data/tasks/test.rake +31 -0
- data/tasks/website/index_page.rb +28 -0
- data/tasks/website/version.rb +117 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +49 -0
- data/tasks/website/website.rb +53 -0
- data/tasks/website.rake +26 -0
- metadata +119 -28
- data/.build.yml +0 -34
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -30
- data/CONTRIBUTING.md +0 -40
- data/docs/images/examples-stock_ticker.rb.png +0 -0
- data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
- data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
- /data/{docs → doc}/application_testing.md +0 -0
- /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
- /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
- /data/{docs → doc}/contributors/design.md +0 -0
- /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
- /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
- /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
- /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
- /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-system_monitor.rb.png +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
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::{
|
|
7
|
+
layout::Rect,
|
|
8
|
+
widgets::calendar::{CalendarEventStore, Monthly},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
use std::convert::TryFrom;
|
|
12
|
+
use time::{Date, Month};
|
|
13
|
+
|
|
14
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
16
|
+
let year: i32 = node.funcall("year", ())?;
|
|
17
|
+
let month_u8: u8 = node.funcall("month", ())?;
|
|
18
|
+
let day_style_val: Value = node.funcall("day_style", ())?;
|
|
19
|
+
let header_style_val: Value = node.funcall("header_style", ())?;
|
|
20
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
21
|
+
|
|
22
|
+
let month = Month::try_from(month_u8)
|
|
23
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
|
|
24
|
+
|
|
25
|
+
let date = Date::from_calendar_date(year, month, 1)
|
|
26
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
|
|
27
|
+
|
|
28
|
+
let mut calendar = Monthly::new(date, CalendarEventStore::default());
|
|
29
|
+
|
|
30
|
+
let header_style = if !header_style_val.is_nil() {
|
|
31
|
+
parse_style(header_style_val)?
|
|
32
|
+
} else {
|
|
33
|
+
ratatui::style::Style::default()
|
|
34
|
+
};
|
|
35
|
+
calendar = calendar
|
|
36
|
+
.show_month_header(header_style)
|
|
37
|
+
.show_weekdays_header(header_style);
|
|
38
|
+
|
|
39
|
+
if !day_style_val.is_nil() {
|
|
40
|
+
calendar = calendar.default_style(parse_style(day_style_val)?);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if !block_val.is_nil() {
|
|
44
|
+
calendar = calendar.block(parse_block(block_val)?);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
frame.render_widget(calendar, area);
|
|
48
|
+
Ok(())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[cfg(test)]
|
|
52
|
+
mod tests {
|
|
53
|
+
use super::*;
|
|
54
|
+
use ratatui::buffer::Buffer;
|
|
55
|
+
use ratatui::widgets::Widget;
|
|
56
|
+
|
|
57
|
+
#[test]
|
|
58
|
+
fn test_calendar_rendering() {
|
|
59
|
+
let date = Date::from_calendar_date(2025, Month::December, 1).unwrap();
|
|
60
|
+
let calendar = Monthly::new(date, CalendarEventStore::default())
|
|
61
|
+
.show_month_header(ratatui::style::Style::default());
|
|
62
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
|
|
63
|
+
calendar.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
64
|
+
let mut content = String::new();
|
|
65
|
+
for y in 0..20 {
|
|
66
|
+
for x in 0..40 {
|
|
67
|
+
content.push_str(buf.cell((x, y)).unwrap().symbol());
|
|
68
|
+
}
|
|
69
|
+
content.push('\n');
|
|
70
|
+
}
|
|
71
|
+
assert!(
|
|
72
|
+
content.contains("December"),
|
|
73
|
+
"Content did not contain December: \n{}",
|
|
74
|
+
content
|
|
75
|
+
);
|
|
76
|
+
assert!(
|
|
77
|
+
content.contains("2025"),
|
|
78
|
+
"Content did not contain 2025: \n{}",
|
|
79
|
+
content
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -36,8 +36,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
let canvas = canvas.paint(|ctx| {
|
|
39
|
-
for shape_val in shapes_val
|
|
40
|
-
let shape_val = shape_val.unwrap();
|
|
39
|
+
for shape_val in shapes_val {
|
|
41
40
|
let class = shape_val.class();
|
|
42
41
|
let class_name = unsafe { class.name() };
|
|
43
42
|
|
|
@@ -5,7 +5,6 @@ use crate::rendering::render_node;
|
|
|
5
5
|
use magnus::{prelude::*, Error, Value};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
layout::{Constraint, Direction, Layout, Rect},
|
|
8
|
-
widgets::Clear,
|
|
9
8
|
Frame,
|
|
10
9
|
};
|
|
11
10
|
|
|
@@ -36,7 +35,6 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
36
35
|
|
|
37
36
|
let center_area = popup_layout_horizontal[1];
|
|
38
37
|
|
|
39
|
-
frame.render_widget(Clear, center_area);
|
|
40
38
|
render_node(frame, center_area, child)?;
|
|
41
39
|
Ok(())
|
|
42
40
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
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::Rect,
|
|
8
|
+
symbols,
|
|
9
|
+
text::Span,
|
|
10
|
+
widgets::{Axis, Chart, Dataset, GraphType},
|
|
11
|
+
Frame,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
16
|
+
let class = node.class();
|
|
17
|
+
let class_name = unsafe { class.name() };
|
|
18
|
+
|
|
19
|
+
if class_name == "RatatuiRuby::LineChart" {
|
|
20
|
+
return render_line_chart(frame, area, node);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
24
|
+
let x_axis_val: Value = node.funcall("x_axis", ())?;
|
|
25
|
+
let y_axis_val: Value = node.funcall("y_axis", ())?;
|
|
26
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
27
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
28
|
+
|
|
29
|
+
let mut datasets = Vec::new();
|
|
30
|
+
// We need to keep the data alive until the chart is rendered
|
|
31
|
+
let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
|
|
32
|
+
|
|
33
|
+
for i in 0..datasets_val.len() {
|
|
34
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
35
|
+
let data_array: magnus::RArray = ds_val.funcall("data", ())?;
|
|
36
|
+
|
|
37
|
+
let mut points = Vec::new();
|
|
38
|
+
for j in 0..data_array.len() {
|
|
39
|
+
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
40
|
+
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
41
|
+
Error::new(ruby.exception_type_error(), "expected array for point")
|
|
42
|
+
})?;
|
|
43
|
+
let x: f64 = point_array.entry(0)?;
|
|
44
|
+
let y: f64 = point_array.entry(1)?;
|
|
45
|
+
points.push((x, y));
|
|
46
|
+
}
|
|
47
|
+
data_storage.push(points);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (i, points) in data_storage.iter().enumerate() {
|
|
51
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
52
|
+
let name: String = ds_val.funcall("name", ())?;
|
|
53
|
+
let marker_sym: Symbol = ds_val.funcall("marker", ())?;
|
|
54
|
+
let graph_type_sym: Symbol = ds_val.funcall("graph_type", ())?;
|
|
55
|
+
|
|
56
|
+
let marker = match marker_sym.to_string().as_str() {
|
|
57
|
+
"dot" => symbols::Marker::Dot,
|
|
58
|
+
"braille" => symbols::Marker::Braille,
|
|
59
|
+
"block" => symbols::Marker::Block,
|
|
60
|
+
"bar" => symbols::Marker::Bar,
|
|
61
|
+
_ => symbols::Marker::Dot,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let graph_type = match graph_type_sym.to_string().as_str() {
|
|
65
|
+
"line" => GraphType::Line,
|
|
66
|
+
"scatter" => GraphType::Scatter,
|
|
67
|
+
_ => GraphType::Line,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let mut ds_style = ratatui::style::Style::default();
|
|
71
|
+
let color_val: Value = ds_val.funcall("color", ())?;
|
|
72
|
+
if !color_val.is_nil() {
|
|
73
|
+
let color_str: String = color_val.funcall("to_s", ())?;
|
|
74
|
+
if let Some(color) = crate::style::parse_color(&color_str) {
|
|
75
|
+
ds_style = ds_style.fg(color);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let ds = Dataset::default()
|
|
80
|
+
.name(name)
|
|
81
|
+
.marker(marker)
|
|
82
|
+
.graph_type(graph_type)
|
|
83
|
+
.style(ds_style)
|
|
84
|
+
.data(points);
|
|
85
|
+
datasets.push(ds);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let x_axis = parse_axis(x_axis_val)?;
|
|
89
|
+
let y_axis = parse_axis(y_axis_val)?;
|
|
90
|
+
|
|
91
|
+
let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
|
92
|
+
|
|
93
|
+
if !block_val.is_nil() {
|
|
94
|
+
chart = chart.block(parse_block(block_val)?);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if !style_val.is_nil() {
|
|
98
|
+
chart = chart.style(parse_style(style_val)?);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
frame.render_widget(chart, area);
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
|
|
106
|
+
let title: String = axis_val.funcall("title", ())?;
|
|
107
|
+
let bounds_val: magnus::RArray = axis_val.funcall("bounds", ())?;
|
|
108
|
+
let labels_val: magnus::RArray = axis_val.funcall("labels", ())?;
|
|
109
|
+
let style_val: Value = axis_val.funcall("style", ())?;
|
|
110
|
+
|
|
111
|
+
let bounds: [f64; 2] = [bounds_val.entry(0)?, bounds_val.entry(1)?];
|
|
112
|
+
|
|
113
|
+
let mut labels = Vec::new();
|
|
114
|
+
for i in 0..labels_val.len() {
|
|
115
|
+
let label: String = labels_val.entry(i as isize)?;
|
|
116
|
+
labels.push(Span::from(label));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let mut axis = Axis::default().title(title).bounds(bounds).labels(labels);
|
|
120
|
+
|
|
121
|
+
if !style_val.is_nil() {
|
|
122
|
+
axis = axis.style(parse_style(style_val)?);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Ok(axis)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
129
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
130
|
+
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
131
|
+
let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
|
|
132
|
+
let y_labels_val: magnus::RArray = node.funcall("y_labels", ())?;
|
|
133
|
+
let y_bounds_val: magnus::RArray = node.funcall("y_bounds", ())?;
|
|
134
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
135
|
+
|
|
136
|
+
let mut datasets = Vec::new();
|
|
137
|
+
let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
|
|
138
|
+
|
|
139
|
+
for i in 0..datasets_val.len() {
|
|
140
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
141
|
+
let data_array: magnus::RArray = ds_val.funcall("data", ())?;
|
|
142
|
+
|
|
143
|
+
let mut points = Vec::new();
|
|
144
|
+
for j in 0..data_array.len() {
|
|
145
|
+
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
146
|
+
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
147
|
+
Error::new(ruby.exception_type_error(), "expected array for point")
|
|
148
|
+
})?;
|
|
149
|
+
let x: f64 = point_array.entry(0)?;
|
|
150
|
+
let y: f64 = point_array.entry(1)?;
|
|
151
|
+
points.push((x, y));
|
|
152
|
+
}
|
|
153
|
+
data_storage.push(points);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (i, points) in data_storage.iter().enumerate() {
|
|
157
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
158
|
+
let name: String = ds_val.funcall("name", ())?;
|
|
159
|
+
|
|
160
|
+
let mut ds_style = ratatui::style::Style::default();
|
|
161
|
+
let color_val: Value = ds_val.funcall("color", ())?;
|
|
162
|
+
let color_str: String = color_val.funcall("to_s", ())?;
|
|
163
|
+
if let Some(color) = crate::style::parse_color(&color_str) {
|
|
164
|
+
ds_style = ds_style.fg(color);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let ds = Dataset::default()
|
|
168
|
+
.name(name)
|
|
169
|
+
.marker(symbols::Marker::Braille)
|
|
170
|
+
.style(ds_style)
|
|
171
|
+
.data(points);
|
|
172
|
+
datasets.push(ds);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let mut x_labels = Vec::new();
|
|
176
|
+
for i in 0..x_labels_val.len() {
|
|
177
|
+
let label: String = x_labels_val.entry(i as isize)?;
|
|
178
|
+
x_labels.push(Span::from(label));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let mut y_labels = Vec::new();
|
|
182
|
+
for i in 0..y_labels_val.len() {
|
|
183
|
+
let label: String = y_labels_val.entry(i as isize)?;
|
|
184
|
+
y_labels.push(Span::from(label));
|
|
185
|
+
}
|
|
186
|
+
// Ratatui 0.29+ requires labels to be present for the axis line to render
|
|
187
|
+
if y_labels.is_empty() {
|
|
188
|
+
y_labels.push(Span::from(""));
|
|
189
|
+
y_labels.push(Span::from(""));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let y_bounds: [f64; 2] = [y_bounds_val.entry(0)?, y_bounds_val.entry(1)?];
|
|
193
|
+
|
|
194
|
+
let mut min_x = 0.0;
|
|
195
|
+
let mut max_x = 0.0;
|
|
196
|
+
let mut first = true;
|
|
197
|
+
for ds_data in &data_storage {
|
|
198
|
+
for (x, _) in ds_data {
|
|
199
|
+
if first {
|
|
200
|
+
min_x = *x;
|
|
201
|
+
max_x = *x;
|
|
202
|
+
first = false;
|
|
203
|
+
} else {
|
|
204
|
+
if *x < min_x {
|
|
205
|
+
min_x = *x;
|
|
206
|
+
}
|
|
207
|
+
if *x > max_x {
|
|
208
|
+
max_x = *x;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if min_x == max_x {
|
|
214
|
+
max_x = min_x + 1.0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let x_axis = Axis::default().labels(x_labels).bounds([min_x, max_x]);
|
|
218
|
+
let y_axis = Axis::default().labels(y_labels).bounds(y_bounds);
|
|
219
|
+
|
|
220
|
+
let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
|
221
|
+
if !block_val.is_nil() {
|
|
222
|
+
chart = chart.block(parse_block(block_val)?);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
frame.render_widget(chart, area);
|
|
226
|
+
Ok(())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[cfg(test)]
|
|
230
|
+
mod tests {
|
|
231
|
+
use super::*;
|
|
232
|
+
use ratatui::buffer::Buffer;
|
|
233
|
+
use ratatui::widgets::Widget;
|
|
234
|
+
|
|
235
|
+
#[test]
|
|
236
|
+
fn test_chart_rendering() {
|
|
237
|
+
let data = vec![(0.0, 0.0), (1.0, 1.0)];
|
|
238
|
+
let datasets = vec![Dataset::default().name("TestDS").data(&data)];
|
|
239
|
+
let chart = Chart::new(datasets)
|
|
240
|
+
.x_axis(
|
|
241
|
+
Axis::default()
|
|
242
|
+
.bounds([0.0, 1.0])
|
|
243
|
+
.labels(vec!["XMIN".into(), "XMAX".into()] as Vec<ratatui::text::Line>),
|
|
244
|
+
)
|
|
245
|
+
.y_axis(
|
|
246
|
+
Axis::default()
|
|
247
|
+
.bounds([0.0, 1.0])
|
|
248
|
+
.labels(vec!["YMIN".into(), "YMAX".into()] as Vec<ratatui::text::Line>),
|
|
249
|
+
);
|
|
250
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
|
|
251
|
+
chart.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
252
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
253
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
254
|
+
assert!(content.contains("XMIN"));
|
|
255
|
+
assert!(content.contains("XMAX"));
|
|
256
|
+
assert!(content.contains("YMIN"));
|
|
257
|
+
assert!(content.contains("YMAX"));
|
|
258
|
+
assert!(content.contains("TestDS"));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use magnus::{prelude::*, Error, Value};
|
|
5
|
+
use ratatui::{layout::Rect, widgets::Widget, Frame};
|
|
6
|
+
|
|
7
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
8
|
+
frame.render_widget(ratatui::widgets::Clear, area);
|
|
9
|
+
|
|
10
|
+
// If a block is provided, render it on top of the cleared area
|
|
11
|
+
if let Ok(block_val) = node.funcall::<_, _, Value>("block", ()) {
|
|
12
|
+
if !block_val.is_nil() {
|
|
13
|
+
let block = crate::style::parse_block(block_val)?;
|
|
14
|
+
block.render(area, frame.buffer_mut());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Ok(())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[cfg(test)]
|
|
22
|
+
mod tests {
|
|
23
|
+
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn test_clear_renders_without_error() {
|
|
27
|
+
let backend = TestBackend::new(10, 5);
|
|
28
|
+
let mut terminal = Terminal::new(backend).unwrap();
|
|
29
|
+
|
|
30
|
+
terminal
|
|
31
|
+
.draw(|frame| {
|
|
32
|
+
let area = Rect::new(0, 0, 10, 5);
|
|
33
|
+
frame.render_widget(ratatui::widgets::Clear, area);
|
|
34
|
+
})
|
|
35
|
+
.unwrap();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -7,7 +7,7 @@ use ratatui::{layout::Rect, Frame};
|
|
|
7
7
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
8
8
|
let x: u16 = node.funcall("x", ())?;
|
|
9
9
|
let y: u16 = node.funcall("y", ())?;
|
|
10
|
-
frame.
|
|
10
|
+
frame.set_cursor_position((area.x + x, area.y + y));
|
|
11
11
|
Ok(())
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -9,10 +9,11 @@ use ratatui::{
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
12
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
12
13
|
let direction_sym: Symbol = node.funcall("direction", ())?;
|
|
13
14
|
let children_val: Value = node.funcall("children", ())?;
|
|
14
15
|
let children_array = magnus::RArray::from_value(children_val)
|
|
15
|
-
.ok_or_else(|| Error::new(
|
|
16
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
16
17
|
|
|
17
18
|
let constraints_val: Value = node.funcall("constraints", ())?;
|
|
18
19
|
let constraints_array = magnus::RArray::from_value(constraints_val);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
|
-
use crate::style::parse_block;
|
|
5
|
-
use magnus::{prelude::*, Error, Value};
|
|
4
|
+
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use magnus::{prelude::*, Error, TryConvert, Value};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
layout::Rect,
|
|
8
8
|
widgets::{List, ListState},
|
|
@@ -10,10 +10,14 @@ use ratatui::{
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
13
14
|
let items_val: Value = node.funcall("items", ())?;
|
|
14
15
|
let items_array = magnus::RArray::from_value(items_val)
|
|
15
|
-
.ok_or_else(|| Error::new(
|
|
16
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
16
17
|
let selected_index_val: Value = node.funcall("selected_index", ())?;
|
|
18
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
19
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
20
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
17
21
|
let block_val: Value = node.funcall("block", ())?;
|
|
18
22
|
|
|
19
23
|
let mut items = Vec::new();
|
|
@@ -22,13 +26,32 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
22
26
|
items.push(item);
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
let symbol: String = if !highlight_symbol_val.is_nil() {
|
|
30
|
+
let s: String = String::try_convert(highlight_symbol_val)?;
|
|
31
|
+
s
|
|
32
|
+
} else {
|
|
33
|
+
String::new()
|
|
34
|
+
};
|
|
35
|
+
|
|
25
36
|
let mut state = ListState::default();
|
|
26
37
|
if !selected_index_val.is_nil() {
|
|
27
38
|
let index: usize = selected_index_val.funcall("to_int", ())?;
|
|
28
39
|
state.select(Some(index));
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
let mut list = List::new(items)
|
|
42
|
+
let mut list = List::new(items);
|
|
43
|
+
|
|
44
|
+
if !highlight_symbol_val.is_nil() {
|
|
45
|
+
list = list.highlight_symbol(&symbol);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if !style_val.is_nil() {
|
|
49
|
+
list = list.style(parse_style(style_val)?);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if !highlight_style_val.is_nil() {
|
|
53
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
if !block_val.is_nil() {
|
|
34
57
|
list = list.block(parse_block(block_val)?);
|
|
@@ -47,7 +70,14 @@ mod tests {
|
|
|
47
70
|
#[test]
|
|
48
71
|
fn test_list_rendering() {
|
|
49
72
|
let items = vec!["Item 1", "Item 2"];
|
|
50
|
-
let list = List::new(items)
|
|
73
|
+
let list = List::new(items)
|
|
74
|
+
.highlight_symbol(">> ")
|
|
75
|
+
.style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
|
|
76
|
+
.highlight_style(
|
|
77
|
+
ratatui::style::Style::default()
|
|
78
|
+
.fg(ratatui::style::Color::Yellow)
|
|
79
|
+
.add_modifier(ratatui::style::Modifier::BOLD),
|
|
80
|
+
);
|
|
51
81
|
let mut state = ListState::default();
|
|
52
82
|
state.select(Some(1));
|
|
53
83
|
|
|
@@ -58,5 +88,14 @@ mod tests {
|
|
|
58
88
|
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
59
89
|
assert!(content.contains("Item 1"));
|
|
60
90
|
assert!(content.contains(">> Item 2"));
|
|
91
|
+
|
|
92
|
+
// Check colors
|
|
93
|
+
assert_eq!(buf.cell((0, 0)).unwrap().fg, ratatui::style::Color::White);
|
|
94
|
+
assert_eq!(buf.cell((0, 1)).unwrap().fg, ratatui::style::Color::Yellow);
|
|
95
|
+
assert!(buf
|
|
96
|
+
.cell((0, 1))
|
|
97
|
+
.unwrap()
|
|
98
|
+
.modifier
|
|
99
|
+
.contains(ratatui::style::Modifier::BOLD));
|
|
61
100
|
}
|
|
62
101
|
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
pub mod barchart;
|
|
5
5
|
pub mod block;
|
|
6
|
+
pub mod calendar;
|
|
6
7
|
pub mod canvas;
|
|
7
8
|
pub mod center;
|
|
9
|
+
pub mod chart;
|
|
10
|
+
pub mod clear;
|
|
8
11
|
pub mod cursor;
|
|
9
12
|
pub mod gauge;
|
|
10
13
|
pub mod layout;
|
|
11
|
-
pub mod linechart;
|
|
12
14
|
pub mod list;
|
|
13
15
|
pub mod overlay;
|
|
14
16
|
pub mod paragraph;
|
|
@@ -6,9 +6,10 @@ use magnus::{prelude::*, Error, Value};
|
|
|
6
6
|
use ratatui::{layout::Rect, Frame};
|
|
7
7
|
|
|
8
8
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
9
10
|
let layers_val: Value = node.funcall("layers", ())?;
|
|
10
11
|
let layers_array = magnus::RArray::from_value(layers_val)
|
|
11
|
-
.ok_or_else(|| Error::new(
|
|
12
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for layers"))?;
|
|
12
13
|
|
|
13
14
|
for i in 0..layers_array.len() {
|
|
14
15
|
let layer: Value = layers_array.entry(i as isize)?;
|
|
@@ -15,6 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
15
15
|
let block_val: Value = node.funcall("block", ())?;
|
|
16
16
|
let wrap: bool = node.funcall("wrap", ())?;
|
|
17
17
|
let align_sym: Symbol = node.funcall("align", ())?;
|
|
18
|
+
let scroll_val: Value = node.funcall("scroll", ())?;
|
|
18
19
|
|
|
19
20
|
let style = parse_style(style_val)?;
|
|
20
21
|
let mut paragraph = Paragraph::new(text).style(style);
|
|
@@ -33,6 +34,15 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
33
34
|
_ => {}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Apply scroll offset if provided
|
|
38
|
+
// Ruby passes (y, x) array matching ratatui's convention
|
|
39
|
+
if !scroll_val.is_nil() {
|
|
40
|
+
let scroll_array: Vec<u16> = Vec::<u16>::try_convert(scroll_val)?;
|
|
41
|
+
if scroll_array.len() >= 2 {
|
|
42
|
+
paragraph = paragraph.scroll((scroll_array[0], scroll_array[1]));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
frame.render_widget(paragraph, area);
|
|
37
47
|
Ok(())
|
|
38
48
|
}
|
|
@@ -5,25 +5,29 @@ use crate::style::{parse_block, parse_style};
|
|
|
5
5
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
layout::{Constraint, Rect},
|
|
8
|
-
widgets::{Cell, Row, Table},
|
|
8
|
+
widgets::{Cell, Row, Table, TableState},
|
|
9
9
|
Frame,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
13
14
|
let header_val: Value = node.funcall("header", ())?;
|
|
14
15
|
let rows_val: Value = node.funcall("rows", ())?;
|
|
15
16
|
let rows_array = magnus::RArray::from_value(rows_val)
|
|
16
|
-
.ok_or_else(|| Error::new(
|
|
17
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
|
|
17
18
|
let widths_val: Value = node.funcall("widths", ())?;
|
|
18
19
|
let widths_array = magnus::RArray::from_value(widths_val)
|
|
19
|
-
.ok_or_else(|| Error::new(
|
|
20
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
|
|
21
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
22
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
23
|
+
let selected_row_val: Value = node.funcall("selected_row", ())?;
|
|
20
24
|
let block_val: Value = node.funcall("block", ())?;
|
|
21
25
|
|
|
22
26
|
let mut rows = Vec::new();
|
|
23
27
|
for i in 0..rows_array.len() {
|
|
24
28
|
let row_val: Value = rows_array.entry(i as isize)?;
|
|
25
29
|
let row_array = magnus::RArray::from_value(row_val)
|
|
26
|
-
.ok_or_else(|| Error::new(
|
|
30
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
|
|
27
31
|
|
|
28
32
|
let mut cells = Vec::new();
|
|
29
33
|
for j in 0..row_array.len() {
|
|
@@ -64,7 +68,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
64
68
|
|
|
65
69
|
if !header_val.is_nil() {
|
|
66
70
|
let header_array = magnus::RArray::from_value(header_val).ok_or_else(|| {
|
|
67
|
-
Error::new(
|
|
71
|
+
Error::new(ruby.exception_type_error(), "expected array for header")
|
|
68
72
|
})?;
|
|
69
73
|
let mut header_cells = Vec::new();
|
|
70
74
|
for i in 0..header_array.len() {
|
|
@@ -89,7 +93,22 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
89
93
|
table = table.block(parse_block(block_val)?);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
if !highlight_style_val.is_nil() {
|
|
97
|
+
table = table.row_highlight_style(parse_style(highlight_style_val)?);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if !highlight_symbol_val.is_nil() {
|
|
101
|
+
let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
|
|
102
|
+
table = table.highlight_symbol(symbol);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let mut state = TableState::default();
|
|
106
|
+
if !selected_row_val.is_nil() {
|
|
107
|
+
let index: usize = selected_row_val.funcall("to_int", ())?;
|
|
108
|
+
state.select(Some(index));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
frame.render_stateful_widget(table, area, &mut state);
|
|
93
112
|
Ok(())
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -6,12 +6,13 @@ use magnus::{prelude::*, Error, Value};
|
|
|
6
6
|
use ratatui::{layout::Rect, text::Line, widgets::Tabs, Frame};
|
|
7
7
|
|
|
8
8
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
9
10
|
let titles_val: Value = node.funcall("titles", ())?;
|
|
10
11
|
let selected_index: usize = node.funcall("selected_index", ())?;
|
|
11
12
|
let block_val: Value = node.funcall("block", ())?;
|
|
12
13
|
|
|
13
14
|
let titles_array = magnus::RArray::from_value(titles_val)
|
|
14
|
-
.ok_or_else(|| Error::new(
|
|
15
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
|
|
15
16
|
|
|
16
17
|
let mut titles = Vec::new();
|
|
17
18
|
for i in 0..titles_array.len() {
|