ratatui_ruby 0.9.1 → 0.10.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 +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +2 -1
- data/CHANGELOG.md +98 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/Steepfile +49 -0
- data/doc/concepts/debugging.md +401 -0
- data/doc/getting_started/quickstart.md +8 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/troubleshooting/async.md +4 -0
- data/examples/app_debugging_showcase/README.md +119 -0
- data/examples/app_debugging_showcase/app.rb +318 -0
- data/examples/widget_canvas/app.rb +19 -14
- data/examples/widget_gauge/app.rb +18 -3
- data/examples/widget_layout_split/app.rb +10 -4
- data/examples/widget_list/app.rb +22 -6
- data/examples/widget_rect/app.rb +7 -6
- data/examples/widget_rich_text/app.rb +62 -37
- data/examples/widget_style_colors/app.rb +26 -47
- data/examples/widget_table/app.rb +28 -5
- data/examples/widget_text_width/app.rb +6 -4
- data/ext/ratatui_ruby/Cargo.lock +48 -1
- data/ext/ratatui_ruby/Cargo.toml +6 -2
- data/ext/ratatui_ruby/src/color.rs +82 -0
- data/ext/ratatui_ruby/src/errors.rs +28 -0
- data/ext/ratatui_ruby/src/events.rs +15 -14
- data/ext/ratatui_ruby/src/lib.rs +56 -0
- data/ext/ratatui_ruby/src/rendering.rs +3 -1
- data/ext/ratatui_ruby/src/style.rs +48 -21
- data/ext/ratatui_ruby/src/terminal.rs +40 -9
- data/ext/ratatui_ruby/src/text.rs +21 -9
- data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
- data/lib/ratatui_ruby/buffer/cell.rb +25 -15
- data/lib/ratatui_ruby/buffer.rb +134 -2
- data/lib/ratatui_ruby/cell.rb +13 -5
- data/lib/ratatui_ruby/debug.rb +215 -0
- data/lib/ratatui_ruby/event/key.rb +3 -2
- data/lib/ratatui_ruby/event.rb +1 -1
- data/lib/ratatui_ruby/layout/constraint.rb +49 -0
- data/lib/ratatui_ruby/layout/layout.rb +119 -13
- data/lib/ratatui_ruby/layout/position.rb +55 -0
- data/lib/ratatui_ruby/layout/rect.rb +188 -0
- data/lib/ratatui_ruby/layout/size.rb +55 -0
- data/lib/ratatui_ruby/layout.rb +4 -0
- data/lib/ratatui_ruby/style/color.rb +149 -0
- data/lib/ratatui_ruby/style/style.rb +51 -4
- data/lib/ratatui_ruby/style.rb +2 -0
- data/lib/ratatui_ruby/symbols.rb +435 -0
- data/lib/ratatui_ruby/synthetic_events.rb +1 -1
- data/lib/ratatui_ruby/table_state.rb +51 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
- data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
- data/lib/ratatui_ruby/test_helper.rb +9 -0
- data/lib/ratatui_ruby/text/line.rb +245 -0
- data/lib/ratatui_ruby/text/span.rb +158 -0
- data/lib/ratatui_ruby/text.rb +99 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
- data/lib/ratatui_ruby/tui/core.rb +13 -2
- data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
- data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
- data/lib/ratatui_ruby/tui.rb +22 -1
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
- data/lib/ratatui_ruby/widgets/block.rb +14 -6
- data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
- data/lib/ratatui_ruby/widgets/cell.rb +2 -0
- data/lib/ratatui_ruby/widgets/center.rb +2 -0
- data/lib/ratatui_ruby/widgets/chart.rb +6 -0
- data/lib/ratatui_ruby/widgets/clear.rb +2 -0
- data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
- data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
- data/lib/ratatui_ruby/widgets/list.rb +87 -3
- data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
- data/lib/ratatui_ruby/widgets/row.rb +45 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
- data/lib/ratatui_ruby/widgets/table.rb +13 -3
- data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
- data/lib/ratatui_ruby/widgets.rb +1 -0
- data/lib/ratatui_ruby.rb +40 -9
- data/sig/examples/app_all_events/model/app_model.rbs +23 -0
- data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
- data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_stateful_interaction/app.rbs +5 -5
- data/sig/examples/widget_block_demo/app.rbs +6 -6
- data/sig/manifest.yaml +5 -0
- data/sig/patches/data.rbs +26 -0
- data/sig/patches/debugger__.rbs +8 -0
- data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
- data/sig/ratatui_ruby/buffer.rbs +18 -0
- data/sig/ratatui_ruby/cell.rbs +44 -0
- data/sig/ratatui_ruby/clear.rbs +18 -0
- data/sig/ratatui_ruby/constraint.rbs +26 -0
- data/sig/ratatui_ruby/debug.rbs +45 -0
- data/sig/ratatui_ruby/draw.rbs +30 -0
- data/sig/ratatui_ruby/event.rbs +68 -8
- data/sig/ratatui_ruby/frame.rbs +4 -4
- data/sig/ratatui_ruby/interfaces.rbs +25 -0
- data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
- data/sig/ratatui_ruby/layout/layout.rbs +45 -0
- data/sig/ratatui_ruby/layout/position.rbs +18 -0
- data/sig/ratatui_ruby/layout/rect.rbs +64 -0
- data/sig/ratatui_ruby/layout/size.rbs +18 -0
- data/sig/ratatui_ruby/output_guard.rbs +23 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +83 -4
- data/sig/ratatui_ruby/rect.rbs +17 -0
- data/sig/ratatui_ruby/style/color.rbs +22 -0
- data/sig/ratatui_ruby/style/style.rbs +29 -0
- data/sig/ratatui_ruby/symbols.rbs +141 -0
- data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
- data/sig/ratatui_ruby/table_state.rbs +6 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
- data/sig/ratatui_ruby/text/line.rbs +27 -0
- data/sig/ratatui_ruby/text/span.rbs +23 -0
- data/sig/ratatui_ruby/text.rbs +12 -0
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
- data/sig/ratatui_ruby/tui/core.rbs +2 -2
- data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
- data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
- data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
- data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
- data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
- data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
- data/sig/ratatui_ruby/widgets/block.rbs +51 -0
- data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
- data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
- data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
- data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
- data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
- data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
- data/sig/ratatui_ruby/widgets/list.rbs +63 -0
- data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
- data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
- data/sig/ratatui_ruby/widgets/row.rbs +43 -0
- data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
- data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
- data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
- data/sig/ratatui_ruby/widgets/table.rbs +78 -0
- data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
- data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
- data/tasks/steep.rake +11 -0
- metadata +80 -63
- data/doc/contributors/v1.0.0_blockers.md +0 -870
- data/doc/troubleshooting/debugging.md +0 -101
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
- data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
- data/lib/ratatui_ruby/schema/block.rb +0 -198
- data/lib/ratatui_ruby/schema/calendar.rb +0 -84
- data/lib/ratatui_ruby/schema/canvas.rb +0 -239
- data/lib/ratatui_ruby/schema/center.rb +0 -67
- data/lib/ratatui_ruby/schema/chart.rb +0 -159
- data/lib/ratatui_ruby/schema/clear.rb +0 -62
- data/lib/ratatui_ruby/schema/constraint.rb +0 -151
- data/lib/ratatui_ruby/schema/cursor.rb +0 -50
- data/lib/ratatui_ruby/schema/gauge.rb +0 -72
- data/lib/ratatui_ruby/schema/layout.rb +0 -122
- data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
- data/lib/ratatui_ruby/schema/list.rb +0 -135
- data/lib/ratatui_ruby/schema/list_item.rb +0 -51
- data/lib/ratatui_ruby/schema/overlay.rb +0 -51
- data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
- data/lib/ratatui_ruby/schema/rect.rb +0 -174
- data/lib/ratatui_ruby/schema/row.rb +0 -76
- data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
- data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
- data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
- data/lib/ratatui_ruby/schema/style.rb +0 -97
- data/lib/ratatui_ruby/schema/table.rb +0 -141
- data/lib/ratatui_ruby/schema/tabs.rb +0 -85
- data/lib/ratatui_ruby/schema/text.rb +0 -217
- data/sig/examples/app_all_events/model/events.rbs +0 -15
- data/sig/examples/app_all_events/view_state.rbs +0 -21
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
- data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
- data/sig/ratatui_ruby/schema/block.rbs +0 -18
- data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
- data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
- data/sig/ratatui_ruby/schema/center.rbs +0 -17
- data/sig/ratatui_ruby/schema/chart.rbs +0 -39
- data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
- data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
- data/sig/ratatui_ruby/schema/draw.rbs +0 -33
- data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
- data/sig/ratatui_ruby/schema/layout.rbs +0 -27
- data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
- data/sig/ratatui_ruby/schema/list.rbs +0 -28
- data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
- data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
- data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
- data/sig/ratatui_ruby/schema/rect.rbs +0 -48
- data/sig/ratatui_ruby/schema/row.rbs +0 -28
- data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
- data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
- data/sig/ratatui_ruby/schema/style.rbs +0 -19
- data/sig/ratatui_ruby/schema/table.rbs +0 -32
- data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
- data/sig/ratatui_ruby/schema/text.rbs +0 -31
- /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! Color conversion functions exposed to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! These functions wrap Ratatui's color conversion methods that require the `palette` feature.
|
|
7
|
+
|
|
8
|
+
use magnus::{Error, Ruby};
|
|
9
|
+
use ratatui::palette::{Hsl, Hsluv};
|
|
10
|
+
use ratatui::style::Color;
|
|
11
|
+
|
|
12
|
+
/// Formats an RGB color as a hex string.
|
|
13
|
+
fn rgb_to_hex(red: u8, green: u8, blue: u8) -> String {
|
|
14
|
+
format!("#{red:02x}{green:02x}{blue:02x}")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Convert HSL values to a hex color string.
|
|
18
|
+
///
|
|
19
|
+
/// # Arguments
|
|
20
|
+
/// * `hue` - Hue in degrees (0-360, wraps)
|
|
21
|
+
/// * `saturation` - Saturation as percentage (0-100)
|
|
22
|
+
/// * `lightness` - Lightness as percentage (0-100)
|
|
23
|
+
///
|
|
24
|
+
/// # Returns
|
|
25
|
+
/// A hex color string like "#rrggbb"
|
|
26
|
+
pub fn from_hsl(_ruby: &Ruby, hue: f32, saturation: f32, lightness: f32) -> String {
|
|
27
|
+
// Normalize: h wraps, s and l are percentages (0-100) that need to be 0-1
|
|
28
|
+
let hsl = Hsl::new(hue, saturation / 100.0, lightness / 100.0);
|
|
29
|
+
let color = Color::from_hsl(hsl);
|
|
30
|
+
|
|
31
|
+
match color {
|
|
32
|
+
Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
|
|
33
|
+
_ => "#000000".to_string(),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Convert `HSLuv` values to a hex color string.
|
|
38
|
+
///
|
|
39
|
+
/// `HSLuv` is a perceptually uniform color space where colors at the same
|
|
40
|
+
/// lightness appear equally bright regardless of hue.
|
|
41
|
+
///
|
|
42
|
+
/// # Arguments
|
|
43
|
+
/// * `hue` - Hue in degrees (-180 to 360, wraps)
|
|
44
|
+
/// * `saturation` - Saturation as percentage (0-100)
|
|
45
|
+
/// * `lightness` - Lightness as percentage (0-100)
|
|
46
|
+
///
|
|
47
|
+
/// # Returns
|
|
48
|
+
/// A hex color string like "#rrggbb"
|
|
49
|
+
pub fn from_hsluv(_ruby: &Ruby, hue: f32, saturation: f32, lightness: f32) -> String {
|
|
50
|
+
// Hsluv expects h in degrees, s and l as percentages (0-100)
|
|
51
|
+
let hsluv = Hsluv::new(hue, saturation, lightness);
|
|
52
|
+
let color = Color::from_hsluv(hsluv);
|
|
53
|
+
|
|
54
|
+
match color {
|
|
55
|
+
Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
|
|
56
|
+
_ => "#000000".to_string(),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Convert a u32 to a hex color string.
|
|
61
|
+
///
|
|
62
|
+
/// # Arguments
|
|
63
|
+
/// * `val` - A u32 in the format 0x00RRGGBB
|
|
64
|
+
///
|
|
65
|
+
/// # Returns
|
|
66
|
+
/// A hex color string like "#rrggbb"
|
|
67
|
+
pub fn from_u32(_ruby: &Ruby, val: u32) -> String {
|
|
68
|
+
let color = Color::from_u32(val);
|
|
69
|
+
|
|
70
|
+
match color {
|
|
71
|
+
Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
|
|
72
|
+
_ => "#000000".to_string(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Register color module functions in the `RatatuiRuby` module.
|
|
77
|
+
pub fn register(_ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
78
|
+
module.define_module_function("_color_from_hsl", magnus::function!(from_hsl, 3))?;
|
|
79
|
+
module.define_module_function("_color_from_hsluv", magnus::function!(from_hsluv, 3))?;
|
|
80
|
+
module.define_module_function("_color_from_u32", magnus::function!(from_u32, 1))?;
|
|
81
|
+
Ok(())
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use magnus::{prelude::*, Error, Value};
|
|
5
|
+
|
|
6
|
+
/// Creates a `TypeError` with context showing the actual value received.
|
|
7
|
+
///
|
|
8
|
+
/// Calls `inspect` on the Ruby value and includes it in the error message.
|
|
9
|
+
/// Long inspect strings (>200 chars) are truncated.
|
|
10
|
+
///
|
|
11
|
+
/// # Example error message
|
|
12
|
+
/// ```text
|
|
13
|
+
/// expected array for rows, got {:title=>"Processes", :header=>["Name", ...]}
|
|
14
|
+
/// ```
|
|
15
|
+
pub fn type_error_with_context(ruby: &magnus::Ruby, expected: &str, got: Value) -> Error {
|
|
16
|
+
let inspect: String = got
|
|
17
|
+
.funcall("inspect", ())
|
|
18
|
+
.unwrap_or_else(|_| "<uninspectable>".to_string());
|
|
19
|
+
let truncated = if inspect.len() > 200 {
|
|
20
|
+
format!("{}...", &inspect[..200])
|
|
21
|
+
} else {
|
|
22
|
+
inspect
|
|
23
|
+
};
|
|
24
|
+
Error::new(
|
|
25
|
+
ruby.exception_type_error(),
|
|
26
|
+
format!("{expected}, got {truncated}"),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -30,25 +30,26 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
|
|
|
30
30
|
|
|
31
31
|
/// Parses a `snake_case` string to `MediaKeyCode`.
|
|
32
32
|
///
|
|
33
|
-
///
|
|
34
|
-
///
|
|
33
|
+
/// Parses a `snake_case` string to `MediaKeyCode`.
|
|
34
|
+
///
|
|
35
|
+
/// Only accepts the `media_`-prefixed codes (canonical). Legacy unprefixed codes are no longer supported.
|
|
35
36
|
fn parse_media_key(s: &str) -> Option<ratatui::crossterm::event::MediaKeyCode> {
|
|
36
37
|
use ratatui::crossterm::event::MediaKeyCode;
|
|
37
38
|
match s {
|
|
38
39
|
// New canonical codes (media_ prefix)
|
|
39
|
-
"media_play"
|
|
40
|
+
"media_play" => Some(MediaKeyCode::Play),
|
|
40
41
|
"media_pause" => Some(MediaKeyCode::Pause),
|
|
41
|
-
"media_play_pause"
|
|
42
|
-
"media_reverse"
|
|
43
|
-
"media_stop"
|
|
44
|
-
"media_fast_forward"
|
|
45
|
-
"media_rewind"
|
|
46
|
-
"media_track_next"
|
|
47
|
-
"media_track_previous"
|
|
48
|
-
"media_record"
|
|
49
|
-
"media_lower_volume"
|
|
50
|
-
"media_raise_volume"
|
|
51
|
-
"media_mute_volume"
|
|
42
|
+
"media_play_pause" => Some(MediaKeyCode::PlayPause),
|
|
43
|
+
"media_reverse" => Some(MediaKeyCode::Reverse),
|
|
44
|
+
"media_stop" => Some(MediaKeyCode::Stop),
|
|
45
|
+
"media_fast_forward" => Some(MediaKeyCode::FastForward),
|
|
46
|
+
"media_rewind" => Some(MediaKeyCode::Rewind),
|
|
47
|
+
"media_track_next" => Some(MediaKeyCode::TrackNext),
|
|
48
|
+
"media_track_previous" => Some(MediaKeyCode::TrackPrevious),
|
|
49
|
+
"media_record" => Some(MediaKeyCode::Record),
|
|
50
|
+
"media_lower_volume" => Some(MediaKeyCode::LowerVolume),
|
|
51
|
+
"media_raise_volume" => Some(MediaKeyCode::RaiseVolume),
|
|
52
|
+
"media_mute_volume" => Some(MediaKeyCode::MuteVolume),
|
|
52
53
|
_ => None,
|
|
53
54
|
}
|
|
54
55
|
}
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
#![allow(clippy::missing_panics_doc)]
|
|
11
11
|
#![allow(clippy::module_name_repetitions)]
|
|
12
12
|
|
|
13
|
+
mod color;
|
|
14
|
+
mod errors;
|
|
13
15
|
mod events;
|
|
14
16
|
mod frame;
|
|
15
17
|
mod rendering;
|
|
@@ -115,6 +117,43 @@ fn draw(args: &[Value]) -> Result<(), Error> {
|
|
|
115
117
|
Ok(())
|
|
116
118
|
}
|
|
117
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
|
+
|
|
118
157
|
#[magnus::init]
|
|
119
158
|
fn init() -> Result<(), Error> {
|
|
120
159
|
let ruby = magnus::Ruby::get().unwrap();
|
|
@@ -123,6 +162,12 @@ fn init() -> Result<(), Error> {
|
|
|
123
162
|
m.define_module_function("_init_terminal", function!(init_terminal, 2))?;
|
|
124
163
|
m.define_module_function("_restore_terminal", function!(restore_terminal, 0))?;
|
|
125
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))?;
|
|
126
171
|
|
|
127
172
|
// Register Frame class
|
|
128
173
|
let frame_class = m.define_class("Frame", ruby.class_object())?;
|
|
@@ -160,11 +205,19 @@ fn init() -> Result<(), Error> {
|
|
|
160
205
|
)?;
|
|
161
206
|
m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
|
|
162
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
|
+
)?;
|
|
163
212
|
|
|
164
213
|
// Register Layout.split on the Layout::Layout class (inside the Layout module)
|
|
165
214
|
let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;
|
|
166
215
|
let layout_class = layout_mod.const_get::<_, magnus::RClass>("Layout")?;
|
|
167
216
|
layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
|
|
217
|
+
layout_class.define_singleton_method(
|
|
218
|
+
"_split_with_spacers",
|
|
219
|
+
function!(widgets::layout::split_with_spacers_layout, 4),
|
|
220
|
+
)?;
|
|
168
221
|
|
|
169
222
|
// Paragraph metrics
|
|
170
223
|
m.define_module_function(
|
|
@@ -182,6 +235,9 @@ fn init() -> Result<(), Error> {
|
|
|
182
235
|
// Text measurement
|
|
183
236
|
m.define_module_function("_text_width", function!(string_width::text_width, 1))?;
|
|
184
237
|
|
|
238
|
+
// Color conversion
|
|
239
|
+
color::register(&ruby, m)?;
|
|
240
|
+
|
|
185
241
|
Ok(())
|
|
186
242
|
}
|
|
187
243
|
|
|
@@ -120,7 +120,9 @@ fn process_draw_command(buffer: &mut Buffer, cmd: Value) -> Result<(), Error> {
|
|
|
120
120
|
for i in 0..mods_array.len() {
|
|
121
121
|
let index = isize::try_from(i)
|
|
122
122
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
123
|
-
let
|
|
123
|
+
let mod_val: Value = mods_array.entry(index)?;
|
|
124
|
+
// Accept both symbols and strings (DWIM)
|
|
125
|
+
let mod_str: String = mod_val.funcall("to_s", ())?;
|
|
124
126
|
if let Some(modifier) = parse_modifier_str(&mod_str) {
|
|
125
127
|
style = style.add_modifier(modifier);
|
|
126
128
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
use crate::errors::type_error_with_context;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
8
|
use ratatui::{
|
|
@@ -65,22 +66,29 @@ pub fn parse_style(style_val: Value) -> Result<Style, Error> {
|
|
|
65
66
|
|
|
66
67
|
let mut style = Style::default();
|
|
67
68
|
|
|
68
|
-
let (fg, bg, modifiers_val
|
|
69
|
-
(
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
let (fg, bg, underline_color, modifiers_val, remove_modifiers_val) =
|
|
70
|
+
if let Some(hash) = magnus::RHash::from_value(style_val) {
|
|
71
|
+
(
|
|
72
|
+
hash.lookup(ruby.to_symbol("fg"))
|
|
73
|
+
.unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
74
|
+
hash.lookup(ruby.to_symbol("bg"))
|
|
75
|
+
.unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
76
|
+
hash.lookup(ruby.to_symbol("underline_color"))
|
|
77
|
+
.unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
78
|
+
hash.lookup(ruby.to_symbol("modifiers"))
|
|
79
|
+
.unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
80
|
+
hash.lookup(ruby.to_symbol("remove_modifiers"))
|
|
81
|
+
.unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
82
|
+
)
|
|
83
|
+
} else {
|
|
84
|
+
(
|
|
85
|
+
style_val.funcall("fg", ())?,
|
|
86
|
+
style_val.funcall("bg", ())?,
|
|
87
|
+
style_val.funcall("underline_color", ())?,
|
|
88
|
+
style_val.funcall("modifiers", ())?,
|
|
89
|
+
style_val.funcall("remove_modifiers", ())?,
|
|
90
|
+
)
|
|
91
|
+
};
|
|
84
92
|
|
|
85
93
|
if !fg.is_nil() {
|
|
86
94
|
if let Ok(fg_str) = fg.funcall::<_, _, String>("to_s", ()) {
|
|
@@ -98,6 +106,14 @@ pub fn parse_style(style_val: Value) -> Result<Style, Error> {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
if !underline_color.is_nil() {
|
|
110
|
+
if let Ok(uc_str) = underline_color.funcall::<_, _, String>("to_s", ()) {
|
|
111
|
+
if let Some(color) = parse_color(&uc_str) {
|
|
112
|
+
style = style.underline_color(color);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
101
117
|
if !modifiers_val.is_nil() {
|
|
102
118
|
if let Some(modifiers_array) = magnus::RArray::from_value(modifiers_val) {
|
|
103
119
|
for i in 0..modifiers_array.len() {
|
|
@@ -112,6 +128,20 @@ pub fn parse_style(style_val: Value) -> Result<Style, Error> {
|
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
if !remove_modifiers_val.is_nil() {
|
|
132
|
+
if let Some(remove_modifiers_array) = magnus::RArray::from_value(remove_modifiers_val) {
|
|
133
|
+
for i in 0..remove_modifiers_array.len() {
|
|
134
|
+
let index = isize::try_from(i)
|
|
135
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
136
|
+
if let Ok(sym) = remove_modifiers_array.entry::<Symbol>(index) {
|
|
137
|
+
if let Some(m) = parse_modifier_str(&sym.to_string()) {
|
|
138
|
+
style = style.remove_modifier(m);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
Ok(style)
|
|
116
146
|
}
|
|
117
147
|
|
|
@@ -121,7 +151,7 @@ pub fn parse_border_set<'a>(
|
|
|
121
151
|
) -> Result<symbols::border::Set<'a>, Error> {
|
|
122
152
|
let ruby = magnus::Ruby::get().unwrap();
|
|
123
153
|
let hash = magnus::RHash::from_value(set_val)
|
|
124
|
-
.ok_or_else(||
|
|
154
|
+
.ok_or_else(|| type_error_with_context(&ruby, "expected hash for border_set", set_val))?;
|
|
125
155
|
|
|
126
156
|
let get_char = |key: &str| -> Result<Option<&'a str>, Error> {
|
|
127
157
|
let mut val: Value = hash
|
|
@@ -188,10 +218,7 @@ pub fn parse_bar_set<'a>(set_val: Value, bump: &'a Bump) -> Result<symbols::bar:
|
|
|
188
218
|
}
|
|
189
219
|
|
|
190
220
|
let hash = magnus::RHash::from_value(set_val).ok_or_else(|| {
|
|
191
|
-
|
|
192
|
-
ruby.exception_type_error(),
|
|
193
|
-
"expected symbol or hash for bar_set",
|
|
194
|
-
)
|
|
221
|
+
type_error_with_context(&ruby, "expected symbol or hash for bar_set", set_val)
|
|
195
222
|
})?;
|
|
196
223
|
|
|
197
224
|
let get_char = |key: &str| -> Result<Option<&'a str>, Error> {
|
|
@@ -109,6 +109,36 @@ pub fn get_buffer_content() -> Result<String, Error> {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
pub fn get_terminal_area() -> Result<magnus::RHash, Error> {
|
|
113
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
114
|
+
let term_lock = TERMINAL.lock().unwrap();
|
|
115
|
+
if let Some(wrapper) = term_lock.as_ref() {
|
|
116
|
+
let hash = ruby.hash_new();
|
|
117
|
+
match wrapper {
|
|
118
|
+
TerminalWrapper::Crossterm(term) => {
|
|
119
|
+
let size = term.size().unwrap_or_default();
|
|
120
|
+
hash.aset("x", 0u16)?;
|
|
121
|
+
hash.aset("y", 0u16)?;
|
|
122
|
+
hash.aset("width", size.width)?;
|
|
123
|
+
hash.aset("height", size.height)?;
|
|
124
|
+
}
|
|
125
|
+
TerminalWrapper::Test(term) => {
|
|
126
|
+
let area = term.backend().buffer().area;
|
|
127
|
+
hash.aset("x", area.x)?;
|
|
128
|
+
hash.aset("y", area.y)?;
|
|
129
|
+
hash.aset("width", area.width)?;
|
|
130
|
+
hash.aset("height", area.height)?;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
Ok(hash)
|
|
134
|
+
} else {
|
|
135
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
136
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
137
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
138
|
+
Err(Error::new(error_class, "Terminal is not initialized"))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
|
|
113
143
|
let ruby = magnus::Ruby::get().unwrap();
|
|
114
144
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
@@ -163,6 +193,7 @@ pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
|
|
|
163
193
|
hash.aset("char", cell.symbol())?;
|
|
164
194
|
hash.aset("fg", color_to_value(cell.fg))?;
|
|
165
195
|
hash.aset("bg", color_to_value(cell.bg))?;
|
|
196
|
+
hash.aset("underline_color", color_to_value(cell.underline_color))?;
|
|
166
197
|
hash.aset("modifiers", modifiers_to_value(cell.modifier))?;
|
|
167
198
|
Ok(hash)
|
|
168
199
|
} else {
|
|
@@ -217,31 +248,31 @@ fn modifiers_to_value(modifier: ratatui::style::Modifier) -> Value {
|
|
|
217
248
|
let ary = ruby.ary_new();
|
|
218
249
|
|
|
219
250
|
if modifier.contains(ratatui::style::Modifier::BOLD) {
|
|
220
|
-
let _ = ary.push(ruby.
|
|
251
|
+
let _ = ary.push(ruby.to_symbol("bold"));
|
|
221
252
|
}
|
|
222
253
|
if modifier.contains(ratatui::style::Modifier::ITALIC) {
|
|
223
|
-
let _ = ary.push(ruby.
|
|
254
|
+
let _ = ary.push(ruby.to_symbol("italic"));
|
|
224
255
|
}
|
|
225
256
|
if modifier.contains(ratatui::style::Modifier::DIM) {
|
|
226
|
-
let _ = ary.push(ruby.
|
|
257
|
+
let _ = ary.push(ruby.to_symbol("dim"));
|
|
227
258
|
}
|
|
228
259
|
if modifier.contains(ratatui::style::Modifier::UNDERLINED) {
|
|
229
|
-
let _ = ary.push(ruby.
|
|
260
|
+
let _ = ary.push(ruby.to_symbol("underlined"));
|
|
230
261
|
}
|
|
231
262
|
if modifier.contains(ratatui::style::Modifier::REVERSED) {
|
|
232
|
-
let _ = ary.push(ruby.
|
|
263
|
+
let _ = ary.push(ruby.to_symbol("reversed"));
|
|
233
264
|
}
|
|
234
265
|
if modifier.contains(ratatui::style::Modifier::HIDDEN) {
|
|
235
|
-
let _ = ary.push(ruby.
|
|
266
|
+
let _ = ary.push(ruby.to_symbol("hidden"));
|
|
236
267
|
}
|
|
237
268
|
if modifier.contains(ratatui::style::Modifier::CROSSED_OUT) {
|
|
238
|
-
let _ = ary.push(ruby.
|
|
269
|
+
let _ = ary.push(ruby.to_symbol("crossed_out"));
|
|
239
270
|
}
|
|
240
271
|
if modifier.contains(ratatui::style::Modifier::SLOW_BLINK) {
|
|
241
|
-
let _ = ary.push(ruby.
|
|
272
|
+
let _ = ary.push(ruby.to_symbol("slow_blink"));
|
|
242
273
|
}
|
|
243
274
|
if modifier.contains(ratatui::style::Modifier::RAPID_BLINK) {
|
|
244
|
-
let _ = ary.push(ruby.
|
|
275
|
+
let _ = ary.push(ruby.to_symbol("rapid_blink"));
|
|
245
276
|
}
|
|
246
277
|
|
|
247
278
|
ary.as_value()
|
|
@@ -1,6 +1,7 @@
|
|
|
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::errors::type_error_with_context;
|
|
4
5
|
use crate::style::parse_style;
|
|
5
6
|
use magnus::{prelude::*, Error, Value};
|
|
6
7
|
use ratatui::text::{Line, Span};
|
|
@@ -82,9 +83,10 @@ pub fn parse_text(value: Value) -> Result<Vec<Line<'static>>, Error> {
|
|
|
82
83
|
Ok(lines)
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
|
-
Err(_) => Err(
|
|
86
|
-
ruby
|
|
86
|
+
Err(_) => Err(type_error_with_context(
|
|
87
|
+
&ruby,
|
|
87
88
|
"expected String, Text::Span, Text::Line, or Array of Text::Lines/Spans",
|
|
89
|
+
value,
|
|
88
90
|
)),
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -98,9 +100,10 @@ pub fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
|
98
100
|
let class_name: String = class_obj.funcall("name", ())?;
|
|
99
101
|
|
|
100
102
|
if !class_name.contains("Span") {
|
|
101
|
-
return Err(
|
|
102
|
-
ruby
|
|
103
|
+
return Err(type_error_with_context(
|
|
104
|
+
&ruby,
|
|
103
105
|
"expected a Text::Span object",
|
|
106
|
+
value,
|
|
104
107
|
));
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -115,6 +118,7 @@ pub fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/// Parses a Ruby `Text::Line` object into a ratatui Line.
|
|
121
|
+
/// Also accepts `Text::Span` objects and auto-coerces them to a Line.
|
|
118
122
|
pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
|
|
119
123
|
let ruby = magnus::Ruby::get().unwrap();
|
|
120
124
|
|
|
@@ -122,10 +126,17 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
|
|
|
122
126
|
let class_obj: Value = value.funcall("class", ())?;
|
|
123
127
|
let class_name: String = class_obj.funcall("name", ())?;
|
|
124
128
|
|
|
129
|
+
// Auto-coerce Span to Line: wrap a single Span in a Line
|
|
130
|
+
if class_name.contains("Span") {
|
|
131
|
+
let span = parse_span(value)?;
|
|
132
|
+
return Ok(Line::from(vec![span]));
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
if !class_name.contains("Line") {
|
|
126
|
-
return Err(
|
|
127
|
-
ruby
|
|
128
|
-
"expected a Text::Line object",
|
|
136
|
+
return Err(type_error_with_context(
|
|
137
|
+
&ruby,
|
|
138
|
+
"expected a Text::Line or Text::Span object",
|
|
139
|
+
value,
|
|
129
140
|
));
|
|
130
141
|
}
|
|
131
142
|
|
|
@@ -141,9 +152,10 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
|
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
let spans_array = magnus::RArray::from_value(spans_val).ok_or_else(|| {
|
|
144
|
-
|
|
145
|
-
ruby
|
|
155
|
+
type_error_with_context(
|
|
156
|
+
&ruby,
|
|
146
157
|
"expected array of Spans in Text::Line.spans",
|
|
158
|
+
spans_val,
|
|
147
159
|
)
|
|
148
160
|
})?;
|
|
149
161
|
|
|
@@ -1,6 +1,7 @@
|
|
|
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::errors::type_error_with_context;
|
|
4
5
|
use crate::style::{parse_block, parse_style};
|
|
5
6
|
use crate::text::parse_line;
|
|
6
7
|
use bumpalo::Bump;
|
|
@@ -41,7 +42,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
41
42
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
42
43
|
let point_array_val: Value = data_array.entry(index)?;
|
|
43
44
|
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
44
|
-
|
|
45
|
+
type_error_with_context(&ruby, "expected array for point", point_array_val)
|
|
45
46
|
})?;
|
|
46
47
|
let x: f64 = point_array.entry(0)?;
|
|
47
48
|
let y: f64 = point_array.entry(1)?;
|
|
@@ -1,6 +1,7 @@
|
|
|
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::errors::type_error_with_context;
|
|
4
5
|
use crate::rendering::render_node;
|
|
5
6
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
7
|
use ratatui::{
|
|
@@ -12,8 +13,9 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
12
13
|
let ruby = magnus::Ruby::get().unwrap();
|
|
13
14
|
let direction_sym: Symbol = node.funcall("direction", ())?;
|
|
14
15
|
let children_val: Value = node.funcall("children", ())?;
|
|
15
|
-
let children_array = magnus::RArray::from_value(children_val)
|
|
16
|
-
|
|
16
|
+
let children_array = magnus::RArray::from_value(children_val).ok_or_else(|| {
|
|
17
|
+
type_error_with_context(&ruby, "expected array for children", children_val)
|
|
18
|
+
})?;
|
|
17
19
|
|
|
18
20
|
let constraints_val: Value = node.funcall("constraints", ())?;
|
|
19
21
|
let constraints_array = magnus::RArray::from_value(constraints_val);
|
|
@@ -201,6 +203,92 @@ pub fn split_layout(
|
|
|
201
203
|
Ok(result)
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
/// Splits an area into multiple rectangles, returning both segments and spacers.
|
|
207
|
+
/// This is the Ratatui `split_with_spacers` equivalent.
|
|
208
|
+
///
|
|
209
|
+
/// # Arguments
|
|
210
|
+
/// * `area` - A Ruby Hash or Rect with :x, :y, :width, :height keys
|
|
211
|
+
/// * `direction` - Symbol :vertical or :horizontal
|
|
212
|
+
/// * `constraints` - Array of Constraint objects
|
|
213
|
+
/// * `flex` - Symbol for flex mode
|
|
214
|
+
///
|
|
215
|
+
/// # Returns
|
|
216
|
+
/// An array containing two arrays: [segments, spacers], each containing Ruby Hashes representing Rect objects
|
|
217
|
+
pub fn split_with_spacers_layout(
|
|
218
|
+
area: Value,
|
|
219
|
+
direction: Symbol,
|
|
220
|
+
constraints: magnus::RArray,
|
|
221
|
+
flex: Symbol,
|
|
222
|
+
) -> Result<magnus::RArray, Error> {
|
|
223
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
224
|
+
|
|
225
|
+
// Parse area from Hash or Rect-like object
|
|
226
|
+
let x: u16 = area.funcall("x", ())?;
|
|
227
|
+
let y: u16 = area.funcall("y", ())?;
|
|
228
|
+
let width: u16 = area.funcall("width", ())?;
|
|
229
|
+
let height: u16 = area.funcall("height", ())?;
|
|
230
|
+
let rect = Rect::new(x, y, width, height);
|
|
231
|
+
|
|
232
|
+
// Parse direction
|
|
233
|
+
let dir = if direction.to_string() == "vertical" {
|
|
234
|
+
Direction::Vertical
|
|
235
|
+
} else {
|
|
236
|
+
Direction::Horizontal
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Parse flex
|
|
240
|
+
let flex_mode = match flex.to_string().as_str() {
|
|
241
|
+
"start" => Flex::Start,
|
|
242
|
+
"center" => Flex::Center,
|
|
243
|
+
"end" => Flex::End,
|
|
244
|
+
"space_between" => Flex::SpaceBetween,
|
|
245
|
+
"space_around" => Flex::SpaceAround,
|
|
246
|
+
"space_evenly" => Flex::SpaceEvenly,
|
|
247
|
+
_ => Flex::Legacy,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Parse constraints
|
|
251
|
+
let mut ratatui_constraints = Vec::new();
|
|
252
|
+
for i in 0..constraints.len() {
|
|
253
|
+
let index = isize::try_from(i)
|
|
254
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
255
|
+
let constraint_obj: Value = constraints.entry(index)?;
|
|
256
|
+
if let Ok(constraint) = parse_constraint(constraint_obj) {
|
|
257
|
+
ratatui_constraints.push(constraint);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Compute layout with spacers
|
|
262
|
+
let (segments, spacers) = Layout::default()
|
|
263
|
+
.direction(dir)
|
|
264
|
+
.flex(flex_mode)
|
|
265
|
+
.constraints(ratatui_constraints)
|
|
266
|
+
.split_with_spacers(rect);
|
|
267
|
+
|
|
268
|
+
// Helper to convert Rc<[Rect]> to Ruby array
|
|
269
|
+
let rects_to_ruby_array = |rects: &[Rect]| -> Result<magnus::RArray, Error> {
|
|
270
|
+
let arr = ruby.ary_new_capa(rects.len());
|
|
271
|
+
for chunk in rects {
|
|
272
|
+
let hash = ruby.hash_new();
|
|
273
|
+
hash.aset(ruby.sym_new("x"), chunk.x)?;
|
|
274
|
+
hash.aset(ruby.sym_new("y"), chunk.y)?;
|
|
275
|
+
hash.aset(ruby.sym_new("width"), chunk.width)?;
|
|
276
|
+
hash.aset(ruby.sym_new("height"), chunk.height)?;
|
|
277
|
+
arr.push(hash)?;
|
|
278
|
+
}
|
|
279
|
+
Ok(arr)
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
let segments_arr = rects_to_ruby_array(&segments)?;
|
|
283
|
+
let spacers_arr = rects_to_ruby_array(&spacers)?;
|
|
284
|
+
|
|
285
|
+
// Return [segments, spacers]
|
|
286
|
+
let result = ruby.ary_new_capa(2);
|
|
287
|
+
result.push(segments_arr)?;
|
|
288
|
+
result.push(spacers_arr)?;
|
|
289
|
+
|
|
290
|
+
Ok(result)
|
|
291
|
+
}
|
|
204
292
|
#[cfg(test)]
|
|
205
293
|
mod tests {
|
|
206
294
|
use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};
|