ratatui_ruby 0.1.0 → 0.2.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-preview3.yml +53 -0
- data/AGENTS.md +2 -2
- data/README.md +33 -13
- data/REUSE.toml +5 -0
- data/Rakefile +3 -100
- data/docs/images/examples-calendar_demo.rb.png +0 -0
- data/docs/images/examples-chart_demo.rb.png +0 -0
- data/docs/images/examples-list_styles.rb.png +0 -0
- data/docs/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/docs/images/examples-stock_ticker.rb.png +0 -0
- data/docs/quickstart.md +57 -11
- data/examples/analytics.rb +2 -1
- data/examples/calendar_demo.rb +55 -0
- data/examples/chart_demo.rb +84 -0
- data/examples/list_styles.rb +66 -0
- data/examples/login_form.rb +2 -1
- data/examples/quickstart_dsl.rb +30 -0
- data/examples/quickstart_lifecycle.rb +40 -0
- data/examples/readme_usage.rb +21 -0
- data/examples/stock_ticker.rb +13 -5
- data/examples/system_monitor.rb +2 -1
- data/examples/test_calendar_demo.rb +66 -0
- data/examples/test_list_styles.rb +61 -0
- data/ext/ratatui_ruby/.cargo/config.toml +5 -0
- data/ext/ratatui_ruby/Cargo.lock +94 -1
- data/ext/ratatui_ruby/Cargo.toml +3 -2
- data/ext/ratatui_ruby/extconf.rb +1 -1
- data/ext/ratatui_ruby/src/events.rs +4 -1
- data/ext/ratatui_ruby/src/rendering.rs +4 -1
- data/ext/ratatui_ruby/src/terminal.rs +4 -6
- data/ext/ratatui_ruby/src/widgets/calendar.rs +81 -0
- data/ext/ratatui_ruby/src/widgets/chart.rs +253 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +41 -4
- data/ext/ratatui_ruby/src/widgets/mod.rs +2 -1
- data/lib/ratatui_ruby/dsl.rb +62 -0
- data/lib/ratatui_ruby/schema/calendar.rb +26 -0
- data/lib/ratatui_ruby/schema/chart.rb +81 -0
- data/lib/ratatui_ruby/schema/list.rb +8 -2
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +21 -1
- data/mise.toml +8 -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/tasks/bump/cargo_lockfile.rb +19 -0
- data/tasks/bump/manifest.rb +23 -0
- data/tasks/bump/ruby_gem.rb +39 -0
- data/tasks/bump/sem_ver.rb +28 -0
- data/tasks/bump.rake +45 -0
- data/tasks/doc.rake +24 -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 +29 -0
- data/tasks/test.rake +31 -0
- data/tasks/website/index_page.rb +28 -0
- data/tasks/website/version.rb +116 -0
- data/tasks/website/versioned_documentation.rb +48 -0
- data/tasks/website/website.rb +50 -0
- data/tasks/website.rake +26 -0
- metadata +51 -10
- data/.build.yml +0 -34
- data/.ruby-version +0 -1
- data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
- data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
|
@@ -0,0 +1,81 @@
|
|
|
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 year: i32 = node.funcall("year", ())?;
|
|
16
|
+
let month_u8: u8 = node.funcall("month", ())?;
|
|
17
|
+
let day_style_val: Value = node.funcall("day_style", ())?;
|
|
18
|
+
let header_style_val: Value = node.funcall("header_style", ())?;
|
|
19
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
20
|
+
|
|
21
|
+
let month = Month::try_from(month_u8)
|
|
22
|
+
.map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
|
|
23
|
+
|
|
24
|
+
let date = Date::from_calendar_date(year, month, 1)
|
|
25
|
+
.map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
|
|
26
|
+
|
|
27
|
+
let mut calendar = Monthly::new(date, CalendarEventStore::default());
|
|
28
|
+
|
|
29
|
+
let header_style = if !header_style_val.is_nil() {
|
|
30
|
+
parse_style(header_style_val)?
|
|
31
|
+
} else {
|
|
32
|
+
ratatui::style::Style::default()
|
|
33
|
+
};
|
|
34
|
+
calendar = calendar
|
|
35
|
+
.show_month_header(header_style)
|
|
36
|
+
.show_weekdays_header(header_style);
|
|
37
|
+
|
|
38
|
+
if !day_style_val.is_nil() {
|
|
39
|
+
calendar = calendar.default_style(parse_style(day_style_val)?);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if !block_val.is_nil() {
|
|
43
|
+
calendar = calendar.block(parse_block(block_val)?);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
frame.render_widget(calendar, area);
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[cfg(test)]
|
|
51
|
+
mod tests {
|
|
52
|
+
use super::*;
|
|
53
|
+
use ratatui::buffer::Buffer;
|
|
54
|
+
use ratatui::widgets::Widget;
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn test_calendar_rendering() {
|
|
58
|
+
let date = Date::from_calendar_date(2025, Month::December, 1).unwrap();
|
|
59
|
+
let calendar = Monthly::new(date, CalendarEventStore::default())
|
|
60
|
+
.show_month_header(ratatui::style::Style::default());
|
|
61
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
|
|
62
|
+
calendar.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
63
|
+
let mut content = String::new();
|
|
64
|
+
for y in 0..20 {
|
|
65
|
+
for x in 0..40 {
|
|
66
|
+
content.push_str(buf.get(x, y).symbol());
|
|
67
|
+
}
|
|
68
|
+
content.push('\n');
|
|
69
|
+
}
|
|
70
|
+
assert!(
|
|
71
|
+
content.contains("December"),
|
|
72
|
+
"Content did not contain December: \n{}",
|
|
73
|
+
content
|
|
74
|
+
);
|
|
75
|
+
assert!(
|
|
76
|
+
content.contains("2025"),
|
|
77
|
+
"Content did not contain 2025: \n{}",
|
|
78
|
+
content
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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 class = node.class();
|
|
16
|
+
let class_name = unsafe { class.name() };
|
|
17
|
+
|
|
18
|
+
if class_name == "RatatuiRuby::LineChart" {
|
|
19
|
+
return render_line_chart(frame, area, node);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
23
|
+
let x_axis_val: Value = node.funcall("x_axis", ())?;
|
|
24
|
+
let y_axis_val: Value = node.funcall("y_axis", ())?;
|
|
25
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
26
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
27
|
+
|
|
28
|
+
let mut datasets = Vec::new();
|
|
29
|
+
// We need to keep the data alive until the chart is rendered
|
|
30
|
+
let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
|
|
31
|
+
|
|
32
|
+
for i in 0..datasets_val.len() {
|
|
33
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
34
|
+
let data_array: magnus::RArray = ds_val.funcall("data", ())?;
|
|
35
|
+
|
|
36
|
+
let mut points = Vec::new();
|
|
37
|
+
for j in 0..data_array.len() {
|
|
38
|
+
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
39
|
+
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
40
|
+
Error::new(magnus::exception::type_error(), "expected array for point")
|
|
41
|
+
})?;
|
|
42
|
+
let x: f64 = point_array.entry(0)?;
|
|
43
|
+
let y: f64 = point_array.entry(1)?;
|
|
44
|
+
points.push((x, y));
|
|
45
|
+
}
|
|
46
|
+
data_storage.push(points);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (i, points) in data_storage.iter().enumerate() {
|
|
50
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
51
|
+
let name: String = ds_val.funcall("name", ())?;
|
|
52
|
+
let marker_sym: Symbol = ds_val.funcall("marker", ())?;
|
|
53
|
+
let graph_type_sym: Symbol = ds_val.funcall("graph_type", ())?;
|
|
54
|
+
|
|
55
|
+
let marker = match marker_sym.to_string().as_str() {
|
|
56
|
+
"dot" => symbols::Marker::Dot,
|
|
57
|
+
"braille" => symbols::Marker::Braille,
|
|
58
|
+
"block" => symbols::Marker::Block,
|
|
59
|
+
"bar" => symbols::Marker::Bar,
|
|
60
|
+
_ => symbols::Marker::Dot,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let graph_type = match graph_type_sym.to_string().as_str() {
|
|
64
|
+
"line" => GraphType::Line,
|
|
65
|
+
"scatter" => GraphType::Scatter,
|
|
66
|
+
_ => GraphType::Line,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let mut ds_style = ratatui::style::Style::default();
|
|
70
|
+
let color_val: Value = ds_val.funcall("color", ())?;
|
|
71
|
+
if !color_val.is_nil() {
|
|
72
|
+
let color_str: String = color_val.funcall("to_s", ())?;
|
|
73
|
+
if let Some(color) = crate::style::parse_color(&color_str) {
|
|
74
|
+
ds_style = ds_style.fg(color);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let ds = Dataset::default()
|
|
79
|
+
.name(name)
|
|
80
|
+
.marker(marker)
|
|
81
|
+
.graph_type(graph_type)
|
|
82
|
+
.style(ds_style)
|
|
83
|
+
.data(points);
|
|
84
|
+
datasets.push(ds);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let x_axis = parse_axis(x_axis_val)?;
|
|
88
|
+
let y_axis = parse_axis(y_axis_val)?;
|
|
89
|
+
|
|
90
|
+
let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
|
91
|
+
|
|
92
|
+
if !block_val.is_nil() {
|
|
93
|
+
chart = chart.block(parse_block(block_val)?);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if !style_val.is_nil() {
|
|
97
|
+
chart = chart.style(parse_style(style_val)?);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
frame.render_widget(chart, area);
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
|
|
105
|
+
let title: String = axis_val.funcall("title", ())?;
|
|
106
|
+
let bounds_val: magnus::RArray = axis_val.funcall("bounds", ())?;
|
|
107
|
+
let labels_val: magnus::RArray = axis_val.funcall("labels", ())?;
|
|
108
|
+
let style_val: Value = axis_val.funcall("style", ())?;
|
|
109
|
+
|
|
110
|
+
let bounds: [f64; 2] = [bounds_val.entry(0)?, bounds_val.entry(1)?];
|
|
111
|
+
|
|
112
|
+
let mut labels = Vec::new();
|
|
113
|
+
for i in 0..labels_val.len() {
|
|
114
|
+
let label: String = labels_val.entry(i as isize)?;
|
|
115
|
+
labels.push(Span::from(label));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let mut axis = Axis::default().title(title).bounds(bounds).labels(labels);
|
|
119
|
+
|
|
120
|
+
if !style_val.is_nil() {
|
|
121
|
+
axis = axis.style(parse_style(style_val)?);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Ok(axis)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
128
|
+
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
129
|
+
let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
|
|
130
|
+
let y_labels_val: magnus::RArray = node.funcall("y_labels", ())?;
|
|
131
|
+
let y_bounds_val: magnus::RArray = node.funcall("y_bounds", ())?;
|
|
132
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
133
|
+
|
|
134
|
+
let mut datasets = Vec::new();
|
|
135
|
+
let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
|
|
136
|
+
|
|
137
|
+
for i in 0..datasets_val.len() {
|
|
138
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
139
|
+
let data_array: magnus::RArray = ds_val.funcall("data", ())?;
|
|
140
|
+
|
|
141
|
+
let mut points = Vec::new();
|
|
142
|
+
for j in 0..data_array.len() {
|
|
143
|
+
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
144
|
+
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
145
|
+
Error::new(magnus::exception::type_error(), "expected array for point")
|
|
146
|
+
})?;
|
|
147
|
+
let x: f64 = point_array.entry(0)?;
|
|
148
|
+
let y: f64 = point_array.entry(1)?;
|
|
149
|
+
points.push((x, y));
|
|
150
|
+
}
|
|
151
|
+
data_storage.push(points);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (i, points) in data_storage.iter().enumerate() {
|
|
155
|
+
let ds_val: Value = datasets_val.entry(i as isize)?;
|
|
156
|
+
let name: String = ds_val.funcall("name", ())?;
|
|
157
|
+
|
|
158
|
+
let mut ds_style = ratatui::style::Style::default();
|
|
159
|
+
let color_val: Value = ds_val.funcall("color", ())?;
|
|
160
|
+
let color_str: String = color_val.funcall("to_s", ())?;
|
|
161
|
+
if let Some(color) = crate::style::parse_color(&color_str) {
|
|
162
|
+
ds_style = ds_style.fg(color);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let ds = Dataset::default()
|
|
166
|
+
.name(name)
|
|
167
|
+
.marker(symbols::Marker::Braille)
|
|
168
|
+
.style(ds_style)
|
|
169
|
+
.data(points);
|
|
170
|
+
datasets.push(ds);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let mut x_labels = Vec::new();
|
|
174
|
+
for i in 0..x_labels_val.len() {
|
|
175
|
+
let label: String = x_labels_val.entry(i as isize)?;
|
|
176
|
+
x_labels.push(Span::from(label));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let mut y_labels = Vec::new();
|
|
180
|
+
for i in 0..y_labels_val.len() {
|
|
181
|
+
let label: String = y_labels_val.entry(i as isize)?;
|
|
182
|
+
y_labels.push(Span::from(label));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let y_bounds: [f64; 2] = [y_bounds_val.entry(0)?, y_bounds_val.entry(1)?];
|
|
186
|
+
|
|
187
|
+
let mut min_x = 0.0;
|
|
188
|
+
let mut max_x = 0.0;
|
|
189
|
+
let mut first = true;
|
|
190
|
+
for ds_data in &data_storage {
|
|
191
|
+
for (x, _) in ds_data {
|
|
192
|
+
if first {
|
|
193
|
+
min_x = *x;
|
|
194
|
+
max_x = *x;
|
|
195
|
+
first = false;
|
|
196
|
+
} else {
|
|
197
|
+
if *x < min_x {
|
|
198
|
+
min_x = *x;
|
|
199
|
+
}
|
|
200
|
+
if *x > max_x {
|
|
201
|
+
max_x = *x;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if min_x == max_x {
|
|
207
|
+
max_x = min_x + 1.0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let x_axis = Axis::default().labels(x_labels).bounds([min_x, max_x]);
|
|
211
|
+
let y_axis = Axis::default().labels(y_labels).bounds(y_bounds);
|
|
212
|
+
|
|
213
|
+
let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
|
214
|
+
if !block_val.is_nil() {
|
|
215
|
+
chart = chart.block(parse_block(block_val)?);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
frame.render_widget(chart, area);
|
|
219
|
+
Ok(())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[cfg(test)]
|
|
223
|
+
mod tests {
|
|
224
|
+
use super::*;
|
|
225
|
+
use ratatui::buffer::Buffer;
|
|
226
|
+
use ratatui::widgets::Widget;
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn test_chart_rendering() {
|
|
230
|
+
let data = vec![(0.0, 0.0), (1.0, 1.0)];
|
|
231
|
+
let datasets = vec![Dataset::default().name("TestDS").data(&data)];
|
|
232
|
+
let chart = Chart::new(datasets)
|
|
233
|
+
.x_axis(
|
|
234
|
+
Axis::default()
|
|
235
|
+
.bounds([0.0, 1.0])
|
|
236
|
+
.labels(vec!["XMIN".into(), "XMAX".into()]),
|
|
237
|
+
)
|
|
238
|
+
.y_axis(
|
|
239
|
+
Axis::default()
|
|
240
|
+
.bounds([0.0, 1.0])
|
|
241
|
+
.labels(vec!["YMIN".into(), "YMAX".into()]),
|
|
242
|
+
);
|
|
243
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
|
|
244
|
+
chart.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
245
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
246
|
+
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
247
|
+
assert!(content.contains("XMIN"));
|
|
248
|
+
assert!(content.contains("XMAX"));
|
|
249
|
+
assert!(content.contains("YMIN"));
|
|
250
|
+
assert!(content.contains("YMAX"));
|
|
251
|
+
assert!(content.contains("TestDS"));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -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},
|
|
@@ -14,6 +14,9 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
14
14
|
let items_array = magnus::RArray::from_value(items_val)
|
|
15
15
|
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array"))?;
|
|
16
16
|
let selected_index_val: Value = node.funcall("selected_index", ())?;
|
|
17
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
18
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
19
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
17
20
|
let block_val: Value = node.funcall("block", ())?;
|
|
18
21
|
|
|
19
22
|
let mut items = Vec::new();
|
|
@@ -22,13 +25,32 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
22
25
|
items.push(item);
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
let symbol: String = if !highlight_symbol_val.is_nil() {
|
|
29
|
+
let s: String = String::try_convert(highlight_symbol_val)?;
|
|
30
|
+
s
|
|
31
|
+
} else {
|
|
32
|
+
String::new()
|
|
33
|
+
};
|
|
34
|
+
|
|
25
35
|
let mut state = ListState::default();
|
|
26
36
|
if !selected_index_val.is_nil() {
|
|
27
37
|
let index: usize = selected_index_val.funcall("to_int", ())?;
|
|
28
38
|
state.select(Some(index));
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
let mut list = List::new(items)
|
|
41
|
+
let mut list = List::new(items);
|
|
42
|
+
|
|
43
|
+
if !highlight_symbol_val.is_nil() {
|
|
44
|
+
list = list.highlight_symbol(&symbol);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if !style_val.is_nil() {
|
|
48
|
+
list = list.style(parse_style(style_val)?);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if !highlight_style_val.is_nil() {
|
|
52
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
53
|
+
}
|
|
32
54
|
|
|
33
55
|
if !block_val.is_nil() {
|
|
34
56
|
list = list.block(parse_block(block_val)?);
|
|
@@ -47,7 +69,14 @@ mod tests {
|
|
|
47
69
|
#[test]
|
|
48
70
|
fn test_list_rendering() {
|
|
49
71
|
let items = vec!["Item 1", "Item 2"];
|
|
50
|
-
let list = List::new(items)
|
|
72
|
+
let list = List::new(items)
|
|
73
|
+
.highlight_symbol(">> ")
|
|
74
|
+
.style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
|
|
75
|
+
.highlight_style(
|
|
76
|
+
ratatui::style::Style::default()
|
|
77
|
+
.fg(ratatui::style::Color::Yellow)
|
|
78
|
+
.add_modifier(ratatui::style::Modifier::BOLD),
|
|
79
|
+
);
|
|
51
80
|
let mut state = ListState::default();
|
|
52
81
|
state.select(Some(1));
|
|
53
82
|
|
|
@@ -58,5 +87,13 @@ mod tests {
|
|
|
58
87
|
let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
|
|
59
88
|
assert!(content.contains("Item 1"));
|
|
60
89
|
assert!(content.contains(">> Item 2"));
|
|
90
|
+
|
|
91
|
+
// Check colors
|
|
92
|
+
assert_eq!(buf.get(0, 0).fg, ratatui::style::Color::White);
|
|
93
|
+
assert_eq!(buf.get(0, 1).fg, ratatui::style::Color::Yellow);
|
|
94
|
+
assert!(buf
|
|
95
|
+
.get(0, 1)
|
|
96
|
+
.modifier
|
|
97
|
+
.contains(ratatui::style::Modifier::BOLD));
|
|
61
98
|
}
|
|
62
99
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
##
|
|
8
|
+
# A wrapper class that provides a concise DSL for creating widgets and interacting
|
|
9
|
+
# with the terminal within the +main_loop+.
|
|
10
|
+
#
|
|
11
|
+
# This class is yielded to the block provided to {RatatuiRuby.main_loop}.
|
|
12
|
+
# It uses metaprogramming to delegate method calls to {RatatuiRuby} module functions
|
|
13
|
+
# and to act as a factory for {RatatuiRuby} widget classes.
|
|
14
|
+
#
|
|
15
|
+
# == Features
|
|
16
|
+
#
|
|
17
|
+
# 1. **Widget Shorthand**: Provides factory methods for every widget class.
|
|
18
|
+
# Converts snake_case method calls (e.g., +paragraph+) into CamelCase class instantiations
|
|
19
|
+
# (e.g., +RatatuiRuby::Paragraph.new+).
|
|
20
|
+
#
|
|
21
|
+
# 2. **Method Shorthand**: Aliases module functions of {RatatuiRuby}, allowing you
|
|
22
|
+
# to call methods like +draw+ and +poll_event+ directly on the DSL object.
|
|
23
|
+
#
|
|
24
|
+
# == Example
|
|
25
|
+
#
|
|
26
|
+
# RatatuiRuby.main_loop do |tui|
|
|
27
|
+
# # Create UI using shorthand methods
|
|
28
|
+
# view = tui.paragraph(
|
|
29
|
+
# text: "Hello World",
|
|
30
|
+
# block: tui.block(borders: [:all])
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# # Use module aliases to draw and handle events
|
|
34
|
+
# tui.draw(view)
|
|
35
|
+
# event = tui.poll_event
|
|
36
|
+
#
|
|
37
|
+
# break if event && event[:code] == "q"
|
|
38
|
+
# end
|
|
39
|
+
class DSL
|
|
40
|
+
# Wrap methods directly
|
|
41
|
+
RatatuiRuby.singleton_methods(false).each do |method_name|
|
|
42
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
43
|
+
RatatuiRuby.public_send(method_name, *args, **kwargs, &block)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Wrap classes as snake_case factories
|
|
48
|
+
RatatuiRuby.constants.each do |const_name|
|
|
49
|
+
klass = RatatuiRuby.const_get(const_name)
|
|
50
|
+
next unless klass.is_a?(Class)
|
|
51
|
+
|
|
52
|
+
method_name = const_name.to_s
|
|
53
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
54
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
55
|
+
.downcase
|
|
56
|
+
|
|
57
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
58
|
+
klass.new(*args, **kwargs, &block)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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 Monthly Calendar widget.
|
|
8
|
+
#
|
|
9
|
+
# [year] Integer (e.g., 2025)
|
|
10
|
+
# [month] Integer (1-12)
|
|
11
|
+
# [day_style] Style (Style for regular days)
|
|
12
|
+
# [header_style] Style (Style for the month title)
|
|
13
|
+
# [block] Block
|
|
14
|
+
class Calendar < Data.define(:year, :month, :day_style, :header_style, :block)
|
|
15
|
+
# Creates a new Calendar.
|
|
16
|
+
#
|
|
17
|
+
# [year] Integer (e.g., 2025)
|
|
18
|
+
# [month] Integer (1-12)
|
|
19
|
+
# [day_style] Style (Style for regular days)
|
|
20
|
+
# [header_style] Style (Style for the month title)
|
|
21
|
+
# [block] Block
|
|
22
|
+
def initialize(year:, month:, day_style: nil, header_style: nil, block: nil)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
# Defines an Axis for a Chart
|
|
8
|
+
# [title] String
|
|
9
|
+
# [bounds] Array<Float> [min, max]
|
|
10
|
+
# [labels] Array<String>
|
|
11
|
+
# [style] Style
|
|
12
|
+
class Axis < Data.define(:title, :bounds, :labels, :style)
|
|
13
|
+
# Creates a new Axis.
|
|
14
|
+
#
|
|
15
|
+
# [title] String
|
|
16
|
+
# [bounds] Array<Float> [min, max]
|
|
17
|
+
# [labels] Array<String>
|
|
18
|
+
# [style] Style
|
|
19
|
+
def initialize(title: "", bounds: [0.0, 10.0], labels: [], style: nil)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Defines a Dataset for a Chart.
|
|
25
|
+
# [name] The name of the dataset.
|
|
26
|
+
# [data] Array of arrays [[x, y], [x, y]] (Floats).
|
|
27
|
+
# [color] The color of the line.
|
|
28
|
+
# [marker] Symbol (:dot, :braille, :block, :bar)
|
|
29
|
+
# [graph_type] Symbol (:line, :scatter)
|
|
30
|
+
class Dataset < Data.define(:name, :data, :color, :marker, :graph_type)
|
|
31
|
+
# Creates a new Dataset.
|
|
32
|
+
#
|
|
33
|
+
# [name] The name of the dataset.
|
|
34
|
+
# [data] Array of arrays [[x, y], [x, y]] (Floats).
|
|
35
|
+
# [color] The color of the line.
|
|
36
|
+
# [marker] Symbol (:dot, :braille, :block, :bar)
|
|
37
|
+
# [graph_type] Symbol (:line, :scatter)
|
|
38
|
+
def initialize(name:, data:, color: "reset", marker: :dot, graph_type: :line)
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A generic Cartesian chart.
|
|
44
|
+
# [datasets] Array<Dataset>
|
|
45
|
+
# [x_axis] Axis
|
|
46
|
+
# [y_axis] Axis
|
|
47
|
+
# [block] Block
|
|
48
|
+
# [style] Style (base style)
|
|
49
|
+
class Chart < Data.define(:datasets, :x_axis, :y_axis, :block, :style)
|
|
50
|
+
# Creates a new Chart widget.
|
|
51
|
+
#
|
|
52
|
+
# [datasets] Array<Dataset>
|
|
53
|
+
# [x_axis] Axis
|
|
54
|
+
# [y_axis] Axis
|
|
55
|
+
# [block] Block
|
|
56
|
+
# [style] Style (base style)
|
|
57
|
+
def initialize(datasets:, x_axis:, y_axis:, block: nil, style: nil)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A complex chart widget. (Legacy/Alias for Chart)
|
|
63
|
+
#
|
|
64
|
+
# [datasets] Array of Dataset objects.
|
|
65
|
+
# [x_labels] Array of Strings for the X-axis labels.
|
|
66
|
+
# [y_labels] Array of Strings for the Y-axis labels.
|
|
67
|
+
# [y_bounds] Array of two Floats [min, max] for the Y-axis.
|
|
68
|
+
# [block] Optional block widget to wrap the chart.
|
|
69
|
+
class LineChart < Data.define(:datasets, :x_labels, :y_labels, :y_bounds, :block)
|
|
70
|
+
# Creates a new LineChart widget.
|
|
71
|
+
#
|
|
72
|
+
# [datasets] Array of Dataset objects.
|
|
73
|
+
# [x_labels] Array of Strings for the X-axis labels.
|
|
74
|
+
# [y_labels] Array of Strings for the Y-axis labels.
|
|
75
|
+
# [y_bounds] Array of two Floats [min, max] for the Y-axis.
|
|
76
|
+
# [block] Optional block widget to wrap the chart.
|
|
77
|
+
def initialize(datasets:, x_labels: [], y_labels: [], y_bounds: [0.0, 100.0], block: nil)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -8,14 +8,20 @@ module RatatuiRuby
|
|
|
8
8
|
#
|
|
9
9
|
# [items] An array of strings to display in the list.
|
|
10
10
|
# [selected_index] The index of the currently selected item, or nil if none.
|
|
11
|
+
# [style] The base style for all items.
|
|
12
|
+
# [highlight_style] The style for the selected item.
|
|
13
|
+
# [highlight_symbol] The symbol to display in front of the selected item.
|
|
11
14
|
# [block] An optional Block widget to wrap the list.
|
|
12
|
-
class List < Data.define(:items, :selected_index, :block)
|
|
15
|
+
class List < Data.define(:items, :selected_index, :style, :highlight_style, :highlight_symbol, :block)
|
|
13
16
|
# Creates a new List.
|
|
14
17
|
#
|
|
15
18
|
# [items] An array of strings to display in the list.
|
|
16
19
|
# [selected_index] The index of the currently selected item, or nil if none.
|
|
20
|
+
# [style] The base style for all items.
|
|
21
|
+
# [highlight_style] The style for the selected item.
|
|
22
|
+
# [highlight_symbol] The symbol to display in front of the selected item.
|
|
17
23
|
# [block] An optional Block widget to wrap the list.
|
|
18
|
-
def initialize(items: [], selected_index: nil, block: nil)
|
|
24
|
+
def initialize(items: [], selected_index: nil, style: nil, highlight_style: nil, highlight_symbol: "> ", block: nil)
|
|
19
25
|
super
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/lib/ratatui_ruby.rb
CHANGED
|
@@ -15,12 +15,13 @@ require_relative "ratatui_ruby/schema/table"
|
|
|
15
15
|
require_relative "ratatui_ruby/schema/tabs"
|
|
16
16
|
require_relative "ratatui_ruby/schema/bar_chart"
|
|
17
17
|
require_relative "ratatui_ruby/schema/sparkline"
|
|
18
|
-
require_relative "ratatui_ruby/schema/
|
|
18
|
+
require_relative "ratatui_ruby/schema/chart"
|
|
19
19
|
require_relative "ratatui_ruby/schema/cursor"
|
|
20
20
|
require_relative "ratatui_ruby/schema/overlay"
|
|
21
21
|
require_relative "ratatui_ruby/schema/center"
|
|
22
22
|
require_relative "ratatui_ruby/schema/scrollbar"
|
|
23
23
|
require_relative "ratatui_ruby/schema/canvas"
|
|
24
|
+
require_relative "ratatui_ruby/schema/calendar"
|
|
24
25
|
|
|
25
26
|
begin
|
|
26
27
|
require "ratatui_ruby/ratatui_ruby"
|
|
@@ -84,4 +85,23 @@ module RatatuiRuby
|
|
|
84
85
|
# inject_test_event("key", { code: "a" })
|
|
85
86
|
#
|
|
86
87
|
# (Native method implemented in Rust)
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Provides a convenience wrapper for the main TUI loop.
|
|
91
|
+
# Initializes the terminal, runs the loop, and ensures the terminal is restored.
|
|
92
|
+
#
|
|
93
|
+
# RatatuiRuby.main_loop do
|
|
94
|
+
# draw RatatuiRuby::Paragraph.new(text: "Hello")
|
|
95
|
+
# event = RatatuiRuby::poll_event
|
|
96
|
+
# break if event && event[:type] == :key && event[:code] == "q"
|
|
97
|
+
# end
|
|
98
|
+
def self.main_loop
|
|
99
|
+
require_relative "ratatui_ruby/dsl"
|
|
100
|
+
init_terminal
|
|
101
|
+
loop do
|
|
102
|
+
yield DSL.new
|
|
103
|
+
end
|
|
104
|
+
ensure
|
|
105
|
+
restore_terminal
|
|
106
|
+
end
|
|
87
107
|
end
|