ratatui_ruby 0.5.0 → 0.7.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 +10 -4
- data/CHANGELOG.md +79 -7
- data/README.md +37 -5
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +96 -22
- data/doc/application_testing.md +76 -30
- data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
- data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
- data/doc/contributors/architectural_overhaul/task.md +37 -0
- data/doc/contributors/design/ruby_frontend.md +288 -56
- data/doc/contributors/design/rust_backend.md +349 -54
- data/doc/contributors/developing_examples.md +134 -49
- data/doc/contributors/index.md +7 -5
- data/doc/contributors/v1.0.0_blockers.md +1729 -0
- data/doc/event_handling.md +11 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.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_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.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_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +11 -6
- data/doc/interactive_design.md +2 -2
- data/doc/quickstart.md +127 -165
- data/doc/terminal_limitations.md +92 -0
- data/doc/v0.7.0_migration.md +236 -0
- data/doc/why.md +93 -0
- data/examples/app_all_events/README.md +47 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +9 -9
- data/examples/app_all_events/view/controls_view.rb +9 -7
- data/examples/app_all_events/view/counts_view.rb +13 -9
- data/examples/app_all_events/view/live_view.rb +9 -8
- data/examples/app_all_events/view/log_view.rb +11 -16
- data/examples/app_color_picker/README.md +84 -42
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +49 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +33 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +49 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +10 -4
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +50 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +36 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_calendar_demo/app.rb +5 -1
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +31 -24
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
- data/examples/widget_overlay_demo/README.md +36 -0
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_logo_demo/app.rb +1 -1
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_render/app.rb +3 -3
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +113 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +116 -3
- data/ext/ratatui_ruby/src/lib.rs +37 -6
- data/ext/ratatui_ruby/src/rendering.rs +22 -21
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +13 -4
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/buffer/cell.rb +168 -0
- data/lib/ratatui_ruby/buffer.rb +15 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +100 -4
- data/lib/ratatui_ruby/layout/constraint.rb +95 -0
- data/lib/ratatui_ruby/layout/layout.rb +106 -0
- data/lib/ratatui_ruby/layout/rect.rb +118 -0
- data/lib/ratatui_ruby/layout.rb +19 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/layout.rb +1 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/row.rb +66 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +29 -11
- data/lib/ratatui_ruby/schema/text.rb +96 -3
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/style/style.rb +81 -0
- data/lib/ratatui_ruby/style.rb +15 -0
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +414 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +65 -358
- data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/core.rb +38 -0
- data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
- data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
- data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
- data/lib/ratatui_ruby/tui.rb +75 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
- data/lib/ratatui_ruby/widgets/block.rb +192 -0
- data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
- data/lib/ratatui_ruby/widgets/cell.rb +47 -0
- data/lib/ratatui_ruby/widgets/center.rb +59 -0
- data/lib/ratatui_ruby/widgets/chart.rb +185 -0
- data/lib/ratatui_ruby/widgets/clear.rb +54 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
- data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
- data/lib/ratatui_ruby/widgets/list.rb +127 -0
- data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
- data/lib/ratatui_ruby/widgets/row.rb +68 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
- data/lib/ratatui_ruby/widgets/table.rb +141 -0
- data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
- data/lib/ratatui_ruby/widgets.rb +40 -0
- data/lib/ratatui_ruby.rb +64 -57
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_all_events/view_state.rbs +1 -1
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/row.rbs +22 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +9 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +41 -48
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/core.rbs +14 -0
- data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
- data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
- data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
- data/sig/ratatui_ruby/tui.rbs +19 -0
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc.rake +7 -35
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +64 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +169 -48
- data/doc/contributors/dwim_dx.md +0 -366
- data/doc/images/app_analytics.png +0 -0
- data/doc/images/app_custom_widget.png +0 -0
- data/doc/images/app_mouse_events.png +0 -0
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/doc/images/widget_table_flex.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/lib/ratatui_ruby/session/autodoc.rb +0 -417
- data/lib/ratatui_ruby/session.rb +0 -163
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/autodoc/inventory.rb +0 -61
- data/tasks/autodoc/notice.rb +0 -26
- data/tasks/autodoc/rbs.rb +0 -38
- data/tasks/autodoc/rdoc.rb +0 -45
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
|
|
6
|
+
/// Calculate the display width of a string in terminal cells.
|
|
7
|
+
///
|
|
8
|
+
/// Handles unicode correctly, including:
|
|
9
|
+
/// - Regular ASCII characters: 1 cell each
|
|
10
|
+
/// - CJK characters: 2 cells each (full-width)
|
|
11
|
+
/// - Emoji: typically 2 cells each (varies by terminal)
|
|
12
|
+
/// - Combining marks and zero-width characters: 0 cells
|
|
13
|
+
///
|
|
14
|
+
/// This uses the same `unicode-width` crate that Ratatui uses internally.
|
|
15
|
+
///
|
|
16
|
+
/// Returns the total display width in cells (not bytes or characters).
|
|
17
|
+
pub fn text_width(string: Value) -> Result<usize, Error> {
|
|
18
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
19
|
+
|
|
20
|
+
let s: String = String::try_convert(string).map_err(|_| {
|
|
21
|
+
Error::new(
|
|
22
|
+
ruby.exception_type_error(),
|
|
23
|
+
"expected a String or object that converts to String",
|
|
24
|
+
)
|
|
25
|
+
})?;
|
|
26
|
+
|
|
27
|
+
// Use unicode_width's width calculation.
|
|
28
|
+
// This is the same mechanism Ratatui uses internally for Paragraph.line_width().
|
|
29
|
+
let width = s
|
|
30
|
+
.chars()
|
|
31
|
+
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
|
|
32
|
+
.sum();
|
|
33
|
+
|
|
34
|
+
Ok(width)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[cfg(test)]
|
|
38
|
+
mod tests {
|
|
39
|
+
use unicode_width::UnicodeWidthChar;
|
|
40
|
+
|
|
41
|
+
fn measure_width(s: &str) -> usize {
|
|
42
|
+
s.chars()
|
|
43
|
+
.map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
|
|
44
|
+
.sum()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn test_ascii_width() {
|
|
49
|
+
// ASCII is 1 cell per character
|
|
50
|
+
assert_eq!(measure_width("hello"), 5);
|
|
51
|
+
assert_eq!(measure_width("Hello, World!"), 13);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn test_emoji_width() {
|
|
56
|
+
// Emoji typically take 2 cells
|
|
57
|
+
// 👍 is U+1F44D THUMBS UP SIGN, width 2
|
|
58
|
+
assert_eq!(measure_width("👍"), 2);
|
|
59
|
+
// 🌍 is U+1F30D EARTH GLOBE EUROPE-AFRICA, width 2
|
|
60
|
+
assert_eq!(measure_width("🌍"), 2);
|
|
61
|
+
// "Hello 👍" = 5 + 1 + 2 = 8
|
|
62
|
+
assert_eq!(measure_width("Hello 👍"), 8);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn test_cjk_width() {
|
|
67
|
+
// CJK characters are full-width, 2 cells each
|
|
68
|
+
// 你 (U+4F60) is width 2
|
|
69
|
+
assert_eq!(measure_width("你"), 2);
|
|
70
|
+
// 好 (U+597D) is width 2
|
|
71
|
+
assert_eq!(measure_width("好"), 2);
|
|
72
|
+
// "你好" should be 4
|
|
73
|
+
assert_eq!(measure_width("你好"), 4);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn test_mixed_width() {
|
|
78
|
+
// "a你b好" = 1 + 2 + 1 + 2 = 6
|
|
79
|
+
assert_eq!(measure_width("a你b好"), 6);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn test_empty_string() {
|
|
84
|
+
assert_eq!(measure_width(""), 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn test_spaces_and_punctuation() {
|
|
89
|
+
// Regular ASCII space and punctuation are 1 cell each
|
|
90
|
+
assert_eq!(measure_width("a b c"), 5);
|
|
91
|
+
assert_eq!(measure_width("!!!"), 3);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn test_combining_marks() {
|
|
96
|
+
// Zero-width marks don't add to width
|
|
97
|
+
// "a" + combining acute accent (U+0301)
|
|
98
|
+
let combining = "a\u{0301}";
|
|
99
|
+
assert_eq!(measure_width(combining), 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use magnus::value::ReprValue;
|
|
5
|
-
use magnus::Error;
|
|
5
|
+
use magnus::{Error, Module};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
backend::{CrosstermBackend, TestBackend},
|
|
8
8
|
Terminal,
|
|
@@ -21,28 +21,32 @@ pub fn init_terminal(focus_events: bool, bracketed_paste: bool) -> Result<(), Er
|
|
|
21
21
|
let ruby = magnus::Ruby::get().unwrap();
|
|
22
22
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
23
23
|
if term_lock.is_none() {
|
|
24
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
25
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
26
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
27
|
+
|
|
24
28
|
ratatui::crossterm::terminal::enable_raw_mode()
|
|
25
|
-
.map_err(|e| Error::new(
|
|
29
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
26
30
|
let mut stdout = io::stdout();
|
|
27
31
|
ratatui::crossterm::execute!(
|
|
28
32
|
stdout,
|
|
29
33
|
ratatui::crossterm::terminal::EnterAlternateScreen,
|
|
30
34
|
ratatui::crossterm::event::EnableMouseCapture
|
|
31
35
|
)
|
|
32
|
-
.map_err(|e| Error::new(
|
|
36
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
33
37
|
|
|
34
38
|
if focus_events {
|
|
35
39
|
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
|
|
36
|
-
.map_err(|e| Error::new(
|
|
40
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
37
41
|
}
|
|
38
42
|
if bracketed_paste {
|
|
39
43
|
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
|
|
40
|
-
.map_err(|e| Error::new(
|
|
44
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
let backend = CrosstermBackend::new(stdout);
|
|
44
|
-
let terminal =
|
|
45
|
-
.map_err(|e| Error::new(
|
|
48
|
+
let terminal =
|
|
49
|
+
Terminal::new(backend).map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
46
50
|
*term_lock = Some(TerminalWrapper::Crossterm(terminal));
|
|
47
51
|
}
|
|
48
52
|
Ok(())
|
|
@@ -52,8 +56,10 @@ pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
|
52
56
|
let ruby = magnus::Ruby::get().unwrap();
|
|
53
57
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
54
58
|
let backend = TestBackend::new(width, height);
|
|
55
|
-
let
|
|
56
|
-
|
|
59
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
60
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
61
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
62
|
+
let terminal = Terminal::new(backend).map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
57
63
|
*term_lock = Some(TerminalWrapper::Test(terminal));
|
|
58
64
|
Ok(())
|
|
59
65
|
}
|
|
@@ -93,8 +99,11 @@ pub fn get_buffer_content() -> Result<String, Error> {
|
|
|
93
99
|
}
|
|
94
100
|
Ok(result)
|
|
95
101
|
} else {
|
|
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")?;
|
|
96
105
|
Err(Error::new(
|
|
97
|
-
|
|
106
|
+
error_class,
|
|
98
107
|
"Terminal is not initialized as TestBackend",
|
|
99
108
|
))
|
|
100
109
|
}
|
|
@@ -104,13 +113,19 @@ pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
|
|
|
104
113
|
let ruby = magnus::Ruby::get().unwrap();
|
|
105
114
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
106
115
|
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
|
|
116
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
117
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
118
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
107
119
|
let pos = terminal
|
|
108
120
|
.get_cursor_position()
|
|
109
|
-
.map_err(|e| Error::new(
|
|
121
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
110
122
|
Ok(Some(pos.into()))
|
|
111
123
|
} else {
|
|
124
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
125
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
126
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
112
127
|
Err(Error::new(
|
|
113
|
-
|
|
128
|
+
error_class,
|
|
114
129
|
"Terminal is not initialized as TestBackend",
|
|
115
130
|
))
|
|
116
131
|
}
|
|
@@ -125,7 +140,10 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
|
125
140
|
TerminalWrapper::Test(terminal) => {
|
|
126
141
|
terminal.backend_mut().resize(width, height);
|
|
127
142
|
if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
|
|
128
|
-
|
|
143
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
144
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
145
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
146
|
+
return Err(Error::new(error_class, e.to_string()));
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
}
|
|
@@ -148,14 +166,20 @@ pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
|
|
|
148
166
|
hash.aset("modifiers", modifiers_to_value(cell.modifier))?;
|
|
149
167
|
Ok(hash)
|
|
150
168
|
} else {
|
|
169
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
170
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
171
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
151
172
|
Err(Error::new(
|
|
152
|
-
|
|
173
|
+
error_class,
|
|
153
174
|
format!("Coordinates ({x}, {y}) out of bounds"),
|
|
154
175
|
))
|
|
155
176
|
}
|
|
156
177
|
} else {
|
|
178
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
179
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
180
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
157
181
|
Err(Error::new(
|
|
158
|
-
|
|
182
|
+
error_class,
|
|
159
183
|
"Terminal is not initialized as TestBackend",
|
|
160
184
|
))
|
|
161
185
|
}
|
|
@@ -90,7 +90,7 @@ pub fn parse_text(value: Value) -> Result<Vec<Line<'static>>, Error> {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/// Parses a Ruby Span object into a ratatui Span.
|
|
93
|
-
fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
93
|
+
pub fn parse_span(value: Value) -> Result<Span<'static>, Error> {
|
|
94
94
|
let ruby = magnus::Ruby::get().unwrap();
|
|
95
95
|
|
|
96
96
|
// Get class name
|
|
@@ -131,6 +131,8 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
|
|
|
131
131
|
|
|
132
132
|
// Extract spans from the Ruby Line
|
|
133
133
|
let spans_val: Value = value.funcall("spans", ())?;
|
|
134
|
+
// v0.7.0: Extract style from the Ruby Line
|
|
135
|
+
let style_val: Value = value.funcall("style", ())?;
|
|
134
136
|
|
|
135
137
|
if spans_val.is_nil() {
|
|
136
138
|
return Ok(Line::from(""));
|
|
@@ -164,11 +166,18 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
if spans.is_empty() {
|
|
168
|
-
|
|
169
|
+
let mut line = if spans.is_empty() {
|
|
170
|
+
Line::from("")
|
|
169
171
|
} else {
|
|
170
|
-
|
|
172
|
+
Line::from(spans)
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// v0.7.0: Apply line-level style if present
|
|
176
|
+
if !style_val.is_nil() {
|
|
177
|
+
line = line.style(parse_style(style_val)?);
|
|
171
178
|
}
|
|
179
|
+
|
|
180
|
+
Ok(line)
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
#[cfg(test)]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
5
|
+
use crate::text::{parse_line, parse_span};
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, RArray, Symbol, Value};
|
|
7
8
|
use ratatui::{
|
|
@@ -11,6 +12,7 @@ use ratatui::{
|
|
|
11
12
|
Frame,
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
#[allow(clippy::too_many_lines)]
|
|
14
16
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
17
|
let bump = Bump::new();
|
|
16
18
|
let data_val: Value = node.funcall("data", ())?;
|
|
@@ -66,16 +68,32 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
66
68
|
|
|
67
69
|
let label_val: Value = bar_obj.funcall("label", ())?;
|
|
68
70
|
if !label_val.is_nil() {
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
71
|
+
// Try to parse as Line (rich text)
|
|
72
|
+
if let Ok(line) = parse_line(label_val) {
|
|
73
|
+
bar = bar.label(line);
|
|
74
|
+
} else if let Ok(span) = parse_span(label_val) {
|
|
75
|
+
bar = bar.label(Line::from(vec![span]));
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback to string
|
|
78
|
+
let s: String = label_val.funcall("to_s", ())?;
|
|
79
|
+
let s_ref = bump.alloc_str(&s) as &str;
|
|
80
|
+
bar = bar.label(Line::from(s_ref));
|
|
81
|
+
}
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
let text_val: Value = bar_obj.funcall("text_value", ())?;
|
|
75
85
|
if !text_val.is_nil() {
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
86
|
+
// Try to parse as Line (rich text)
|
|
87
|
+
if let Ok(line) = parse_line(text_val) {
|
|
88
|
+
bar = bar.text_value(line);
|
|
89
|
+
} else if let Ok(span) = parse_span(text_val) {
|
|
90
|
+
bar = bar.text_value(Line::from(vec![span]));
|
|
91
|
+
} else {
|
|
92
|
+
// Fallback to string
|
|
93
|
+
let s: String = text_val.funcall("to_s", ())?;
|
|
94
|
+
let s_ref = bump.alloc_str(&s) as &str;
|
|
95
|
+
bar = bar.text_value(s_ref);
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
let style_val: Value = bar_obj.funcall("style", ())?;
|
|
@@ -57,7 +57,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
57
57
|
let class_name = unsafe { class.name() }.into_owned();
|
|
58
58
|
|
|
59
59
|
match class_name.as_str() {
|
|
60
|
-
"RatatuiRuby::Shape::Line" => {
|
|
60
|
+
"RatatuiRuby::Widgets::Shape::Line" => {
|
|
61
61
|
let x1: f64 = shape_val.funcall("x1", ()).unwrap_or(0.0);
|
|
62
62
|
let y1: f64 = shape_val.funcall("y1", ()).unwrap_or(0.0);
|
|
63
63
|
let x2: f64 = shape_val.funcall("x2", ()).unwrap_or(0.0);
|
|
@@ -73,7 +73,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
73
73
|
color,
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
"RatatuiRuby::Shape::Rectangle" => {
|
|
76
|
+
"RatatuiRuby::Widgets::Shape::Rectangle" => {
|
|
77
77
|
let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
|
|
78
78
|
let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
|
|
79
79
|
let width: f64 = shape_val.funcall("width", ()).unwrap_or(0.0);
|
|
@@ -89,7 +89,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
89
89
|
color,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
-
"RatatuiRuby::Shape::Circle" => {
|
|
92
|
+
"RatatuiRuby::Widgets::Shape::Circle" => {
|
|
93
93
|
let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
|
|
94
94
|
let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
|
|
95
95
|
let radius: f64 = shape_val.funcall("radius", ()).unwrap_or(0.0);
|
|
@@ -103,7 +103,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
103
103
|
color,
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
-
"RatatuiRuby::Shape::Map" => {
|
|
106
|
+
"RatatuiRuby::Widgets::Shape::Map" => {
|
|
107
107
|
let color_val: Value = shape_val.funcall("color", ()).unwrap();
|
|
108
108
|
let color =
|
|
109
109
|
parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
|
|
@@ -114,7 +114,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
114
114
|
};
|
|
115
115
|
ctx.draw(&Map { color, resolution });
|
|
116
116
|
}
|
|
117
|
-
"RatatuiRuby::Shape::Label" => {
|
|
117
|
+
"RatatuiRuby::Widgets::Shape::Label" => {
|
|
118
118
|
let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
|
|
119
119
|
let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
|
|
120
120
|
let text_val: Value = shape_val.funcall("text", ()).unwrap();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::text::parse_span;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, Value};
|
|
7
8
|
use ratatui::{layout::Rect, widgets::Gauge, Frame};
|
|
@@ -17,8 +18,14 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
17
18
|
let mut gauge = Gauge::default().ratio(ratio).use_unicode(use_unicode);
|
|
18
19
|
|
|
19
20
|
if !label_val.is_nil() {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// Try to parse as a Span (rich text)
|
|
22
|
+
if let Ok(span) = parse_span(label_val) {
|
|
23
|
+
gauge = gauge.label(span);
|
|
24
|
+
} else {
|
|
25
|
+
// Fallback to string
|
|
26
|
+
let label_str: String = label_val.funcall("to_s", ())?;
|
|
27
|
+
gauge = gauge.label(label_str);
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
if !style_val.is_nil() {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::text::parse_span;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
7
|
use magnus::{prelude::*, Error, Value};
|
|
7
8
|
use ratatui::{layout::Rect, widgets::LineGauge, Frame};
|
|
@@ -23,8 +24,14 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
23
24
|
.unfilled_symbol(&unfilled_symbol_val);
|
|
24
25
|
|
|
25
26
|
if !label_val.is_nil() {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Try to parse as a Span (rich text)
|
|
28
|
+
if let Ok(span) = parse_span(label_val) {
|
|
29
|
+
gauge = gauge.label(span);
|
|
30
|
+
} else {
|
|
31
|
+
// Fallback to string
|
|
32
|
+
let label_str: String = label_val.funcall("to_s", ())?;
|
|
33
|
+
gauge = gauge.label(label_str);
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
if !style_val.is_nil() {
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::text::{parse_line, parse_span};
|
|
6
|
+
use crate::widgets::list_state::RubyListState;
|
|
5
7
|
use bumpalo::Bump;
|
|
6
8
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
9
|
use ratatui::{
|
|
8
10
|
layout::Rect,
|
|
9
11
|
text::Line,
|
|
10
|
-
widgets::{HighlightSpacing, List, ListState},
|
|
12
|
+
widgets::{HighlightSpacing, List, ListItem, ListState},
|
|
11
13
|
Frame,
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -27,11 +29,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
27
29
|
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
28
30
|
let block_val: Value = node.funcall("block", ())?;
|
|
29
31
|
|
|
30
|
-
let mut items: Vec<
|
|
32
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
31
33
|
for i in 0..items_array.len() {
|
|
32
34
|
let index = isize::try_from(i)
|
|
33
35
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
34
|
-
let
|
|
36
|
+
let item_val: Value = items_array.entry(index)?;
|
|
37
|
+
let item = parse_list_item(item_val)?;
|
|
35
38
|
items.push(item);
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -47,6 +50,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
47
50
|
state.select(Some(index));
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
let offset_val: Value = node.funcall("offset", ())?;
|
|
54
|
+
if !offset_val.is_nil() {
|
|
55
|
+
let offset: usize = offset_val.funcall("to_int", ())?;
|
|
56
|
+
*state.offset_mut() = offset;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
let mut list = List::new(items);
|
|
51
60
|
|
|
52
61
|
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
@@ -101,6 +110,173 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
101
110
|
Ok(())
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
/// Renders a List with an external state object.
|
|
114
|
+
///
|
|
115
|
+
/// This function ignores `selected_index` and `offset` from the widget.
|
|
116
|
+
/// The State object is the single source of truth for selection and scroll position.
|
|
117
|
+
pub fn render_stateful(
|
|
118
|
+
frame: &mut Frame,
|
|
119
|
+
area: Rect,
|
|
120
|
+
node: Value,
|
|
121
|
+
state_wrapper: Value,
|
|
122
|
+
) -> Result<(), Error> {
|
|
123
|
+
let bump = Bump::new();
|
|
124
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
125
|
+
|
|
126
|
+
// Extract the RubyListState wrapper
|
|
127
|
+
let state: &RubyListState = TryConvert::try_convert(state_wrapper)?;
|
|
128
|
+
|
|
129
|
+
// Build items
|
|
130
|
+
let items_val: Value = node.funcall("items", ())?;
|
|
131
|
+
let items_array = magnus::RArray::from_value(items_val)
|
|
132
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
133
|
+
|
|
134
|
+
let mut items: Vec<ListItem> = Vec::new();
|
|
135
|
+
for i in 0..items_array.len() {
|
|
136
|
+
let index = isize::try_from(i)
|
|
137
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
138
|
+
let item_val: Value = items_array.entry(index)?;
|
|
139
|
+
let item = parse_list_item(item_val)?;
|
|
140
|
+
items.push(item);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build widget (ignoring selected_index and offset — State is truth)
|
|
144
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
145
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
146
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
147
|
+
let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
|
|
148
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
149
|
+
let direction_val: Value = node.funcall("direction", ())?;
|
|
150
|
+
let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
|
|
151
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
152
|
+
|
|
153
|
+
let symbol: String = if highlight_symbol_val.is_nil() {
|
|
154
|
+
String::new()
|
|
155
|
+
} else {
|
|
156
|
+
String::try_convert(highlight_symbol_val)?
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let mut list = List::new(items);
|
|
160
|
+
|
|
161
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
162
|
+
"always" => HighlightSpacing::Always,
|
|
163
|
+
"never" => HighlightSpacing::Never,
|
|
164
|
+
_ => HighlightSpacing::WhenSelected,
|
|
165
|
+
};
|
|
166
|
+
list = list.highlight_spacing(highlight_spacing);
|
|
167
|
+
|
|
168
|
+
if !highlight_symbol_val.is_nil() {
|
|
169
|
+
list = list.highlight_symbol(Line::from(symbol));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if !repeat_highlight_symbol_val.is_nil() {
|
|
173
|
+
let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
|
|
174
|
+
list = list.repeat_highlight_symbol(repeat);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if !direction_val.is_nil() {
|
|
178
|
+
let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
|
|
179
|
+
let direction_str = direction_sym.name().unwrap();
|
|
180
|
+
match direction_str.as_ref() {
|
|
181
|
+
"top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
|
|
182
|
+
"bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
|
|
183
|
+
_ => {
|
|
184
|
+
return Err(Error::new(
|
|
185
|
+
ruby.exception_arg_error(),
|
|
186
|
+
"direction must be :top_to_bottom or :bottom_to_top",
|
|
187
|
+
))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if !scroll_padding_val.is_nil() {
|
|
193
|
+
let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
|
|
194
|
+
list = list.scroll_padding(padding);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if !style_val.is_nil() {
|
|
198
|
+
list = list.style(parse_style(style_val)?);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if !highlight_style_val.is_nil() {
|
|
202
|
+
list = list.highlight_style(parse_style(highlight_style_val)?);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if !block_val.is_nil() {
|
|
206
|
+
list = list.block(parse_block(block_val, &bump)?);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Borrow the inner ListState, render, and release the borrow immediately
|
|
210
|
+
{
|
|
211
|
+
let mut inner_state = state.borrow_mut();
|
|
212
|
+
frame.render_stateful_widget(list, area, &mut inner_state);
|
|
213
|
+
}
|
|
214
|
+
// Borrow is now released
|
|
215
|
+
|
|
216
|
+
Ok(())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Parses a Ruby list item into a ratatui `ListItem`.
|
|
220
|
+
///
|
|
221
|
+
/// Accepts:
|
|
222
|
+
/// - `String`: Plain text item
|
|
223
|
+
/// - `Text::Span`: A single styled fragment
|
|
224
|
+
/// - `Text::Line`: A line composed of multiple spans
|
|
225
|
+
/// - `RatatuiRuby::ListItem`: A `ListItem` object with content and optional style
|
|
226
|
+
fn parse_list_item(value: Value) -> Result<ListItem<'static>, Error> {
|
|
227
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
228
|
+
|
|
229
|
+
// Check if it's a RatatuiRuby::ListItem
|
|
230
|
+
if let Ok(class_obj) = value.funcall::<_, _, Value>("class", ()) {
|
|
231
|
+
if let Ok(class_name) = class_obj.funcall::<_, _, String>("name", ()) {
|
|
232
|
+
if class_name.contains("ListItem") {
|
|
233
|
+
// Extract content and style from the ListItem
|
|
234
|
+
let content_val: Value = value.funcall("content", ())?;
|
|
235
|
+
let style_val: Value = value.funcall("style", ())?;
|
|
236
|
+
|
|
237
|
+
// Parse content as a Line
|
|
238
|
+
let line = if let Ok(s) = String::try_convert(content_val) {
|
|
239
|
+
Line::from(s)
|
|
240
|
+
} else if let Ok(line) = parse_line(content_val) {
|
|
241
|
+
line
|
|
242
|
+
} else if let Ok(span) = parse_span(content_val) {
|
|
243
|
+
Line::from(vec![span])
|
|
244
|
+
} else {
|
|
245
|
+
Line::from("")
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Parse and apply style if present
|
|
249
|
+
let mut item = ListItem::new(line);
|
|
250
|
+
if !style_val.is_nil() {
|
|
251
|
+
item = item.style(parse_style(style_val)?);
|
|
252
|
+
}
|
|
253
|
+
return Ok(item);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try as String
|
|
259
|
+
if let Ok(s) = String::try_convert(value) {
|
|
260
|
+
return Ok(ListItem::new(Line::from(s)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try as Line
|
|
264
|
+
if let Ok(line) = parse_line(value) {
|
|
265
|
+
return Ok(ListItem::new(line));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Try as Span
|
|
269
|
+
if let Ok(span) = parse_span(value) {
|
|
270
|
+
return Ok(ListItem::new(Line::from(vec![span])));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Fallback
|
|
274
|
+
Err(Error::new(
|
|
275
|
+
ruby.exception_type_error(),
|
|
276
|
+
"expected String, Text::Span, Text::Line, or ListItem",
|
|
277
|
+
))
|
|
278
|
+
}
|
|
279
|
+
|
|
104
280
|
#[cfg(test)]
|
|
105
281
|
mod tests {
|
|
106
282
|
use super::*;
|