ratatui_ruby 0.7.4 → 0.9.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 +2 -2
- data/.builds/ruby-3.3.yml +2 -2
- data/.builds/ruby-3.4.yml +2 -2
- data/.builds/ruby-4.0.0.yml +2 -2
- data/.pre-commit-config.yaml +1 -1
- data/AGENTS.md +3 -3
- data/CHANGELOG.md +70 -1
- data/LICENSES/LGPL-3.0-or-later.txt +304 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/README.md +33 -5
- data/Rakefile +1 -1
- data/doc/concepts/application_architecture.md +44 -3
- data/doc/concepts/application_testing.md +43 -1
- data/doc/concepts/async.md +32 -2
- data/doc/concepts/custom_widgets.md +247 -0
- data/doc/concepts/event_handling.md +32 -3
- data/doc/concepts/interactive_design.md +32 -2
- data/doc/contributors/auditing/parity.md +7 -1
- data/doc/contributors/design/ruby_frontend.md +85 -1
- data/doc/contributors/design/rust_backend.md +67 -1
- data/doc/contributors/developing_examples.md +56 -2
- data/doc/contributors/documentation_style.md +20 -3
- data/doc/contributors/future_work.md +169 -0
- data/doc/contributors/index.md +1 -1
- data/doc/contributors/v1.0.0_blockers.md +15 -175
- data/doc/getting_started/quickstart.md +35 -9
- data/doc/getting_started/why.md +1 -1
- data/doc/index.md +2 -1
- data/doc/troubleshooting/debugging.md +32 -2
- data/doc/troubleshooting/terminal_limitations.md +8 -2
- data/doc/troubleshooting/tui_output.md +127 -6
- data/examples/app_all_events/README.md +14 -2
- data/examples/app_all_events/app.rb +1 -1
- data/examples/app_all_events/model/app_model.rb +1 -1
- data/examples/app_all_events/model/event_color_cycle.rb +1 -1
- data/examples/app_all_events/model/event_entry.rb +1 -1
- data/examples/app_all_events/model/msg.rb +1 -1
- data/examples/app_all_events/model/timestamp.rb +1 -1
- data/examples/app_all_events/update.rb +1 -1
- data/examples/app_all_events/view/app_view.rb +1 -1
- data/examples/app_all_events/view/controls_view.rb +1 -1
- data/examples/app_all_events/view/counts_view.rb +1 -1
- data/examples/app_all_events/view/live_view.rb +1 -1
- data/examples/app_all_events/view/log_view.rb +1 -1
- data/examples/app_all_events/view.rb +1 -1
- data/examples/app_color_picker/README.md +20 -2
- data/examples/app_color_picker/app.rb +1 -1
- data/examples/app_color_picker/clipboard.rb +1 -1
- data/examples/app_color_picker/color.rb +1 -1
- data/examples/app_color_picker/controls.rb +1 -1
- data/examples/app_color_picker/copy_dialog.rb +1 -1
- data/examples/app_color_picker/export_pane.rb +1 -1
- data/examples/app_color_picker/harmony.rb +1 -1
- data/examples/app_color_picker/input.rb +1 -1
- data/examples/app_color_picker/main_container.rb +1 -1
- data/examples/app_color_picker/palette.rb +1 -1
- data/examples/app_login_form/README.md +8 -2
- data/examples/app_login_form/app.rb +1 -1
- data/examples/app_stateful_interaction/README.md +2 -2
- data/examples/app_stateful_interaction/app.rb +71 -17
- data/examples/timeout_demo.rb +1 -1
- data/examples/verify_quickstart_dsl/README.md +6 -0
- data/examples/verify_quickstart_dsl/app.rb +3 -3
- data/examples/verify_quickstart_layout/README.md +6 -0
- data/examples/verify_quickstart_layout/app.rb +3 -3
- data/examples/verify_quickstart_lifecycle/README.md +13 -1
- data/examples/verify_quickstart_lifecycle/app.rb +10 -4
- data/examples/verify_readme_usage/README.md +6 -0
- data/examples/verify_readme_usage/app.rb +3 -3
- data/examples/widget_barchart/README.md +6 -0
- data/examples/widget_barchart/app.rb +2 -2
- data/examples/widget_block/README.md +7 -1
- data/examples/widget_block/app.rb +2 -2
- data/examples/widget_box/README.md +6 -0
- data/examples/widget_box/app.rb +9 -6
- data/examples/widget_calendar/README.md +6 -0
- data/examples/widget_calendar/app.rb +2 -2
- data/examples/widget_canvas/README.md +4 -0
- data/examples/widget_canvas/app.rb +2 -2
- data/examples/widget_cell/README.md +6 -0
- data/examples/widget_cell/app.rb +2 -3
- data/examples/widget_center/README.md +4 -0
- data/examples/widget_center/app.rb +2 -2
- data/examples/widget_chart/README.md +6 -0
- data/examples/widget_chart/app.rb +2 -2
- data/examples/widget_gauge/README.md +6 -0
- data/examples/widget_gauge/app.rb +2 -2
- data/examples/widget_layout_split/README.md +6 -0
- data/examples/widget_layout_split/app.rb +3 -3
- data/examples/widget_line_gauge/README.md +6 -0
- data/examples/widget_line_gauge/app.rb +2 -2
- data/examples/widget_list/README.md +6 -0
- data/examples/widget_list/app.rb +2 -2
- data/examples/widget_map/README.md +8 -2
- data/examples/widget_map/app.rb +2 -2
- data/examples/widget_overlay/README.md +7 -1
- data/examples/widget_overlay/app.rb +2 -2
- data/examples/widget_popup/README.md +6 -0
- data/examples/widget_popup/app.rb +2 -2
- data/examples/widget_ratatui_logo/README.md +6 -0
- data/examples/widget_ratatui_logo/app.rb +2 -3
- data/examples/widget_ratatui_mascot/README.md +6 -0
- data/examples/widget_ratatui_mascot/app.rb +2 -2
- data/examples/widget_rect/README.md +12 -0
- data/examples/widget_rect/app.rb +40 -26
- data/examples/widget_render/README.md +6 -0
- data/examples/widget_render/app.rb +2 -2
- data/examples/widget_render/app.rbs +41 -0
- data/examples/widget_rich_text/README.md +6 -0
- data/examples/widget_rich_text/app.rb +2 -2
- data/examples/widget_scroll_text/README.md +6 -0
- data/examples/widget_scroll_text/app.rb +2 -2
- data/examples/widget_scrollbar/README.md +6 -0
- data/examples/widget_scrollbar/app.rb +2 -2
- data/examples/widget_sparkline/README.md +6 -0
- data/examples/widget_sparkline/app.rb +2 -2
- data/examples/widget_style_colors/README.md +6 -0
- data/examples/widget_style_colors/app.rb +2 -2
- data/examples/widget_table/README.md +8 -2
- data/examples/widget_table/app.rb +2 -2
- data/examples/widget_tabs/README.md +6 -0
- data/examples/widget_tabs/app.rb +2 -2
- data/examples/widget_text_width/README.md +6 -0
- data/examples/widget_text_width/app.rb +4 -4
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/extconf.rb +2 -2
- data/ext/ratatui_ruby/src/rendering.rs +1 -1
- data/ext/ratatui_ruby/src/style.rs +0 -8
- data/ext/ratatui_ruby/src/widgets/chart.rs +0 -118
- data/ext/ratatui_ruby/src/widgets/list_state.rs +36 -0
- data/lib/ratatui_ruby/buffer/cell.rb +34 -2
- data/lib/ratatui_ruby/buffer.rb +2 -2
- data/lib/ratatui_ruby/cell.rb +34 -2
- data/lib/ratatui_ruby/event/focus_gained.rb +26 -2
- data/lib/ratatui_ruby/event/focus_lost.rb +26 -2
- data/lib/ratatui_ruby/event/key/character.rb +18 -2
- data/lib/ratatui_ruby/event/key/media.rb +2 -2
- data/lib/ratatui_ruby/event/key/modifier.rb +10 -2
- data/lib/ratatui_ruby/event/key/navigation.rb +2 -2
- data/lib/ratatui_ruby/event/key/system.rb +2 -2
- data/lib/ratatui_ruby/event/key.rb +114 -2
- data/lib/ratatui_ruby/event/mouse.rb +42 -2
- data/lib/ratatui_ruby/event/none.rb +10 -2
- data/lib/ratatui_ruby/event/paste.rb +34 -2
- data/lib/ratatui_ruby/event/resize.rb +34 -2
- data/lib/ratatui_ruby/event.rb +26 -2
- data/lib/ratatui_ruby/frame.rb +74 -2
- data/lib/ratatui_ruby/layout/constraint.rb +58 -2
- data/lib/ratatui_ruby/layout/layout.rb +47 -2
- data/lib/ratatui_ruby/layout/rect.rb +403 -2
- data/lib/ratatui_ruby/layout.rb +2 -2
- data/lib/ratatui_ruby/list_state.rb +113 -2
- data/lib/ratatui_ruby/output_guard.rb +171 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +2 -2
- data/lib/ratatui_ruby/schema/bar_chart.rb +50 -2
- data/lib/ratatui_ruby/schema/block.rb +21 -15
- data/lib/ratatui_ruby/schema/calendar.rb +2 -2
- data/lib/ratatui_ruby/schema/canvas.rb +10 -2
- data/lib/ratatui_ruby/schema/center.rb +10 -2
- data/lib/ratatui_ruby/schema/chart.rb +2 -28
- data/lib/ratatui_ruby/schema/clear.rb +10 -2
- data/lib/ratatui_ruby/schema/constraint.rb +58 -2
- data/lib/ratatui_ruby/schema/cursor.rb +10 -2
- data/lib/ratatui_ruby/schema/draw.rb +10 -2
- data/lib/ratatui_ruby/schema/gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/layout.rb +18 -2
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +10 -2
- data/lib/ratatui_ruby/schema/list_item.rb +10 -2
- data/lib/ratatui_ruby/schema/overlay.rb +10 -2
- data/lib/ratatui_ruby/schema/paragraph.rb +10 -2
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +2 -2
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +2 -2
- data/lib/ratatui_ruby/schema/rect.rb +58 -2
- data/lib/ratatui_ruby/schema/row.rb +10 -2
- data/lib/ratatui_ruby/schema/scrollbar.rb +2 -2
- data/lib/ratatui_ruby/schema/shape/label.rb +10 -2
- data/lib/ratatui_ruby/schema/sparkline.rb +10 -2
- data/lib/ratatui_ruby/schema/style.rb +18 -2
- data/lib/ratatui_ruby/schema/table.rb +2 -2
- data/lib/ratatui_ruby/schema/tabs.rb +2 -2
- data/lib/ratatui_ruby/schema/text.rb +34 -2
- data/lib/ratatui_ruby/scrollbar_state.rb +10 -2
- data/lib/ratatui_ruby/style/style.rb +18 -2
- data/lib/ratatui_ruby/style.rb +2 -2
- data/lib/ratatui_ruby/table_state.rb +10 -2
- data/lib/ratatui_ruby/terminal_lifecycle.rb +144 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +34 -2
- data/lib/ratatui_ruby/test_helper/snapshot.rb +74 -9
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +98 -2
- data/lib/ratatui_ruby/test_helper/terminal.rb +50 -2
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +18 -2
- data/lib/ratatui_ruby/test_helper.rb +10 -2
- data/lib/ratatui_ruby/tui/buffer_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/canvas_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/core.rb +2 -2
- data/lib/ratatui_ruby/tui/layout_factories.rb +32 -2
- data/lib/ratatui_ruby/tui/state_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/style_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/text_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/widget_factories.rb +2 -2
- data/lib/ratatui_ruby/tui.rb +11 -3
- data/lib/ratatui_ruby/version.rb +3 -3
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -2
- data/lib/ratatui_ruby/widgets/bar_chart.rb +58 -2
- data/lib/ratatui_ruby/widgets/block.rb +37 -15
- data/lib/ratatui_ruby/widgets/calendar.rb +2 -2
- data/lib/ratatui_ruby/widgets/canvas.rb +10 -2
- data/lib/ratatui_ruby/widgets/cell.rb +10 -2
- data/lib/ratatui_ruby/widgets/center.rb +10 -2
- data/lib/ratatui_ruby/widgets/chart.rb +2 -28
- data/lib/ratatui_ruby/widgets/clear.rb +10 -2
- data/lib/ratatui_ruby/widgets/cursor.rb +10 -2
- data/lib/ratatui_ruby/widgets/gauge.rb +16 -2
- data/lib/ratatui_ruby/widgets/line_gauge.rb +16 -2
- data/lib/ratatui_ruby/widgets/list.rb +41 -2
- data/lib/ratatui_ruby/widgets/list_item.rb +10 -2
- data/lib/ratatui_ruby/widgets/overlay.rb +10 -2
- data/lib/ratatui_ruby/widgets/paragraph.rb +10 -2
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -2
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -2
- data/lib/ratatui_ruby/widgets/row.rb +10 -2
- data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -2
- data/lib/ratatui_ruby/widgets/shape/label.rb +10 -2
- data/lib/ratatui_ruby/widgets/sparkline.rb +10 -2
- data/lib/ratatui_ruby/widgets/table.rb +62 -2
- data/lib/ratatui_ruby/widgets/tabs.rb +2 -2
- data/lib/ratatui_ruby/widgets.rb +2 -2
- data/lib/ratatui_ruby.rb +116 -81
- data/sig/examples/app_all_events/view.rbs +7 -1
- data/sig/examples/app_all_events/view_state.rbs +7 -1
- data/sig/examples/app_color_picker/app.rbs +5 -0
- data/sig/examples/app_stateful_interaction/app.rbs +7 -1
- data/sig/examples/verify_quickstart_dsl/app.rbs +7 -1
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +7 -1
- data/sig/examples/verify_readme_usage/app.rbs +7 -1
- data/sig/examples/widget_block_demo/app.rbs +6 -0
- data/sig/examples/widget_box_demo/app.rbs +7 -1
- data/sig/examples/widget_calendar_demo/app.rbs +7 -1
- data/sig/examples/widget_cell_demo/app.rbs +7 -1
- data/sig/examples/widget_chart_demo/app.rbs +7 -1
- data/sig/examples/widget_gauge_demo/app.rbs +7 -1
- data/sig/examples/widget_layout_split/app.rbs +7 -1
- data/sig/examples/widget_line_gauge_demo/app.rbs +7 -1
- data/sig/examples/widget_list_demo/app.rbs +5 -0
- data/sig/examples/widget_map_demo/app.rbs +7 -1
- data/sig/examples/widget_popup_demo/app.rbs +7 -1
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +7 -1
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +7 -1
- data/sig/examples/widget_rect/app.rbs +7 -1
- data/sig/examples/widget_render/app.rbs +7 -1
- data/sig/examples/widget_rich_text/app.rbs +7 -1
- data/sig/examples/widget_scroll_text/app.rbs +7 -1
- data/sig/examples/widget_scrollbar_demo/app.rbs +7 -1
- data/sig/examples/widget_sparkline_demo/app.rbs +7 -1
- data/sig/examples/widget_style_colors/app.rbs +7 -1
- data/sig/examples/widget_table_demo/app.rbs +7 -1
- data/sig/examples/widget_text_width/app.rbs +7 -1
- data/sig/ratatui_ruby/event.rbs +7 -1
- data/sig/ratatui_ruby/frame.rbs +15 -3
- data/sig/ratatui_ruby/list_state.rbs +11 -1
- data/sig/ratatui_ruby/ratatui_ruby.rbs +8 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +7 -1
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +6 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +6 -0
- data/sig/ratatui_ruby/schema/block.rbs +7 -1
- data/sig/ratatui_ruby/schema/calendar.rbs +6 -0
- data/sig/ratatui_ruby/schema/canvas.rbs +6 -0
- data/sig/ratatui_ruby/schema/center.rbs +6 -0
- data/sig/ratatui_ruby/schema/chart.rbs +6 -9
- data/sig/ratatui_ruby/schema/constraint.rbs +6 -0
- data/sig/ratatui_ruby/schema/cursor.rbs +6 -0
- data/sig/ratatui_ruby/schema/draw.rbs +6 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +9 -1
- data/sig/ratatui_ruby/schema/layout.rbs +6 -0
- data/sig/ratatui_ruby/schema/line_gauge.rbs +9 -1
- data/sig/ratatui_ruby/schema/list.rbs +9 -1
- data/sig/ratatui_ruby/schema/list_item.rbs +7 -1
- data/sig/ratatui_ruby/schema/overlay.rbs +6 -0
- data/sig/ratatui_ruby/schema/paragraph.rbs +6 -0
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +6 -0
- data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +5 -0
- data/sig/ratatui_ruby/schema/rect.rbs +30 -0
- data/sig/ratatui_ruby/schema/row.rbs +7 -1
- data/sig/ratatui_ruby/schema/scrollbar.rbs +6 -0
- data/sig/ratatui_ruby/schema/sparkline.rbs +6 -0
- data/sig/ratatui_ruby/schema/style.rbs +7 -1
- data/sig/ratatui_ruby/schema/table.rbs +11 -1
- data/sig/ratatui_ruby/schema/tabs.rbs +6 -0
- data/sig/ratatui_ruby/schema/text.rbs +7 -1
- data/sig/ratatui_ruby/scrollbar_state.rbs +7 -1
- data/sig/ratatui_ruby/session.rbs +7 -1
- data/sig/ratatui_ruby/table_state.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/terminal.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -1
- data/sig/ratatui_ruby/test_helper.rbs +7 -1
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/core.rbs +7 -1
- data/sig/ratatui_ruby/tui/layout_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/state_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/style_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/text_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/widget_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui.rbs +7 -1
- data/sig/ratatui_ruby/version.rbs +6 -0
- data/tasks/autodoc/examples.rb +9 -3
- data/tasks/autodoc/member.rb +1 -1
- data/tasks/autodoc/name.rb +1 -1
- data/tasks/bump/cargo_lockfile.rb +1 -1
- data/tasks/bump/changelog.rb +1 -1
- data/tasks/bump/header.rb +1 -1
- data/tasks/bump/history.rb +1 -1
- data/tasks/bump/links.rb +1 -1
- data/tasks/bump/manifest.rb +1 -1
- data/tasks/bump/ruby_gem.rb +1 -1
- data/tasks/bump/sem_ver.rb +1 -1
- data/tasks/bump/unreleased_section.rb +1 -1
- data/tasks/license/headers_md.rb +223 -0
- data/tasks/license/headers_rb.rb +210 -0
- data/tasks/license/license_utils.rb +130 -0
- data/tasks/license/snippets_md.rb +315 -0
- data/tasks/license/snippets_rdoc.rb +150 -0
- data/tasks/license.rake +91 -0
- data/tasks/rdoc_config.rb +1 -1
- data/tasks/resources/build.yml.erb +13 -7
- data/tasks/sourcehut.rake +3 -1
- data/tasks/terminal_preview/app_screenshot.rb +1 -1
- data/tasks/terminal_preview/crash_report.rb +1 -1
- data/tasks/terminal_preview/example_app.rb +1 -1
- data/tasks/terminal_preview/launcher_script.rb +1 -1
- data/tasks/terminal_preview/preview_collection.rb +1 -1
- data/tasks/terminal_preview/preview_timing.rb +1 -1
- data/tasks/terminal_preview/safety_confirmation.rb +1 -1
- data/tasks/terminal_preview/saved_screenshot.rb +1 -1
- data/tasks/terminal_preview/system_appearance.rb +1 -1
- data/tasks/terminal_preview/terminal_window.rb +1 -1
- data/tasks/terminal_preview/window_id.rb +1 -1
- data/tasks/website/index_page.rb +1 -1
- data/tasks/website/version.rb +1 -1
- data/tasks/website/version_menu.rb +1 -1
- data/tasks/website/versioned_documentation.rb +1 -1
- data/tasks/website/website.rb +1 -1
- metadata +15 -3
- data/doc/migration/v0_7_0.md +0 -236
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Custom Widgets
|
|
7
|
+
|
|
8
|
+
Build anything. Escape the widget library.
|
|
9
|
+
|
|
10
|
+
## What Terminals Offer
|
|
11
|
+
|
|
12
|
+
Terminals do not have pixels. They have character cells arranged in a grid. Each cell holds one character with foreground color, background color, and text modifiers (bold, italic, underline).
|
|
13
|
+
|
|
14
|
+
This constraint shapes what you can draw:
|
|
15
|
+
|
|
16
|
+
- **Characters**: Any Unicode character fits in a cell
|
|
17
|
+
- **Box-drawing**: Lines, corners, and boxes (`│`, `┌`, `─`, `└`)
|
|
18
|
+
- **Block elements**: Partial fills (`▀`, `▄`, `█`, `░`, `▒`, `▓`)
|
|
19
|
+
- **Braille patterns**: 2×4 "pixel" grids per cell for pseudo-graphics
|
|
20
|
+
- **Nerd Fonts**: Icons and glyphs if the user's font supports them
|
|
21
|
+
|
|
22
|
+
The built-in Canvas widget uses Braille patterns for line graphs and shapes. Custom widgets give you direct control over every cell.
|
|
23
|
+
|
|
24
|
+
## The Problem
|
|
25
|
+
|
|
26
|
+
Standard widgets handle common needs. Paragraphs display text. Lists show selections. Tables organize data.
|
|
27
|
+
|
|
28
|
+
But terminals can do more. You want a game board, a network graph, or a custom visualization. The built-in widgets cannot help you here.
|
|
29
|
+
|
|
30
|
+
## The Solution
|
|
31
|
+
|
|
32
|
+
Any Ruby object that implements `render(area)` works as a widget. You are not limited to what the library ships. Define a class. Implement one method. Pass it to `frame.render_widget`.
|
|
33
|
+
|
|
34
|
+
The Engine calls your `render` method with the area where your widget should draw. You return an array of Draw commands. The Engine executes them.
|
|
35
|
+
|
|
36
|
+
## The Contract
|
|
37
|
+
|
|
38
|
+
Your custom widget implements [the `_CustomWidget` interface](../../sig/ratatui_ruby/frame.rbs). The `area` parameter is a `Rect` with `x`, `y`, `width`, and `height`. It tells you where to draw and how much space you have.
|
|
39
|
+
|
|
40
|
+
## Draw Commands
|
|
41
|
+
|
|
42
|
+
Two commands describe what to draw:
|
|
43
|
+
|
|
44
|
+
| Command | Purpose |
|
|
45
|
+
|---------|---------|
|
|
46
|
+
| `Draw.string(x, y, text, style)` | Draw a styled string at absolute coordinates |
|
|
47
|
+
| `Draw.cell(x, y, cell)` | Draw a single cell (character + style) |
|
|
48
|
+
|
|
49
|
+
<!-- SPDX-SnippetBegin -->
|
|
50
|
+
<!--
|
|
51
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
52
|
+
SPDX-License-Identifier: MIT-0
|
|
53
|
+
-->
|
|
54
|
+
```ruby
|
|
55
|
+
class HelloWidget
|
|
56
|
+
def render(area)
|
|
57
|
+
[
|
|
58
|
+
RatatuiRuby::Draw.string(
|
|
59
|
+
area.x,
|
|
60
|
+
area.y,
|
|
61
|
+
"Hello, World!",
|
|
62
|
+
RatatuiRuby::Style::Style.new(fg: :green, modifiers: [:bold])
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
<!-- SPDX-SnippetEnd -->
|
|
69
|
+
|
|
70
|
+
## Coordinate Offsets
|
|
71
|
+
|
|
72
|
+
The `area.x` and `area.y` values are not always zero. When your widget renders inside a `Block` with borders, or within a nested layout, the area's origin shifts.
|
|
73
|
+
|
|
74
|
+
Always add `area.x` and `area.y` to your drawing coordinates. This pattern ensures your widget works regardless of where it appears on screen.
|
|
75
|
+
|
|
76
|
+
<!-- SPDX-SnippetBegin -->
|
|
77
|
+
<!--
|
|
78
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
79
|
+
SPDX-License-Identifier: MIT-0
|
|
80
|
+
-->
|
|
81
|
+
```ruby
|
|
82
|
+
class DiagonalWidget
|
|
83
|
+
def render(area)
|
|
84
|
+
(0...area.height).filter_map do |i|
|
|
85
|
+
next if i >= area.width # Stay within bounds
|
|
86
|
+
|
|
87
|
+
RatatuiRuby::Draw.string(
|
|
88
|
+
area.x + i, # Offset from area origin
|
|
89
|
+
area.y + i,
|
|
90
|
+
"\\",
|
|
91
|
+
RatatuiRuby::Style::Style.new(fg: :red)
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
<!-- SPDX-SnippetEnd -->
|
|
98
|
+
|
|
99
|
+
## Composability
|
|
100
|
+
|
|
101
|
+
Custom widgets compose with standard widgets. Wrap them in Blocks. Place them in layouts. Mix them with Paragraphs and Lists.
|
|
102
|
+
|
|
103
|
+
<!-- SPDX-SnippetBegin -->
|
|
104
|
+
<!--
|
|
105
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
106
|
+
SPDX-License-Identifier: MIT-0
|
|
107
|
+
-->
|
|
108
|
+
```ruby
|
|
109
|
+
RatatuiRuby.run do |tui|
|
|
110
|
+
tui.draw do |frame|
|
|
111
|
+
areas = tui.layout_split(
|
|
112
|
+
frame.area,
|
|
113
|
+
direction: :horizontal,
|
|
114
|
+
constraints: [tui.constraint_percentage(50), tui.constraint_percentage(50)]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Standard widget on the left
|
|
118
|
+
frame.render_widget(tui.paragraph(text: "Standard"), areas[0])
|
|
119
|
+
|
|
120
|
+
# Custom widget on the right
|
|
121
|
+
frame.render_widget(DiagonalWidget.new, areas[1])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
<!-- SPDX-SnippetEnd -->
|
|
126
|
+
|
|
127
|
+
To render inside a bordered Block, calculate the inner area first:
|
|
128
|
+
|
|
129
|
+
<!-- SPDX-SnippetBegin -->
|
|
130
|
+
<!--
|
|
131
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
132
|
+
SPDX-License-Identifier: MIT-0
|
|
133
|
+
-->
|
|
134
|
+
```ruby
|
|
135
|
+
tui.draw do |frame|
|
|
136
|
+
# Render the block frame
|
|
137
|
+
block = tui.block(title: "Custom", borders: [:all])
|
|
138
|
+
frame.render_widget(block, frame.area)
|
|
139
|
+
|
|
140
|
+
# Calculate inner area (1-cell border on all sides)
|
|
141
|
+
inner = tui.rect(
|
|
142
|
+
x: frame.area.x + 1,
|
|
143
|
+
y: frame.area.y + 1,
|
|
144
|
+
width: [frame.area.width - 2, 0].max,
|
|
145
|
+
height: [frame.area.height - 2, 0].max
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Render custom widget inside
|
|
149
|
+
frame.render_widget(MyWidget.new, inner)
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
<!-- SPDX-SnippetEnd -->
|
|
153
|
+
|
|
154
|
+
## Using Custom Widgets in Layouts
|
|
155
|
+
|
|
156
|
+
Custom widgets work as children in Layout trees. The layout system passes the calculated area to your `render` method.
|
|
157
|
+
|
|
158
|
+
<!-- SPDX-SnippetBegin -->
|
|
159
|
+
<!--
|
|
160
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
161
|
+
SPDX-License-Identifier: MIT-0
|
|
162
|
+
-->
|
|
163
|
+
```ruby
|
|
164
|
+
layout = RatatuiRuby::Layout::Layout.new(
|
|
165
|
+
direction: :vertical,
|
|
166
|
+
constraints: [
|
|
167
|
+
RatatuiRuby::Layout::Constraint.length(1),
|
|
168
|
+
RatatuiRuby::Layout::Constraint.fill(1),
|
|
169
|
+
],
|
|
170
|
+
children: [
|
|
171
|
+
RatatuiRuby::Widgets::Paragraph.new(text: "Header"),
|
|
172
|
+
MyCustomWidget.new, # Your widget here
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
RatatuiRuby.draw(layout)
|
|
177
|
+
```
|
|
178
|
+
<!-- SPDX-SnippetEnd -->
|
|
179
|
+
|
|
180
|
+
## Testing Custom Widgets
|
|
181
|
+
|
|
182
|
+
Custom widgets return arrays. Test them by calling `render` directly and asserting on the result.
|
|
183
|
+
|
|
184
|
+
<!-- SPDX-SnippetBegin -->
|
|
185
|
+
<!--
|
|
186
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
187
|
+
SPDX-License-Identifier: MIT-0
|
|
188
|
+
-->
|
|
189
|
+
```ruby
|
|
190
|
+
def test_hello_widget_output
|
|
191
|
+
area = RatatuiRuby::Rect.new(x: 0, y: 0, width: 20, height: 5)
|
|
192
|
+
widget = HelloWidget.new
|
|
193
|
+
commands = widget.render(area)
|
|
194
|
+
|
|
195
|
+
assert_equal 1, commands.length
|
|
196
|
+
assert_equal 0, commands[0].x
|
|
197
|
+
assert_equal 0, commands[0].y
|
|
198
|
+
assert_equal "Hello, World!", commands[0].string
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
<!-- SPDX-SnippetEnd -->
|
|
202
|
+
|
|
203
|
+
For visual testing, use the test helper to render to a buffer and assert on content:
|
|
204
|
+
|
|
205
|
+
<!-- SPDX-SnippetBegin -->
|
|
206
|
+
<!--
|
|
207
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
208
|
+
SPDX-License-Identifier: MIT-0
|
|
209
|
+
-->
|
|
210
|
+
```ruby
|
|
211
|
+
class TestMyWidget < Minitest::Test
|
|
212
|
+
include RatatuiRuby::TestHelper
|
|
213
|
+
|
|
214
|
+
def test_renders_in_terminal
|
|
215
|
+
with_test_terminal(10, 5) do
|
|
216
|
+
RatatuiRuby.draw(MyWidget.new)
|
|
217
|
+
assert_equal "Expected ", buffer_content[0]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
<!-- SPDX-SnippetEnd -->
|
|
223
|
+
|
|
224
|
+
## Typing Your Widgets (RBS)
|
|
225
|
+
|
|
226
|
+
Type your custom widgets by implementing the `_CustomWidget` interface:
|
|
227
|
+
|
|
228
|
+
<!-- SPDX-SnippetBegin -->
|
|
229
|
+
<!--
|
|
230
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
231
|
+
SPDX-License-Identifier: MIT-0
|
|
232
|
+
-->
|
|
233
|
+
```rbs
|
|
234
|
+
# my_widget.rbs
|
|
235
|
+
class MyWidget
|
|
236
|
+
def render: (RatatuiRuby::Rect area) -> Array[RatatuiRuby::Draw::StringCmd | RatatuiRuby::Draw::CellCmd]
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
<!-- SPDX-SnippetEnd -->
|
|
240
|
+
|
|
241
|
+
The interface uses structural typing. Any class with a matching `render` signature satisfies it.
|
|
242
|
+
|
|
243
|
+
## Related Resources
|
|
244
|
+
|
|
245
|
+
- [Custom Render Example](../examples/widget_render/README.md) — Full working example
|
|
246
|
+
- [Cell Example](../examples/widget_cell/README.md) — Low-level cell drawing
|
|
247
|
+
- [Application Testing](./application_testing.md) — Test helper reference
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
3
|
-
|
|
4
|
-
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
4
|
-->
|
|
6
5
|
|
|
7
6
|
# Event Handling
|
|
@@ -30,6 +29,11 @@ For simple key events, `RatatuiRuby::Event::Key` objects can be compared directl
|
|
|
30
29
|
|
|
31
30
|
For a complete list of supported keys, modifiers, and event types, please refer to the [API Documentation for RatatuiRuby::Event](file:///Users/kerrick/Developer/ratatui_ruby/lib/ratatui_ruby/event.rb).
|
|
32
31
|
|
|
32
|
+
<!-- SPDX-SnippetBegin -->
|
|
33
|
+
<!--
|
|
34
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
35
|
+
SPDX-License-Identifier: MIT-0
|
|
36
|
+
-->
|
|
33
37
|
```ruby
|
|
34
38
|
event = RatatuiRuby.poll_event
|
|
35
39
|
|
|
@@ -43,6 +47,7 @@ if event == :enter
|
|
|
43
47
|
submit_form
|
|
44
48
|
end
|
|
45
49
|
```
|
|
50
|
+
<!-- SPDX-SnippetEnd -->
|
|
46
51
|
|
|
47
52
|
## 3. Predicate Methods (Intermediate)
|
|
48
53
|
|
|
@@ -54,6 +59,11 @@ Safe to call on *any* event object. They return `true` only for the matching eve
|
|
|
54
59
|
|
|
55
60
|
Available: `key?`, `mouse?`, `resize?`, `paste?`, `focus_gained?`, `focus_lost?`.
|
|
56
61
|
|
|
62
|
+
<!-- SPDX-SnippetBegin -->
|
|
63
|
+
<!--
|
|
64
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
65
|
+
SPDX-License-Identifier: MIT-0
|
|
66
|
+
-->
|
|
57
67
|
```ruby
|
|
58
68
|
event = RatatuiRuby.poll_event
|
|
59
69
|
|
|
@@ -65,6 +75,7 @@ elsif event.resize?
|
|
|
65
75
|
resize_layout(event.width, event.height)
|
|
66
76
|
end
|
|
67
77
|
```
|
|
78
|
+
<!-- SPDX-SnippetEnd -->
|
|
68
79
|
|
|
69
80
|
### Helper Predicates
|
|
70
81
|
|
|
@@ -74,26 +85,43 @@ Specific to certain event classes to simplify checks.
|
|
|
74
85
|
* `ctrl?`, `alt?`, `shift?`: Check if modifier is held.
|
|
75
86
|
* `text?`: Returns `true` if the event is a printable character (length == 1).
|
|
76
87
|
|
|
88
|
+
<!-- SPDX-SnippetBegin -->
|
|
89
|
+
<!--
|
|
90
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
91
|
+
SPDX-License-Identifier: MIT-0
|
|
92
|
+
-->
|
|
77
93
|
```ruby
|
|
78
94
|
if event.key? && event.ctrl? && event.code == "s"
|
|
79
95
|
save_file
|
|
80
96
|
end
|
|
81
97
|
```
|
|
98
|
+
<!-- SPDX-SnippetEnd -->
|
|
82
99
|
|
|
83
100
|
#### `RatatuiRuby::Event::Mouse`
|
|
84
101
|
* `down?`, `up?`, `drag?`: Check mouse action.
|
|
85
102
|
* `scroll_up?`, `scroll_down?`: Check scroll direction.
|
|
86
103
|
|
|
104
|
+
<!-- SPDX-SnippetBegin -->
|
|
105
|
+
<!--
|
|
106
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
107
|
+
SPDX-License-Identifier: MIT-0
|
|
108
|
+
-->
|
|
87
109
|
```ruby
|
|
88
110
|
if event.mouse? && event.scroll_up?
|
|
89
111
|
scroll_view(-1)
|
|
90
112
|
end
|
|
91
113
|
```
|
|
114
|
+
<!-- SPDX-SnippetEnd -->
|
|
92
115
|
|
|
93
116
|
## 4. Pattern Matching (Powerful)
|
|
94
117
|
|
|
95
118
|
For complex applications, Ruby 3.0+ Pattern Matching with the `type:` discriminator is the most idiomatic and concise approach.
|
|
96
119
|
|
|
120
|
+
<!-- SPDX-SnippetBegin -->
|
|
121
|
+
<!--
|
|
122
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
123
|
+
SPDX-License-Identifier: MIT-0
|
|
124
|
+
-->
|
|
97
125
|
```ruby
|
|
98
126
|
loop do
|
|
99
127
|
case RatatuiRuby.poll_event
|
|
@@ -119,6 +147,7 @@ loop do
|
|
|
119
147
|
end
|
|
120
148
|
end
|
|
121
149
|
```
|
|
150
|
+
<!-- SPDX-SnippetEnd -->
|
|
122
151
|
|
|
123
152
|
## Summary of Event Classes
|
|
124
153
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
# Interactive TUI Design Patterns
|
|
@@ -19,6 +19,11 @@ Canonical patterns for building responsive, interactive terminal user interfaces
|
|
|
19
19
|
|
|
20
20
|
Structure your event loop into three clear phases:
|
|
21
21
|
|
|
22
|
+
<!-- SPDX-SnippetBegin -->
|
|
23
|
+
<!--
|
|
24
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
25
|
+
SPDX-License-Identifier: MIT-0
|
|
26
|
+
-->
|
|
22
27
|
```ruby
|
|
23
28
|
def run
|
|
24
29
|
RatatuiRuby.run do |tui|
|
|
@@ -33,11 +38,17 @@ def run
|
|
|
33
38
|
end
|
|
34
39
|
end
|
|
35
40
|
```
|
|
41
|
+
<!-- SPDX-SnippetEnd -->
|
|
36
42
|
|
|
37
43
|
**Phase 1: Layout Calculation**
|
|
38
44
|
|
|
39
45
|
Call this inside your `draw` block. It uses the current terminal area provided by the frame:
|
|
40
46
|
|
|
47
|
+
<!-- SPDX-SnippetBegin -->
|
|
48
|
+
<!--
|
|
49
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
50
|
+
SPDX-License-Identifier: MIT-0
|
|
51
|
+
-->
|
|
41
52
|
```ruby
|
|
42
53
|
def calculate_layout(area)
|
|
43
54
|
# Main area vs sidebar (70% / 30%)
|
|
@@ -61,22 +72,34 @@ def calculate_layout(area)
|
|
|
61
72
|
)
|
|
62
73
|
end
|
|
63
74
|
```
|
|
75
|
+
<!-- SPDX-SnippetEnd -->
|
|
64
76
|
|
|
65
77
|
**Phase 2: Rendering**
|
|
66
78
|
|
|
67
79
|
Reuse the cached rects. Build and draw:
|
|
68
80
|
|
|
81
|
+
<!-- SPDX-SnippetBegin -->
|
|
82
|
+
<!--
|
|
83
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
84
|
+
SPDX-License-Identifier: MIT-0
|
|
85
|
+
-->
|
|
69
86
|
```ruby
|
|
70
87
|
def render(frame)
|
|
71
88
|
frame.render_widget(build_widget(@left_rect), @left_rect)
|
|
72
89
|
frame.render_widget(build_widget(@right_rect), @right_rect)
|
|
73
90
|
end
|
|
74
91
|
```
|
|
92
|
+
<!-- SPDX-SnippetEnd -->
|
|
75
93
|
|
|
76
94
|
**Phase 3: Event Handling**
|
|
77
95
|
|
|
78
96
|
Reuse the cached rects. Test clicks:
|
|
79
97
|
|
|
98
|
+
<!-- SPDX-SnippetBegin -->
|
|
99
|
+
<!--
|
|
100
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
101
|
+
SPDX-License-Identifier: MIT-0
|
|
102
|
+
-->
|
|
80
103
|
```ruby
|
|
81
104
|
def handle_input
|
|
82
105
|
event = RatatuiRuby.poll_event
|
|
@@ -93,6 +116,7 @@ def handle_input
|
|
|
93
116
|
end
|
|
94
117
|
end
|
|
95
118
|
```
|
|
119
|
+
<!-- SPDX-SnippetEnd -->
|
|
96
120
|
|
|
97
121
|
### Why This Matters
|
|
98
122
|
|
|
@@ -105,6 +129,11 @@ end
|
|
|
105
129
|
|
|
106
130
|
`Layout.split` computes layout geometry without rendering. It returns an array of `Rect` objects. While you can call `RatatuiRuby::Layout.split` directly, we recommend using the `TUI` helper (`tui.layout_split`) for cleaner application code.
|
|
107
131
|
|
|
132
|
+
<!-- SPDX-SnippetBegin -->
|
|
133
|
+
<!--
|
|
134
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
135
|
+
SPDX-License-Identifier: MIT-0
|
|
136
|
+
-->
|
|
108
137
|
```ruby
|
|
109
138
|
# Preferred (TUI API)
|
|
110
139
|
left, right = tui.layout_split(area, constraints: [...])
|
|
@@ -112,5 +141,6 @@ left, right = tui.layout_split(area, constraints: [...])
|
|
|
112
141
|
# Manual (Core API)
|
|
113
142
|
left, right = RatatuiRuby::Layout.split(area, constraints: [...])
|
|
114
143
|
```
|
|
144
|
+
<!-- SPDX-SnippetEnd -->
|
|
115
145
|
|
|
116
146
|
Use it to establish the single source of truth inside your `draw` block. Store the results in instance variables and reuse them in both `render` and `handle_input`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
3
|
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
4
|
-->
|
|
5
5
|
|
|
@@ -32,6 +32,11 @@ Every audit begins with a question:
|
|
|
32
32
|
|
|
33
33
|
Every RatatuiRuby feature has three layers. Gaps can occur at any layer:
|
|
34
34
|
|
|
35
|
+
<!-- SPDX-SnippetBegin -->
|
|
36
|
+
<!--
|
|
37
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
38
|
+
SPDX-License-Identifier: MIT-0
|
|
39
|
+
-->
|
|
35
40
|
```
|
|
36
41
|
┌─────────────────────────────┐
|
|
37
42
|
│ Ruby API (lib/**/*.rb) │ ← What users see
|
|
@@ -41,6 +46,7 @@ Every RatatuiRuby feature has three layers. Gaps can occur at any layer:
|
|
|
41
46
|
│ Upstream Ratatui │ ← Source of truth
|
|
42
47
|
└─────────────────────────────┘
|
|
43
48
|
```
|
|
49
|
+
<!-- SPDX-SnippetEnd -->
|
|
44
50
|
|
|
45
51
|
### Layer 1: Ruby API Gaps
|
|
46
52
|
Ruby doesn't expose a parameter that upstream supports.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
3
|
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
4
|
-->
|
|
5
5
|
|
|
@@ -39,11 +39,17 @@ Located in `lib/ratatui_ruby/widgets/`, `lib/ratatui_ruby/layout/`, etc.
|
|
|
39
39
|
|
|
40
40
|
These are the actual `Data.define` classes that the Rust backend expects. They have deep, explicit namespaces that match Ratatui:
|
|
41
41
|
|
|
42
|
+
<!-- SPDX-SnippetBegin -->
|
|
43
|
+
<!--
|
|
44
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
45
|
+
SPDX-License-Identifier: MIT-0
|
|
46
|
+
-->
|
|
42
47
|
```ruby
|
|
43
48
|
RatatuiRuby::Widgets::Paragraph.new(text: "Hello")
|
|
44
49
|
RatatuiRuby::Layout::Constraint.length(20)
|
|
45
50
|
RatatuiRuby::Style::Style.new(fg: :red)
|
|
46
51
|
```
|
|
52
|
+
<!-- SPDX-SnippetEnd -->
|
|
47
53
|
|
|
48
54
|
**Layer 2: TUI Facade (The DSL)**
|
|
49
55
|
|
|
@@ -51,6 +57,11 @@ Located in `lib/ratatui_ruby/tui.rb` and `lib/ratatui_ruby/tui/*.rb` mixins.
|
|
|
51
57
|
|
|
52
58
|
The `TUI` class provides shorthand factory methods that hide namespace verbosity:
|
|
53
59
|
|
|
60
|
+
<!-- SPDX-SnippetBegin -->
|
|
61
|
+
<!--
|
|
62
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
63
|
+
SPDX-License-Identifier: MIT-0
|
|
64
|
+
-->
|
|
54
65
|
```ruby
|
|
55
66
|
RatatuiRuby.run do |tui|
|
|
56
67
|
tui.paragraph(text: "Hello")
|
|
@@ -58,6 +69,7 @@ RatatuiRuby.run do |tui|
|
|
|
58
69
|
tui.style(fg: :red)
|
|
59
70
|
end
|
|
60
71
|
```
|
|
72
|
+
<!-- SPDX-SnippetEnd -->
|
|
61
73
|
|
|
62
74
|
**Why This Matters:**
|
|
63
75
|
|
|
@@ -69,6 +81,11 @@ The TUI facade uses explicit factory method definitions, not runtime metaprogram
|
|
|
69
81
|
|
|
70
82
|
**What We Do:**
|
|
71
83
|
|
|
84
|
+
<!-- SPDX-SnippetBegin -->
|
|
85
|
+
<!--
|
|
86
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
87
|
+
SPDX-License-Identifier: MIT-0
|
|
88
|
+
-->
|
|
72
89
|
```ruby
|
|
73
90
|
# lib/ratatui_ruby/tui/widget_factories.rb
|
|
74
91
|
module RatatuiRuby
|
|
@@ -85,15 +102,22 @@ module RatatuiRuby
|
|
|
85
102
|
end
|
|
86
103
|
end
|
|
87
104
|
```
|
|
105
|
+
<!-- SPDX-SnippetEnd -->
|
|
88
106
|
|
|
89
107
|
**What We Don't Do:**
|
|
90
108
|
|
|
109
|
+
<!-- SPDX-SnippetBegin -->
|
|
110
|
+
<!--
|
|
111
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
112
|
+
SPDX-License-Identifier: MIT-0
|
|
113
|
+
-->
|
|
91
114
|
```ruby
|
|
92
115
|
# NO: Dynamic method generation
|
|
93
116
|
RatatuiRuby.constants.each do |const|
|
|
94
117
|
define_method(const.underscore) { |**kw| RatatuiRuby.const_get(const).new(**kw) }
|
|
95
118
|
end
|
|
96
119
|
```
|
|
120
|
+
<!-- SPDX-SnippetEnd -->
|
|
97
121
|
|
|
98
122
|
**Benefits of Explicit Definitions:**
|
|
99
123
|
|
|
@@ -109,6 +133,11 @@ All UI components are pure, immutable `Data.define` value objects. They describe
|
|
|
109
133
|
|
|
110
134
|
**Widgets Are Inputs:**
|
|
111
135
|
|
|
136
|
+
<!-- SPDX-SnippetBegin -->
|
|
137
|
+
<!--
|
|
138
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
139
|
+
SPDX-License-Identifier: MIT-0
|
|
140
|
+
-->
|
|
112
141
|
```ruby
|
|
113
142
|
# This is just data. It has no behavior, no side effects.
|
|
114
143
|
paragraph = RatatuiRuby::Widgets::Paragraph.new(
|
|
@@ -119,11 +148,17 @@ paragraph = RatatuiRuby::Widgets::Paragraph.new(
|
|
|
119
148
|
# Pass to renderer as input
|
|
120
149
|
frame.render_widget(paragraph, area)
|
|
121
150
|
```
|
|
151
|
+
<!-- SPDX-SnippetEnd -->
|
|
122
152
|
|
|
123
153
|
**Immediate Mode Loop:**
|
|
124
154
|
|
|
125
155
|
Every frame, the application constructs a fresh view tree and passes it to `draw`. No widget state persists between frames. This is Ratatui's core paradigm.
|
|
126
156
|
|
|
157
|
+
<!-- SPDX-SnippetBegin -->
|
|
158
|
+
<!--
|
|
159
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
160
|
+
SPDX-License-Identifier: MIT-0
|
|
161
|
+
-->
|
|
127
162
|
```ruby
|
|
128
163
|
loop do
|
|
129
164
|
tui.draw do |frame|
|
|
@@ -133,6 +168,7 @@ loop do
|
|
|
133
168
|
break if tui.poll_event.key? && tui.poll_event.code == "q"
|
|
134
169
|
end
|
|
135
170
|
```
|
|
171
|
+
<!-- SPDX-SnippetEnd -->
|
|
136
172
|
|
|
137
173
|
### 5. Separation of Configuration and Status
|
|
138
174
|
|
|
@@ -142,14 +178,25 @@ Widgets (Configuration) and State (Status) are strictly separated.
|
|
|
142
178
|
|
|
143
179
|
Widgets define *what* to render. They are created, rendered, and discarded.
|
|
144
180
|
|
|
181
|
+
<!-- SPDX-SnippetBegin -->
|
|
182
|
+
<!--
|
|
183
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
184
|
+
SPDX-License-Identifier: MIT-0
|
|
185
|
+
-->
|
|
145
186
|
```ruby
|
|
146
187
|
list = tui.list(items: ["A", "B", "C", "D", "E"])
|
|
147
188
|
```
|
|
189
|
+
<!-- SPDX-SnippetEnd -->
|
|
148
190
|
|
|
149
191
|
**Status (Output):**
|
|
150
192
|
|
|
151
193
|
State objects track *runtime metrics* computed by the Rust backend: scroll offsets, selection positions, etc. They persist across frames.
|
|
152
194
|
|
|
195
|
+
<!-- SPDX-SnippetBegin -->
|
|
196
|
+
<!--
|
|
197
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
198
|
+
SPDX-License-Identifier: MIT-0
|
|
199
|
+
-->
|
|
153
200
|
```ruby
|
|
154
201
|
# Created once
|
|
155
202
|
@list_state = RatatuiRuby::ListState.new
|
|
@@ -160,6 +207,7 @@ frame.render_stateful_widget(list, area, @list_state)
|
|
|
160
207
|
# Read back computed values
|
|
161
208
|
puts "Scroll offset: #{@list_state.offset}"
|
|
162
209
|
```
|
|
210
|
+
<!-- SPDX-SnippetEnd -->
|
|
163
211
|
|
|
164
212
|
**Precedence Rule:**
|
|
165
213
|
|
|
@@ -188,6 +236,11 @@ This separation ensures rendering performance remains in Rust while Ruby handles
|
|
|
188
236
|
|
|
189
237
|
## Directory Structure
|
|
190
238
|
|
|
239
|
+
<!-- SPDX-SnippetBegin -->
|
|
240
|
+
<!--
|
|
241
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
242
|
+
SPDX-License-Identifier: MIT-0
|
|
243
|
+
-->
|
|
191
244
|
```
|
|
192
245
|
lib/ratatui_ruby/
|
|
193
246
|
├── tui.rb # TUI class, includes all mixins
|
|
@@ -221,6 +274,7 @@ lib/ratatui_ruby/
|
|
|
221
274
|
│ └── cell.rb # For get_cell_at inspection
|
|
222
275
|
└── schema/ # Legacy location (being migrated)
|
|
223
276
|
```
|
|
277
|
+
<!-- SPDX-SnippetEnd -->
|
|
224
278
|
|
|
225
279
|
---
|
|
226
280
|
|
|
@@ -230,6 +284,11 @@ lib/ratatui_ruby/
|
|
|
230
284
|
|
|
231
285
|
Define the Data class in the appropriate namespace directory:
|
|
232
286
|
|
|
287
|
+
<!-- SPDX-SnippetBegin -->
|
|
288
|
+
<!--
|
|
289
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
290
|
+
SPDX-License-Identifier: MIT-0
|
|
291
|
+
-->
|
|
233
292
|
```ruby
|
|
234
293
|
# lib/ratatui_ruby/widgets/my_widget.rb
|
|
235
294
|
module RatatuiRuby
|
|
@@ -247,9 +306,15 @@ module RatatuiRuby
|
|
|
247
306
|
end
|
|
248
307
|
end
|
|
249
308
|
```
|
|
309
|
+
<!-- SPDX-SnippetEnd -->
|
|
250
310
|
|
|
251
311
|
### Step 2: Add the RBS Type
|
|
252
312
|
|
|
313
|
+
<!-- SPDX-SnippetBegin -->
|
|
314
|
+
<!--
|
|
315
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
316
|
+
SPDX-License-Identifier: MIT-0
|
|
317
|
+
-->
|
|
253
318
|
```rbs
|
|
254
319
|
# sig/ratatui_ruby/widgets/my_widget.rbs
|
|
255
320
|
module RatatuiRuby
|
|
@@ -264,15 +329,22 @@ module RatatuiRuby
|
|
|
264
329
|
end
|
|
265
330
|
end
|
|
266
331
|
```
|
|
332
|
+
<!-- SPDX-SnippetEnd -->
|
|
267
333
|
|
|
268
334
|
### Step 3: Add the TUI Factory Method
|
|
269
335
|
|
|
336
|
+
<!-- SPDX-SnippetBegin -->
|
|
337
|
+
<!--
|
|
338
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
339
|
+
SPDX-License-Identifier: MIT-0
|
|
340
|
+
-->
|
|
270
341
|
```ruby
|
|
271
342
|
# lib/ratatui_ruby/tui/widget_factories.rb
|
|
272
343
|
def my_widget(**kwargs)
|
|
273
344
|
Widgets::MyWidget.new(**kwargs)
|
|
274
345
|
end
|
|
275
346
|
```
|
|
347
|
+
<!-- SPDX-SnippetEnd -->
|
|
276
348
|
|
|
277
349
|
### Step 4: Implement Rust Rendering
|
|
278
350
|
|
|
@@ -282,9 +354,15 @@ See `rust_backend.md` for the Rust implementation steps.
|
|
|
282
354
|
|
|
283
355
|
Add to `lib/ratatui_ruby.rb`:
|
|
284
356
|
|
|
357
|
+
<!-- SPDX-SnippetBegin -->
|
|
358
|
+
<!--
|
|
359
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
360
|
+
SPDX-License-Identifier: MIT-0
|
|
361
|
+
-->
|
|
285
362
|
```ruby
|
|
286
363
|
require_relative "ratatui_ruby/widgets/my_widget"
|
|
287
364
|
```
|
|
365
|
+
<!-- SPDX-SnippetEnd -->
|
|
288
366
|
|
|
289
367
|
---
|
|
290
368
|
|
|
@@ -324,6 +402,11 @@ These have side effects and are intentionally not Ractor-safe:
|
|
|
324
402
|
- `TUI` — Has terminal I/O methods
|
|
325
403
|
- `Frame` — Valid only during the `draw` block; invalid after
|
|
326
404
|
|
|
405
|
+
<!-- SPDX-SnippetBegin -->
|
|
406
|
+
<!--
|
|
407
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
408
|
+
SPDX-License-Identifier: MIT-0
|
|
409
|
+
-->
|
|
327
410
|
```ruby
|
|
328
411
|
# OK: Cache TUI during run loop
|
|
329
412
|
RatatuiRuby.run do |tui|
|
|
@@ -334,3 +417,4 @@ end
|
|
|
334
417
|
# NOT OK: Include in immutable Model
|
|
335
418
|
Model = Data.define(:tui, :count) # Don't do this
|
|
336
419
|
```
|
|
420
|
+
<!-- SPDX-SnippetEnd -->
|