ratatui_ruby 1.0.0.pre.beta.2 → 1.0.0.pre.beta.3

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.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratatui_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.2
4
+ version: 1.0.0.pre.beta.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerrick Long
@@ -604,14 +604,16 @@ files:
604
604
  - ext/ratatui_ruby/src/events.rs
605
605
  - ext/ratatui_ruby/src/frame.rs
606
606
  - ext/ratatui_ruby/src/lib.rs
607
- - ext/ratatui_ruby/src/lib.rs.bak
608
607
  - ext/ratatui_ruby/src/rendering.rs
609
- - ext/ratatui_ruby/src/rendering.rs.bak
610
608
  - ext/ratatui_ruby/src/string_width.rs
611
609
  - ext/ratatui_ruby/src/style.rs
612
- - ext/ratatui_ruby/src/terminal.rs
613
- - ext/ratatui_ruby/src/terminal.rs.bak
614
- - ext/ratatui_ruby/src/terminal.rs.orig
610
+ - ext/ratatui_ruby/src/terminal/init.rs
611
+ - ext/ratatui_ruby/src/terminal/mod.rs
612
+ - ext/ratatui_ruby/src/terminal/mutations.rs
613
+ - ext/ratatui_ruby/src/terminal/queries.rs
614
+ - ext/ratatui_ruby/src/terminal/query.rs
615
+ - ext/ratatui_ruby/src/terminal/storage.rs
616
+ - ext/ratatui_ruby/src/terminal/wrapper.rs
615
617
  - ext/ratatui_ruby/src/text.rs
616
618
  - ext/ratatui_ruby/src/widgets/barchart.rs
617
619
  - ext/ratatui_ruby/src/widgets/block.rs
