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
|
@@ -28,25 +28,120 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
|
|
|
28
28
|
Ok(())
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/// Parses a `snake_case` string to `MediaKeyCode`.
|
|
32
|
+
///
|
|
33
|
+
/// Accepts both the new `media_`-prefixed codes (canonical) and the legacy
|
|
34
|
+
/// unprefixed codes for backward compatibility with existing tests.
|
|
35
|
+
fn parse_media_key(s: &str) -> Option<ratatui::crossterm::event::MediaKeyCode> {
|
|
36
|
+
use ratatui::crossterm::event::MediaKeyCode;
|
|
37
|
+
match s {
|
|
38
|
+
// New canonical codes (media_ prefix)
|
|
39
|
+
"media_play" | "play" => Some(MediaKeyCode::Play),
|
|
40
|
+
"media_pause" => Some(MediaKeyCode::Pause),
|
|
41
|
+
"media_play_pause" | "play_pause" => Some(MediaKeyCode::PlayPause),
|
|
42
|
+
"media_reverse" | "reverse" => Some(MediaKeyCode::Reverse),
|
|
43
|
+
"media_stop" | "stop" => Some(MediaKeyCode::Stop),
|
|
44
|
+
"media_fast_forward" | "fast_forward" => Some(MediaKeyCode::FastForward),
|
|
45
|
+
"media_rewind" | "rewind" => Some(MediaKeyCode::Rewind),
|
|
46
|
+
"media_track_next" | "track_next" => Some(MediaKeyCode::TrackNext),
|
|
47
|
+
"media_track_previous" | "track_previous" => Some(MediaKeyCode::TrackPrevious),
|
|
48
|
+
"media_record" | "record" => Some(MediaKeyCode::Record),
|
|
49
|
+
"media_lower_volume" | "lower_volume" => Some(MediaKeyCode::LowerVolume),
|
|
50
|
+
"media_raise_volume" | "raise_volume" => Some(MediaKeyCode::RaiseVolume),
|
|
51
|
+
"media_mute_volume" | "mute_volume" => Some(MediaKeyCode::MuteVolume),
|
|
52
|
+
_ => None,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Parses a `snake_case` string to `ModifierKeyCode`.
|
|
57
|
+
fn parse_modifier_key(s: &str) -> Option<ratatui::crossterm::event::ModifierKeyCode> {
|
|
58
|
+
use ratatui::crossterm::event::ModifierKeyCode;
|
|
59
|
+
match s {
|
|
60
|
+
"left_shift" => Some(ModifierKeyCode::LeftShift),
|
|
61
|
+
"left_control" => Some(ModifierKeyCode::LeftControl),
|
|
62
|
+
"left_alt" => Some(ModifierKeyCode::LeftAlt),
|
|
63
|
+
"left_super" => Some(ModifierKeyCode::LeftSuper),
|
|
64
|
+
"left_hyper" => Some(ModifierKeyCode::LeftHyper),
|
|
65
|
+
"left_meta" => Some(ModifierKeyCode::LeftMeta),
|
|
66
|
+
"right_shift" => Some(ModifierKeyCode::RightShift),
|
|
67
|
+
"right_control" => Some(ModifierKeyCode::RightControl),
|
|
68
|
+
"right_alt" => Some(ModifierKeyCode::RightAlt),
|
|
69
|
+
"right_super" => Some(ModifierKeyCode::RightSuper),
|
|
70
|
+
"right_hyper" => Some(ModifierKeyCode::RightHyper),
|
|
71
|
+
"right_meta" => Some(ModifierKeyCode::RightMeta),
|
|
72
|
+
"iso_level3_shift" => Some(ModifierKeyCode::IsoLevel3Shift),
|
|
73
|
+
"iso_level5_shift" => Some(ModifierKeyCode::IsoLevel5Shift),
|
|
74
|
+
_ => None,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
fn parse_key_event(
|
|
32
79
|
data: magnus::RHash,
|
|
33
80
|
ruby: &magnus::Ruby,
|
|
34
81
|
) -> Result<ratatui::crossterm::event::Event, Error> {
|
|
82
|
+
use ratatui::crossterm::event::KeyCode;
|
|
83
|
+
|
|
35
84
|
let code_val: Value = data
|
|
36
85
|
.get(ruby.to_symbol("code"))
|
|
37
86
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "Missing 'code' in key event"))?;
|
|
38
87
|
let code_str: String = String::try_convert(code_val)?;
|
|
39
88
|
let code = match code_str.as_str() {
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
// Arrow keys
|
|
90
|
+
"up" => KeyCode::Up,
|
|
91
|
+
"down" => KeyCode::Down,
|
|
92
|
+
"left" => KeyCode::Left,
|
|
93
|
+
"right" => KeyCode::Right,
|
|
94
|
+
// Common keys
|
|
95
|
+
"enter" => KeyCode::Enter,
|
|
96
|
+
"esc" => KeyCode::Esc,
|
|
97
|
+
"backspace" => KeyCode::Backspace,
|
|
98
|
+
"tab" => KeyCode::Tab,
|
|
99
|
+
"back_tab" => KeyCode::BackTab,
|
|
100
|
+
// Navigation keys
|
|
101
|
+
"home" => KeyCode::Home,
|
|
102
|
+
"end" => KeyCode::End,
|
|
103
|
+
"page_up" => KeyCode::PageUp,
|
|
104
|
+
"page_down" => KeyCode::PageDown,
|
|
105
|
+
"insert" => KeyCode::Insert,
|
|
106
|
+
"delete" => KeyCode::Delete,
|
|
107
|
+
// Lock keys
|
|
108
|
+
"caps_lock" => KeyCode::CapsLock,
|
|
109
|
+
"scroll_lock" => KeyCode::ScrollLock,
|
|
110
|
+
"num_lock" => KeyCode::NumLock,
|
|
111
|
+
// System keys
|
|
112
|
+
"print_screen" => KeyCode::PrintScreen,
|
|
113
|
+
"pause" => KeyCode::Pause,
|
|
114
|
+
"menu" => KeyCode::Menu,
|
|
115
|
+
"keypad_begin" => KeyCode::KeypadBegin,
|
|
116
|
+
"null" => KeyCode::Null,
|
|
117
|
+
// Dynamic parsing for media, modifiers, function keys, and characters
|
|
118
|
+
s => {
|
|
119
|
+
// Media keys (check first - "fast_forward" starts with 'f' but isn't F-key)
|
|
120
|
+
if let Some(m) = parse_media_key(s) {
|
|
121
|
+
KeyCode::Media(m)
|
|
122
|
+
}
|
|
123
|
+
// Modifier keys
|
|
124
|
+
else if let Some(m) = parse_modifier_key(s) {
|
|
125
|
+
KeyCode::Modifier(m)
|
|
126
|
+
}
|
|
127
|
+
// Function keys: f1, f2, ..., f12, etc.
|
|
128
|
+
else if let Some(num_str) = s.strip_prefix('f') {
|
|
129
|
+
if let Ok(n) = num_str.parse::<u8>() {
|
|
130
|
+
KeyCode::F(n)
|
|
131
|
+
} else {
|
|
132
|
+
// "f" alone or invalid suffix - treat as character
|
|
133
|
+
KeyCode::Char(s.chars().next().unwrap_or('\0'))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Single character
|
|
137
|
+
else if s.len() == 1 {
|
|
138
|
+
KeyCode::Char(s.chars().next().unwrap())
|
|
139
|
+
}
|
|
140
|
+
// Unknown - default to Null
|
|
141
|
+
else {
|
|
142
|
+
KeyCode::Null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
50
145
|
};
|
|
51
146
|
|
|
52
147
|
let mut modifiers = ratatui::crossterm::event::KeyModifiers::empty();
|
|
@@ -178,7 +273,7 @@ pub fn clear_events() {
|
|
|
178
273
|
EVENT_QUEUE.lock().unwrap().clear();
|
|
179
274
|
}
|
|
180
275
|
|
|
181
|
-
pub fn poll_event(ruby: &magnus::Ruby) -> Result<Value, Error> {
|
|
276
|
+
pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value, Error> {
|
|
182
277
|
let event = {
|
|
183
278
|
let mut queue = EVENT_QUEUE.lock().unwrap();
|
|
184
279
|
if queue.is_empty() {
|
|
@@ -204,14 +299,23 @@ pub fn poll_event(ruby: &magnus::Ruby) -> Result<Value, Error> {
|
|
|
204
299
|
return Ok(ruby.qnil().into_value_with(ruby));
|
|
205
300
|
}
|
|
206
301
|
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
302
|
+
if let Some(secs) = timeout_val {
|
|
303
|
+
// Timed poll: wait up to the specified duration
|
|
304
|
+
let duration = std::time::Duration::from_secs_f64(secs);
|
|
305
|
+
if ratatui::crossterm::event::poll(duration)
|
|
306
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?
|
|
307
|
+
{
|
|
308
|
+
let event = ratatui::crossterm::event::read()
|
|
309
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
310
|
+
handle_event(event)
|
|
311
|
+
} else {
|
|
312
|
+
Ok(ruby.qnil().into_value_with(ruby))
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Blocking: wait indefinitely for an event
|
|
210
316
|
let event = ratatui::crossterm::event::read()
|
|
211
317
|
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
212
318
|
handle_event(event)
|
|
213
|
-
} else {
|
|
214
|
-
Ok(ruby.qnil().into_value_with(ruby))
|
|
215
319
|
}
|
|
216
320
|
}
|
|
217
321
|
|
|
@@ -226,26 +330,134 @@ fn handle_event(event: ratatui::crossterm::event::Event) -> Result<Value, Error>
|
|
|
226
330
|
}
|
|
227
331
|
}
|
|
228
332
|
|
|
333
|
+
/// Converts `MediaKeyCode` to `snake_case` string.
|
|
334
|
+
///
|
|
335
|
+
/// All media keys are consistently prefixed with `media_` to reflect that they
|
|
336
|
+
/// belong to the `KeyCode::Media(_)` variant in Crossterm. This allows Ruby's
|
|
337
|
+
/// "Smart Predicates" to provide DWIM behavior (e.g., `pause?` matching both
|
|
338
|
+
/// system and media pause).
|
|
339
|
+
fn media_key_to_string(m: ratatui::crossterm::event::MediaKeyCode) -> &'static str {
|
|
340
|
+
use ratatui::crossterm::event::MediaKeyCode;
|
|
341
|
+
match m {
|
|
342
|
+
MediaKeyCode::Play => "media_play",
|
|
343
|
+
MediaKeyCode::Pause => "media_pause",
|
|
344
|
+
MediaKeyCode::PlayPause => "media_play_pause",
|
|
345
|
+
MediaKeyCode::Reverse => "media_reverse",
|
|
346
|
+
MediaKeyCode::Stop => "media_stop",
|
|
347
|
+
MediaKeyCode::FastForward => "media_fast_forward",
|
|
348
|
+
MediaKeyCode::Rewind => "media_rewind",
|
|
349
|
+
MediaKeyCode::TrackNext => "media_track_next",
|
|
350
|
+
MediaKeyCode::TrackPrevious => "media_track_previous",
|
|
351
|
+
MediaKeyCode::Record => "media_record",
|
|
352
|
+
MediaKeyCode::LowerVolume => "media_lower_volume",
|
|
353
|
+
MediaKeyCode::RaiseVolume => "media_raise_volume",
|
|
354
|
+
MediaKeyCode::MuteVolume => "media_mute_volume",
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Converts `ModifierKeyCode` to `snake_case` string.
|
|
359
|
+
fn modifier_key_to_string(m: ratatui::crossterm::event::ModifierKeyCode) -> &'static str {
|
|
360
|
+
use ratatui::crossterm::event::ModifierKeyCode;
|
|
361
|
+
match m {
|
|
362
|
+
ModifierKeyCode::LeftShift => "left_shift",
|
|
363
|
+
ModifierKeyCode::LeftControl => "left_control",
|
|
364
|
+
ModifierKeyCode::LeftAlt => "left_alt",
|
|
365
|
+
ModifierKeyCode::LeftSuper => "left_super",
|
|
366
|
+
ModifierKeyCode::LeftHyper => "left_hyper",
|
|
367
|
+
ModifierKeyCode::LeftMeta => "left_meta",
|
|
368
|
+
ModifierKeyCode::RightShift => "right_shift",
|
|
369
|
+
ModifierKeyCode::RightControl => "right_control",
|
|
370
|
+
ModifierKeyCode::RightAlt => "right_alt",
|
|
371
|
+
ModifierKeyCode::RightSuper => "right_super",
|
|
372
|
+
ModifierKeyCode::RightHyper => "right_hyper",
|
|
373
|
+
ModifierKeyCode::RightMeta => "right_meta",
|
|
374
|
+
ModifierKeyCode::IsoLevel3Shift => "iso_level3_shift",
|
|
375
|
+
ModifierKeyCode::IsoLevel5Shift => "iso_level5_shift",
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
229
379
|
fn handle_key_event(key: ratatui::crossterm::event::KeyEvent) -> Result<Value, Error> {
|
|
380
|
+
use ratatui::crossterm::event::KeyCode;
|
|
381
|
+
|
|
230
382
|
let ruby = magnus::Ruby::get().unwrap();
|
|
231
383
|
if key.kind != ratatui::crossterm::event::KeyEventKind::Press {
|
|
232
384
|
return Ok(ruby.qnil().into_value_with(&ruby));
|
|
233
385
|
}
|
|
234
386
|
let hash = ruby.hash_new();
|
|
235
387
|
hash.aset(ruby.to_symbol("type"), ruby.to_symbol("key"))?;
|
|
388
|
+
|
|
389
|
+
// Determine the kind (category) of the key
|
|
390
|
+
let kind = match key.code {
|
|
391
|
+
KeyCode::Char(_)
|
|
392
|
+
| KeyCode::Enter
|
|
393
|
+
| KeyCode::Tab
|
|
394
|
+
| KeyCode::Backspace
|
|
395
|
+
| KeyCode::BackTab
|
|
396
|
+
| KeyCode::Up
|
|
397
|
+
| KeyCode::Down
|
|
398
|
+
| KeyCode::Left
|
|
399
|
+
| KeyCode::Right
|
|
400
|
+
| KeyCode::Home
|
|
401
|
+
| KeyCode::End
|
|
402
|
+
| KeyCode::PageUp
|
|
403
|
+
| KeyCode::PageDown
|
|
404
|
+
| KeyCode::Insert
|
|
405
|
+
| KeyCode::Delete
|
|
406
|
+
| KeyCode::Null => "standard",
|
|
407
|
+
KeyCode::F(_) => "function",
|
|
408
|
+
KeyCode::Media(_) => "media",
|
|
409
|
+
KeyCode::Modifier(_) => "modifier",
|
|
410
|
+
KeyCode::Esc
|
|
411
|
+
| KeyCode::CapsLock
|
|
412
|
+
| KeyCode::ScrollLock
|
|
413
|
+
| KeyCode::NumLock
|
|
414
|
+
| KeyCode::PrintScreen
|
|
415
|
+
| KeyCode::Pause
|
|
416
|
+
| KeyCode::Menu
|
|
417
|
+
| KeyCode::KeypadBegin => "system",
|
|
418
|
+
};
|
|
419
|
+
|
|
236
420
|
let code = match key.code {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
421
|
+
// Characters
|
|
422
|
+
KeyCode::Char(c) => c.to_string(),
|
|
423
|
+
// Arrow keys
|
|
424
|
+
KeyCode::Up => "up".to_string(),
|
|
425
|
+
KeyCode::Down => "down".to_string(),
|
|
426
|
+
KeyCode::Left => "left".to_string(),
|
|
427
|
+
KeyCode::Right => "right".to_string(),
|
|
428
|
+
// Common keys
|
|
429
|
+
KeyCode::Enter => "enter".to_string(),
|
|
430
|
+
KeyCode::Esc => "esc".to_string(),
|
|
431
|
+
KeyCode::Backspace => "backspace".to_string(),
|
|
432
|
+
KeyCode::Tab => "tab".to_string(),
|
|
433
|
+
KeyCode::BackTab => "back_tab".to_string(),
|
|
434
|
+
// Navigation keys
|
|
435
|
+
KeyCode::Home => "home".to_string(),
|
|
436
|
+
KeyCode::End => "end".to_string(),
|
|
437
|
+
KeyCode::PageUp => "page_up".to_string(),
|
|
438
|
+
KeyCode::PageDown => "page_down".to_string(),
|
|
439
|
+
KeyCode::Insert => "insert".to_string(),
|
|
440
|
+
KeyCode::Delete => "delete".to_string(),
|
|
441
|
+
// Function keys
|
|
442
|
+
KeyCode::F(n) => format!("f{n}"),
|
|
443
|
+
// Lock keys
|
|
444
|
+
KeyCode::CapsLock => "caps_lock".to_string(),
|
|
445
|
+
KeyCode::ScrollLock => "scroll_lock".to_string(),
|
|
446
|
+
KeyCode::NumLock => "num_lock".to_string(),
|
|
447
|
+
// System keys
|
|
448
|
+
KeyCode::PrintScreen => "print_screen".to_string(),
|
|
449
|
+
KeyCode::Pause => "pause".to_string(),
|
|
450
|
+
KeyCode::Menu => "menu".to_string(),
|
|
451
|
+
KeyCode::KeypadBegin => "keypad_begin".to_string(),
|
|
452
|
+
KeyCode::Null => "null".to_string(),
|
|
453
|
+
// Compound variants
|
|
454
|
+
KeyCode::Media(m) => media_key_to_string(m).to_string(),
|
|
455
|
+
KeyCode::Modifier(m) => modifier_key_to_string(m).to_string(),
|
|
247
456
|
};
|
|
457
|
+
|
|
248
458
|
hash.aset(ruby.to_symbol("code"), code)?;
|
|
459
|
+
hash.aset(ruby.to_symbol("kind"), ruby.to_symbol(kind))?;
|
|
460
|
+
|
|
249
461
|
let mut modifiers = Vec::new();
|
|
250
462
|
if key
|
|
251
463
|
.modifiers
|
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
//! The `'static` lifetime is a lie, but a safe one within these constraints.
|
|
19
19
|
|
|
20
20
|
use crate::rendering;
|
|
21
|
+
use crate::widgets;
|
|
21
22
|
use magnus::{prelude::*, Error, Value};
|
|
22
23
|
use ratatui::layout::Rect;
|
|
23
24
|
use ratatui::Frame;
|
|
24
25
|
use std::cell::UnsafeCell;
|
|
25
26
|
use std::ptr::NonNull;
|
|
27
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
28
|
+
use std::sync::Arc;
|
|
26
29
|
|
|
27
30
|
/// A wrapper around Ratatui's `Frame` that can be exposed to Ruby.
|
|
28
31
|
///
|
|
@@ -36,10 +39,17 @@ use std::ptr::NonNull;
|
|
|
36
39
|
/// 2. The Ruby VM is single-threaded (GVL), so the frame pointer is never accessed
|
|
37
40
|
/// from multiple threads simultaneously
|
|
38
41
|
/// 3. `RubyFrame` never escapes the draw callback scope
|
|
42
|
+
///
|
|
43
|
+
/// The `active` flag provides runtime safety by preventing use after the draw
|
|
44
|
+
/// callback completes. Without this, a user could store the frame and cause
|
|
45
|
+
/// undefined behavior by accessing it after the underlying pointer is invalid.
|
|
39
46
|
#[magnus::wrap(class = "RatatuiRuby::Frame")]
|
|
40
47
|
pub struct RubyFrame {
|
|
41
48
|
/// Pointer to the underlying frame. Valid only during the draw callback.
|
|
42
49
|
inner: UnsafeCell<NonNull<Frame<'static>>>,
|
|
50
|
+
/// Shared flag to invalidate the frame when the block finishes.
|
|
51
|
+
/// Set to `true` during draw, `false` immediately after yield returns.
|
|
52
|
+
active: Arc<AtomicBool>,
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
// SAFETY: RubyFrame is only used within Terminal::draw() callbacks, which are
|
|
@@ -49,12 +59,18 @@ unsafe impl Send for RubyFrame {}
|
|
|
49
59
|
impl RubyFrame {
|
|
50
60
|
/// Creates a new `RubyFrame` wrapping the given frame reference.
|
|
51
61
|
///
|
|
62
|
+
/// # Arguments
|
|
63
|
+
///
|
|
64
|
+
/// * `frame` - Mutable reference to the underlying Ratatui frame
|
|
65
|
+
/// * `active` - Shared atomic flag that controls frame validity
|
|
66
|
+
///
|
|
52
67
|
/// # Safety
|
|
53
68
|
///
|
|
54
69
|
/// The caller must ensure that:
|
|
55
70
|
/// 1. The `RubyFrame` does not outlive the frame reference
|
|
56
71
|
/// 2. No other mutable references to the frame exist while `RubyFrame` is in use
|
|
57
|
-
|
|
72
|
+
/// 3. The `active` flag is set to `false` after the draw callback completes
|
|
73
|
+
pub fn new(frame: &mut Frame<'_>, active: Arc<AtomicBool>) -> Self {
|
|
58
74
|
// SAFETY: We cast the frame pointer to 'static lifetime. This is safe because:
|
|
59
75
|
// - RubyFrame is only used within Terminal::draw() callbacks
|
|
60
76
|
// - The Ruby block completes before the callback returns
|
|
@@ -67,6 +83,26 @@ impl RubyFrame {
|
|
|
67
83
|
|
|
68
84
|
Self {
|
|
69
85
|
inner: UnsafeCell::new(static_ptr),
|
|
86
|
+
active,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Checks that the frame is still valid for use.
|
|
91
|
+
///
|
|
92
|
+
/// Returns `Ok(())` if the frame can be used, or an error if the draw
|
|
93
|
+
/// callback has already completed.
|
|
94
|
+
fn ensure_active(&self) -> Result<(), Error> {
|
|
95
|
+
if self.active.load(Ordering::Relaxed) {
|
|
96
|
+
Ok(())
|
|
97
|
+
} else {
|
|
98
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
99
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
100
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
101
|
+
let error_class = error_base.const_get("Safety")?;
|
|
102
|
+
Err(Error::new(
|
|
103
|
+
error_class,
|
|
104
|
+
"Frame cannot be used outside of the draw block",
|
|
105
|
+
))
|
|
70
106
|
}
|
|
71
107
|
}
|
|
72
108
|
|
|
@@ -74,15 +110,18 @@ impl RubyFrame {
|
|
|
74
110
|
///
|
|
75
111
|
/// This mirrors `frame.area()` in Rust Ratatui.
|
|
76
112
|
pub fn area(&self) -> Result<Value, Error> {
|
|
113
|
+
self.ensure_active()?;
|
|
77
114
|
let ruby = magnus::Ruby::get().unwrap();
|
|
78
115
|
|
|
79
116
|
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
80
117
|
// We only read from the frame, which is safe with an immutable reference.
|
|
118
|
+
// The ensure_active() check above guarantees we're still in the callback.
|
|
81
119
|
let area = unsafe { (*self.inner.get()).as_ref().area() };
|
|
82
120
|
|
|
83
|
-
// Create a Ruby Rect object
|
|
121
|
+
// Create a Ruby Layout::Rect object
|
|
84
122
|
let module = ruby.define_module("RatatuiRuby")?;
|
|
85
|
-
let
|
|
123
|
+
let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
|
|
124
|
+
let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
|
|
86
125
|
class.funcall("new", (area.x, area.y, area.width, area.height))
|
|
87
126
|
}
|
|
88
127
|
|
|
@@ -95,6 +134,8 @@ impl RubyFrame {
|
|
|
95
134
|
/// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::Paragraph`)
|
|
96
135
|
/// * `area` - A Ruby `Rect` or hash-like object with `x`, `y`, `width`, `height`
|
|
97
136
|
pub fn render_widget(&self, widget: Value, area: Value) -> Result<(), Error> {
|
|
137
|
+
self.ensure_active()?;
|
|
138
|
+
|
|
98
139
|
// Parse the Ruby area into a Rust Rect
|
|
99
140
|
let x: u16 = area.funcall("x", ())?;
|
|
100
141
|
let y: u16 = area.funcall("y", ())?;
|
|
@@ -107,9 +148,81 @@ impl RubyFrame {
|
|
|
107
148
|
// 1. RubyFrame is only used within Terminal::draw() callbacks
|
|
108
149
|
// 2. Ruby's GVL ensures single-threaded access
|
|
109
150
|
// 3. No other code holds a reference to the frame during this call
|
|
151
|
+
// 4. ensure_active() above guarantees we're still in the callback
|
|
110
152
|
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
111
153
|
|
|
112
154
|
// Delegate to the existing render_node function
|
|
113
155
|
rendering::render_node(frame, rect, widget)
|
|
114
156
|
}
|
|
157
|
+
|
|
158
|
+
/// Renders a stateful widget at the specified area.
|
|
159
|
+
///
|
|
160
|
+
/// This mirrors `frame.render_stateful_widget(widget, area, &mut state)` in Rust Ratatui.
|
|
161
|
+
/// The State object is the single source of truth for selection and offset.
|
|
162
|
+
/// Widget properties (`selected_index`, `selected_row`, `offset`) are ignored.
|
|
163
|
+
///
|
|
164
|
+
/// # Arguments
|
|
165
|
+
///
|
|
166
|
+
/// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::List`)
|
|
167
|
+
/// * `area` - A Ruby `Rect`
|
|
168
|
+
/// * `state` - A Ruby state object (e.g., `RatatuiRuby::ListState`)
|
|
169
|
+
pub fn render_stateful_widget(
|
|
170
|
+
&self,
|
|
171
|
+
widget: Value,
|
|
172
|
+
area: Value,
|
|
173
|
+
state: Value,
|
|
174
|
+
) -> Result<(), Error> {
|
|
175
|
+
self.ensure_active()?;
|
|
176
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
177
|
+
|
|
178
|
+
// Parse the Ruby area into a Rust Rect
|
|
179
|
+
let x: u16 = area.funcall("x", ())?;
|
|
180
|
+
let y: u16 = area.funcall("y", ())?;
|
|
181
|
+
let width: u16 = area.funcall("width", ())?;
|
|
182
|
+
let height: u16 = area.funcall("height", ())?;
|
|
183
|
+
let rect = Rect::new(x, y, width, height);
|
|
184
|
+
|
|
185
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
186
|
+
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
187
|
+
|
|
188
|
+
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
189
|
+
let widget_class = unsafe { widget.class().name() }.into_owned();
|
|
190
|
+
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
191
|
+
let state_class = unsafe { state.class().name() }.into_owned();
|
|
192
|
+
|
|
193
|
+
match (widget_class.as_str(), state_class.as_str()) {
|
|
194
|
+
("RatatuiRuby::Widgets::List", "RatatuiRuby::ListState") => {
|
|
195
|
+
widgets::list::render_stateful(frame, rect, widget, state)
|
|
196
|
+
}
|
|
197
|
+
("RatatuiRuby::Widgets::Table", "RatatuiRuby::TableState") => {
|
|
198
|
+
widgets::table::render_stateful(frame, rect, widget, state)
|
|
199
|
+
}
|
|
200
|
+
("RatatuiRuby::Widgets::Scrollbar", "RatatuiRuby::ScrollbarState") => {
|
|
201
|
+
widgets::scrollbar::render_stateful(frame, rect, widget, state)
|
|
202
|
+
}
|
|
203
|
+
_ => Err(Error::new(
|
|
204
|
+
ruby.exception_arg_error(),
|
|
205
|
+
format!("Unsupported widget/state combination: {widget_class} with {state_class}"),
|
|
206
|
+
)),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Sets the cursor position in the terminal.
|
|
211
|
+
///
|
|
212
|
+
/// This mirrors `frame.set_cursor_position((x, y))` in Rust Ratatui.
|
|
213
|
+
/// Use this for text input fields to show the cursor at the correct location.
|
|
214
|
+
///
|
|
215
|
+
/// # Arguments
|
|
216
|
+
///
|
|
217
|
+
/// * `x` - Column position (0-indexed from left)
|
|
218
|
+
/// * `y` - Row position (0-indexed from top)
|
|
219
|
+
pub fn set_cursor_position(&self, x: u16, y: u16) -> Result<(), Error> {
|
|
220
|
+
self.ensure_active()?;
|
|
221
|
+
|
|
222
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
223
|
+
// ensure_active() above guarantees we're still in the callback.
|
|
224
|
+
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
225
|
+
frame.set_cursor_position((x, y));
|
|
226
|
+
Ok(())
|
|
227
|
+
}
|
|
115
228
|
}
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
mod events;
|
|
14
14
|
mod frame;
|
|
15
15
|
mod rendering;
|
|
16
|
+
mod string_width;
|
|
16
17
|
mod style;
|
|
17
18
|
mod terminal;
|
|
18
19
|
mod text;
|
|
@@ -67,10 +68,17 @@ fn draw(args: &[Value]) -> Result<(), Error> {
|
|
|
67
68
|
let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
|
|
68
69
|
if block_given {
|
|
69
70
|
// New API: yield RubyFrame to block
|
|
70
|
-
|
|
71
|
+
// Create validity flag — set to true while the block is executing
|
|
72
|
+
let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
|
73
|
+
|
|
74
|
+
let ruby_frame = RubyFrame::new(f, active.clone());
|
|
71
75
|
if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
|
|
72
76
|
render_error = Some(e);
|
|
73
77
|
}
|
|
78
|
+
|
|
79
|
+
// Invalidate frame immediately after block returns
|
|
80
|
+
// This prevents use-after-free if user stored the frame object
|
|
81
|
+
active.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
74
82
|
} else if let Some(tree_value) = tree {
|
|
75
83
|
// Legacy API: render tree to full area
|
|
76
84
|
if let Err(e) = rendering::render_node(f, f.area(), tree_value) {
|
|
@@ -82,12 +90,18 @@ fn draw(args: &[Value]) -> Result<(), Error> {
|
|
|
82
90
|
if let Some(wrapper) = term_lock.as_mut() {
|
|
83
91
|
match wrapper {
|
|
84
92
|
terminal::TerminalWrapper::Crossterm(term) => {
|
|
93
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
94
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
95
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
85
96
|
term.draw(&mut draw_callback)
|
|
86
|
-
.map_err(|e| Error::new(
|
|
97
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
87
98
|
}
|
|
88
99
|
terminal::TerminalWrapper::Test(term) => {
|
|
100
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
101
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
102
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
89
103
|
term.draw(&mut draw_callback)
|
|
90
|
-
.map_err(|e| Error::new(
|
|
104
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
} else {
|
|
@@ -114,10 +128,23 @@ fn init() -> Result<(), Error> {
|
|
|
114
128
|
let frame_class = m.define_class("Frame", ruby.class_object())?;
|
|
115
129
|
frame_class.define_method("area", method!(RubyFrame::area, 0))?;
|
|
116
130
|
frame_class.define_method("render_widget", method!(RubyFrame::render_widget, 2))?;
|
|
117
|
-
|
|
131
|
+
frame_class.define_method(
|
|
132
|
+
"render_stateful_widget",
|
|
133
|
+
method!(RubyFrame::render_stateful_widget, 3),
|
|
134
|
+
)?;
|
|
135
|
+
frame_class.define_method(
|
|
136
|
+
"set_cursor_position",
|
|
137
|
+
method!(RubyFrame::set_cursor_position, 2),
|
|
138
|
+
)?;
|
|
139
|
+
m.define_module_function("_poll_event", function!(events::poll_event, 1))?;
|
|
118
140
|
m.define_module_function("inject_test_event", function!(events::inject_test_event, 2))?;
|
|
119
141
|
m.define_module_function("clear_events", function!(events::clear_events, 0))?;
|
|
120
142
|
|
|
143
|
+
// Register State classes
|
|
144
|
+
widgets::list_state::register(&ruby, m)?;
|
|
145
|
+
widgets::table_state::register(&ruby, m)?;
|
|
146
|
+
widgets::scrollbar_state::register(&ruby, m)?;
|
|
147
|
+
|
|
121
148
|
// Test backend helpers
|
|
122
149
|
m.define_module_function(
|
|
123
150
|
"init_test_terminal",
|
|
@@ -134,8 +161,9 @@ fn init() -> Result<(), Error> {
|
|
|
134
161
|
m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
|
|
135
162
|
m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
|
|
136
163
|
|
|
137
|
-
// Register Layout.split on the Layout class
|
|
138
|
-
let
|
|
164
|
+
// Register Layout.split on the Layout::Layout class (inside the Layout module)
|
|
165
|
+
let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;
|
|
166
|
+
let layout_class = layout_mod.const_get::<_, magnus::RClass>("Layout")?;
|
|
139
167
|
layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
|
|
140
168
|
|
|
141
169
|
// Paragraph metrics
|
|
@@ -151,6 +179,9 @@ fn init() -> Result<(), Error> {
|
|
|
151
179
|
// Tabs metrics
|
|
152
180
|
m.define_module_function("_tabs_width", function!(widgets::tabs::width, 1))?;
|
|
153
181
|
|
|
182
|
+
// Text measurement
|
|
183
|
+
m.define_module_function("_text_width", function!(string_width::text_width, 1))?;
|
|
184
|
+
|
|
154
185
|
Ok(())
|
|
155
186
|
}
|
|
156
187
|
|
|
@@ -11,7 +11,8 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
|
|
|
11
11
|
let ruby = magnus::Ruby::get().unwrap();
|
|
12
12
|
let ruby_area = {
|
|
13
13
|
let module = ruby.define_module("RatatuiRuby")?;
|
|
14
|
-
let
|
|
14
|
+
let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
|
|
15
|
+
let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
|
|
15
16
|
class.funcall::<_, _, Value>("new", (area.x, area.y, area.width, area.height))?
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -35,28 +36,28 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
|
|
|
35
36
|
let class_name = unsafe { node.class().name() }.into_owned();
|
|
36
37
|
|
|
37
38
|
match class_name.as_str() {
|
|
38
|
-
"RatatuiRuby::Paragraph" => widgets::paragraph::render(frame, area, node)?,
|
|
39
|
-
"RatatuiRuby::Clear" => widgets::clear::render(frame, area, node)?,
|
|
40
|
-
"RatatuiRuby::Cursor" => widgets::cursor::render(frame, area, node)?,
|
|
41
|
-
"RatatuiRuby::Overlay" => widgets::overlay::render(frame, area, node)?,
|
|
42
|
-
"RatatuiRuby::Center" => widgets::center::render(frame, area, node)?,
|
|
43
|
-
"RatatuiRuby::Layout" => widgets::layout::render(frame, area, node)?,
|
|
44
|
-
"RatatuiRuby::List" => widgets::list::render(frame, area, node)?,
|
|
45
|
-
"RatatuiRuby::Gauge" => widgets::gauge::render(frame, area, node)?,
|
|
46
|
-
"RatatuiRuby::LineGauge" => widgets::line_gauge::render(frame, area, node)?,
|
|
47
|
-
"RatatuiRuby::Table" => widgets::table::render(frame, area, node)?,
|
|
48
|
-
"RatatuiRuby::Block" => widgets::block::render(frame, area, node)?,
|
|
49
|
-
"RatatuiRuby::Tabs" => widgets::tabs::render(frame, area, node)?,
|
|
50
|
-
"RatatuiRuby::Scrollbar" => widgets::scrollbar::render(frame, area, node)?,
|
|
51
|
-
"RatatuiRuby::BarChart" => widgets::barchart::render(frame, area, node)?,
|
|
52
|
-
"RatatuiRuby::Canvas" => widgets::canvas::render(frame, area, node)?,
|
|
53
|
-
"RatatuiRuby::Calendar" => widgets::calendar::render(frame, area, node)?,
|
|
54
|
-
"RatatuiRuby::Sparkline" => widgets::sparkline::render(frame, area, node)?,
|
|
55
|
-
"RatatuiRuby::Chart" | "RatatuiRuby::LineChart" => {
|
|
39
|
+
"RatatuiRuby::Widgets::Paragraph" => widgets::paragraph::render(frame, area, node)?,
|
|
40
|
+
"RatatuiRuby::Widgets::Clear" => widgets::clear::render(frame, area, node)?,
|
|
41
|
+
"RatatuiRuby::Widgets::Cursor" => widgets::cursor::render(frame, area, node)?,
|
|
42
|
+
"RatatuiRuby::Widgets::Overlay" => widgets::overlay::render(frame, area, node)?,
|
|
43
|
+
"RatatuiRuby::Widgets::Center" => widgets::center::render(frame, area, node)?,
|
|
44
|
+
"RatatuiRuby::Layout::Layout" => widgets::layout::render(frame, area, node)?,
|
|
45
|
+
"RatatuiRuby::Widgets::List" => widgets::list::render(frame, area, node)?,
|
|
46
|
+
"RatatuiRuby::Widgets::Gauge" => widgets::gauge::render(frame, area, node)?,
|
|
47
|
+
"RatatuiRuby::Widgets::LineGauge" => widgets::line_gauge::render(frame, area, node)?,
|
|
48
|
+
"RatatuiRuby::Widgets::Table" => widgets::table::render(frame, area, node)?,
|
|
49
|
+
"RatatuiRuby::Widgets::Block" => widgets::block::render(frame, area, node)?,
|
|
50
|
+
"RatatuiRuby::Widgets::Tabs" => widgets::tabs::render(frame, area, node)?,
|
|
51
|
+
"RatatuiRuby::Widgets::Scrollbar" => widgets::scrollbar::render(frame, area, node)?,
|
|
52
|
+
"RatatuiRuby::Widgets::BarChart" => widgets::barchart::render(frame, area, node)?,
|
|
53
|
+
"RatatuiRuby::Widgets::Canvas" => widgets::canvas::render(frame, area, node)?,
|
|
54
|
+
"RatatuiRuby::Widgets::Calendar" => widgets::calendar::render(frame, area, node)?,
|
|
55
|
+
"RatatuiRuby::Widgets::Sparkline" => widgets::sparkline::render(frame, area, node)?,
|
|
56
|
+
"RatatuiRuby::Widgets::Chart" | "RatatuiRuby::LineChart" => {
|
|
56
57
|
widgets::chart::render(frame, area, node)?;
|
|
57
58
|
}
|
|
58
|
-
"RatatuiRuby::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
|
|
59
|
-
"RatatuiRuby::RatatuiMascot" => {
|
|
59
|
+
"RatatuiRuby::Widgets::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
|
|
60
|
+
"RatatuiRuby::Widgets::RatatuiMascot" => {
|
|
60
61
|
widgets::ratatui_mascot::render_ratatui_mascot(frame, area, node)?;
|
|
61
62
|
}
|
|
62
63
|
_ => {}
|