ratatui_ruby 0.3.1 → 0.5.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 +14 -12
- data/.builds/ruby-3.3.yml +14 -12
- data/.builds/ruby-3.4.yml +14 -12
- data/.builds/ruby-4.0.0.yml +14 -12
- data/AGENTS.md +89 -132
- data/CHANGELOG.md +223 -1
- data/README.md +23 -16
- data/REUSE.toml +20 -0
- data/doc/application_architecture.md +176 -0
- data/doc/application_testing.md +17 -10
- data/doc/contributors/design/ruby_frontend.md +11 -7
- data/doc/contributors/developing_examples.md +261 -0
- data/doc/contributors/documentation_style.md +104 -0
- data/doc/contributors/dwim_dx.md +366 -0
- data/doc/contributors/index.md +2 -0
- data/doc/custom.css +14 -0
- data/doc/event_handling.md +125 -0
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_analytics.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_custom_widget.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_map_demo.png +0 -0
- data/doc/images/app_mouse_events.png +0 -0
- data/doc/images/app_table_select.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_padding.png +0 -0
- data/doc/images/widget_block_titles.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_cell_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_list_styles.png +0 -0
- data/doc/images/widget_popup_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_rect.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_flex.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/index.md +1 -0
- data/doc/interactive_design.md +116 -0
- data/doc/quickstart.md +186 -84
- data/examples/app_all_events/README.md +81 -0
- data/examples/app_all_events/app.rb +93 -0
- data/examples/app_all_events/model/event_color_cycle.rb +41 -0
- data/examples/app_all_events/model/event_entry.rb +75 -0
- data/examples/app_all_events/model/events.rb +180 -0
- data/examples/app_all_events/model/highlight.rb +57 -0
- data/examples/app_all_events/model/timestamp.rb +54 -0
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_key_a.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_paste.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_resize.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_right_click.txt +24 -0
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +24 -0
- data/examples/app_all_events/test/snapshots/initial_state.txt +24 -0
- data/examples/app_all_events/view/app_view.rb +78 -0
- data/examples/app_all_events/view/controls_view.rb +50 -0
- data/examples/app_all_events/view/counts_view.rb +55 -0
- data/examples/app_all_events/view/live_view.rb +69 -0
- data/examples/app_all_events/view/log_view.rb +60 -0
- data/{lib/ratatui_ruby/output.rb → examples/app_all_events/view.rb} +1 -1
- data/examples/app_all_events/view_state.rb +42 -0
- data/examples/app_color_picker/README.md +94 -0
- data/examples/app_color_picker/app.rb +112 -0
- data/examples/app_color_picker/clipboard.rb +84 -0
- data/examples/app_color_picker/color.rb +191 -0
- data/examples/app_color_picker/copy_dialog.rb +170 -0
- data/examples/app_color_picker/harmony.rb +56 -0
- data/examples/app_color_picker/input.rb +142 -0
- data/examples/app_color_picker/palette.rb +80 -0
- data/examples/app_color_picker/scene.rb +201 -0
- data/examples/app_login_form/app.rb +108 -0
- data/examples/app_map_demo/app.rb +93 -0
- data/examples/app_table_select/app.rb +201 -0
- data/examples/verify_quickstart_dsl/app.rb +45 -0
- data/examples/verify_quickstart_layout/app.rb +69 -0
- data/examples/verify_quickstart_lifecycle/app.rb +48 -0
- data/examples/verify_readme_usage/app.rb +34 -0
- data/examples/widget_barchart_demo/app.rb +238 -0
- data/examples/widget_block_padding/app.rb +67 -0
- data/examples/widget_block_titles/app.rb +69 -0
- data/examples/widget_box_demo/app.rb +250 -0
- data/examples/widget_calendar_demo/app.rb +109 -0
- data/examples/widget_cell_demo/app.rb +104 -0
- data/examples/widget_chart_demo/app.rb +213 -0
- data/examples/widget_gauge_demo/app.rb +212 -0
- data/examples/widget_layout_split/app.rb +246 -0
- data/examples/widget_line_gauge_demo/app.rb +217 -0
- data/examples/widget_list_demo/app.rb +382 -0
- data/examples/widget_list_styles/app.rb +141 -0
- data/examples/widget_popup_demo/app.rb +104 -0
- data/examples/widget_ratatui_logo_demo/app.rb +103 -0
- data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
- data/examples/widget_rect/app.rb +205 -0
- data/examples/widget_render/app.rb +184 -0
- data/examples/widget_rich_text/app.rb +137 -0
- data/examples/widget_scroll_text/app.rb +108 -0
- data/examples/widget_scrollbar_demo/app.rb +153 -0
- data/examples/widget_sparkline_demo/app.rb +274 -0
- data/examples/widget_style_colors/app.rb +102 -0
- data/examples/widget_table_flex/app.rb +95 -0
- data/examples/widget_tabs_demo/app.rb +167 -0
- data/ext/ratatui_ruby/Cargo.lock +889 -115
- data/ext/ratatui_ruby/Cargo.toml +4 -3
- data/ext/ratatui_ruby/clippy.toml +7 -0
- data/ext/ratatui_ruby/extconf.rb +7 -0
- data/ext/ratatui_ruby/src/events.rs +293 -219
- data/ext/ratatui_ruby/src/frame.rs +115 -0
- data/ext/ratatui_ruby/src/lib.rs +105 -24
- data/ext/ratatui_ruby/src/rendering.rs +94 -10
- data/ext/ratatui_ruby/src/style.rs +357 -93
- data/ext/ratatui_ruby/src/terminal.rs +121 -31
- data/ext/ratatui_ruby/src/text.rs +178 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +99 -24
- data/ext/ratatui_ruby/src/widgets/block.rs +32 -3
- data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
- data/ext/ratatui_ruby/src/widgets/canvas.rs +44 -9
- data/ext/ratatui_ruby/src/widgets/chart.rs +79 -27
- data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
- data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
- data/ext/ratatui_ruby/src/widgets/layout.rs +223 -15
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +114 -11
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +4 -2
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +51 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +61 -7
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +73 -6
- data/ext/ratatui_ruby/src/widgets/table.rs +177 -64
- data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
- data/lib/ratatui_ruby/cell.rb +166 -0
- data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
- data/lib/ratatui_ruby/event/key.rb +211 -0
- data/lib/ratatui_ruby/event/mouse.rb +124 -0
- data/lib/ratatui_ruby/event/none.rb +43 -0
- data/lib/ratatui_ruby/event/paste.rb +71 -0
- data/lib/ratatui_ruby/event/resize.rb +80 -0
- data/lib/ratatui_ruby/event.rb +131 -0
- data/lib/ratatui_ruby/frame.rb +87 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +23 -0
- data/lib/ratatui_ruby/schema/bar_chart.rb +226 -17
- data/lib/ratatui_ruby/schema/block.rb +178 -11
- data/lib/ratatui_ruby/schema/calendar.rb +70 -14
- data/lib/ratatui_ruby/schema/canvas.rb +213 -46
- data/lib/ratatui_ruby/schema/center.rb +46 -8
- data/lib/ratatui_ruby/schema/chart.rb +134 -32
- data/lib/ratatui_ruby/schema/clear.rb +22 -53
- data/lib/ratatui_ruby/schema/constraint.rb +72 -12
- data/lib/ratatui_ruby/schema/cursor.rb +23 -5
- data/lib/ratatui_ruby/schema/draw.rb +53 -0
- data/lib/ratatui_ruby/schema/gauge.rb +56 -12
- data/lib/ratatui_ruby/schema/layout.rb +91 -9
- data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
- data/lib/ratatui_ruby/schema/list.rb +92 -16
- data/lib/ratatui_ruby/schema/overlay.rb +29 -3
- data/lib/ratatui_ruby/schema/paragraph.rb +82 -25
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +29 -0
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +34 -0
- data/lib/ratatui_ruby/schema/rect.rb +59 -10
- data/lib/ratatui_ruby/schema/scrollbar.rb +127 -19
- data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
- data/lib/ratatui_ruby/schema/sparkline.rb +120 -12
- data/lib/ratatui_ruby/schema/style.rb +39 -11
- data/lib/ratatui_ruby/schema/table.rb +109 -18
- data/lib/ratatui_ruby/schema/tabs.rb +71 -10
- data/lib/ratatui_ruby/schema/text.rb +90 -0
- data/lib/ratatui_ruby/session/autodoc.rb +417 -0
- data/lib/ratatui_ruby/session.rb +163 -0
- data/lib/ratatui_ruby/test_helper.rb +322 -13
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +184 -38
- data/sig/examples/app_all_events/app.rbs +11 -0
- 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/sig/examples/app_color_picker/app.rbs +12 -0
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_map_demo/app.rbs +11 -0
- data/sig/examples/app_table_select/app.rbs +11 -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_padding/app.rbs +11 -0
- data/sig/examples/widget_block_titles/app.rbs +11 -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/sig/examples/widget_gauge_demo/app.rbs +11 -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_list_styles/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/sig/examples/widget_style_colors/app.rbs +14 -0
- data/sig/examples/widget_table_flex/app.rbs +11 -0
- data/sig/ratatui_ruby/event.rbs +69 -0
- data/sig/ratatui_ruby/frame.rbs +9 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -3
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
- data/sig/ratatui_ruby/schema/block.rbs +5 -4
- data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
- data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
- data/sig/ratatui_ruby/schema/center.rbs +3 -3
- data/sig/ratatui_ruby/schema/chart.rbs +8 -5
- data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
- data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
- data/sig/ratatui_ruby/schema/draw.rbs +27 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
- data/sig/ratatui_ruby/schema/layout.rbs +11 -1
- data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
- data/sig/ratatui_ruby/schema/list.rbs +5 -1
- data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +8 -0
- data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
- data/sig/ratatui_ruby/schema/rect.rbs +2 -1
- data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
- data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
- data/sig/ratatui_ruby/schema/table.rbs +8 -1
- data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
- data/sig/ratatui_ruby/schema/text.rbs +22 -0
- data/sig/ratatui_ruby/session.rbs +94 -0
- data/tasks/autodoc/inventory.rb +61 -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 +47 -0
- data/tasks/bump/history.rb +2 -2
- data/tasks/doc.rake +600 -6
- data/tasks/example_viewer.html.erb +172 -0
- data/tasks/lint.rake +8 -4
- data/tasks/resources/build.yml.erb +13 -11
- data/tasks/resources/index.html.erb +6 -0
- data/tasks/sourcehut.rake +4 -4
- data/tasks/terminal_preview/app_screenshot.rb +33 -0
- data/tasks/terminal_preview/crash_report.rb +52 -0
- data/tasks/terminal_preview/example_app.rb +25 -0
- data/tasks/terminal_preview/launcher_script.rb +46 -0
- data/tasks/terminal_preview/preview_collection.rb +58 -0
- data/tasks/terminal_preview/preview_timing.rb +22 -0
- data/tasks/terminal_preview/safety_confirmation.rb +56 -0
- data/tasks/terminal_preview/saved_screenshot.rb +53 -0
- data/tasks/terminal_preview/system_appearance.rb +11 -0
- data/tasks/terminal_preview/terminal_window.rb +136 -0
- data/tasks/terminal_preview/window_id.rb +14 -0
- data/tasks/terminal_preview.rake +28 -0
- data/tasks/test.rake +2 -2
- 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 +206 -54
- data/LICENSES/BSD-2-Clause.txt +0 -9
- data/doc/images/examples-analytics.rb.png +0 -0
- data/doc/images/examples-box_demo.rb.png +0 -0
- data/doc/images/examples-calendar_demo.rb.png +0 -0
- data/doc/images/examples-chart_demo.rb.png +0 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-dashboard.rb.png +0 -0
- data/doc/images/examples-list_styles.rb.png +0 -0
- data/doc/images/examples-login_form.rb.png +0 -0
- data/doc/images/examples-map_demo.rb.png +0 -0
- data/doc/images/examples-mouse_events.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-scrollbar_demo.rb.png +0 -0
- data/doc/images/examples-stock_ticker.rb.png +0 -0
- data/doc/images/examples-system_monitor.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/examples/analytics.rb +0 -88
- data/examples/box_demo.rb +0 -71
- data/examples/calendar_demo.rb +0 -55
- data/examples/chart_demo.rb +0 -84
- data/examples/custom_widget.rb +0 -43
- data/examples/dashboard.rb +0 -72
- data/examples/list_styles.rb +0 -66
- data/examples/login_form.rb +0 -115
- data/examples/map_demo.rb +0 -58
- data/examples/mouse_events.rb +0 -95
- data/examples/popup_demo.rb +0 -105
- data/examples/quickstart_dsl.rb +0 -30
- data/examples/quickstart_lifecycle.rb +0 -40
- data/examples/readme_usage.rb +0 -21
- data/examples/scroll_text.rb +0 -74
- data/examples/scrollbar_demo.rb +0 -75
- data/examples/stock_ticker.rb +0 -93
- data/examples/system_monitor.rb +0 -94
- data/examples/table_select.rb +0 -70
- data/examples/test_analytics.rb +0 -65
- data/examples/test_box_demo.rb +0 -38
- data/examples/test_calendar_demo.rb +0 -66
- data/examples/test_dashboard.rb +0 -38
- data/examples/test_list_styles.rb +0 -61
- data/examples/test_login_form.rb +0 -63
- data/examples/test_map_demo.rb +0 -100
- data/examples/test_popup_demo.rb +0 -62
- data/examples/test_scroll_text.rb +0 -130
- data/examples/test_stock_ticker.rb +0 -39
- data/examples/test_system_monitor.rb +0 -40
- data/examples/test_table_select.rb +0 -37
- data/ext/ratatui_ruby/src/buffer.rs +0 -54
- data/lib/ratatui_ruby/dsl.rb +0 -64
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Interactive TUI Design Patterns
|
|
7
|
+
|
|
8
|
+
Canonical patterns for building responsive, interactive terminal user interfaces with ratatui_ruby.
|
|
9
|
+
|
|
10
|
+
## The Cached Layout Pattern
|
|
11
|
+
|
|
12
|
+
**Context:** In immediate-mode TUI development, you render once per event loop. The render happens, the user clicks, you respond. This cycle repeats 60 times a second.
|
|
13
|
+
|
|
14
|
+
**Problem:** Your layout has constraints. When you render, you calculate where each widget goes. When the user clicks, you need to know which widget was under the cursor. Two separate calculations means two separate constraint definitions. Change the layout once and forget to update the hit test logic—bugs happen.
|
|
15
|
+
|
|
16
|
+
**Solution:** Calculate layout once. Cache the results. Reuse them everywhere.
|
|
17
|
+
|
|
18
|
+
### The Three-Phase Lifecycle
|
|
19
|
+
|
|
20
|
+
Structure your event loop into three clear phases:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
def run
|
|
24
|
+
RatatuiRuby.run do |tui|
|
|
25
|
+
@tui = tui
|
|
26
|
+
loop do
|
|
27
|
+
@tui.draw do |frame|
|
|
28
|
+
calculate_layout(frame.area) # Phase 1: Geometry (once per frame)
|
|
29
|
+
render(frame) # Phase 2: Draw
|
|
30
|
+
end
|
|
31
|
+
break if handle_input == :quit # Phase 3: Input
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Phase 1: Layout Calculation**
|
|
38
|
+
|
|
39
|
+
Call this inside your `draw` block. It uses the current terminal area provided by the frame:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
def calculate_layout(area)
|
|
43
|
+
# Main area vs sidebar (70% / 30%)
|
|
44
|
+
main_area, @sidebar_area = @tui.layout_split(
|
|
45
|
+
area,
|
|
46
|
+
direction: :horizontal,
|
|
47
|
+
constraints: [
|
|
48
|
+
@tui.constraint_percentage(70),
|
|
49
|
+
@tui.constraint_percentage(30),
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Within main area, left vs right panels
|
|
54
|
+
@left_rect, @right_rect = @tui.layout_split(
|
|
55
|
+
main_area,
|
|
56
|
+
direction: :horizontal,
|
|
57
|
+
constraints: [
|
|
58
|
+
@tui.constraint_percentage(@left_ratio),
|
|
59
|
+
@tui.constraint_percentage(100 - @left_ratio)
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Phase 2: Rendering**
|
|
66
|
+
|
|
67
|
+
Reuse the cached rects. Build and draw:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
def render(frame)
|
|
71
|
+
frame.render_widget(build_widget(@left_rect), @left_rect)
|
|
72
|
+
frame.render_widget(build_widget(@right_rect), @right_rect)
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Phase 3: Event Handling**
|
|
77
|
+
|
|
78
|
+
Reuse the cached rects. Test clicks:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
def handle_input
|
|
82
|
+
event = RatatuiRuby.poll_event
|
|
83
|
+
|
|
84
|
+
case event
|
|
85
|
+
in type: :mouse, kind: "down", x:, y:
|
|
86
|
+
if @left_rect.contains?(x, y)
|
|
87
|
+
handle_left_click
|
|
88
|
+
elsif @right_rect.contains?(x, y)
|
|
89
|
+
handle_right_click
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Why This Matters
|
|
98
|
+
|
|
99
|
+
- **Single source of truth:** Change constraints once. They apply everywhere.
|
|
100
|
+
- **No duplication:** Write `Layout.split(area, constraints:)` once. Use the result in render and input.
|
|
101
|
+
- **Testable:** Layout geometry is explicit. You can verify it.
|
|
102
|
+
- **Foundation for components:** In Gem 1.5, the `Component` class automates this caching. This pattern teaches the mental model.
|
|
103
|
+
|
|
104
|
+
## Layout.split
|
|
105
|
+
|
|
106
|
+
`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 `Session` helper (`tui.layout_split`) for cleaner application code.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# Preferred (Session API)
|
|
110
|
+
left, right = tui.layout_split(area, constraints: [...])
|
|
111
|
+
|
|
112
|
+
# Manual (Core API)
|
|
113
|
+
left, right = RatatuiRuby::Layout.split(area, constraints: [...])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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`.
|
data/doc/quickstart.md
CHANGED
|
@@ -14,19 +14,24 @@ Add this line to your application's Gemfile:
|
|
|
14
14
|
gem "ratatui_ruby"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
And then execute:
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
21
|
bundle install
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
|
|
23
25
|
Or install it yourself as:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
gem install ratatui_ruby
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
## Tutorials
|
|
33
|
+
|
|
34
|
+
### Basic Application
|
|
30
35
|
|
|
31
36
|
Here is a "Hello World" application that demonstrates the core lifecycle of a **ratatui_ruby** app.
|
|
32
37
|
|
|
@@ -43,22 +48,24 @@ begin
|
|
|
43
48
|
# We define a Paragraph widget inside a Block with a title and borders.
|
|
44
49
|
view = RatatuiRuby::Paragraph.new(
|
|
45
50
|
text: "Hello, Ratatui! Press 'q' to quit.",
|
|
46
|
-
|
|
51
|
+
alignment: :center,
|
|
47
52
|
block: RatatuiRuby::Block.new(
|
|
48
53
|
title: "My Ruby TUI App",
|
|
54
|
+
title_alignment: :center,
|
|
49
55
|
borders: [:all],
|
|
50
|
-
border_color: "cyan"
|
|
56
|
+
border_color: "cyan",
|
|
57
|
+
style: { fg: "white" }
|
|
51
58
|
)
|
|
52
59
|
)
|
|
53
60
|
|
|
54
61
|
# 3. Draw the UI
|
|
55
|
-
RatatuiRuby.draw
|
|
62
|
+
RatatuiRuby.draw do |frame|
|
|
63
|
+
frame.render_widget(view, frame.area)
|
|
64
|
+
end
|
|
56
65
|
|
|
57
66
|
# 4. Poll for events
|
|
58
67
|
event = RatatuiRuby.poll_event
|
|
59
|
-
if event
|
|
60
|
-
break
|
|
61
|
-
end
|
|
68
|
+
break if event.key? && event.code == "q"
|
|
62
69
|
end
|
|
63
70
|
ensure
|
|
64
71
|
# 5. Restore the terminal to its original state
|
|
@@ -66,131 +73,226 @@ ensure
|
|
|
66
73
|
end
|
|
67
74
|
```
|
|
68
75
|
|
|
69
|
-

|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
#### How it works
|
|
72
79
|
|
|
73
80
|
1. **`RatatuiRuby.init_terminal`**: Enters raw mode and switches to the alternate screen.
|
|
74
|
-
2. **Immediate Mode UI**: On every iteration
|
|
75
|
-
3. **`RatatuiRuby.draw
|
|
76
|
-
4. **`RatatuiRuby.poll_event`**:
|
|
77
|
-
5. **`RatatuiRuby.restore_terminal`**:
|
|
81
|
+
2. **Immediate Mode UI**: On every iteration, describe your UI by creating `Data` objects (e.g., `Paragraph`, `Block`).
|
|
82
|
+
3. **`RatatuiRuby.draw { |frame| ... }`**: The block receives a `Frame` object as a canvas. Render widgets onto specific areas. Nothing is drawn until the block finishes, ensuring flicker-free updates.
|
|
83
|
+
4. **`RatatuiRuby.poll_event`**: Returns a typed `Event` object with predicates like `key?`, `mouse?`, `resize?`, etc. Returns `RatatuiRuby::Event::None` if no events are pending. Use predicates to check event type without pattern matching.
|
|
84
|
+
5. **`RatatuiRuby.restore_terminal`**: Essential for leaving raw mode and returning to the shell. Always wrap your loop in `begin...ensure` to guarantee this runs.
|
|
78
85
|
|
|
79
|
-
###
|
|
86
|
+
### Idiomatic Session
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
You can simplify your code by using `RatatuiRuby.run`. This method handles the terminal lifecycle for you, yielding a `Session` object with factory methods for widgets.
|
|
82
89
|
|
|
83
90
|
```rb
|
|
84
91
|
require "ratatui_ruby"
|
|
85
92
|
|
|
86
|
-
# 1. Initialize the terminal
|
|
87
|
-
RatatuiRuby.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
# 1. Initialize the terminal and ensure it is restored.
|
|
94
|
+
RatatuiRuby.run do |tui|
|
|
95
|
+
loop do
|
|
96
|
+
# 2. Create your UI with methods instead of classes.
|
|
97
|
+
view = tui.paragraph(
|
|
98
|
+
text: "Hello, Ratatui! Press 'q' to quit.",
|
|
99
|
+
alignment: :center,
|
|
100
|
+
block: tui.block(
|
|
101
|
+
title: "My Ruby TUI App",
|
|
102
|
+
title_alignment: :center,
|
|
103
|
+
borders: [:all],
|
|
104
|
+
border_color: "cyan",
|
|
105
|
+
style: { fg: "white" }
|
|
106
|
+
)
|
|
96
107
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
|
|
109
|
+
# 3. Use RatatuiRuby methods, too.
|
|
110
|
+
tui.draw do |frame|
|
|
111
|
+
frame.render_widget(view, frame.area)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# 4. Poll for events with pattern matching
|
|
115
|
+
case tui.poll_event
|
|
116
|
+
in { type: :key, code: "q" }
|
|
117
|
+
break
|
|
118
|
+
else
|
|
119
|
+
# Ignore other events
|
|
120
|
+
end
|
|
105
121
|
end
|
|
106
122
|
end
|
|
107
123
|
```
|
|
108
124
|
|
|
109
125
|
#### How it works
|
|
110
126
|
|
|
111
|
-
1. **`RatatuiRuby.
|
|
112
|
-
2. **Widget Shorthand**: The block yields a
|
|
113
|
-
3. **Method
|
|
127
|
+
1. **`RatatuiRuby.run`**: This context manager initializes the terminal before the block starts and ensures `restore_terminal` is called when the block exits (even if an error occurs).
|
|
128
|
+
2. **Widget Shorthand**: The block yields a `Session` object (here named `tui`). This object provides factory methods for every widget, allowing you to write `tui.paragraph(...)` instead of the more verbose `RatatuiRuby::Paragraph.new(...)`.
|
|
129
|
+
3. **Method Shorthand**: The session object also provides aliases for module functions of `RatatuiRuby`, allowing you to write `tui.draw(...)` instead of the more verbose `RatatuiRuby.draw(...)`.
|
|
130
|
+
4. **Pattern Matching for Events**: Use `case...in` with pattern matching for elegant event dispatch. Always include an `else` clause at the end to catch unmatched event types (mouse, resize, paste, focus, etc.), otherwise Ruby raises `NoMatchingPatternError`.
|
|
131
|
+
|
|
132
|
+
For a deeper dive into the available application architectures (Manual vs Managed), see [Application Architecture](./application_architecture.md).
|
|
133
|
+
|
|
134
|
+
### Adding Layouts
|
|
135
|
+
|
|
136
|
+
Real-world applications often need to split the screen into multiple areas. `RatatuiRuby::Layout` lets you do this easily.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
require "ratatui_ruby"
|
|
140
|
+
|
|
141
|
+
RatatuiRuby.run do |tui|
|
|
142
|
+
loop do
|
|
143
|
+
tui.draw do |frame|
|
|
144
|
+
# 1. Split the screen
|
|
145
|
+
top, bottom = tui.layout_split(
|
|
146
|
+
frame.area,
|
|
147
|
+
direction: :vertical,
|
|
148
|
+
constraints: [
|
|
149
|
+
tui.constraint_percentage(75),
|
|
150
|
+
tui.constraint_percentage(25),
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# 2. Render Top Widget
|
|
155
|
+
frame.render_widget(
|
|
156
|
+
tui.paragraph(
|
|
157
|
+
text: "Hello, Ratatui!",
|
|
158
|
+
alignment: :center,
|
|
159
|
+
block: tui.block(title: "Content", borders: [:all], border_color: "cyan")
|
|
160
|
+
),
|
|
161
|
+
top
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 3. Render Bottom Widget with Styled Text
|
|
165
|
+
# We use a Line of Spans to style specific characters
|
|
166
|
+
text_line = tui.text_line(
|
|
167
|
+
spans: [
|
|
168
|
+
tui.text_span(content: "Press '"),
|
|
169
|
+
tui.text_span(
|
|
170
|
+
content: "q",
|
|
171
|
+
style: tui.style(modifiers: [:bold, :underlined])
|
|
172
|
+
),
|
|
173
|
+
tui.text_span(content: "' to quit."),
|
|
174
|
+
],
|
|
175
|
+
alignment: :center
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
frame.render_widget(
|
|
179
|
+
tui.paragraph(
|
|
180
|
+
text: text_line,
|
|
181
|
+
block: tui.block(title: "Controls", borders: [:all])
|
|
182
|
+
),
|
|
183
|
+
bottom
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
case tui.poll_event
|
|
188
|
+
in { type: :key, code: "q" }
|
|
189
|
+
break
|
|
190
|
+
else
|
|
191
|
+
# Ignore other events
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### How it works
|
|
198
|
+
|
|
199
|
+
1. **`tui.layout_split` (`RatatuiRuby::Layout.split`)**: Takes an area (like `frame.area`) and splits it into multiple sub-areas based on constraints.
|
|
200
|
+
2. **`tui.constraint_*` (`RatatuiRuby::Constraint`)**: Defines how space is distributed (e.g., `percentage`, `length`, `min`, `max`).
|
|
201
|
+
3. **`Frame#render_widget(widget, rect)`**: You pass the specific area (like `top` or `bottom`) to render the widget into that exact region.
|
|
202
|
+
4. **`tui.text_span` (`RatatuiRuby::Text::Span`)**: Allows for rich styling within a single line of text.
|
|
114
203
|
|
|
115
204
|
## Examples
|
|
116
205
|
|
|
117
|
-
|
|
206
|
+
These examples showcase the full power of **ratatui_ruby**. You can find their source code in the [examples directory](../examples).
|
|
118
207
|
|
|
119
|
-
###
|
|
120
|
-
Demonstrates the use of `Tabs` and `BarChart` widgets with a simple data-switching mechanism.
|
|
208
|
+
### Sample Applications
|
|
121
209
|
|
|
122
|
-
|
|
210
|
+
Full-featured examples demonstrating complex layouts and real-world TUI patterns.
|
|
123
211
|
|
|
124
|
-
### [Box Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/box_demo.rb)
|
|
125
|
-
A simple demonstration of `Block` and `Paragraph` widgets, reacting to arrow key presses to change colors.
|
|
126
212
|
|
|
127
|
-

|
|
128
213
|
|
|
129
|
-
|
|
130
|
-
A simple demo application for the `Calendar` widget.
|
|
214
|
+
#### [All Events](../examples/app_all_events/app.rb)
|
|
131
215
|
|
|
132
|
-
|
|
216
|
+
Handling terminal events is unpredictable. Developers need to know exactly what the terminal sends for `Ctrl+C` or a mouse drag.
|
|
133
217
|
|
|
134
|
-
|
|
135
|
-
Demonstrates the `Chart` widget with both scatter and line datasets, including custom axes.
|
|
218
|
+
This app captures and visualizes every event—keys, mouse, resize, paste, and focus.
|
|
136
219
|
|
|
137
|
-
|
|
220
|
+
Use it to debug your input handling or verify terminal behavior.
|
|
138
221
|
|
|
139
|
-
|
|
140
|
-
Demonstrates how to define a custom widget in pure Ruby using the `render(area, buffer)` escape hatch for low-level drawing.
|
|
222
|
+
**What you'll learn:**
|
|
141
223
|
|
|
142
|
-
|
|
224
|
+
* **MVVM Architecture**: Separates logic (Model), state (ViewModel), and rendering (View) for clean, testable code.
|
|
225
|
+
* **Event Handling**: Captures and distinguishes all input types, including modifiers (`Ctrl+C`) and focus changes.
|
|
226
|
+
* **Scalable Structure**: Organizes a non-trivial application into small, focused classes instead of a monolithic script.
|
|
143
227
|
|
|
144
|
-
|
|
145
|
-
Uses `Layout`, `List`, and `Paragraph` to create a classic sidebar-and-content interface.
|
|
228
|
+

|
|
146
229
|
|
|
147
|
-
|
|
230
|
+
#### [Color Picker](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_color_picker/app.rb)
|
|
148
231
|
|
|
149
|
-
|
|
150
|
-
Showcases advanced styling options for the `List` widget, including selection highlighting.
|
|
232
|
+
Interactive tools require complex state. Mapping mouse clicks to widgets and handling modal dialogs creates messy code if handled in the main loop.
|
|
151
233
|
|
|
152
|
-
|
|
153
|
-
Shows how to use `Overlay`, `Center`, and `Cursor` to build a modal login form with text input.
|
|
234
|
+
This app implements a full Color Picker using a "Scene-Orchestrated" pattern. The Scene calculates layout and exposes cached rectangles for hit testing.
|
|
154
235
|
|
|
155
|
-
|
|
236
|
+
Use it to build forms, editors, and mouse-driven tools.
|
|
156
237
|
|
|
157
|
-
|
|
158
|
-
Exhibits the `Canvas` widget's power, rendering a world map along with animated circles and lines.
|
|
238
|
+
**What you'll learn:**
|
|
159
239
|
|
|
160
|
-
|
|
240
|
+
* **Scene-Orchestrated MVC**: Separates the View (layout/rendering) from the Controller (event loop) and Model (business logic).
|
|
241
|
+
* **Hit Testing**: Caches layout rectangles during the render pass to handle mouse clicks on specific elements.
|
|
242
|
+
* **Modal Dialogs**: Implements overlay patterns that intercept input.
|
|
161
243
|
|
|
162
|
-
|
|
163
|
-
Detailed plumbing of mouse events, including clicks, drags, and movement tracking.
|
|
244
|
+
#### [Custom Widget (Escape Hatch)](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_custom_widget/app.rb)
|
|
164
245
|
|
|
165
|
-
|
|
246
|
+
Demonstrates how to define a custom widget in pure Ruby using the `render(area, buffer)` escape hatch for low-level drawing.
|
|
247
|
+
|
|
248
|
+

|
|
249
|
+
|
|
250
|
+
#### [Layout Split Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/widget_layout_split/app.rb)
|
|
166
251
|
|
|
167
|
-
|
|
168
|
-
Demonstrates the `Clear` widget and how to prevent "style bleed" when rendering opaque popups over colored backgrounds.
|
|
252
|
+
Demonstrates `Layout.split` with interactive attribute cycling. Features hotkey controls For direction (vertical/horizontal), all 7 flex modes (legacy, start, center, end, space_between, space_around, space_evenly), and constraint types (fill, length, percentage, min, ratio).
|
|
169
253
|
|
|
170
|
-

|
|
255
|
+
|
|
256
|
+
#### [Login Form](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_login_form/app.rb)
|
|
257
|
+
|
|
258
|
+
Shows how to use `Overlay`, `Center`, and `Cursor` to build a modal login form with text input.
|
|
171
259
|
|
|
172
|
-
|
|
173
|
-
A simple example of integrating the `Scrollbar` widget and handling mouse wheel events for scrolling.
|
|
260
|
+

|
|
174
261
|
|
|
175
|
-
|
|
262
|
+
#### [Map Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_map_demo/app.rb)
|
|
176
263
|
|
|
177
|
-
|
|
178
|
-
Demonstrates the `Paragraph` widget's scroll functionality, allowing navigation through long text content using arrow keys for both horizontal and vertical scrolling.
|
|
264
|
+
Exhibits the `Canvas` widget's power, rendering a world map with city labels, animated circles, and lines.
|
|
179
265
|
|
|
180
|
-

|
|
181
267
|
|
|
182
|
-
|
|
183
|
-
Utilizes `Sparkline` and `Chart` widgets to visualize real-time (simulated) data.
|
|
268
|
+
#### [Table Select](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_table_select/app.rb)
|
|
184
269
|
|
|
185
|
-
|
|
270
|
+
Demonstrates interactive row selection in the `Table` widget with keyboard navigation, highlighting selected rows with custom styles and symbols, applying a base style, and dynamically adjusting `column_spacing`. Also demonstrates `column_highlight_style` and the new `cell_highlight_style` for precise selection visualization.
|
|
186
271
|
|
|
187
|
-
|
|
188
|
-
Combines `Table` and `Gauge` widgets in a vertical layout to create a functional system overview.
|
|
272
|
+

|
|
189
273
|
|
|
190
|
-

|
|
191
274
|
|
|
192
|
-
###
|
|
193
|
-
Demonstrates interactive row selection in the `Table` widget with keyboard navigation, highlighting selected rows with custom styles and symbols.
|
|
275
|
+
### Widget Demos
|
|
194
276
|
|
|
195
|
-
|
|
277
|
+
These smaller, focused examples demonstrate specific widgets and their configuration options.
|
|
196
278
|
|
|
279
|
+
* [Bar Chart](../examples/widget_barchart_demo/app.rb)
|
|
280
|
+
* [Block Padding](../examples/widget_block_padding/app.rb)
|
|
281
|
+
* [Block Titles](../examples/widget_block_titles/app.rb)
|
|
282
|
+
* [Box (Block/Paragraph)](../examples/widget_box_demo/app.rb)
|
|
283
|
+
* [Calendar](../examples/widget_calendar_demo/app.rb)
|
|
284
|
+
* [Chart](../examples/widget_chart_demo/app.rb)
|
|
285
|
+
* [Gauge](../examples/widget_gauge_demo/app.rb)
|
|
286
|
+
* [Line Gauge](../examples/widget_line_gauge_demo/app.rb)
|
|
287
|
+
* [List](../examples/widget_list_demo/app.rb)
|
|
288
|
+
* [Popup (Clear)](../examples/widget_popup_demo/app.rb)
|
|
289
|
+
* [Rect](../examples/widget_rect/app.rb)
|
|
290
|
+
* [Ratatui Logo](../examples/widget_ratatui_logo_demo/app.rb)
|
|
291
|
+
* [Ratatui Mascot](../examples/widget_ratatui_mascot_demo/app.rb)
|
|
292
|
+
* [Rich Text](../examples/widget_rich_text/app.rb)
|
|
293
|
+
* [Scrollbar](../examples/widget_scrollbar_demo/app.rb)
|
|
294
|
+
* [Scroll Text](../examples/widget_scroll_text/app.rb)
|
|
295
|
+
* [Sparkline](../examples/widget_sparkline_demo/app.rb)
|
|
296
|
+
* [Table Flex](../examples/widget_table_flex/app.rb)
|
|
297
|
+
* [Tabs](../examples/widget_tabs_demo/app.rb)
|
|
298
|
+
* [Widget Style Colors](../examples/widget_style_colors/app.rb)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# App All Events Example
|
|
7
|
+
|
|
8
|
+
This example application captures and visualizes every event supported by `ratatui_ruby`. It serves as a comprehensive reference for event handling and a demonstration of a clean, scalable architectural pattern.
|
|
9
|
+
|
|
10
|
+
## Architecture: MVVM (Model-View-ViewModel)
|
|
11
|
+
|
|
12
|
+
This application demonstrates the **Model-View-ViewModel (MVVM)** pattern, modified for the immediate-mode nature of terminal UIs. This separation of concerns ensures that the UI logic is completely decoupled from the business logic, making the application easier to test and maintain.
|
|
13
|
+
|
|
14
|
+
### 1. Model (`model/`)
|
|
15
|
+
The **Model** manages the application's domain data and logic. It knows nothing about the UI.
|
|
16
|
+
|
|
17
|
+
* **`Events` (`model/events.rb`)**: The core store. It records incoming events, maintains statistics (counts), and handles business logic like "highlight this event type for 300ms."
|
|
18
|
+
* **`EventEntry` (`model/event_entry.rb`)**: A value object representing a single recorded event.
|
|
19
|
+
|
|
20
|
+
### 2. View State (ViewModel) (`view_state.rb`)
|
|
21
|
+
The **View State** (comparable to a ViewModel or Presenter) is an immutable data structure built specifically for the View.
|
|
22
|
+
|
|
23
|
+
* **`ViewState`**: It acts as a bridge. In every render loop, the application builds a fresh `ViewState` object, calculating derived data (like styles, active flags, and formatted strings) from the raw Model data.
|
|
24
|
+
* **Why?**: This prevents the View from having to contain logic. The View doesn't ask "is the app focused so I should use green?"; it just asks `state.border_color`.
|
|
25
|
+
|
|
26
|
+
### 3. View (`view/`)
|
|
27
|
+
The **View** is responsible **only** for rendering. It receives the `ViewState` and draws to the screen.
|
|
28
|
+
|
|
29
|
+
* **`View::App` (`view/app_view.rb`)**: The root view. It handles the high-level layout (splitting the screen into areas).
|
|
30
|
+
* **Sub-views**: `Counts`, `Live`, `Log`, `Controls`. Each is a small, focused component that renders a specific part of the screen based on the data in `ViewState`.
|
|
31
|
+
|
|
32
|
+
### 4. Controller/App (`app.rb`)
|
|
33
|
+
The **`AppAllEvents`** class ties it all together. It owns the main loop:
|
|
34
|
+
|
|
35
|
+
1. **Poll**: Waits for an event from the terminal.
|
|
36
|
+
2. **Update**: Passes the event to the **Model** (`@events.record`).
|
|
37
|
+
3. **Build State**: Creates a new **ViewState** from the current Model and global state.
|
|
38
|
+
4. **Render**: Passes the **ViewState** to the **View** to draw the frame.
|
|
39
|
+
|
|
40
|
+
## Library Features Showcased
|
|
41
|
+
|
|
42
|
+
Reading this code will teach you how to:
|
|
43
|
+
|
|
44
|
+
* **Handle All Events**:
|
|
45
|
+
* **Keyboard**: Capture normal keys and modifiers (`Ctrl+c`, `q`).
|
|
46
|
+
* **Mouse**: track clicks, drags, and scroll events.
|
|
47
|
+
* **Focus**: React to the terminal window gaining or losing focus (`FocusGained`/`FocusLost`).
|
|
48
|
+
* **Resize**: Dynamically adapt layouts when the terminal size changes.
|
|
49
|
+
* **Paste**: Handle bracketed paste events (if supported by the terminal).
|
|
50
|
+
* **Layouts**: Use `tui.layout_split` with constraints (`Length`, `Fill`) to create complex, responsive dashboards.
|
|
51
|
+
* **Styling**: Apply dynamic styles (bold, colors) based on application state.
|
|
52
|
+
* **Structure**: Organize a non-trivial CLI tool into small, single-purpose classes.
|
|
53
|
+
|
|
54
|
+
## What Problems Does This Solve?
|
|
55
|
+
|
|
56
|
+
### "What key code is my terminal sending?"
|
|
57
|
+
If you are building an app and your logic isn't catching `Ctrl+Left`, run this app and press the keys. You will see exactly how `ratatui_ruby` parses that input (e.g., is it a `Key` event? What are the modifiers?).
|
|
58
|
+
|
|
59
|
+
### "How do I structure a real app?"
|
|
60
|
+
Hello World examples are great, but they don't scale. This example shows how to structure an application that can grow. By simulating a "dashboard" with multiple independent widgets updating in real-time, it solves the problem of "how do I pass data around without global variables?"
|
|
61
|
+
|
|
62
|
+
### "How do I implement an event loop?"
|
|
63
|
+
It provides a robust reference implementation of the standard `loop { draw; handle_input }` cycle, including the correct way to handle quit signals.
|
|
64
|
+
|
|
65
|
+
## Comparison: Choosing an Architecture
|
|
66
|
+
|
|
67
|
+
Complex applications require structured state habits. `AppAllEvents` and the [Color Picker](../app_color_picker/README.md) demonstrate two different approaches.
|
|
68
|
+
|
|
69
|
+
### The Dashboard Approach (AppAllEvents)
|
|
70
|
+
|
|
71
|
+
Dashboards display data. They rarely require complex mouse interaction. Strict MVVM works best here. The View is a pure function. It accepts a `ViewState` and draws it. It ignores input. This simplifies testing.
|
|
72
|
+
|
|
73
|
+
Use this pattern for logs, monitors, and data viewers.
|
|
74
|
+
|
|
75
|
+
### The Tool Approach (Color Picker)
|
|
76
|
+
|
|
77
|
+
Tools require interaction. Users click buttons and drag sliders. The Controller needs to know where components exist on screen. MVVM hides this layout data.
|
|
78
|
+
|
|
79
|
+
The Color Picker uses a "Scene" pattern. The View exposes layout rectangles. The Controller uses these rectangles to handle mouse clicks.
|
|
80
|
+
|
|
81
|
+
Use this pattern for forms, editors, and mouse-driven tools.
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
7
|
+
$LOAD_PATH.unshift File.expand_path(__dir__)
|
|
8
|
+
|
|
9
|
+
require "ratatui_ruby"
|
|
10
|
+
require_relative "model/events"
|
|
11
|
+
require_relative "view_state"
|
|
12
|
+
require_relative "view/app_view"
|
|
13
|
+
|
|
14
|
+
# Demonstrates the full range of terminal events supported by RatatuiRuby.
|
|
15
|
+
#
|
|
16
|
+
# Developers need a comprehensive example to understand how keys, mouse, resize, and focus events behave.
|
|
17
|
+
# Testing event handling across different terminal emulators and platforms can be unpredictable.
|
|
18
|
+
#
|
|
19
|
+
# This application captures and logs every event received from the backend, providing real-time feedback and history.
|
|
20
|
+
#
|
|
21
|
+
# Use it to verify your terminal's capabilities or as a reference for complex event handling.
|
|
22
|
+
#
|
|
23
|
+
# === Examples
|
|
24
|
+
#
|
|
25
|
+
# # Run from the command line:
|
|
26
|
+
# # ruby examples/app_all_events/app.rb
|
|
27
|
+
#
|
|
28
|
+
# app = AppAllEvents.new
|
|
29
|
+
# app.run
|
|
30
|
+
class AppAllEvents
|
|
31
|
+
# List of all event types tracked by this application.
|
|
32
|
+
EVENT_TYPES = %i[key mouse resize paste focus none].freeze
|
|
33
|
+
|
|
34
|
+
# Creates a new AppAllEvents instance and initializes its state.
|
|
35
|
+
def initialize
|
|
36
|
+
@view = View::App.new
|
|
37
|
+
@events = Events.new
|
|
38
|
+
@focused = true
|
|
39
|
+
@last_dimensions = [80, 24]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Starts the application event loop.
|
|
43
|
+
#
|
|
44
|
+
# === Example
|
|
45
|
+
#
|
|
46
|
+
# app.run
|
|
47
|
+
def run
|
|
48
|
+
RatatuiRuby.run do |tui|
|
|
49
|
+
@tui = tui
|
|
50
|
+
loop do
|
|
51
|
+
render
|
|
52
|
+
break if handle_input == :quit
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private def render
|
|
58
|
+
view_state = ViewState.build(
|
|
59
|
+
@events,
|
|
60
|
+
@focused,
|
|
61
|
+
@tui,
|
|
62
|
+
nil
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@tui.draw { |frame| @view.call(view_state, @tui, frame, frame.area) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private def handle_input
|
|
69
|
+
event = @tui.poll_event
|
|
70
|
+
|
|
71
|
+
case event
|
|
72
|
+
when RatatuiRuby::Event::Key
|
|
73
|
+
return :quit if event.code == "q"
|
|
74
|
+
return :quit if event.code == "c" && event.modifiers.include?("ctrl")
|
|
75
|
+
@events.record(event)
|
|
76
|
+
when RatatuiRuby::Event::Resize
|
|
77
|
+
@events.record(event, context: { last_dimensions: @last_dimensions })
|
|
78
|
+
@last_dimensions = [event.width, event.height]
|
|
79
|
+
when RatatuiRuby::Event::FocusGained
|
|
80
|
+
@focused = true
|
|
81
|
+
@events.record(event)
|
|
82
|
+
when RatatuiRuby::Event::FocusLost
|
|
83
|
+
@focused = false
|
|
84
|
+
@events.record(event)
|
|
85
|
+
else
|
|
86
|
+
@events.record(event)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
AppAllEvents.new.run if __FILE__ == $PROGRAM_NAME
|