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,169 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `ScrollbarState` wrapper for exposing Ratatui's `ScrollbarState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyScrollbarState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<ScrollbarState>` for interior mutability during stateful rendering.
|
|
8
|
+
|
|
9
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
10
|
+
use ratatui::widgets::ScrollbarState;
|
|
11
|
+
use std::cell::RefCell;
|
|
12
|
+
|
|
13
|
+
/// A wrapper around Ratatui's `ScrollbarState` exposed to Ruby.
|
|
14
|
+
///
|
|
15
|
+
/// Ratatui's `ScrollbarState` doesn't expose getters for `position`, `content_length`,
|
|
16
|
+
/// or `viewport_content_length`. We track these values internally.
|
|
17
|
+
#[magnus::wrap(class = "RatatuiRuby::ScrollbarState")]
|
|
18
|
+
pub struct RubyScrollbarState {
|
|
19
|
+
inner: RefCell<ScrollbarState>,
|
|
20
|
+
/// We store these values ourselves since Ratatui's `ScrollbarState`
|
|
21
|
+
/// doesn't expose getters for them.
|
|
22
|
+
position_val: RefCell<usize>,
|
|
23
|
+
content_len: RefCell<usize>,
|
|
24
|
+
viewport_len: RefCell<usize>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl RubyScrollbarState {
|
|
28
|
+
/// Creates a new `RubyScrollbarState` with the given content length.
|
|
29
|
+
pub fn new(content_length: usize) -> Self {
|
|
30
|
+
Self {
|
|
31
|
+
inner: RefCell::new(ScrollbarState::new(content_length)),
|
|
32
|
+
position_val: RefCell::new(0),
|
|
33
|
+
content_len: RefCell::new(content_length),
|
|
34
|
+
viewport_len: RefCell::new(0),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Returns the current scroll position.
|
|
39
|
+
pub fn position(&self) -> usize {
|
|
40
|
+
*self.position_val.borrow()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Sets the current scroll position.
|
|
44
|
+
pub fn set_position(&self, position: usize) {
|
|
45
|
+
*self.position_val.borrow_mut() = position;
|
|
46
|
+
let mut state = self.inner.borrow_mut();
|
|
47
|
+
*state = state.position(position);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Returns the total content length.
|
|
51
|
+
pub fn content_length(&self) -> usize {
|
|
52
|
+
*self.content_len.borrow()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Sets the total content length.
|
|
56
|
+
pub fn set_content_length(&self, length: usize) {
|
|
57
|
+
*self.content_len.borrow_mut() = length;
|
|
58
|
+
let mut state = self.inner.borrow_mut();
|
|
59
|
+
*state = state.content_length(length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the viewport content length.
|
|
63
|
+
pub fn viewport_content_length(&self) -> usize {
|
|
64
|
+
*self.viewport_len.borrow()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Sets the viewport content length.
|
|
68
|
+
pub fn set_viewport_content_length(&self, length: usize) {
|
|
69
|
+
*self.viewport_len.borrow_mut() = length;
|
|
70
|
+
let mut state = self.inner.borrow_mut();
|
|
71
|
+
*state = state.viewport_content_length(length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Scrolls to the first position.
|
|
75
|
+
pub fn first(&self) {
|
|
76
|
+
*self.position_val.borrow_mut() = 0;
|
|
77
|
+
self.inner.borrow_mut().first();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Scrolls to the last position.
|
|
81
|
+
pub fn last(&self) {
|
|
82
|
+
let content_len = *self.content_len.borrow();
|
|
83
|
+
let new_pos = content_len.saturating_sub(1);
|
|
84
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
85
|
+
self.inner.borrow_mut().last();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Scrolls to the next position.
|
|
89
|
+
pub fn next(&self) {
|
|
90
|
+
let content_len = *self.content_len.borrow();
|
|
91
|
+
let current = *self.position_val.borrow();
|
|
92
|
+
let new_pos = (current + 1).min(content_len.saturating_sub(1));
|
|
93
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
94
|
+
self.inner.borrow_mut().next();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Scrolls to the previous position.
|
|
98
|
+
pub fn prev(&self) {
|
|
99
|
+
let current = *self.position_val.borrow();
|
|
100
|
+
let new_pos = current.saturating_sub(1);
|
|
101
|
+
*self.position_val.borrow_mut() = new_pos;
|
|
102
|
+
self.inner.borrow_mut().prev();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Borrows the inner `ScrollbarState` mutably for rendering.
|
|
106
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ScrollbarState> {
|
|
107
|
+
self.inner.borrow_mut()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Registers the `ScrollbarState` class with Ruby.
|
|
112
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
113
|
+
let class = module.define_class("ScrollbarState", ruby.class_object())?;
|
|
114
|
+
class.define_singleton_method("new", function!(RubyScrollbarState::new, 1))?;
|
|
115
|
+
class.define_method("position", method!(RubyScrollbarState::position, 0))?;
|
|
116
|
+
class.define_method("position=", method!(RubyScrollbarState::set_position, 1))?;
|
|
117
|
+
class.define_method(
|
|
118
|
+
"content_length",
|
|
119
|
+
method!(RubyScrollbarState::content_length, 0),
|
|
120
|
+
)?;
|
|
121
|
+
class.define_method(
|
|
122
|
+
"content_length=",
|
|
123
|
+
method!(RubyScrollbarState::set_content_length, 1),
|
|
124
|
+
)?;
|
|
125
|
+
class.define_method(
|
|
126
|
+
"viewport_content_length",
|
|
127
|
+
method!(RubyScrollbarState::viewport_content_length, 0),
|
|
128
|
+
)?;
|
|
129
|
+
class.define_method(
|
|
130
|
+
"viewport_content_length=",
|
|
131
|
+
method!(RubyScrollbarState::set_viewport_content_length, 1),
|
|
132
|
+
)?;
|
|
133
|
+
class.define_method("first", method!(RubyScrollbarState::first, 0))?;
|
|
134
|
+
class.define_method("last", method!(RubyScrollbarState::last, 0))?;
|
|
135
|
+
class.define_method("next", method!(RubyScrollbarState::next, 0))?;
|
|
136
|
+
class.define_method("prev", method!(RubyScrollbarState::prev, 0))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[cfg(test)]
|
|
141
|
+
mod tests {
|
|
142
|
+
use super::*;
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn test_new_with_content_length() {
|
|
146
|
+
let state = RubyScrollbarState::new(100);
|
|
147
|
+
assert_eq!(state.content_length(), 100);
|
|
148
|
+
assert_eq!(state.position(), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_position_navigation() {
|
|
153
|
+
let state = RubyScrollbarState::new(10);
|
|
154
|
+
state.next();
|
|
155
|
+
assert_eq!(state.position(), 1);
|
|
156
|
+
state.prev();
|
|
157
|
+
assert_eq!(state.position(), 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn test_first_and_last() {
|
|
162
|
+
let state = RubyScrollbarState::new(10);
|
|
163
|
+
state.set_position(5);
|
|
164
|
+
state.first();
|
|
165
|
+
assert_eq!(state.position(), 0);
|
|
166
|
+
state.last();
|
|
167
|
+
assert_eq!(state.position(), 9);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
|
-
use crate::style::{parse_block, parse_style
|
|
4
|
+
use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
5
5
|
use bumpalo::Bump;
|
|
6
6
|
use magnus::{prelude::*, Error, RString, Value};
|
|
7
|
-
use ratatui::{layout::Rect, widgets::
|
|
7
|
+
use ratatui::{layout::Rect, widgets::RenderDirection, widgets::Sparkline, Frame};
|
|
8
8
|
|
|
9
9
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
10
10
|
let bump = Bump::new();
|
|
@@ -20,7 +20,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
20
20
|
|
|
21
21
|
let mut data_vec = Vec::new();
|
|
22
22
|
for i in 0..data_val.len() {
|
|
23
|
-
let index = isize::try_from(i)
|
|
23
|
+
let index = isize::try_from(i)
|
|
24
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
24
25
|
let val: Value = data_val.entry(index)?;
|
|
25
26
|
if val.is_nil() {
|
|
26
27
|
data_vec.push(None);
|
|
@@ -100,22 +101,24 @@ mod tests {
|
|
|
100
101
|
fn test_sparkline_absent_value_symbol() {
|
|
101
102
|
// Data with absent (None) and present values: [Some(5), None, Some(8), None]
|
|
102
103
|
let data = vec![Some(5), None, Some(8), None];
|
|
103
|
-
let sparkline = Sparkline::default()
|
|
104
|
-
.data(&data)
|
|
105
|
-
.absent_value_symbol("-");
|
|
104
|
+
let sparkline = Sparkline::default().data(&data).absent_value_symbol("-");
|
|
106
105
|
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
|
|
107
106
|
sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
// Collect all rendered symbols
|
|
110
109
|
let symbols: Vec<&str> = buf.content().iter().map(|c| c.symbol()).collect();
|
|
111
|
-
|
|
110
|
+
|
|
112
111
|
// Check that we have 4 cells rendered
|
|
113
|
-
assert_eq!(
|
|
114
|
-
|
|
112
|
+
assert_eq!(
|
|
113
|
+
symbols.len(),
|
|
114
|
+
4,
|
|
115
|
+
"Should have 4 cells rendered for 4 data points"
|
|
116
|
+
);
|
|
117
|
+
|
|
115
118
|
// Absent values (None) should render as "-"
|
|
116
119
|
assert_eq!(symbols[1], "-", "Second value (None) should render as dash");
|
|
117
120
|
assert_eq!(symbols[3], "-", "Fourth value (None) should render as dash");
|
|
118
|
-
|
|
121
|
+
|
|
119
122
|
// Present values should not be dashes
|
|
120
123
|
assert_ne!(symbols[0], "-", "First value (Some(5)) should not be dash");
|
|
121
124
|
assert_ne!(symbols[2], "-", "Third value (Some(8)) should not be dash");
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::widgets::table_state::RubyTableState;
|
|
5
6
|
use bumpalo::Bump;
|
|
6
|
-
use magnus::{prelude::*, Error, Symbol, Value};
|
|
7
|
+
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
7
8
|
use ratatui::{
|
|
8
9
|
layout::{Constraint, Flex, Rect},
|
|
9
10
|
widgets::{Cell, HighlightSpacing, Row, Table, TableState},
|
|
@@ -33,7 +34,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
33
34
|
|
|
34
35
|
let mut rows = Vec::new();
|
|
35
36
|
for i in 0..rows_array.len() {
|
|
36
|
-
let index = isize::try_from(i)
|
|
37
|
+
let index = isize::try_from(i)
|
|
38
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
37
39
|
let row_val: Value = rows_array.entry(index)?;
|
|
38
40
|
rows.push(parse_row(row_val)?);
|
|
39
41
|
}
|
|
@@ -109,10 +111,121 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
109
111
|
state.select_column(Some(index));
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
let offset_val: Value = node.funcall("offset", ())?;
|
|
115
|
+
if !offset_val.is_nil() {
|
|
116
|
+
let offset: usize = offset_val.funcall("to_int", ())?;
|
|
117
|
+
*state.offset_mut() = offset;
|
|
118
|
+
}
|
|
119
|
+
|
|
112
120
|
frame.render_stateful_widget(table, area, &mut state);
|
|
113
121
|
Ok(())
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
/// Renders a Table with an external state object.
|
|
125
|
+
///
|
|
126
|
+
/// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
|
|
127
|
+
/// The State object is the single source of truth for selection and scroll position.
|
|
128
|
+
pub fn render_stateful(
|
|
129
|
+
frame: &mut Frame,
|
|
130
|
+
area: Rect,
|
|
131
|
+
node: Value,
|
|
132
|
+
state_wrapper: Value,
|
|
133
|
+
) -> Result<(), Error> {
|
|
134
|
+
let bump = Bump::new();
|
|
135
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
136
|
+
|
|
137
|
+
// Extract the RubyTableState wrapper
|
|
138
|
+
let state: &RubyTableState = TryConvert::try_convert(state_wrapper)?;
|
|
139
|
+
|
|
140
|
+
// Parse rows
|
|
141
|
+
let rows_value: Value = node.funcall("rows", ())?;
|
|
142
|
+
let rows_array = magnus::RArray::from_value(rows_value)
|
|
143
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
|
|
144
|
+
let widths_val: Value = node.funcall("widths", ())?;
|
|
145
|
+
let widths_array = magnus::RArray::from_value(widths_val)
|
|
146
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
|
|
147
|
+
|
|
148
|
+
let mut rows = Vec::new();
|
|
149
|
+
for i in 0..rows_array.len() {
|
|
150
|
+
let index = isize::try_from(i)
|
|
151
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
152
|
+
let row_val: Value = rows_array.entry(index)?;
|
|
153
|
+
rows.push(parse_row(row_val)?);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let constraints = parse_constraints(widths_array)?;
|
|
157
|
+
|
|
158
|
+
// Build table (ignoring selected_row, selected_column, offset — State is truth)
|
|
159
|
+
let header_val: Value = node.funcall("header", ())?;
|
|
160
|
+
let footer_val: Value = node.funcall("footer", ())?;
|
|
161
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
162
|
+
let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
|
|
163
|
+
let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
|
|
164
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
165
|
+
let block_val: Value = node.funcall("block", ())?;
|
|
166
|
+
let flex_sym: Symbol = node.funcall("flex", ())?;
|
|
167
|
+
let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
|
|
168
|
+
let style_val: Value = node.funcall("style", ())?;
|
|
169
|
+
let column_spacing_val: Value = node.funcall("column_spacing", ())?;
|
|
170
|
+
|
|
171
|
+
let flex = match flex_sym.to_string().as_str() {
|
|
172
|
+
"start" => Flex::Start,
|
|
173
|
+
"center" => Flex::Center,
|
|
174
|
+
"end" => Flex::End,
|
|
175
|
+
"space_between" => Flex::SpaceBetween,
|
|
176
|
+
"space_around" => Flex::SpaceAround,
|
|
177
|
+
"space_evenly" => Flex::SpaceEvenly,
|
|
178
|
+
_ => Flex::Legacy,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let mut table = Table::new(rows, constraints).flex(flex);
|
|
182
|
+
|
|
183
|
+
let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
|
|
184
|
+
"always" => HighlightSpacing::Always,
|
|
185
|
+
"never" => HighlightSpacing::Never,
|
|
186
|
+
_ => HighlightSpacing::WhenSelected,
|
|
187
|
+
};
|
|
188
|
+
table = table.highlight_spacing(highlight_spacing);
|
|
189
|
+
|
|
190
|
+
if !header_val.is_nil() {
|
|
191
|
+
table = table.header(parse_row(header_val)?);
|
|
192
|
+
}
|
|
193
|
+
if !footer_val.is_nil() {
|
|
194
|
+
table = table.footer(parse_row(footer_val)?);
|
|
195
|
+
}
|
|
196
|
+
if !block_val.is_nil() {
|
|
197
|
+
table = table.block(parse_block(block_val, &bump)?);
|
|
198
|
+
}
|
|
199
|
+
if !highlight_style_val.is_nil() {
|
|
200
|
+
table = table.row_highlight_style(parse_style(highlight_style_val)?);
|
|
201
|
+
}
|
|
202
|
+
if !column_highlight_style_val.is_nil() {
|
|
203
|
+
table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
|
|
204
|
+
}
|
|
205
|
+
if !cell_highlight_style_val.is_nil() {
|
|
206
|
+
table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
|
|
207
|
+
}
|
|
208
|
+
if !highlight_symbol_val.is_nil() {
|
|
209
|
+
let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
|
|
210
|
+
table = table.highlight_symbol(symbol);
|
|
211
|
+
}
|
|
212
|
+
if !style_val.is_nil() {
|
|
213
|
+
table = table.style(parse_style(style_val)?);
|
|
214
|
+
}
|
|
215
|
+
if !column_spacing_val.is_nil() {
|
|
216
|
+
let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
|
|
217
|
+
table = table.column_spacing(spacing);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Borrow the inner TableState, render, and release the borrow immediately
|
|
221
|
+
{
|
|
222
|
+
let mut inner_state = state.borrow_mut();
|
|
223
|
+
frame.render_stateful_widget(table, area, &mut inner_state);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
Ok(())
|
|
227
|
+
}
|
|
228
|
+
|
|
116
229
|
fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
|
|
117
230
|
let ruby = magnus::Ruby::get().unwrap();
|
|
118
231
|
let row_array = magnus::RArray::from_value(row_val)
|
|
@@ -120,7 +233,8 @@ fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
|
|
|
120
233
|
|
|
121
234
|
let mut cells = Vec::new();
|
|
122
235
|
for i in 0..row_array.len() {
|
|
123
|
-
let index = isize::try_from(i)
|
|
236
|
+
let index = isize::try_from(i)
|
|
237
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
124
238
|
let cell_val: Value = row_array.entry(index)?;
|
|
125
239
|
cells.push(parse_cell(cell_val)?);
|
|
126
240
|
}
|
|
@@ -159,7 +273,8 @@ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
|
|
|
159
273
|
if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
|
|
160
274
|
let ruby = magnus::Ruby::get().unwrap();
|
|
161
275
|
for i in 0..mods_array.len() {
|
|
162
|
-
let index = isize::try_from(i)
|
|
276
|
+
let index = isize::try_from(i)
|
|
277
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
163
278
|
let mod_str: String = mods_array.entry::<String>(index)?;
|
|
164
279
|
if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
|
|
165
280
|
style = style.add_modifier(modifier);
|
|
@@ -177,7 +292,8 @@ fn parse_constraints(widths_array: magnus::RArray) -> Result<Vec<Constraint>, Er
|
|
|
177
292
|
let ruby = magnus::Ruby::get().unwrap();
|
|
178
293
|
let mut constraints = Vec::new();
|
|
179
294
|
for i in 0..widths_array.len() {
|
|
180
|
-
let index = isize::try_from(i)
|
|
295
|
+
let index = isize::try_from(i)
|
|
296
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
181
297
|
let constraint_obj: Value = widths_array.entry(index)?;
|
|
182
298
|
let type_sym: Symbol = constraint_obj.funcall("type", ())?;
|
|
183
299
|
let value_obj: Value = constraint_obj.funcall("value", ())?;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! `TableState` wrapper for exposing Ratatui's `TableState` to Ruby.
|
|
5
|
+
//!
|
|
6
|
+
//! This module provides `RubyTableState`, a Magnus-wrapped struct that holds
|
|
7
|
+
//! a `RefCell<TableState>` for interior mutability during stateful rendering.
|
|
8
|
+
//!
|
|
9
|
+
//! # Design
|
|
10
|
+
//!
|
|
11
|
+
//! When using `render_stateful_widget`, the State object is the single source
|
|
12
|
+
//! of truth for selection and offset. Widget properties (`selected_row`,
|
|
13
|
+
//! `selected_column`, `offset`) are ignored in stateful mode.
|
|
14
|
+
|
|
15
|
+
use magnus::{function, method, prelude::*, Error, Module, Ruby};
|
|
16
|
+
use ratatui::widgets::TableState;
|
|
17
|
+
use std::cell::RefCell;
|
|
18
|
+
|
|
19
|
+
/// A wrapper around Ratatui's `TableState` exposed to Ruby.
|
|
20
|
+
#[magnus::wrap(class = "RatatuiRuby::TableState")]
|
|
21
|
+
pub struct RubyTableState {
|
|
22
|
+
inner: RefCell<TableState>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl RubyTableState {
|
|
26
|
+
/// Creates a new `RubyTableState` with optional initial selection.
|
|
27
|
+
pub fn new(selected: Option<usize>) -> Self {
|
|
28
|
+
let mut state = TableState::default();
|
|
29
|
+
if let Some(idx) = selected {
|
|
30
|
+
state.select(Some(idx));
|
|
31
|
+
}
|
|
32
|
+
Self {
|
|
33
|
+
inner: RefCell::new(state),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Sets the selected row index.
|
|
38
|
+
pub fn select(&self, index: Option<usize>) {
|
|
39
|
+
self.inner.borrow_mut().select(index);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Returns the currently selected row index.
|
|
43
|
+
pub fn selected(&self) -> Option<usize> {
|
|
44
|
+
self.inner.borrow().selected()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Sets the selected column index.
|
|
48
|
+
pub fn select_column(&self, index: Option<usize>) {
|
|
49
|
+
self.inner.borrow_mut().select_column(index);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Returns the currently selected column index.
|
|
53
|
+
pub fn selected_column(&self) -> Option<usize> {
|
|
54
|
+
self.inner.borrow().selected_column()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the current scroll offset.
|
|
58
|
+
pub fn offset(&self) -> usize {
|
|
59
|
+
self.inner.borrow().offset()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Scrolls down by the given number of rows.
|
|
63
|
+
pub fn scroll_down_by(&self, amount: u16) {
|
|
64
|
+
self.inner.borrow_mut().scroll_down_by(amount);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Scrolls up by the given number of rows.
|
|
68
|
+
pub fn scroll_up_by(&self, amount: u16) {
|
|
69
|
+
self.inner.borrow_mut().scroll_up_by(amount);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Borrows the inner `TableState` mutably for rendering.
|
|
73
|
+
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, TableState> {
|
|
74
|
+
self.inner.borrow_mut()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Registers the `TableState` class with Ruby.
|
|
79
|
+
pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
|
|
80
|
+
let class = module.define_class("TableState", ruby.class_object())?;
|
|
81
|
+
class.define_singleton_method("new", function!(RubyTableState::new, 1))?;
|
|
82
|
+
class.define_method("select", method!(RubyTableState::select, 1))?;
|
|
83
|
+
class.define_method("selected", method!(RubyTableState::selected, 0))?;
|
|
84
|
+
class.define_method("select_column", method!(RubyTableState::select_column, 1))?;
|
|
85
|
+
class.define_method(
|
|
86
|
+
"selected_column",
|
|
87
|
+
method!(RubyTableState::selected_column, 0),
|
|
88
|
+
)?;
|
|
89
|
+
class.define_method("offset", method!(RubyTableState::offset, 0))?;
|
|
90
|
+
class.define_method("scroll_down_by", method!(RubyTableState::scroll_down_by, 1))?;
|
|
91
|
+
class.define_method("scroll_up_by", method!(RubyTableState::scroll_up_by, 1))?;
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[cfg(test)]
|
|
96
|
+
mod tests {
|
|
97
|
+
use super::*;
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn test_new_with_no_selection() {
|
|
101
|
+
let state = RubyTableState::new(None);
|
|
102
|
+
assert_eq!(state.selected(), None);
|
|
103
|
+
assert_eq!(state.selected_column(), None);
|
|
104
|
+
assert_eq!(state.offset(), 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn test_new_with_selection() {
|
|
109
|
+
let state = RubyTableState::new(Some(3));
|
|
110
|
+
assert_eq!(state.selected(), Some(3));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn test_column_selection() {
|
|
115
|
+
let state = RubyTableState::new(None);
|
|
116
|
+
state.select_column(Some(2));
|
|
117
|
+
assert_eq!(state.selected_column(), Some(2));
|
|
118
|
+
state.select_column(None);
|
|
119
|
+
assert_eq!(state.selected_column(), None);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -15,7 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
15
15
|
|
|
16
16
|
use crate::text::parse_line;
|
|
17
17
|
|
|
18
|
-
fn create_tabs
|
|
18
|
+
fn create_tabs(node: Value, bump: &Bump) -> Result<Tabs<'_>, Error> {
|
|
19
19
|
let ruby = magnus::Ruby::get().unwrap();
|
|
20
20
|
let titles_val: Value = node.funcall("titles", ())?;
|
|
21
21
|
let selected_index: usize = node.funcall("selected_index", ())?;
|
|
@@ -30,7 +30,8 @@ fn create_tabs<'a>(node: Value, bump: &'a Bump) -> Result<Tabs<'a>, Error> {
|
|
|
30
30
|
|
|
31
31
|
let mut titles = Vec::new();
|
|
32
32
|
for i in 0..titles_array.len() {
|
|
33
|
-
let index = isize::try_from(i)
|
|
33
|
+
let index = isize::try_from(i)
|
|
34
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
34
35
|
let val: Value = titles_array.entry(index)?;
|
|
35
36
|
if let Ok(line) = parse_line(val) {
|
|
36
37
|
titles.push(line);
|
|
@@ -81,10 +82,11 @@ pub fn width(node: Value) -> Result<usize, Error> {
|
|
|
81
82
|
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
|
|
82
83
|
|
|
83
84
|
let mut total_width = padding_left + padding_right;
|
|
84
|
-
|
|
85
|
+
|
|
85
86
|
let mut titles_count = 0;
|
|
86
87
|
for i in 0..titles_array.len() {
|
|
87
|
-
let index = isize::try_from(i)
|
|
88
|
+
let index = isize::try_from(i)
|
|
89
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
88
90
|
let val: Value = titles_array.entry(index)?;
|
|
89
91
|
let line_width = if let Ok(line) = parse_line(val) {
|
|
90
92
|
line.width()
|
|
@@ -97,11 +99,11 @@ pub fn width(node: Value) -> Result<usize, Error> {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
if titles_count > 1 {
|
|
100
|
-
let divider_width = if
|
|
102
|
+
let divider_width = if divider_val.is_nil() {
|
|
103
|
+
1 // Default divider is "|"
|
|
104
|
+
} else {
|
|
101
105
|
let d: String = divider_val.funcall("to_s", ())?;
|
|
102
106
|
ratatui::text::Span::raw(d).width()
|
|
103
|
-
} else {
|
|
104
|
-
1 // Default divider is "|"
|
|
105
107
|
};
|
|
106
108
|
total_width += (titles_count - 1) * divider_width;
|
|
107
109
|
}
|
|
@@ -134,10 +136,8 @@ mod tests {
|
|
|
134
136
|
fn test_tabs_highlight_style() {
|
|
135
137
|
let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
|
|
136
138
|
let highlight_style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
|
|
137
|
-
let tabs = Tabs::new(titles)
|
|
138
|
-
|
|
139
|
-
.highlight_style(highlight_style);
|
|
140
|
-
|
|
139
|
+
let tabs = Tabs::new(titles).select(0).highlight_style(highlight_style);
|
|
140
|
+
|
|
141
141
|
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
|
142
142
|
tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
|
|
143
143
|
|
data/lib/ratatui_ruby/cell.rb
CHANGED
|
@@ -67,7 +67,7 @@ module RatatuiRuby
|
|
|
67
67
|
# Cell.symbol("X") # => #<RatatuiRuby::Cell symbol="X">
|
|
68
68
|
#
|
|
69
69
|
def self.symbol(symbol)
|
|
70
|
-
new(symbol
|
|
70
|
+
new(symbol:, fg: nil, bg: nil, modifiers: [])
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Alias for Rubyists who prefer a shorter name.
|
|
@@ -82,10 +82,10 @@ module RatatuiRuby
|
|
|
82
82
|
# [bg] Symbol or String (nullable).
|
|
83
83
|
# [modifiers] Array of Strings.
|
|
84
84
|
def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
|
|
85
|
-
@symbol = symbol || char || " "
|
|
86
|
-
@fg = fg
|
|
87
|
-
@bg = bg
|
|
88
|
-
@modifiers = modifiers.freeze
|
|
85
|
+
@symbol = (symbol || char || " ").freeze
|
|
86
|
+
@fg = fg&.freeze
|
|
87
|
+
@bg = bg&.freeze
|
|
88
|
+
@modifiers = modifiers.map(&:freeze).freeze
|
|
89
89
|
freeze
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -149,7 +149,7 @@ module RatatuiRuby
|
|
|
149
149
|
parts << "fg=#{fg.inspect}" if fg
|
|
150
150
|
parts << "bg=#{bg.inspect}" if bg
|
|
151
151
|
parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
|
|
152
|
-
"#<#{self.class} #{parts.join(
|
|
152
|
+
"#<#{self.class} #{parts.join(' ')}>"
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
# Returns the cell's character.
|
|
@@ -160,7 +160,7 @@ module RatatuiRuby
|
|
|
160
160
|
# Support for pattern matching.
|
|
161
161
|
# Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
|
|
162
162
|
def deconstruct_keys(keys)
|
|
163
|
-
{ symbol
|
|
163
|
+
{ symbol:, char: symbol, fg:, bg:, modifiers: }
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
class Event
|
|
8
|
+
class Key < Event
|
|
9
|
+
# Methods for handling printable characters.
|
|
10
|
+
module Character
|
|
11
|
+
# Returns true if the key represents a single printable character.
|
|
12
|
+
#
|
|
13
|
+
# RatatuiRuby::Event::Key.new(code: "a").text? # => true
|
|
14
|
+
# RatatuiRuby::Event::Key.new(code: "enter").text? # => false
|
|
15
|
+
# RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
|
|
16
|
+
def text?
|
|
17
|
+
@code.length == 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the key as a printable character (if applicable).
|
|
21
|
+
#
|
|
22
|
+
# [Printable Characters]
|
|
23
|
+
# Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
|
|
24
|
+
# [Special Keys]
|
|
25
|
+
# Returns <tt>nil</tt> (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
|
|
26
|
+
#
|
|
27
|
+
# RatatuiRuby::Event::Key.new(code: "a").char # => "a"
|
|
28
|
+
# RatatuiRuby::Event::Key.new(code: "enter").char # => nil
|
|
29
|
+
def char
|
|
30
|
+
text? ? @code : nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|