ratatui_ruby 0.4.0 → 0.6.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 +98 -176
- data/CHANGELOG.md +80 -6
- data/README.md +19 -7
- data/REUSE.toml +15 -0
- data/doc/application_architecture.md +179 -45
- data/doc/application_testing.md +80 -32
- data/doc/contributors/design/ruby_frontend.md +48 -8
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +191 -48
- data/doc/contributors/documentation_style.md +7 -0
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/contributors/index.md +2 -0
- data/doc/event_handling.md +21 -7
- 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_box_demo.png +0 -0
- data/doc/images/widget_calendar_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_gauge_demo.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge_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_ratatui_logo_demo.png +0 -0
- data/doc/images/widget_ratatui_mascot_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_scrollbar_demo.png +0 -0
- data/doc/images/widget_sparkline_demo.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_table_flex.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/interactive_design.md +25 -30
- data/doc/quickstart.md +150 -130
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +99 -0
- data/examples/app_all_events/app.rb +96 -0
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_color_cycle.rb +41 -0
- data/examples/app_all_events/model/event_entry.rb +92 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/model/timestamp.rb +54 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +78 -0
- data/examples/app_all_events/view/controls_view.rb +52 -0
- data/examples/app_all_events/view/counts_view.rb +59 -0
- data/examples/app_all_events/view/live_view.rb +70 -0
- data/examples/app_all_events/view/log_view.rb +55 -0
- data/examples/app_all_events/view.rb +7 -0
- data/examples/app_color_picker/README.md +134 -0
- data/examples/app_color_picker/app.rb +74 -0
- data/examples/app_color_picker/clipboard.rb +84 -0
- data/examples/app_color_picker/color.rb +191 -0
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +166 -0
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/harmony.rb +56 -0
- data/examples/app_color_picker/input.rb +174 -0
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +109 -0
- data/examples/app_login_form/README.md +47 -0
- data/examples/{login_form → app_login_form}/app.rb +38 -42
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +71 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +54 -0
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +40 -0
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +238 -0
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_calendar_demo/app.rb +109 -0
- 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 +111 -0
- 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 +218 -0
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_gauge_demo/app.rb +212 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_layout_split/app.rb +246 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_line_gauge_demo/app.rb +217 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +366 -0
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_popup_demo/app.rb +104 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_logo_demo/app.rb +103 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_rect/app.rb +205 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_render/app.rb +184 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +166 -0
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +107 -0
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_scrollbar_demo/app.rb +153 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +275 -0
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_style_colors/app.rb +19 -21
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/widget_table_demo/app.rb +239 -0
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +181 -0
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +359 -62
- data/ext/ratatui_ruby/src/frame.rs +227 -0
- data/ext/ratatui_ruby/src/lib.rs +110 -27
- data/ext/ratatui_ruby/src/rendering.rs +8 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/style.rs +138 -57
- data/ext/ratatui_ruby/src/terminal.rs +42 -22
- data/ext/ratatui_ruby/src/text.rs +14 -7
- data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
- data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
- 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/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
- data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
- data/lib/ratatui_ruby/cell.rb +7 -7
- 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 +112 -52
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/none.rb +43 -0
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/event.rb +56 -4
- data/lib/ratatui_ruby/frame.rb +183 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
- data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
- data/lib/ratatui_ruby/schema/block.rb +163 -168
- data/lib/ratatui_ruby/schema/calendar.rb +66 -67
- data/lib/ratatui_ruby/schema/canvas.rb +63 -63
- data/lib/ratatui_ruby/schema/center.rb +46 -46
- data/lib/ratatui_ruby/schema/chart.rb +135 -143
- data/lib/ratatui_ruby/schema/clear.rb +42 -42
- data/lib/ratatui_ruby/schema/constraint.rb +76 -76
- data/lib/ratatui_ruby/schema/cursor.rb +30 -25
- data/lib/ratatui_ruby/schema/gauge.rb +54 -52
- data/lib/ratatui_ruby/schema/layout.rb +87 -87
- data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
- data/lib/ratatui_ruby/schema/list.rb +103 -80
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/overlay.rb +31 -31
- data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
- data/lib/ratatui_ruby/schema/rect.rb +99 -56
- data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
- data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
- data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
- data/lib/ratatui_ruby/schema/style.rb +66 -46
- data/lib/ratatui_ruby/schema/table.rb +126 -115
- data/lib/ratatui_ruby/schema/tabs.rb +66 -67
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +482 -0
- data/lib/ratatui_ruby/session.rb +55 -23
- 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 +390 -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 +66 -193
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +100 -51
- data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
- data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
- data/sig/examples/app_all_events/model/events.rbs +15 -0
- data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
- data/sig/examples/app_all_events/view/app_view.rbs +8 -0
- data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
- data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
- data/sig/examples/app_all_events/view/live_view.rbs +6 -0
- data/sig/examples/app_all_events/view/log_view.rbs +6 -0
- data/sig/examples/app_all_events/view.rbs +8 -0
- data/sig/examples/app_all_events/view_state.rbs +15 -0
- data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
- data/sig/examples/verify_readme_usage/app.rbs +11 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/widget_box_demo/app.rbs +11 -0
- data/sig/examples/widget_calendar_demo/app.rbs +11 -0
- data/sig/examples/widget_cell_demo/app.rbs +11 -0
- data/sig/examples/widget_chart_demo/app.rbs +11 -0
- data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
- data/sig/examples/widget_layout_split/app.rbs +10 -0
- data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
- data/sig/examples/widget_list_demo/app.rbs +12 -0
- data/sig/examples/widget_map_demo/app.rbs +11 -0
- data/sig/examples/widget_popup_demo/app.rbs +11 -0
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
- data/sig/examples/widget_rect/app.rbs +12 -0
- data/sig/examples/widget_render/app.rbs +10 -0
- data/sig/examples/widget_rich_text/app.rbs +11 -0
- data/sig/examples/widget_scroll_text/app.rbs +11 -0
- data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
- data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
- data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
- data/sig/examples/widget_table_demo/app.rbs +11 -0
- data/sig/examples/widget_text_width/app.rbs +10 -0
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +11 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/draw.rbs +4 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/layout.rbs +1 -1
- 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/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +107 -0
- 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/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +63 -0
- data/tasks/autodoc/member.rb +56 -0
- data/tasks/autodoc/name.rb +19 -0
- data/tasks/autodoc/notice.rb +26 -0
- data/tasks/autodoc/rbs.rb +38 -0
- data/tasks/autodoc/rdoc.rb +45 -0
- data/tasks/autodoc.rake +53 -0
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/history.rb +2 -2
- data/tasks/bump/links.rb +67 -0
- data/tasks/doc.rake +600 -6
- data/tasks/example_viewer.html.erb +172 -0
- data/tasks/lint.rake +8 -4
- data/tasks/resources/index.html.erb +6 -0
- data/tasks/sourcehut.rake +70 -30
- data/tasks/terminal_preview/app_screenshot.rb +14 -6
- data/tasks/terminal_preview/crash_report.rb +7 -9
- data/tasks/terminal_preview/launcher_script.rb +4 -6
- data/tasks/terminal_preview/preview_collection.rb +4 -6
- data/tasks/terminal_preview/safety_confirmation.rb +3 -5
- data/tasks/terminal_preview/saved_screenshot.rb +10 -11
- data/tasks/terminal_preview/terminal_window.rb +7 -9
- data/tasks/test.rake +1 -1
- data/tasks/website/index_page.rb +3 -3
- data/tasks/website/version.rb +10 -10
- data/tasks/website/version_menu.rb +10 -12
- data/tasks/website/versioned_documentation.rb +49 -17
- data/tasks/website/website.rb +6 -8
- data/tasks/website.rake +4 -4
- metadata +232 -127
- data/LICENSES/BSD-2-Clause.txt +0 -9
- data/doc/contributors/better_dx.md +0 -543
- data/doc/contributors/example_analysis.md +0 -82
- data/doc/images/all_events.png +0 -0
- data/doc/images/block_padding.png +0 -0
- data/doc/images/block_titles.png +0 -0
- data/doc/images/box_demo.png +0 -0
- data/doc/images/calendar_demo.png +0 -0
- data/doc/images/cell_demo.png +0 -0
- data/doc/images/chart_demo.png +0 -0
- data/doc/images/flex_layout.png +0 -0
- data/doc/images/gauge_demo.png +0 -0
- data/doc/images/line_gauge_demo.png +0 -0
- data/doc/images/list_demo.png +0 -0
- data/doc/images/list_styles.png +0 -0
- data/doc/images/login_form.png +0 -0
- data/doc/images/quickstart_dsl.png +0 -0
- data/doc/images/quickstart_lifecycle.png +0 -0
- data/doc/images/readme_usage.png +0 -0
- data/doc/images/rich_text.png +0 -0
- data/doc/images/scroll_text.png +0 -0
- data/doc/images/scrollbar_demo.png +0 -0
- data/doc/images/sparkline_demo.png +0 -0
- data/doc/images/table_flex.png +0 -0
- data/doc/images/table_select.png +0 -0
- data/examples/all_events/app.rb +0 -169
- data/examples/all_events/app.rbs +0 -7
- data/examples/all_events/test_app.rb +0 -139
- data/examples/analytics/app.rb +0 -258
- data/examples/analytics/app.rbs +0 -7
- data/examples/analytics/test_app.rb +0 -132
- data/examples/block_padding/app.rb +0 -63
- data/examples/block_padding/app.rbs +0 -7
- data/examples/block_padding/test_app.rb +0 -31
- data/examples/block_titles/app.rb +0 -61
- data/examples/block_titles/app.rbs +0 -7
- data/examples/block_titles/test_app.rb +0 -34
- data/examples/box_demo/app.rbs +0 -7
- data/examples/box_demo/test_app.rb +0 -88
- data/examples/calendar_demo/app.rb +0 -101
- data/examples/calendar_demo/app.rbs +0 -7
- data/examples/calendar_demo/test_app.rb +0 -108
- data/examples/cell_demo/app.rb +0 -108
- data/examples/cell_demo/app.rbs +0 -7
- data/examples/cell_demo/test_app.rb +0 -36
- data/examples/chart_demo/app.rb +0 -203
- data/examples/chart_demo/app.rbs +0 -7
- data/examples/chart_demo/test_app.rb +0 -102
- data/examples/custom_widget/app.rb +0 -51
- data/examples/custom_widget/app.rbs +0 -7
- data/examples/custom_widget/test_app.rb +0 -30
- data/examples/flex_layout/app.rb +0 -156
- data/examples/flex_layout/app.rbs +0 -7
- data/examples/flex_layout/test_app.rb +0 -65
- data/examples/gauge_demo/app.rb +0 -182
- data/examples/gauge_demo/test_app.rb +0 -120
- data/examples/hit_test/app.rb +0 -175
- data/examples/hit_test/app.rbs +0 -7
- data/examples/hit_test/test_app.rb +0 -102
- data/examples/line_gauge_demo/app.rb +0 -190
- data/examples/line_gauge_demo/app.rbs +0 -7
- data/examples/line_gauge_demo/test_app.rb +0 -129
- data/examples/list_demo/app.rb +0 -253
- data/examples/list_demo/test_app.rb +0 -237
- data/examples/list_styles/app.rb +0 -140
- data/examples/list_styles/app.rbs +0 -7
- data/examples/list_styles/test_app.rb +0 -157
- data/examples/login_form/app.rbs +0 -7
- data/examples/login_form/test_app.rb +0 -51
- data/examples/map_demo/app.rbs +0 -7
- data/examples/map_demo/test_app.rb +0 -149
- data/examples/mouse_events/app.rb +0 -97
- data/examples/mouse_events/app.rbs +0 -7
- data/examples/mouse_events/test_app.rb +0 -53
- data/examples/popup_demo/app.rb +0 -103
- data/examples/popup_demo/app.rbs +0 -7
- data/examples/popup_demo/test_app.rb +0 -54
- data/examples/quickstart_dsl/app.rbs +0 -7
- data/examples/quickstart_dsl/test_app.rb +0 -29
- data/examples/quickstart_lifecycle/app.rb +0 -39
- data/examples/quickstart_lifecycle/app.rbs +0 -7
- data/examples/quickstart_lifecycle/test_app.rb +0 -29
- data/examples/ratatui_logo_demo/app.rb +0 -79
- data/examples/ratatui_logo_demo/app.rbs +0 -7
- data/examples/ratatui_logo_demo/test_app.rb +0 -51
- data/examples/ratatui_mascot_demo/app.rb +0 -84
- data/examples/ratatui_mascot_demo/app.rbs +0 -7
- data/examples/ratatui_mascot_demo/test_app.rb +0 -47
- data/examples/readme_usage/app.rb +0 -29
- data/examples/readme_usage/app.rbs +0 -7
- data/examples/readme_usage/test_app.rb +0 -29
- data/examples/rich_text/app.rb +0 -141
- data/examples/rich_text/app.rbs +0 -7
- data/examples/rich_text/test_app.rb +0 -166
- data/examples/scroll_text/app.rb +0 -103
- data/examples/scroll_text/app.rbs +0 -7
- data/examples/scroll_text/test_app.rb +0 -110
- data/examples/scrollbar_demo/app.rb +0 -143
- data/examples/scrollbar_demo/app.rbs +0 -7
- data/examples/scrollbar_demo/test_app.rb +0 -77
- data/examples/sparkline_demo/app.rb +0 -240
- data/examples/sparkline_demo/test_app.rb +0 -107
- data/examples/table_flex/app.rb +0 -65
- data/examples/table_flex/app.rbs +0 -7
- data/examples/table_flex/test_app.rb +0 -36
- data/examples/table_select/app.rb +0 -198
- data/examples/table_select/app.rbs +0 -7
- data/examples/table_select/test_app.rb +0 -180
- data/examples/widget_style_colors/test_app.rb +0 -48
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{analytics.png → app_analytics.png} +0 -0
- /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
- /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
- /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
- /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
- /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
- /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! Frame wrapper for exposing Ratatui's Frame to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyFrame`, a struct that wraps `ratatui::Frame` and exposes
|
|
7
|
+
//! it to Ruby via Magnus. It enables explicit widget placement through `render_widget`,
|
|
8
|
+
//! aligning `RatatuiRuby` with native Rust Ratatui patterns.
|
|
9
|
+
//!
|
|
10
|
+
//! # Safety
|
|
11
|
+
//!
|
|
12
|
+
//! `RubyFrame` uses raw pointer casting to store a `Frame` reference with an erased
|
|
13
|
+
//! lifetime. This is safe because:
|
|
14
|
+
//! 1. `RubyFrame` is only created within `Terminal::draw()` callbacks
|
|
15
|
+
//! 2. `RubyFrame` is never returned from or stored beyond the callback scope
|
|
16
|
+
//! 3. The Ruby block receiving `RubyFrame` completes before the callback returns
|
|
17
|
+
//!
|
|
18
|
+
//! The `'static` lifetime is a lie, but a safe one within these constraints.
|
|
19
|
+
|
|
20
|
+
use crate::rendering;
|
|
21
|
+
use crate::widgets;
|
|
22
|
+
use magnus::{prelude::*, Error, Value};
|
|
23
|
+
use ratatui::layout::Rect;
|
|
24
|
+
use ratatui::Frame;
|
|
25
|
+
use std::cell::UnsafeCell;
|
|
26
|
+
use std::ptr::NonNull;
|
|
27
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
28
|
+
use std::sync::Arc;
|
|
29
|
+
|
|
30
|
+
/// A wrapper around Ratatui's `Frame` that can be exposed to Ruby.
|
|
31
|
+
///
|
|
32
|
+
/// This struct uses raw pointers to hold a mutable reference to the frame,
|
|
33
|
+
/// which is valid only for the duration of the draw callback.
|
|
34
|
+
///
|
|
35
|
+
/// # Safety
|
|
36
|
+
///
|
|
37
|
+
/// We implement `Send` manually because:
|
|
38
|
+
/// 1. `RubyFrame` is only created and used within a single `Terminal::draw()` callback
|
|
39
|
+
/// 2. The Ruby VM is single-threaded (GVL), so the frame pointer is never accessed
|
|
40
|
+
/// from multiple threads simultaneously
|
|
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.
|
|
46
|
+
#[magnus::wrap(class = "RatatuiRuby::Frame")]
|
|
47
|
+
pub struct RubyFrame {
|
|
48
|
+
/// Pointer to the underlying frame. Valid only during the draw callback.
|
|
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>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// SAFETY: RubyFrame is only used within Terminal::draw() callbacks, which are
|
|
56
|
+
// single-threaded. The Ruby VM's GVL ensures no concurrent access.
|
|
57
|
+
unsafe impl Send for RubyFrame {}
|
|
58
|
+
|
|
59
|
+
impl RubyFrame {
|
|
60
|
+
/// Creates a new `RubyFrame` wrapping the given frame reference.
|
|
61
|
+
///
|
|
62
|
+
/// # Arguments
|
|
63
|
+
///
|
|
64
|
+
/// * `frame` - Mutable reference to the underlying Ratatui frame
|
|
65
|
+
/// * `active` - Shared atomic flag that controls frame validity
|
|
66
|
+
///
|
|
67
|
+
/// # Safety
|
|
68
|
+
///
|
|
69
|
+
/// The caller must ensure that:
|
|
70
|
+
/// 1. The `RubyFrame` does not outlive the frame reference
|
|
71
|
+
/// 2. No other mutable references to the frame exist while `RubyFrame` is in use
|
|
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 {
|
|
74
|
+
// SAFETY: We cast the frame pointer to 'static lifetime. This is safe because:
|
|
75
|
+
// - RubyFrame is only used within Terminal::draw() callbacks
|
|
76
|
+
// - The Ruby block completes before the callback returns
|
|
77
|
+
// - No reference to RubyFrame escapes the callback scope
|
|
78
|
+
let ptr = NonNull::from(frame);
|
|
79
|
+
let static_ptr: NonNull<Frame<'static>> =
|
|
80
|
+
// SAFETY: Lifetime erasure is safe within the draw callback scope.
|
|
81
|
+
// The frame pointer remains valid for the entire callback duration.
|
|
82
|
+
unsafe { std::mem::transmute(ptr) };
|
|
83
|
+
|
|
84
|
+
Self {
|
|
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
|
+
))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Returns the terminal area as a Ruby `RatatuiRuby::Rect`.
|
|
110
|
+
///
|
|
111
|
+
/// This mirrors `frame.area()` in Rust Ratatui.
|
|
112
|
+
pub fn area(&self) -> Result<Value, Error> {
|
|
113
|
+
self.ensure_active()?;
|
|
114
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
115
|
+
|
|
116
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
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.
|
|
119
|
+
let area = unsafe { (*self.inner.get()).as_ref().area() };
|
|
120
|
+
|
|
121
|
+
// Create a Ruby Rect object
|
|
122
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
123
|
+
let class = module.const_get::<_, magnus::RClass>("Rect")?;
|
|
124
|
+
class.funcall("new", (area.x, area.y, area.width, area.height))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Renders a widget at the specified area.
|
|
128
|
+
///
|
|
129
|
+
/// This mirrors `frame.render_widget(widget, area)` in Rust Ratatui.
|
|
130
|
+
///
|
|
131
|
+
/// # Arguments
|
|
132
|
+
///
|
|
133
|
+
/// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::Paragraph`)
|
|
134
|
+
/// * `area` - A Ruby `Rect` or hash-like object with `x`, `y`, `width`, `height`
|
|
135
|
+
pub fn render_widget(&self, widget: Value, area: Value) -> Result<(), Error> {
|
|
136
|
+
self.ensure_active()?;
|
|
137
|
+
|
|
138
|
+
// Parse the Ruby area into a Rust Rect
|
|
139
|
+
let x: u16 = area.funcall("x", ())?;
|
|
140
|
+
let y: u16 = area.funcall("y", ())?;
|
|
141
|
+
let width: u16 = area.funcall("width", ())?;
|
|
142
|
+
let height: u16 = area.funcall("height", ())?;
|
|
143
|
+
let rect = Rect::new(x, y, width, height);
|
|
144
|
+
|
|
145
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
146
|
+
// We take a mutable reference which is safe because:
|
|
147
|
+
// 1. RubyFrame is only used within Terminal::draw() callbacks
|
|
148
|
+
// 2. Ruby's GVL ensures single-threaded access
|
|
149
|
+
// 3. No other code holds a reference to the frame during this call
|
|
150
|
+
// 4. ensure_active() above guarantees we're still in the callback
|
|
151
|
+
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
152
|
+
|
|
153
|
+
// Delegate to the existing render_node function
|
|
154
|
+
rendering::render_node(frame, rect, widget)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Renders a stateful widget at the specified area.
|
|
158
|
+
///
|
|
159
|
+
/// This mirrors `frame.render_stateful_widget(widget, area, &mut state)` in Rust Ratatui.
|
|
160
|
+
/// The State object is the single source of truth for selection and offset.
|
|
161
|
+
/// Widget properties (`selected_index`, `selected_row`, `offset`) are ignored.
|
|
162
|
+
///
|
|
163
|
+
/// # Arguments
|
|
164
|
+
///
|
|
165
|
+
/// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::List`)
|
|
166
|
+
/// * `area` - A Ruby `Rect`
|
|
167
|
+
/// * `state` - A Ruby state object (e.g., `RatatuiRuby::ListState`)
|
|
168
|
+
pub fn render_stateful_widget(
|
|
169
|
+
&self,
|
|
170
|
+
widget: Value,
|
|
171
|
+
area: Value,
|
|
172
|
+
state: Value,
|
|
173
|
+
) -> Result<(), Error> {
|
|
174
|
+
self.ensure_active()?;
|
|
175
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
176
|
+
|
|
177
|
+
// Parse the Ruby area into a Rust Rect
|
|
178
|
+
let x: u16 = area.funcall("x", ())?;
|
|
179
|
+
let y: u16 = area.funcall("y", ())?;
|
|
180
|
+
let width: u16 = area.funcall("width", ())?;
|
|
181
|
+
let height: u16 = area.funcall("height", ())?;
|
|
182
|
+
let rect = Rect::new(x, y, width, height);
|
|
183
|
+
|
|
184
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
185
|
+
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
186
|
+
|
|
187
|
+
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
188
|
+
let widget_class = unsafe { widget.class().name() }.into_owned();
|
|
189
|
+
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
190
|
+
let state_class = unsafe { state.class().name() }.into_owned();
|
|
191
|
+
|
|
192
|
+
match (widget_class.as_str(), state_class.as_str()) {
|
|
193
|
+
("RatatuiRuby::List", "RatatuiRuby::ListState") => {
|
|
194
|
+
widgets::list::render_stateful(frame, rect, widget, state)
|
|
195
|
+
}
|
|
196
|
+
("RatatuiRuby::Table", "RatatuiRuby::TableState") => {
|
|
197
|
+
widgets::table::render_stateful(frame, rect, widget, state)
|
|
198
|
+
}
|
|
199
|
+
("RatatuiRuby::Scrollbar", "RatatuiRuby::ScrollbarState") => {
|
|
200
|
+
widgets::scrollbar::render_stateful(frame, rect, widget, state)
|
|
201
|
+
}
|
|
202
|
+
_ => Err(Error::new(
|
|
203
|
+
ruby.exception_arg_error(),
|
|
204
|
+
format!("Unsupported widget/state combination: {widget_class} with {state_class}"),
|
|
205
|
+
)),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Sets the cursor position in the terminal.
|
|
210
|
+
///
|
|
211
|
+
/// This mirrors `frame.set_cursor_position((x, y))` in Rust Ratatui.
|
|
212
|
+
/// Use this for text input fields to show the cursor at the correct location.
|
|
213
|
+
///
|
|
214
|
+
/// # Arguments
|
|
215
|
+
///
|
|
216
|
+
/// * `x` - Column position (0-indexed from left)
|
|
217
|
+
/// * `y` - Row position (0-indexed from top)
|
|
218
|
+
pub fn set_cursor_position(&self, x: u16, y: u16) -> Result<(), Error> {
|
|
219
|
+
self.ensure_active()?;
|
|
220
|
+
|
|
221
|
+
// SAFETY: The frame pointer is valid for the duration of the draw callback.
|
|
222
|
+
// ensure_active() above guarantees we're still in the callback.
|
|
223
|
+
let frame = unsafe { (*self.inner.get()).as_mut() };
|
|
224
|
+
frame.set_cursor_position((x, y));
|
|
225
|
+
Ok(())
|
|
226
|
+
}
|
|
227
|
+
}
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -11,36 +11,97 @@
|
|
|
11
11
|
#![allow(clippy::module_name_repetitions)]
|
|
12
12
|
|
|
13
13
|
mod events;
|
|
14
|
+
mod frame;
|
|
14
15
|
mod rendering;
|
|
16
|
+
mod string_width;
|
|
15
17
|
mod style;
|
|
16
18
|
mod terminal;
|
|
17
19
|
mod text;
|
|
18
20
|
mod widgets;
|
|
19
21
|
|
|
20
|
-
use
|
|
22
|
+
use frame::RubyFrame;
|
|
23
|
+
use magnus::{function, method, Error, Module, Object, Ruby, Value};
|
|
21
24
|
use terminal::{init_terminal, restore_terminal, TERMINAL};
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/// Draw to the terminal.
|
|
27
|
+
///
|
|
28
|
+
/// Supports two calling conventions:
|
|
29
|
+
/// - Legacy: `RatatuiRuby.draw(tree)` - Renders a widget tree to the full terminal area
|
|
30
|
+
/// - New: `RatatuiRuby.draw { |frame| ... }` - Yields a Frame for explicit widget placement
|
|
31
|
+
fn draw(args: &[Value]) -> Result<(), Error> {
|
|
32
|
+
let ruby = Ruby::get().unwrap();
|
|
33
|
+
|
|
34
|
+
// Parse arguments: check for optional tree argument
|
|
35
|
+
let tree: Option<Value> = if args.is_empty() {
|
|
36
|
+
None
|
|
37
|
+
} else if args.len() == 1 {
|
|
38
|
+
Some(args[0])
|
|
39
|
+
} else {
|
|
40
|
+
return Err(Error::new(
|
|
41
|
+
ruby.exception_arg_error(),
|
|
42
|
+
format!(
|
|
43
|
+
"wrong number of arguments (given {}, expected 0..1)",
|
|
44
|
+
args.len()
|
|
45
|
+
),
|
|
46
|
+
));
|
|
47
|
+
};
|
|
48
|
+
let block_given = ruby.block_given();
|
|
49
|
+
|
|
50
|
+
// Validate: must have either tree or block, but not both
|
|
51
|
+
if tree.is_some() && block_given {
|
|
52
|
+
return Err(Error::new(
|
|
53
|
+
ruby.exception_arg_error(),
|
|
54
|
+
"Cannot provide both a tree and a block to draw",
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
if tree.is_none() && !block_given {
|
|
58
|
+
return Err(Error::new(
|
|
59
|
+
ruby.exception_arg_error(),
|
|
60
|
+
"Must provide either a tree or a block to draw",
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
let mut term_lock = TERMINAL.lock().unwrap();
|
|
26
|
-
let mut render_error = None;
|
|
65
|
+
let mut render_error: Option<Error> = None;
|
|
66
|
+
|
|
67
|
+
// Helper closure to execute the draw callback logic for either terminal type
|
|
68
|
+
let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
|
|
69
|
+
if block_given {
|
|
70
|
+
// New API: yield RubyFrame to block
|
|
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());
|
|
75
|
+
if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
|
|
76
|
+
render_error = Some(e);
|
|
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);
|
|
82
|
+
} else if let Some(tree_value) = tree {
|
|
83
|
+
// Legacy API: render tree to full area
|
|
84
|
+
if let Err(e) = rendering::render_node(f, f.area(), tree_value) {
|
|
85
|
+
render_error = Some(e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
27
90
|
if let Some(wrapper) = term_lock.as_mut() {
|
|
28
91
|
match wrapper {
|
|
29
92
|
terminal::TerminalWrapper::Crossterm(term) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
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")?;
|
|
96
|
+
term.draw(&mut draw_callback)
|
|
97
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
36
98
|
}
|
|
37
99
|
terminal::TerminalWrapper::Test(term) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
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")?;
|
|
103
|
+
term.draw(&mut draw_callback)
|
|
104
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
44
105
|
}
|
|
45
106
|
}
|
|
46
107
|
} else {
|
|
@@ -59,15 +120,31 @@ fn init() -> Result<(), Error> {
|
|
|
59
120
|
let ruby = magnus::Ruby::get().unwrap();
|
|
60
121
|
let m = ruby.define_module("RatatuiRuby")?;
|
|
61
122
|
|
|
62
|
-
|
|
63
|
-
|
|
64
123
|
m.define_module_function("_init_terminal", function!(init_terminal, 2))?;
|
|
65
124
|
m.define_module_function("restore_terminal", function!(restore_terminal, 0))?;
|
|
66
|
-
m.define_module_function("
|
|
67
|
-
|
|
125
|
+
m.define_module_function("_draw", function!(draw, -1))?;
|
|
126
|
+
|
|
127
|
+
// Register Frame class
|
|
128
|
+
let frame_class = m.define_class("Frame", ruby.class_object())?;
|
|
129
|
+
frame_class.define_method("area", method!(RubyFrame::area, 0))?;
|
|
130
|
+
frame_class.define_method("render_widget", method!(RubyFrame::render_widget, 2))?;
|
|
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))?;
|
|
68
140
|
m.define_module_function("inject_test_event", function!(events::inject_test_event, 2))?;
|
|
69
141
|
m.define_module_function("clear_events", function!(events::clear_events, 0))?;
|
|
70
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
|
+
|
|
71
148
|
// Test backend helpers
|
|
72
149
|
m.define_module_function(
|
|
73
150
|
"init_test_terminal",
|
|
@@ -81,23 +158,29 @@ fn init() -> Result<(), Error> {
|
|
|
81
158
|
"get_cursor_position",
|
|
82
159
|
function!(terminal::get_cursor_position, 0),
|
|
83
160
|
)?;
|
|
84
|
-
m.define_module_function(
|
|
85
|
-
"_get_cell_at",
|
|
86
|
-
function!(terminal::get_cell_at, 2),
|
|
87
|
-
)?;
|
|
161
|
+
m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
|
|
88
162
|
m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
|
|
89
163
|
|
|
90
164
|
// Register Layout.split on the Layout class
|
|
91
165
|
let layout_class = m.const_get::<_, magnus::RClass>("Layout")?;
|
|
92
166
|
layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
|
|
93
|
-
|
|
167
|
+
|
|
94
168
|
// Paragraph metrics
|
|
95
|
-
m.define_module_function(
|
|
96
|
-
|
|
169
|
+
m.define_module_function(
|
|
170
|
+
"_paragraph_line_count",
|
|
171
|
+
function!(widgets::paragraph::line_count, 2),
|
|
172
|
+
)?;
|
|
173
|
+
m.define_module_function(
|
|
174
|
+
"_paragraph_line_width",
|
|
175
|
+
function!(widgets::paragraph::line_width, 1),
|
|
176
|
+
)?;
|
|
97
177
|
|
|
98
178
|
// Tabs metrics
|
|
99
179
|
m.define_module_function("_tabs_width", function!(widgets::tabs::width, 1))?;
|
|
100
180
|
|
|
181
|
+
// Text measurement
|
|
182
|
+
m.define_module_function("_text_width", function!(string_width::text_width, 1))?;
|
|
183
|
+
|
|
101
184
|
Ok(())
|
|
102
185
|
}
|
|
103
186
|
|
|
@@ -22,7 +22,8 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
|
|
|
22
22
|
if let Some(arr) = RArray::from_value(commands) {
|
|
23
23
|
for i in 0..arr.len() {
|
|
24
24
|
let ruby = magnus::Ruby::get().unwrap();
|
|
25
|
-
let index = isize::try_from(i)
|
|
25
|
+
let index = isize::try_from(i)
|
|
26
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
26
27
|
let cmd: Value = arr.entry(index)?;
|
|
27
28
|
process_draw_command(frame.buffer_mut(), cmd)?;
|
|
28
29
|
}
|
|
@@ -54,8 +55,10 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
|
|
|
54
55
|
"RatatuiRuby::Chart" | "RatatuiRuby::LineChart" => {
|
|
55
56
|
widgets::chart::render(frame, area, node)?;
|
|
56
57
|
}
|
|
57
|
-
"RatatuiRuby::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node)
|
|
58
|
-
"RatatuiRuby::RatatuiMascot" =>
|
|
58
|
+
"RatatuiRuby::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
|
|
59
|
+
"RatatuiRuby::RatatuiMascot" => {
|
|
60
|
+
widgets::ratatui_mascot::render_ratatui_mascot(frame, area, node)?;
|
|
61
|
+
}
|
|
59
62
|
_ => {}
|
|
60
63
|
}
|
|
61
64
|
Ok(())
|
|
@@ -105,7 +108,8 @@ fn process_draw_command(buffer: &mut Buffer, cmd: Value) -> Result<(), Error> {
|
|
|
105
108
|
|
|
106
109
|
if let Some(mods_array) = RArray::from_value(modifiers_val) {
|
|
107
110
|
for i in 0..mods_array.len() {
|
|
108
|
-
let index = isize::try_from(i)
|
|
111
|
+
let index = isize::try_from(i)
|
|
112
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
109
113
|
let mod_str: String = mods_array.entry::<String>(index)?;
|
|
110
114
|
if let Some(modifier) = parse_modifier_str(&mod_str) {
|
|
111
115
|
style = style.add_modifier(modifier);
|
|
@@ -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
|
+
}
|