ratatui_ruby 0.8.0 → 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 +53 -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 +22 -4
- 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 +42 -0
- 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 +6 -0
- data/examples/verify_quickstart_lifecycle/app.rb +3 -3
- 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 +26 -3
- 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 +18 -3
- 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 +90 -2
- 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 +1 -1
- 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 +13 -3
- data/doc/migration/v0_7_0.md +0 -236
|
@@ -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
|
# Application Architecture
|
|
@@ -24,6 +23,11 @@ Terminals have state. They remember cursor positions, input modes, and screen bu
|
|
|
24
23
|
|
|
25
24
|
This method acts as a safety net. It initializes the terminal, yields control to your block, and restores the terminal afterwards—even if your code raises an exception.
|
|
26
25
|
|
|
26
|
+
<!-- SPDX-SnippetBegin -->
|
|
27
|
+
<!--
|
|
28
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
29
|
+
SPDX-License-Identifier: MIT-0
|
|
30
|
+
-->
|
|
27
31
|
```ruby
|
|
28
32
|
RatatuiRuby.run do |tui|
|
|
29
33
|
loop do
|
|
@@ -35,11 +39,17 @@ RatatuiRuby.run do |tui|
|
|
|
35
39
|
end
|
|
36
40
|
# Terminal is restored here
|
|
37
41
|
```
|
|
42
|
+
<!-- SPDX-SnippetEnd -->
|
|
38
43
|
|
|
39
44
|
#### Manual Management
|
|
40
45
|
|
|
41
46
|
Need granular control? You can initialize and restore the terminal yourself. Use `ensure` blocks to guarantee cleanup.
|
|
42
47
|
|
|
48
|
+
<!-- SPDX-SnippetBegin -->
|
|
49
|
+
<!--
|
|
50
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
51
|
+
SPDX-License-Identifier: MIT-0
|
|
52
|
+
-->
|
|
43
53
|
```ruby
|
|
44
54
|
RatatuiRuby.init_terminal
|
|
45
55
|
begin
|
|
@@ -51,6 +61,7 @@ ensure
|
|
|
51
61
|
# Terminal is restored here
|
|
52
62
|
end
|
|
53
63
|
```
|
|
64
|
+
<!-- SPDX-SnippetEnd -->
|
|
54
65
|
|
|
55
66
|
#### Signal Handling
|
|
56
67
|
|
|
@@ -69,6 +80,11 @@ External processes send signals. Your TUI must handle them gracefully.
|
|
|
69
80
|
> [!IMPORTANT]
|
|
70
81
|
> **Ctrl+C in Raw Mode:** When your app is in raw mode, pressing Ctrl+C does *not* send SIGINT. It's captured as a `:ctrl_c` key event. Handle this in your event loop—don't use `trap("INT")`.
|
|
71
82
|
|
|
83
|
+
<!-- SPDX-SnippetBegin -->
|
|
84
|
+
<!--
|
|
85
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
86
|
+
SPDX-License-Identifier: MIT-0
|
|
87
|
+
-->
|
|
72
88
|
```ruby
|
|
73
89
|
RatatuiRuby.run do |tui|
|
|
74
90
|
loop do
|
|
@@ -78,6 +94,7 @@ RatatuiRuby.run do |tui|
|
|
|
78
94
|
end
|
|
79
95
|
end
|
|
80
96
|
```
|
|
97
|
+
<!-- SPDX-SnippetEnd -->
|
|
81
98
|
|
|
82
99
|
**Recovery:** If a TUI app leaves your terminal broken, run `reset` in the shell to restore normal behavior.
|
|
83
100
|
|
|
@@ -97,6 +114,11 @@ Most widgets are stateless configuration. You create them, render them, and they
|
|
|
97
114
|
|
|
98
115
|
**Use Case:** When you need to read back the scroll offset (e.g., for mouse hit testing) or persist selection without managing indexes manually.
|
|
99
116
|
|
|
117
|
+
<!-- SPDX-SnippetBegin -->
|
|
118
|
+
<!--
|
|
119
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
120
|
+
SPDX-License-Identifier: MIT-0
|
|
121
|
+
-->
|
|
100
122
|
```ruby
|
|
101
123
|
# Initialize state once
|
|
102
124
|
@list_state = RatatuiRuby::ListState.new
|
|
@@ -116,6 +138,7 @@ RatatuiRuby.run do |tui|
|
|
|
116
138
|
end
|
|
117
139
|
end
|
|
118
140
|
```
|
|
141
|
+
<!-- SPDX-SnippetEnd -->
|
|
119
142
|
|
|
120
143
|
### API Convenience
|
|
121
144
|
|
|
@@ -125,6 +148,11 @@ Writing UI trees involves nesting many widgets.
|
|
|
125
148
|
|
|
126
149
|
**The Solution:** The TUI API (`tui`) provides shorthand factories for every widget. It yields a TUI object to your block.
|
|
127
150
|
|
|
151
|
+
<!-- SPDX-SnippetBegin -->
|
|
152
|
+
<!--
|
|
153
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
154
|
+
SPDX-License-Identifier: MIT-0
|
|
155
|
+
-->
|
|
128
156
|
```ruby
|
|
129
157
|
RatatuiRuby.run do |tui|
|
|
130
158
|
loop do
|
|
@@ -167,11 +195,17 @@ RatatuiRuby.run do |tui|
|
|
|
167
195
|
end
|
|
168
196
|
end
|
|
169
197
|
```
|
|
198
|
+
<!-- SPDX-SnippetEnd -->
|
|
170
199
|
|
|
171
200
|
#### Raw API
|
|
172
201
|
|
|
173
202
|
Building your own abstractions? You might prefer explicit class instantiation. The raw constants are always available.
|
|
174
203
|
|
|
204
|
+
<!-- SPDX-SnippetBegin -->
|
|
205
|
+
<!--
|
|
206
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
207
|
+
SPDX-License-Identifier: MIT-0
|
|
208
|
+
-->
|
|
175
209
|
```ruby
|
|
176
210
|
RatatuiRuby.run do
|
|
177
211
|
loop do
|
|
@@ -212,6 +246,7 @@ RatatuiRuby.run do
|
|
|
212
246
|
end
|
|
213
247
|
end
|
|
214
248
|
```
|
|
249
|
+
<!-- SPDX-SnippetEnd -->
|
|
215
250
|
|
|
216
251
|
## Thread and Ractor Safety
|
|
217
252
|
|
|
@@ -236,6 +271,11 @@ These have side effects and are intentionally not shareable:
|
|
|
236
271
|
| `TUI` | Cache in `@tui` during run loop. Don't include in Models. |
|
|
237
272
|
| `Frame` | Pass to helpers during draw block. Invalid after block returns. |
|
|
238
273
|
|
|
274
|
+
<!-- SPDX-SnippetBegin -->
|
|
275
|
+
<!--
|
|
276
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
277
|
+
SPDX-License-Identifier: MIT-0
|
|
278
|
+
-->
|
|
239
279
|
```ruby
|
|
240
280
|
# Good: Cache session in instance variable
|
|
241
281
|
RatatuiRuby.run do |tui|
|
|
@@ -246,6 +286,7 @@ end
|
|
|
246
286
|
# Bad: Include in immutable Model (won't work with Ractors)
|
|
247
287
|
Model = Data.define(:tui, :count) # Don't do this
|
|
248
288
|
```
|
|
289
|
+
<!-- SPDX-SnippetEnd -->
|
|
249
290
|
|
|
250
291
|
|
|
251
292
|
## Reference Architectures
|
|
@@ -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
|
# Application Testing Guide
|
|
@@ -18,19 +18,31 @@ Use it to write fast, deterministic tests for your TUI applications.
|
|
|
18
18
|
|
|
19
19
|
First, require the test helper in your test file or `test_helper.rb`:
|
|
20
20
|
|
|
21
|
+
<!-- SPDX-SnippetBegin -->
|
|
22
|
+
<!--
|
|
23
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
24
|
+
SPDX-License-Identifier: MIT-0
|
|
25
|
+
-->
|
|
21
26
|
```ruby
|
|
22
27
|
require "ratatui_ruby/test_helper"
|
|
23
28
|
require "minitest/autorun" # or your preferred test framework
|
|
24
29
|
```
|
|
30
|
+
<!-- SPDX-SnippetEnd -->
|
|
25
31
|
|
|
26
32
|
Then, include the module in your test class:
|
|
27
33
|
|
|
34
|
+
<!-- SPDX-SnippetBegin -->
|
|
35
|
+
<!--
|
|
36
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
37
|
+
SPDX-License-Identifier: MIT-0
|
|
38
|
+
-->
|
|
28
39
|
```ruby
|
|
29
40
|
class MyApplicationTest < Minitest::Test
|
|
30
41
|
include RatatuiRuby::TestHelper
|
|
31
42
|
# ...
|
|
32
43
|
end
|
|
33
44
|
```
|
|
45
|
+
<!-- SPDX-SnippetEnd -->
|
|
34
46
|
|
|
35
47
|
## Writing a View Test
|
|
36
48
|
|
|
@@ -40,6 +52,11 @@ To test a view or widget, wrap your assertions in `with_test_terminal`. This set
|
|
|
40
52
|
2. **Render your code:** Instantiate your widget and draw it to a frame.
|
|
41
53
|
3. **Assert output:** Check the `buffer_content` against your expectations.
|
|
42
54
|
|
|
55
|
+
<!-- SPDX-SnippetBegin -->
|
|
56
|
+
<!--
|
|
57
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
58
|
+
SPDX-License-Identifier: MIT-0
|
|
59
|
+
-->
|
|
43
60
|
```ruby
|
|
44
61
|
def test_rendering
|
|
45
62
|
# Uses default 80x24 terminal
|
|
@@ -57,6 +74,7 @@ def test_rendering
|
|
|
57
74
|
end
|
|
58
75
|
end
|
|
59
76
|
```
|
|
77
|
+
<!-- SPDX-SnippetEnd -->
|
|
60
78
|
|
|
61
79
|
For the full API list, including `buffer_content` and `cursor_position`, see [RatatuiRuby::TestHelper::Terminal](../lib/ratatui_ruby/test_helper/terminal.rb).
|
|
62
80
|
|
|
@@ -66,6 +84,11 @@ You often need to check colors and modifiers (bold, italic) to ensure your highl
|
|
|
66
84
|
|
|
67
85
|
Use `assert_fg_color`, `assert_bg_color`, and modifier helpers like `assert_bold`.
|
|
68
86
|
|
|
87
|
+
<!-- SPDX-SnippetBegin -->
|
|
88
|
+
<!--
|
|
89
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
90
|
+
SPDX-License-Identifier: MIT-0
|
|
91
|
+
-->
|
|
69
92
|
```ruby
|
|
70
93
|
# Assert specific cell style
|
|
71
94
|
assert_fg_color(:red, 0, 0)
|
|
@@ -74,6 +97,7 @@ assert_bold(0, 0)
|
|
|
74
97
|
# Or check a whole area
|
|
75
98
|
assert_area_style({ x: 0, y: 0, w: 10, h: 1 }, bg: :blue)
|
|
76
99
|
```
|
|
100
|
+
<!-- SPDX-SnippetEnd -->
|
|
77
101
|
|
|
78
102
|
See [RatatuiRuby::TestHelper::StyleAssertions](../lib/ratatui_ruby/test_helper/style_assertions.rb) for the comprehensive list of style helpers.
|
|
79
103
|
|
|
@@ -86,6 +110,11 @@ Use `inject_event` to push mock events into the queue. This ensures safe, determ
|
|
|
86
110
|
> [!IMPORTANT]
|
|
87
111
|
> Call `inject_event` inside a `with_test_terminal` block to avoid race conditions.
|
|
88
112
|
|
|
113
|
+
<!-- SPDX-SnippetBegin -->
|
|
114
|
+
<!--
|
|
115
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
116
|
+
SPDX-License-Identifier: MIT-0
|
|
117
|
+
-->
|
|
89
118
|
```ruby
|
|
90
119
|
with_test_terminal do
|
|
91
120
|
# Simulate 'q' key press
|
|
@@ -96,6 +125,7 @@ with_test_terminal do
|
|
|
96
125
|
assert_equal "q", event.code
|
|
97
126
|
end
|
|
98
127
|
```
|
|
128
|
+
<!-- SPDX-SnippetEnd -->
|
|
99
129
|
|
|
100
130
|
See [RatatuiRuby::TestHelper::EventInjection](../lib/ratatui_ruby/test_helper/event_injection.rb) for helper methods like `inject_keys` and `inject_click`.
|
|
101
131
|
|
|
@@ -105,12 +135,18 @@ Snapshots let you verify complex layouts without manually asserting every line.
|
|
|
105
135
|
|
|
106
136
|
Use `assert_snapshots` to compare the current screen against stored reference files.
|
|
107
137
|
|
|
138
|
+
<!-- SPDX-SnippetBegin -->
|
|
139
|
+
<!--
|
|
140
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
141
|
+
SPDX-License-Identifier: MIT-0
|
|
142
|
+
-->
|
|
108
143
|
```ruby
|
|
109
144
|
with_test_terminal do
|
|
110
145
|
MyApp.new.run
|
|
111
146
|
assert_snapshots("dashboard_view")
|
|
112
147
|
end
|
|
113
148
|
```
|
|
149
|
+
<!-- SPDX-SnippetEnd -->
|
|
114
150
|
|
|
115
151
|
This generates both `.txt` (plain text) and `.ansi` (styled) snapshot files. The `.ansi` files contain ANSI escape codes—`cat` them in a terminal to see exactly what the screen looked like. For a visual tour of your test suite, try `cat **/*.ansi` in any shell that supports globbing.
|
|
116
152
|
|
|
@@ -130,6 +166,11 @@ Sometimes you want to test a single view component without spinning up the full
|
|
|
130
166
|
|
|
131
167
|
Use `MockFrame` and `StubRect` to test render logic in isolation.
|
|
132
168
|
|
|
169
|
+
<!-- SPDX-SnippetBegin -->
|
|
170
|
+
<!--
|
|
171
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
172
|
+
SPDX-License-Identifier: MIT-0
|
|
173
|
+
-->
|
|
133
174
|
```ruby
|
|
134
175
|
def test_logs_view
|
|
135
176
|
frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
|
|
@@ -143,6 +184,7 @@ def test_logs_view
|
|
|
143
184
|
assert_equal "Logs", rendered[:widget].block.title
|
|
144
185
|
end
|
|
145
186
|
```
|
|
187
|
+
<!-- SPDX-SnippetEnd -->
|
|
146
188
|
|
|
147
189
|
See [RatatuiRuby::TestHelper::TestDoubles](../lib/ratatui_ruby/test_helper/test_doubles.rb).
|
|
148
190
|
|
data/doc/concepts/async.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
3
|
-
SPDX-License-Identifier:
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
# Async Operations in TUI Applications
|
|
@@ -21,12 +21,18 @@ This guide explains async patterns that work with raw terminal mode.
|
|
|
21
21
|
|
|
22
22
|
### What Breaks
|
|
23
23
|
|
|
24
|
+
<!-- SPDX-SnippetBegin -->
|
|
25
|
+
<!--
|
|
26
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
27
|
+
SPDX-License-Identifier: MIT-0
|
|
28
|
+
-->
|
|
24
29
|
```ruby
|
|
25
30
|
# These fail inside a Thread during raw mode:
|
|
26
31
|
`git ls-remote --tags origin` # Returns empty or hangs
|
|
27
32
|
IO.popen(["git", "ls-remote", ...]) # Same
|
|
28
33
|
Open3.capture2("git", "ls-remote", ...) # Same
|
|
29
34
|
```
|
|
35
|
+
<!-- SPDX-SnippetEnd -->
|
|
30
36
|
|
|
31
37
|
The commands succeed synchronously. They fail asynchronously. The difference: thread context inherits the parent's raw terminal state.
|
|
32
38
|
|
|
@@ -46,11 +52,17 @@ Ruby's GIL releases during I/O. But:
|
|
|
46
52
|
|
|
47
53
|
Run slow operations before entering the TUI:
|
|
48
54
|
|
|
55
|
+
<!-- SPDX-SnippetBegin -->
|
|
56
|
+
<!--
|
|
57
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
58
|
+
SPDX-License-Identifier: MIT-0
|
|
59
|
+
-->
|
|
49
60
|
```ruby
|
|
50
61
|
def initialize
|
|
51
62
|
@data = fetch_data # Runs before RatatuiRuby.run
|
|
52
63
|
end
|
|
53
64
|
```
|
|
65
|
+
<!-- SPDX-SnippetEnd -->
|
|
54
66
|
|
|
55
67
|
**Trade-off**: Delays startup.
|
|
56
68
|
|
|
@@ -58,6 +70,11 @@ end
|
|
|
58
70
|
|
|
59
71
|
Spawn a separate process before entering raw mode. Write results to a temp file. Poll for completion:
|
|
60
72
|
|
|
73
|
+
<!-- SPDX-SnippetBegin -->
|
|
74
|
+
<!--
|
|
75
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
76
|
+
SPDX-License-Identifier: MIT-0
|
|
77
|
+
-->
|
|
61
78
|
```ruby
|
|
62
79
|
class AsyncChecker
|
|
63
80
|
CACHE_FILE = File.join(Dir.tmpdir, "my_check_result.txt")
|
|
@@ -81,6 +98,7 @@ class AsyncChecker
|
|
|
81
98
|
end
|
|
82
99
|
end
|
|
83
100
|
```
|
|
101
|
+
<!-- SPDX-SnippetEnd -->
|
|
84
102
|
|
|
85
103
|
**Key points**:
|
|
86
104
|
|
|
@@ -93,9 +111,15 @@ end
|
|
|
93
111
|
|
|
94
112
|
Ruby threads work for pure computation:
|
|
95
113
|
|
|
114
|
+
<!-- SPDX-SnippetBegin -->
|
|
115
|
+
<!--
|
|
116
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
117
|
+
SPDX-License-Identifier: MIT-0
|
|
118
|
+
-->
|
|
96
119
|
```ruby
|
|
97
120
|
Thread.new { @result = expensive_calculation }
|
|
98
121
|
```
|
|
122
|
+
<!-- SPDX-SnippetEnd -->
|
|
99
123
|
|
|
100
124
|
Avoid threads for shell commands.
|
|
101
125
|
|
|
@@ -122,6 +146,11 @@ For TUI async, `Process.spawn` solves the problem cleanly.
|
|
|
122
146
|
|
|
123
147
|
Check if a tag exists on the remote:
|
|
124
148
|
|
|
149
|
+
<!-- SPDX-SnippetBegin -->
|
|
150
|
+
<!--
|
|
151
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
152
|
+
SPDX-License-Identifier: MIT-0
|
|
153
|
+
-->
|
|
125
154
|
```ruby
|
|
126
155
|
class GitRepo
|
|
127
156
|
CACHE_FILE = File.join(Dir.tmpdir, "git_tag_pushed.txt")
|
|
@@ -156,5 +185,6 @@ class GitRepo
|
|
|
156
185
|
end
|
|
157
186
|
end
|
|
158
187
|
```
|
|
188
|
+
<!-- SPDX-SnippetEnd -->
|
|
159
189
|
|
|
160
190
|
The TUI starts instantly. The tag check runs in the background. The checklist updates when the result arrives.
|
|
@@ -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
|