@@ -1,286 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
- // SPDX-License-Identifier: AGPL-3.0-or-later
3
-
4
- // Require SAFETY comments on all unsafe blocks
5
- #![warn(clippy::undocumented_unsafe_blocks)]
6
- // Enable pedantic lints for stricter code quality
7
- #![warn(clippy::pedantic)]
8
- // Allow certain pedantic lints that are too noisy for FFI code
9
- #![allow(clippy::missing_errors_doc)]
10
- #![allow(clippy::missing_panics_doc)]
11
- #![allow(clippy::module_name_repetitions)]
12
-
13
- mod color;
14
- mod errors;
15
- mod events;
16
- mod frame;
17
- mod rendering;
18
- mod string_width;
19
- mod style;
20
- mod terminal;
21
- mod text;
22
- mod widgets;
23
-
24
- use frame::RubyFrame;
25
- use magnus::{function, method, Error, Module, Object, Ruby, Value};
26
- use terminal::{init_terminal, restore_terminal, TERMINAL};
27
-
28
- /// Draw to the terminal.
29
- ///
30
- /// Supports two calling conventions:
31
- /// - Legacy: `RatatuiRuby.draw(tree)` - Renders a widget tree to the full terminal area
32
- /// - New: `RatatuiRuby.draw { |frame| ... }` - Yields a Frame for explicit widget placement
33
- fn draw(args: &[Value]) -> Result<(), Error> {
34
- let ruby = Ruby::get().unwrap();
35
-
36
- // Parse arguments: check for optional tree argument
37
- let tree: Option<Value> = if args.is_empty() {
38
- None
39
- } else if args.len() == 1 {
40
- Some(args[0])
41
- } else {
42
- return Err(Error::new(
43
- ruby.exception_arg_error(),
44
- format!(
45
- "wrong number of arguments (given {}, expected 0..1)",
46
- args.len()
47
- ),
48
- ));
49
- };
50
- let block_given = ruby.block_given();
51
-
52
- // Validate: must have either tree or block, but not both
53
- if tree.is_some() && block_given {
54
- return Err(Error::new(
55
- ruby.exception_arg_error(),
56
- "Cannot provide both a tree and a block to draw",
57
- ));
58
- }
59
- if tree.is_none() && !block_given {
60
- return Err(Error::new(
61
- ruby.exception_arg_error(),
62
- "Must provide either a tree or a block to draw",
63
- ));
64
- }
65
-
66
- let mut term_lock = TERMINAL.lock().unwrap();
67
- let mut render_error: Option<Error> = None;
68
-
69
- // Helper closure to execute the draw callback logic for either terminal type
70
- let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
71
- if block_given {
72
- // New API: yield RubyFrame to block
73
- // Create validity flag — set to true while the block is executing
74
- let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
75
-
76
- let ruby_frame = RubyFrame::new(f, active.clone());
77
- if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
78
- render_error = Some(e);
79
- }
80
-
81
- // Invalidate frame immediately after block returns
82
- // This prevents use-after-free if user stored the frame object
83
- active.store(false, std::sync::atomic::Ordering::Relaxed);
84
- } else if let Some(tree_value) = tree {
85
- // Legacy API: render tree to full area
86
- if let Err(e) = rendering::render_node(f, f.area(), tree_value) {
87
- render_error = Some(e);
88
- }
89
- }
90
- };
91
-
92
- if let Some(wrapper) = term_lock.as_mut() {
93
- match wrapper {
94
- terminal::TerminalWrapper::Crossterm(term) => {
95
- let module = ruby.define_module("RatatuiRuby")?;
96
- let error_base = module.const_get::<_, magnus::RClass>("Error")?;
97
- let error_class = error_base.const_get("Terminal")?;
98
- term.draw(&mut draw_callback)
99
- .map_err(|e| Error::new(error_class, e.to_string()))?;
100
- }
101
- terminal::TerminalWrapper::Test(term) => {
102
- let module = ruby.define_module("RatatuiRuby")?;
103
- let error_base = module.const_get::<_, magnus::RClass>("Error")?;
104
- let error_class = error_base.const_get("Terminal")?;
105
- term.draw(&mut draw_callback)
106
- .map_err(|e| Error::new(error_class, e.to_string()))?;
107
- }
108
- }
109
- } else {
110
- eprintln!("Terminal is None!");
111
- }
112
-
113
- if let Some(e) = render_error {
114
- return Err(e);
115
- }
116
-
117
- Ok(())
118
- }
119
-
120
- /// Storage for the last panic info, to be retrieved and printed after terminal restore.
121
- static LAST_PANIC: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
122
-
123
- /// Enables Rust backtraces and installs a custom panic hook.
124
- ///
125
- /// The panic hook stores the backtrace info instead of printing immediately.
126
- /// This allows Ruby to retrieve and print it after terminal restoration,
127
- /// preventing output from being lost on the alternate screen.
128
- fn enable_rust_backtrace(_ruby: &magnus::Ruby) {
129
- std::env::set_var("RUST_BACKTRACE", "1");
130
- std::panic::set_hook(Box::new(|info| {
131
- let backtrace = std::backtrace::Backtrace::force_capture();
132
- let message = format!("Rust panic: {info}\n{backtrace}");
133
- if let Ok(mut guard) = LAST_PANIC.lock() {
134
- *guard = Some(message);
135
- }
136
- }));
137
- }
138
-
139
- /// Returns the last panic info (if any) and clears it.
140
- ///
141
- /// Call this after terminal restoration to get deferred panic output.
142
- fn get_last_panic(_ruby: &magnus::Ruby) -> Option<String> {
143
- if let Ok(mut guard) = LAST_PANIC.lock() {
144
- guard.take()
145
- } else {
146
- None
147
- }
148
- }
149
-
150
- /// Intentionally panics to test backtrace output.
151
- ///
152
- /// Only use this for debugging/testing the backtrace feature.
153
- fn test_panic(_ruby: &magnus::Ruby) {
154
- panic!("Test panic triggered by RatatuiRuby._test_panic");
155
- }
156
-
157
- #[magnus::init]
158
- fn init() -> Result<(), Error> {
159
- let ruby = magnus::Ruby::get().unwrap();
160
- let m = ruby.define_module("RatatuiRuby")?;
161
-
162
- m.define_module_function("_init_terminal", function!(init_terminal, 4))?;
163
- m.define_module_function("_restore_terminal", function!(restore_terminal, 0))?;
164
- m.define_module_function("_draw", function!(draw, -1))?;
165
- m.define_module_function(
166
- "_enable_rust_backtrace",
167
- function!(enable_rust_backtrace, 0),
168
- )?;
169
- m.define_module_function("_test_panic", function!(test_panic, 0))?;
170
- m.define_module_function("_get_last_panic", function!(get_last_panic, 0))?;
171
-
172
- // Register Frame class
173
- let frame_class = m.define_class("Frame", ruby.class_object())?;
174
- frame_class.define_method("area", method!(RubyFrame::area, 0))?;
175
- frame_class.define_method("render_widget", method!(RubyFrame::render_widget, 2))?;
176
- frame_class.define_method(
177
- "render_stateful_widget",
178
- method!(RubyFrame::render_stateful_widget, 3),
179
- )?;
180
- frame_class.define_method(
181
- "set_cursor_position",
182
- method!(RubyFrame::set_cursor_position, 2),
183
- )?;
184
- m.define_module_function("_poll_event", function!(events::poll_event, 1))?;
185
- m.define_module_function("inject_test_event", function!(events::inject_test_event, 2))?;
186
- m.define_module_function("clear_events", function!(events::clear_events, 0))?;
187
-
188
- // Register State classes
189
- widgets::list_state::register(&ruby, m)?;
190
- widgets::table_state::register(&ruby, m)?;
191
- widgets::scrollbar_state::register(&ruby, m)?;
192
-
193
- // Test backend helpers
194
- m.define_module_function(
195
- "_init_test_terminal",
196
- function!(terminal::init_test_terminal, 4),
197
- )?;
198
- m.define_module_function(
199
- "get_buffer_content",
200
- function!(terminal::get_buffer_content, 0),
201
- )?;
202
- m.define_module_function(
203
- "get_cursor_position",
204
- function!(terminal::get_cursor_position, 0),
205
- )?;
206
- m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
207
- m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
208
- m.define_module_function(
209
- "_get_terminal_area",
210
- function!(terminal::get_terminal_area, 0),
211
- )?;
212
- m.define_module_function(
213
- "_insert_before",
214
- function!(terminal::insert_before, 2),
215
- )?;
216
-
217
- // Register Layout.split on the Layout::Layout class (inside the Layout module)
218
- let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;
219
- let layout_class = layout_mod.const_get::<_, magnus::RClass>("Layout")?;
220
- layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
221
- layout_class.define_singleton_method(
222
- "_split_with_spacers",
223
- function!(widgets::layout::split_with_spacers_layout, 4),
224
- )?;
225
-
226
- // Paragraph metrics
227
- m.define_module_function(
228
- "_paragraph_line_count",
229
- function!(widgets::paragraph::line_count, 2),
230
- )?;
231
- m.define_module_function(
232
- "_paragraph_line_width",
233
- function!(widgets::paragraph::line_width, 1),
234
- )?;
235
-
236
- // Tabs metrics
237
- m.define_module_function("_tabs_width", function!(widgets::tabs::width, 1))?;
238
-
239
- // Text measurement
240
- m.define_module_function("_text_width", function!(string_width::text_width, 1))?;
241
-
242
- // Color conversion
243
- color::register(&ruby, m)?;
244
-
245
- Ok(())
246
- }
247
-
248
- #[cfg(test)]
249
- mod tests {
250
- use ratatui::layout::Rect;
251
- use ratatui::style::Color;
252
- use ratatui::widgets::Widget;
253
- use ratatui::widgets::{Chart, Dataset, Sparkline};
254
-
255
- #[test]
256
- fn test_parse_color() {
257
- // We can test this through the style module directly now
258
- use crate::style::parse_color;
259
- assert_eq!(parse_color("red"), Some(Color::Red));
260
- assert_eq!(parse_color("blue"), Some(Color::Blue));
261
- assert_eq!(parse_color("#ffffff"), Some(Color::Rgb(255, 255, 255)));
262
- assert_eq!(parse_color("#000000"), Some(Color::Rgb(0, 0, 0)));
263
- assert_eq!(parse_color("invalid"), None);
264
- }
265
-
266
- #[test]
267
- fn test_sparkline_render() {
268
- let mut buf = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 10, 1));
269
- let data = vec![1, 2, 3];
270
- let sparkline = Sparkline::default().data(&data);
271
- sparkline.render(Rect::new(0, 0, 10, 1), &mut buf);
272
- assert!(buf.content().iter().any(|c| c.symbol() != " "));
273
- }
274
-
275
- #[test]
276
- fn test_line_chart_render() {
277
- let mut buf = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 20, 10));
278
- let data = vec![(0.0, 0.0), (1.0, 1.0)];
279
- let datasets = vec![Dataset::default().data(&data)];
280
- let chart = Chart::new(datasets)
281
- .x_axis(ratatui::widgets::Axis::default().bounds([0.0, 1.0]))
282
- .y_axis(ratatui::widgets::Axis::default().bounds([0.0, 1.0]));
283
- chart.render(Rect::new(0, 0, 20, 10), &mut buf);
284
- assert!(buf.content().iter().any(|c| c.symbol() != " "));
285
- }
286
- }
@@ -1,152 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
- // SPDX-License-Identifier: AGPL-3.0-or-later
3
-
4
- use crate::style::{parse_color_value, parse_modifier_str, parse_style};
5
- use crate::widgets;
6
- use magnus::{prelude::*, Error, RArray, Value};
7
- use ratatui::{buffer::Buffer, layout::Rect, style::Style, Frame};
8
-
9
- pub fn render_node(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
10
- if node.respond_to("render", true)? {
11
- let ruby = magnus::Ruby::get().unwrap();
12
- let ruby_area = {
13
- let module = ruby.define_module("RatatuiRuby")?;
14
- let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
15
- let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
16
- class.funcall::<_, _, Value>("new", (area.x, area.y, area.width, area.height))?
17
- };
18
-
19
- // Call render with just the area (no buffer!)
20
- let commands: Value = node.funcall("render", (ruby_area,))?;
21
-
22
- // Process returned draw commands
23
- if let Some(arr) = RArray::from_value(commands) {
24
- for i in 0..arr.len() {
25
- let ruby = magnus::Ruby::get().unwrap();
26
- let index = isize::try_from(i)
27
- .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
28
- let cmd: Value = arr.entry(index)?;
29
- process_draw_command(buffer, cmd)?;
30
- }
31
- }
32
- return Ok(());
33
- }
34
-
35
- // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
36
- let class_name = unsafe { node.class().name() }.into_owned();
37
-
38
- match class_name.as_str() {
39
- "RatatuiRuby::Widgets::Paragraph" => widgets::paragraph::render(buffer, area, node)?,
40
- "RatatuiRuby::Widgets::Clear" => widgets::clear::render(buffer, area, node)?,
41
- "RatatuiRuby::Widgets::Cursor" => widgets::cursor::render(buffer, area, node)?,
42
- "RatatuiRuby::Widgets::Overlay" => widgets::overlay::render(buffer, area, node)?,
43
- "RatatuiRuby::Widgets::Center" => widgets::center::render(buffer, area, node)?,
44
- "RatatuiRuby::Layout::Layout" => widgets::layout::render(buffer, area, node)?,
45
- "RatatuiRuby::Widgets::List" => widgets::list::render(buffer, area, node)?,
46
- "RatatuiRuby::Widgets::Gauge" => widgets::gauge::render(buffer, area, node)?,
47
- "RatatuiRuby::Widgets::LineGauge" => widgets::line_gauge::render(buffer, area, node)?,
48
- "RatatuiRuby::Widgets::Table" => widgets::table::render(buffer, area, node)?,
49
- "RatatuiRuby::Widgets::Block" => widgets::block::render(buffer, area, node)?,
50
- "RatatuiRuby::Widgets::Tabs" => widgets::tabs::render(buffer, area, node)?,
51
- "RatatuiRuby::Widgets::Scrollbar" => widgets::scrollbar::render(buffer, area, node)?,
52
- "RatatuiRuby::Widgets::BarChart" => widgets::barchart::render(buffer, area, node)?,
53
- "RatatuiRuby::Widgets::Canvas" => widgets::canvas::render(buffer, area, node)?,
54
- "RatatuiRuby::Widgets::Calendar" => widgets::calendar::render(buffer, area, node)?,
55
- "RatatuiRuby::Widgets::Sparkline" => widgets::sparkline::render(buffer, area, node)?,
56
- "RatatuiRuby::Widgets::Chart" => {
57
- widgets::chart::render(buffer, area, node)?;
58
- }
59
- "RatatuiRuby::Widgets::RatatuiLogo" => widgets::ratatui_logo::render(buffer, area, node),
60
- "RatatuiRuby::Widgets::RatatuiMascot" => {
61
- widgets::ratatui_mascot::render_ratatui_mascot(buffer, area, node)?;
62
- }
63
- // Text primitives can also be rendered directly as widgets
64
- "RatatuiRuby::Text::Line" => {
65
- let line = crate::text::parse_line(node)?;
66
- use ratatui::widgets::Widget;
67
- line.render(area, buffer);
68
- }
69
- "RatatuiRuby::Text::Span" => {
70
- let span = crate::text::parse_span(node)?;
71
- use ratatui::widgets::Widget;
72
- span.render(area, buffer);
73
- }
74
- _ => {}
75
- }
76
- Ok(())
77
- }
78
-
79
- pub fn render_widget_to_buffer(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
80
- // Just delegate to render_node since it now works with Buffer
81
- render_node(buffer, area, node)
82
- }
83
-
84
- fn process_draw_command(buffer: &mut Buffer, cmd: Value) -> Result<(), Error> {
85
- let ruby = magnus::Ruby::get().unwrap();
86
- // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
87
- let class_name = unsafe { cmd.class().name() }.into_owned();
88
-
89
- match class_name.as_str() {
90
- "RatatuiRuby::Draw::StringCmd" => {
91
- let x: u16 = cmd.funcall("x", ())?;
92
- let y: u16 = cmd.funcall("y", ())?;
93
- let string: String = cmd.funcall("string", ())?;
94
- let style_val: Value = cmd.funcall("style", ())?;
95
- let style = parse_style(style_val)?;
96
- buffer.set_string(x, y, string, style);
97
- }
98
- "RatatuiRuby::Draw::CellCmd" => {
99
- let x: u16 = cmd.funcall("x", ())?;
100
- let y: u16 = cmd.funcall("y", ())?;
101
- let cell_val: Value = cmd.funcall("cell", ())?;
102
-
103
- let area = buffer.area;
104
- if x >= area.x + area.width || y >= area.y + area.height {
105
- return Ok(());
106
- }
107
-
108
- let symbol: String = cell_val.funcall("char", ())?;
109
- let fg_val: Value = cell_val.funcall("fg", ())?;
110
- let bg_val: Value = cell_val.funcall("bg", ())?;
111
- let modifiers_val: Value = cell_val.funcall("modifiers", ())?;
112
-
113
- let mut style = Style::default();
114
-
115
- if !fg_val.is_nil() {
116
- if let Some(color) = parse_color_value(fg_val)? {
117
- style = style.fg(color);
118
- }
119
- }
120
- if !bg_val.is_nil() {
121
- if let Some(color) = parse_color_value(bg_val)? {
122
- style = style.bg(color);
123
- }
124
- }
125
-
126
- if let Some(mods_array) = RArray::from_value(modifiers_val) {
127
- for i in 0..mods_array.len() {
128
- let index = isize::try_from(i)
129
- .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
130
- let mod_val: Value = mods_array.entry(index)?;
131
- // Accept both symbols and strings (DWIM)
132
- let mod_str: String = mod_val.funcall("to_s", ())?;
133
- if let Some(modifier) = parse_modifier_str(&mod_str) {
134
- style = style.add_modifier(modifier);
135
- }
136
- }
137
- }
138
-
139
- if let Some(cell) = buffer.cell_mut((x, y)) {
140
- cell.set_symbol(&symbol).set_style(style);
141
- }
142
- }
143
- _ => {
144
- return Err(Error::new(
145
- ruby.exception_type_error(),
146
- format!("Unknown draw command: {class_name}"),
147
- ));
148
- }
149
- }
150
-
151
- Ok(())
152
- }