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,131 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use magnus::Error;
|
|
5
|
+
use ratatui::{
|
|
6
|
+
backend::{CrosstermBackend, TestBackend},
|
|
7
|
+
Terminal,
|
|
8
|
+
};
|
|
9
|
+
use std::io;
|
|
10
|
+
use std::sync::Mutex;
|
|
11
|
+
|
|
12
|
+
pub enum TerminalWrapper {
|
|
13
|
+
Crossterm(Terminal<CrosstermBackend<io::Stdout>>),
|
|
14
|
+
Test(Terminal<TestBackend>), // We don't need Mutex inside the enum variant because the global is a Mutex
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
lazy_static::lazy_static! {
|
|
18
|
+
pub static ref TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn init_terminal() -> Result<(), Error> {
|
|
22
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
23
|
+
if term_lock.is_none() {
|
|
24
|
+
crossterm::terminal::enable_raw_mode()
|
|
25
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
|
26
|
+
let mut stdout = io::stdout();
|
|
27
|
+
crossterm::execute!(
|
|
28
|
+
stdout,
|
|
29
|
+
crossterm::terminal::EnterAlternateScreen,
|
|
30
|
+
crossterm::event::EnableMouseCapture
|
|
31
|
+
)
|
|
32
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
|
33
|
+
let backend = CrosstermBackend::new(stdout);
|
|
34
|
+
let terminal = Terminal::new(backend)
|
|
35
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
|
36
|
+
*term_lock = Some(TerminalWrapper::Crossterm(terminal));
|
|
37
|
+
}
|
|
38
|
+
Ok(())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
42
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
43
|
+
if term_lock.is_none() {
|
|
44
|
+
let backend = TestBackend::new(width, height);
|
|
45
|
+
let terminal = Terminal::new(backend)
|
|
46
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
|
47
|
+
*term_lock = Some(TerminalWrapper::Test(terminal));
|
|
48
|
+
}
|
|
49
|
+
Ok(())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn restore_terminal() -> Result<(), Error> {
|
|
53
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
54
|
+
if let Some(TerminalWrapper::Crossterm(mut terminal)) = term_lock.take() {
|
|
55
|
+
let _ = crossterm::terminal::disable_raw_mode();
|
|
56
|
+
let _ = crossterm::execute!(
|
|
57
|
+
terminal.backend_mut(),
|
|
58
|
+
crossterm::terminal::LeaveAlternateScreen,
|
|
59
|
+
crossterm::event::DisableMouseCapture
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
Ok(())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn get_buffer_content() -> Result<String, Error> {
|
|
66
|
+
let term_lock = TERMINAL.lock().unwrap();
|
|
67
|
+
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
|
|
68
|
+
// We need to access the buffer.
|
|
69
|
+
// Since we are mocking, we can just print the buffer to a string.
|
|
70
|
+
let buffer = terminal.backend().buffer();
|
|
71
|
+
// Simple representation: each cell's symbol.
|
|
72
|
+
// For a more complex representation we could return an array of strings.
|
|
73
|
+
// Let's just return the full string representation for now which is useful for debugging/asserting.
|
|
74
|
+
// Actually, let's reconstruct it line by line.
|
|
75
|
+
let area = buffer.area;
|
|
76
|
+
let mut result = String::new();
|
|
77
|
+
for y in 0..area.height {
|
|
78
|
+
for x in 0..area.width {
|
|
79
|
+
let cell = buffer.get(x, y);
|
|
80
|
+
result.push_str(cell.symbol());
|
|
81
|
+
}
|
|
82
|
+
result.push('\n');
|
|
83
|
+
}
|
|
84
|
+
Ok(result)
|
|
85
|
+
} else {
|
|
86
|
+
Err(Error::new(
|
|
87
|
+
magnus::exception::runtime_error(),
|
|
88
|
+
"Terminal is not initialized as TestBackend",
|
|
89
|
+
))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
|
|
94
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
95
|
+
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
|
|
96
|
+
let pos = terminal
|
|
97
|
+
.get_cursor()
|
|
98
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
|
99
|
+
Ok(Some(pos))
|
|
100
|
+
} else {
|
|
101
|
+
Err(Error::new(
|
|
102
|
+
magnus::exception::runtime_error(),
|
|
103
|
+
"Terminal is not initialized as TestBackend",
|
|
104
|
+
))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
109
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
110
|
+
if let Some(wrapper) = term_lock.as_mut() {
|
|
111
|
+
match wrapper {
|
|
112
|
+
TerminalWrapper::Crossterm(_) => {
|
|
113
|
+
// Resize happens automatically for Crossterm via signals usually,
|
|
114
|
+
// but we can't easily force it here without OS interaction.
|
|
115
|
+
// Ignoring for now as it's less critical for unit testing the logic.
|
|
116
|
+
}
|
|
117
|
+
TerminalWrapper::Test(terminal) => {
|
|
118
|
+
terminal.backend_mut().resize(width, height);
|
|
119
|
+
// Also resize the terminal wrapper itself if needed, but TestBackend resize handles the buffer.
|
|
120
|
+
// We might need to call terminal.resize() too if Ratatui caches the size.
|
|
121
|
+
if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
|
|
122
|
+
return Err(Error::new(
|
|
123
|
+
magnus::exception::runtime_error(),
|
|
124
|
+
e.to_string(),
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
Ok(())
|
|
131
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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::BarChart, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let data_val: magnus::RHash = node.funcall("data", ())?;
|
|
10
|
+
let bar_width: u16 = node.funcall("bar_width", ())?;
|
|
11
|
+
let bar_gap: u16 = node.funcall("bar_gap", ())?;
|
|
12
|
+
let max_val: Value = node.funcall("max", ())?;
|
|
13
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
14
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
15
|
+
|
|
16
|
+
let keys: magnus::RArray = data_val.funcall("keys", ())?;
|
|
17
|
+
let mut labels = Vec::new();
|
|
18
|
+
let mut data_vec = Vec::new();
|
|
19
|
+
|
|
20
|
+
for i in 0..keys.len() {
|
|
21
|
+
let key: Value = keys.entry(i as isize)?;
|
|
22
|
+
let val: u64 = data_val.funcall("[]", (key,))?;
|
|
23
|
+
let label: String = key.funcall("to_s", ())?;
|
|
24
|
+
labels.push(label);
|
|
25
|
+
data_vec.push(val);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let chart_data: Vec<(&str, u64)> = labels
|
|
29
|
+
.iter()
|
|
30
|
+
.zip(data_vec.iter())
|
|
31
|
+
.map(|(l, v)| (l.as_str(), *v))
|
|
32
|
+
.collect();
|
|
33
|
+
|
|
34
|
+
let mut bar_chart = BarChart::default()
|
|
35
|
+
.data(&chart_data)
|
|
36
|
+
.bar_width(bar_width)
|
|
37
|
+
.bar_gap(bar_gap);
|
|
38
|
+
|
|
39
|
+
if !max_val.is_nil() {
|
|
40
|
+
let max: u64 = u64::try_convert(max_val)?;
|
|
41
|
+
bar_chart = bar_chart.max(max);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if !style_val.is_nil() {
|
|
45
|
+
bar_chart = bar_chart.style(parse_style(style_val)?);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if !block_val.is_nil() {
|
|
49
|
+
bar_chart = bar_chart.block(parse_block(block_val)?);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
frame.render_widget(bar_chart, area);
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[cfg(test)]
|
|
57
|
+
mod tests {
|
|
58
|
+
use super::*;
|
|
59
|
+
use ratatui::buffer::Buffer;
|
|
60
|
+
use ratatui::widgets::{BarChart, Widget};
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn test_barchart_rendering() {
|
|
64
|
+
let data = [("B1", 10), ("B2", 20)];
|
|
65
|
+
let chart = BarChart::default().data(&data).bar_width(3);
|
|
66
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
|
|
67
|
+
chart.render(Rect::new(0, 0, 10, 5), &mut buf);
|
|
68
|
+
// Should have bars rendered (non-space characters)
|
|
69
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
70
|
+
// Should have labels
|
|
71
|
+
assert!(buf.content().iter().any(|c| c.symbol().contains('B')));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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::{Error, Value};
|
|
6
|
+
use ratatui::{layout::Rect, widgets::Widget, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let block = parse_block(node)?;
|
|
10
|
+
block.render(area, frame.buffer_mut());
|
|
11
|
+
Ok(())
|
|
12
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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, RArray, Symbol, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
symbols::Marker,
|
|
8
|
+
widgets::canvas::{Canvas, Circle, Line, Map, MapResolution, Rectangle},
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let shapes_val: RArray = node.funcall("shapes", ())?;
|
|
14
|
+
let x_bounds_val: RArray = node.funcall("x_bounds", ())?;
|
|
15
|
+
let y_bounds_val: RArray = node.funcall("y_bounds", ())?;
|
|
16
|
+
let marker_sym: Symbol = node.funcall("marker", ())?;
|
|
17
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
18
|
+
|
|
19
|
+
let x_bounds: [f64; 2] = [x_bounds_val.entry::<f64>(0)?, x_bounds_val.entry::<f64>(1)?];
|
|
20
|
+
let y_bounds: [f64; 2] = [y_bounds_val.entry::<f64>(0)?, y_bounds_val.entry::<f64>(1)?];
|
|
21
|
+
|
|
22
|
+
let marker = match marker_sym.to_string().as_str() {
|
|
23
|
+
"dot" => Marker::Dot,
|
|
24
|
+
"block" => Marker::Block,
|
|
25
|
+
"bar" => Marker::Bar,
|
|
26
|
+
_ => Marker::Braille,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let mut canvas = Canvas::default()
|
|
30
|
+
.x_bounds(x_bounds)
|
|
31
|
+
.y_bounds(y_bounds)
|
|
32
|
+
.marker(marker);
|
|
33
|
+
|
|
34
|
+
if !block_val.is_nil() {
|
|
35
|
+
canvas = canvas.block(parse_block(block_val)?);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let canvas = canvas.paint(|ctx| {
|
|
39
|
+
for shape_val in shapes_val.each() {
|
|
40
|
+
let shape_val = shape_val.unwrap();
|
|
41
|
+
let class = shape_val.class();
|
|
42
|
+
let class_name = unsafe { class.name() };
|
|
43
|
+
|
|
44
|
+
match class_name.as_ref() {
|
|
45
|
+
"RatatuiRuby::Line" => {
|
|
46
|
+
let x1: f64 = shape_val.funcall("x1", ()).unwrap_or(0.0);
|
|
47
|
+
let y1: f64 = shape_val.funcall("y1", ()).unwrap_or(0.0);
|
|
48
|
+
let x2: f64 = shape_val.funcall("x2", ()).unwrap_or(0.0);
|
|
49
|
+
let y2: f64 = shape_val.funcall("y2", ()).unwrap_or(0.0);
|
|
50
|
+
let color_val: Value = shape_val.funcall("color", ()).unwrap();
|
|
51
|
+
let color =
|
|
52
|
+
parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
|
|
53
|
+
ctx.draw(&Line {
|
|
54
|
+
x1,
|
|
55
|
+
y1,
|
|
56
|
+
x2,
|
|
57
|
+
y2,
|
|
58
|
+
color,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
"RatatuiRuby::Rectangle" => {
|
|
62
|
+
let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
|
|
63
|
+
let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
|
|
64
|
+
let width: f64 = shape_val.funcall("width", ()).unwrap_or(0.0);
|
|
65
|
+
let height: f64 = shape_val.funcall("height", ()).unwrap_or(0.0);
|
|
66
|
+
let color_val: Value = shape_val.funcall("color", ()).unwrap();
|
|
67
|
+
let color =
|
|
68
|
+
parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
|
|
69
|
+
ctx.draw(&Rectangle {
|
|
70
|
+
x,
|
|
71
|
+
y,
|
|
72
|
+
width,
|
|
73
|
+
height,
|
|
74
|
+
color,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
"RatatuiRuby::Circle" => {
|
|
78
|
+
let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
|
|
79
|
+
let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
|
|
80
|
+
let radius: f64 = shape_val.funcall("radius", ()).unwrap_or(0.0);
|
|
81
|
+
let color_val: Value = shape_val.funcall("color", ()).unwrap();
|
|
82
|
+
let color =
|
|
83
|
+
parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
|
|
84
|
+
ctx.draw(&Circle {
|
|
85
|
+
x,
|
|
86
|
+
y,
|
|
87
|
+
radius,
|
|
88
|
+
color,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
"RatatuiRuby::Map" => {
|
|
92
|
+
let color_val: Value = shape_val.funcall("color", ()).unwrap();
|
|
93
|
+
let color =
|
|
94
|
+
parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
|
|
95
|
+
let resolution_sym: Symbol = shape_val.funcall("resolution", ()).unwrap();
|
|
96
|
+
let resolution = match resolution_sym.to_string().as_str() {
|
|
97
|
+
"high" => MapResolution::High,
|
|
98
|
+
_ => MapResolution::Low,
|
|
99
|
+
};
|
|
100
|
+
ctx.draw(&Map { color, resolution });
|
|
101
|
+
}
|
|
102
|
+
_ => {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
frame.render_widget(canvas, area);
|
|
108
|
+
Ok(())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[cfg(test)]
|
|
112
|
+
mod tests {
|
|
113
|
+
use super::*;
|
|
114
|
+
use ratatui::buffer::Buffer;
|
|
115
|
+
use ratatui::layout::Rect;
|
|
116
|
+
use ratatui::widgets::Widget;
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn test_canvas_rendering() {
|
|
120
|
+
let canvas = Canvas::default()
|
|
121
|
+
.x_bounds([0.0, 10.0])
|
|
122
|
+
.y_bounds([0.0, 10.0])
|
|
123
|
+
.marker(Marker::Braille)
|
|
124
|
+
.paint(|ctx| {
|
|
125
|
+
ctx.draw(&Line {
|
|
126
|
+
x1: 0.0,
|
|
127
|
+
y1: 0.0,
|
|
128
|
+
x2: 10.0,
|
|
129
|
+
y2: 10.0,
|
|
130
|
+
color: ratatui::style::Color::Red,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 5));
|
|
134
|
+
canvas.render(Rect::new(0, 0, 5, 5), &mut buf);
|
|
135
|
+
|
|
136
|
+
// Verify that some Braille characters are rendered
|
|
137
|
+
let mut found_braille = false;
|
|
138
|
+
for cell in buf.content() {
|
|
139
|
+
if !cell.symbol().trim().is_empty() {
|
|
140
|
+
found_braille = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
assert!(found_braille, "Canvas should render Braille characters");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -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::rendering::render_node;
|
|
5
|
+
use magnus::{prelude::*, Error, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::{Constraint, Direction, Layout, Rect},
|
|
8
|
+
widgets::Clear,
|
|
9
|
+
Frame,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let child: Value = node.funcall("child", ())?;
|
|
14
|
+
let width_percent: u16 = node.funcall("width_percent", ())?;
|
|
15
|
+
let height_percent: u16 = node.funcall("height_percent", ())?;
|
|
16
|
+
|
|
17
|
+
let popup_layout = Layout::default()
|
|
18
|
+
.direction(Direction::Vertical)
|
|
19
|
+
.constraints([
|
|
20
|
+
Constraint::Percentage((100 - height_percent) / 2),
|
|
21
|
+
Constraint::Percentage(height_percent),
|
|
22
|
+
Constraint::Percentage((100 - height_percent) / 2),
|
|
23
|
+
])
|
|
24
|
+
.split(area);
|
|
25
|
+
|
|
26
|
+
let vertical_center_area = popup_layout[1];
|
|
27
|
+
|
|
28
|
+
let popup_layout_horizontal = Layout::default()
|
|
29
|
+
.direction(Direction::Horizontal)
|
|
30
|
+
.constraints([
|
|
31
|
+
Constraint::Percentage((100 - width_percent) / 2),
|
|
32
|
+
Constraint::Percentage(width_percent),
|
|
33
|
+
Constraint::Percentage((100 - width_percent) / 2),
|
|
34
|
+
])
|
|
35
|
+
.split(vertical_center_area);
|
|
36
|
+
|
|
37
|
+
let center_area = popup_layout_horizontal[1];
|
|
38
|
+
|
|
39
|
+
frame.render_widget(Clear, center_area);
|
|
40
|
+
render_node(frame, center_area, child)?;
|
|
41
|
+
Ok(())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[cfg(test)]
|
|
45
|
+
mod tests {
|
|
46
|
+
|
|
47
|
+
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn test_center_logic() {
|
|
51
|
+
let area = Rect::new(0, 0, 100, 100);
|
|
52
|
+
let width_percent = 50;
|
|
53
|
+
let height_percent = 50;
|
|
54
|
+
|
|
55
|
+
let popup_layout = Layout::default()
|
|
56
|
+
.direction(Direction::Vertical)
|
|
57
|
+
.constraints([
|
|
58
|
+
Constraint::Percentage((100 - height_percent) / 2),
|
|
59
|
+
Constraint::Percentage(height_percent),
|
|
60
|
+
Constraint::Percentage((100 - height_percent) / 2),
|
|
61
|
+
])
|
|
62
|
+
.split(area);
|
|
63
|
+
let vertical_center = popup_layout[1];
|
|
64
|
+
|
|
65
|
+
// Vertical check
|
|
66
|
+
assert_eq!(vertical_center.height, 50);
|
|
67
|
+
|
|
68
|
+
let popup_layout_horizontal = Layout::default()
|
|
69
|
+
.direction(Direction::Horizontal)
|
|
70
|
+
.constraints([
|
|
71
|
+
Constraint::Percentage((100 - width_percent) / 2),
|
|
72
|
+
Constraint::Percentage(width_percent),
|
|
73
|
+
Constraint::Percentage((100 - width_percent) / 2),
|
|
74
|
+
])
|
|
75
|
+
.split(vertical_center);
|
|
76
|
+
let center = popup_layout_horizontal[1];
|
|
77
|
+
|
|
78
|
+
// Horizontal check
|
|
79
|
+
assert_eq!(center.width, 50);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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, Frame};
|
|
6
|
+
|
|
7
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
8
|
+
let x: u16 = node.funcall("x", ())?;
|
|
9
|
+
let y: u16 = node.funcall("y", ())?;
|
|
10
|
+
frame.set_cursor(area.x + x, area.y + y);
|
|
11
|
+
Ok(())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod tests {
|
|
16
|
+
|
|
17
|
+
use ratatui::layout::Rect;
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn test_cursor_math() {
|
|
21
|
+
let area = Rect::new(10, 10, 50, 50);
|
|
22
|
+
let x = 5;
|
|
23
|
+
let y = 5;
|
|
24
|
+
let abs_x = area.x + x;
|
|
25
|
+
let abs_y = area.y + y;
|
|
26
|
+
assert_eq!(abs_x, 15);
|
|
27
|
+
assert_eq!(abs_y, 15);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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::Gauge, Frame};
|
|
7
|
+
|
|
8
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let ratio: f64 = node.funcall("ratio", ())?;
|
|
10
|
+
let label_val: Value = node.funcall("label", ())?;
|
|
11
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
12
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
13
|
+
|
|
14
|
+
let mut gauge = Gauge::default().ratio(ratio);
|
|
15
|
+
|
|
16
|
+
if !label_val.is_nil() {
|
|
17
|
+
let label_str: String = label_val.funcall("to_s", ())?;
|
|
18
|
+
gauge = gauge.label(label_str);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if !style_val.is_nil() {
|
|
22
|
+
gauge = gauge.gauge_style(parse_style(style_val)?);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if !block_val.is_nil() {
|
|
26
|
+
gauge = gauge.block(parse_block(block_val)?);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
frame.render_widget(gauge, area);
|
|
30
|
+
Ok(())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[cfg(test)]
|
|
34
|
+
mod tests {
|
|
35
|
+
use super::*;
|
|
36
|
+
use ratatui::buffer::Buffer;
|
|
37
|
+
use ratatui::widgets::{Gauge, Widget};
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn test_gauge_rendering() {
|
|
41
|
+
let gauge = Gauge::default().ratio(0.5).label("50%");
|
|
42
|
+
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
|
43
|
+
gauge.render(Rect::new(0, 0, 10, 1), &mut buf);
|
|
44
|
+
// Gauge renders block characters
|
|
45
|
+
assert!(buf.content().iter().any(|c| c.symbol() != " "));
|
|
46
|
+
// Should contain label
|
|
47
|
+
assert!(buf.content().iter().any(|c| c.symbol() == "5"));
|
|
48
|
+
assert!(buf.content().iter().any(|c| c.symbol() == "%"));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -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::rendering::render_node;
|
|
5
|
+
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
layout::{Constraint, Direction, Layout, Rect},
|
|
8
|
+
Frame,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
12
|
+
let direction_sym: Symbol = node.funcall("direction", ())?;
|
|
13
|
+
let children_val: Value = node.funcall("children", ())?;
|
|
14
|
+
let children_array = magnus::RArray::from_value(children_val)
|
|
15
|
+
.ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array"))?;
|
|
16
|
+
|
|
17
|
+
let constraints_val: Value = node.funcall("constraints", ())?;
|
|
18
|
+
let constraints_array = magnus::RArray::from_value(constraints_val);
|
|
19
|
+
|
|
20
|
+
let direction = if direction_sym.to_string() == "vertical" {
|
|
21
|
+
Direction::Vertical
|
|
22
|
+
} else {
|
|
23
|
+
Direction::Horizontal
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let len = children_array.len();
|
|
27
|
+
if len > 0 {
|
|
28
|
+
let mut ratatui_constraints = Vec::new();
|
|
29
|
+
|
|
30
|
+
if let Some(arr) = constraints_array {
|
|
31
|
+
for i in 0..arr.len() {
|
|
32
|
+
let constraint_obj: Value = arr.entry(i as isize)?;
|
|
33
|
+
let type_sym: Symbol = constraint_obj.funcall("type", ())?;
|
|
34
|
+
let value: u16 = constraint_obj.funcall("value", ())?;
|
|
35
|
+
|
|
36
|
+
match type_sym.to_string().as_str() {
|
|
37
|
+
"length" => ratatui_constraints.push(Constraint::Length(value)),
|
|
38
|
+
"percentage" => ratatui_constraints.push(Constraint::Percentage(value)),
|
|
39
|
+
"min" => ratatui_constraints.push(Constraint::Min(value)),
|
|
40
|
+
_ => {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If constraints don't match children, adjust or default
|
|
46
|
+
if ratatui_constraints.len() != len {
|
|
47
|
+
ratatui_constraints = (0..len)
|
|
48
|
+
.map(|_| Constraint::Percentage(100 / (len as u16).max(1)))
|
|
49
|
+
.collect();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let chunks = Layout::default()
|
|
53
|
+
.direction(direction)
|
|
54
|
+
.constraints(ratatui_constraints)
|
|
55
|
+
.split(area);
|
|
56
|
+
|
|
57
|
+
for i in 0..len {
|
|
58
|
+
let child: Value = children_array.entry(i as isize)?;
|
|
59
|
+
if let Err(e) = render_node(frame, chunks[i], child) {
|
|
60
|
+
eprintln!("Error rendering child {}: {:?}", i, e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Ok(())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[cfg(test)]
|
|
68
|
+
mod tests {
|
|
69
|
+
|
|
70
|
+
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn test_layout_logic() {
|
|
74
|
+
let area = Rect::new(0, 0, 100, 100);
|
|
75
|
+
let chunks = Layout::default()
|
|
76
|
+
.direction(Direction::Vertical)
|
|
77
|
+
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
78
|
+
.split(area);
|
|
79
|
+
assert_eq!(chunks.len(), 2);
|
|
80
|
+
assert_eq!(chunks[0].height, 50);
|
|
81
|
+
}
|
|
82
|
+
}
|