ratatui_ruby 1.4.0-x86_64-linux
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 +7 -0
- data/LICENSE +15 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/LGPL-3.0-or-later.txt +304 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/LICENSES/MIT.txt +21 -0
- data/REUSE.toml +42 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/.cargo/config.toml +13 -0
- data/ext/ratatui_ruby/.gitignore +4 -0
- data/ext/ratatui_ruby/Cargo.lock +1737 -0
- data/ext/ratatui_ruby/Cargo.toml +24 -0
- data/ext/ratatui_ruby/clippy.toml +7 -0
- data/ext/ratatui_ruby/extconf.rb +21 -0
- data/ext/ratatui_ruby/src/color.rs +82 -0
- data/ext/ratatui_ruby/src/errors.rs +28 -0
- data/ext/ratatui_ruby/src/events.rs +700 -0
- data/ext/ratatui_ruby/src/frame.rs +241 -0
- data/ext/ratatui_ruby/src/lib.rs +343 -0
- data/ext/ratatui_ruby/src/lib_header.rs +11 -0
- data/ext/ratatui_ruby/src/rendering.rs +158 -0
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/style.rs +469 -0
- data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
- data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
- data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
- data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
- data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
- data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
- data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
- data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
- data/ext/ratatui_ruby/src/text.rs +225 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
- data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
- data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
- data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
- data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
- data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
- data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
- data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
- data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
- data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
- data/lib/ratatui_ruby/backend/window_size.rb +50 -0
- data/lib/ratatui_ruby/backend.rb +59 -0
- data/lib/ratatui_ruby/buffer/cell.rb +212 -0
- data/lib/ratatui_ruby/buffer.rb +149 -0
- data/lib/ratatui_ruby/cell.rb +208 -0
- data/lib/ratatui_ruby/debug.rb +215 -0
- data/lib/ratatui_ruby/draw.rb +63 -0
- data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
- data/lib/ratatui_ruby/event/key/character.rb +53 -0
- data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
- data/lib/ratatui_ruby/event/key/media.rb +46 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
- data/lib/ratatui_ruby/event/key/system.rb +47 -0
- data/lib/ratatui_ruby/event/key.rb +479 -0
- data/lib/ratatui_ruby/event/mouse.rb +291 -0
- data/lib/ratatui_ruby/event/none.rb +53 -0
- data/lib/ratatui_ruby/event/paste.rb +130 -0
- data/lib/ratatui_ruby/event/resize.rb +221 -0
- data/lib/ratatui_ruby/event/sync.rb +52 -0
- data/lib/ratatui_ruby/event.rb +163 -0
- data/lib/ratatui_ruby/frame.rb +257 -0
- data/lib/ratatui_ruby/labs/a11y.rb +182 -0
- data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
- data/lib/ratatui_ruby/labs.rb +47 -0
- data/lib/ratatui_ruby/layout/alignment.rb +91 -0
- data/lib/ratatui_ruby/layout/constraint.rb +337 -0
- data/lib/ratatui_ruby/layout/layout.rb +258 -0
- data/lib/ratatui_ruby/layout/position.rb +81 -0
- data/lib/ratatui_ruby/layout/rect.rb +733 -0
- data/lib/ratatui_ruby/layout/size.rb +62 -0
- data/lib/ratatui_ruby/layout.rb +29 -0
- data/lib/ratatui_ruby/list_state.rb +201 -0
- data/lib/ratatui_ruby/output_guard.rb +171 -0
- data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
- data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
- data/lib/ratatui_ruby/style/color.rb +149 -0
- data/lib/ratatui_ruby/style/style.rb +147 -0
- data/lib/ratatui_ruby/style.rb +19 -0
- data/lib/ratatui_ruby/symbols.rb +435 -0
- data/lib/ratatui_ruby/synthetic_events.rb +106 -0
- data/lib/ratatui_ruby/table_state.rb +251 -0
- data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
- data/lib/ratatui_ruby/terminal.rb +66 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
- data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
- data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
- data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
- data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
- data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
- data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
- data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
- data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
- data/lib/ratatui_ruby/test_helper.rb +115 -0
- data/lib/ratatui_ruby/text/line.rb +245 -0
- data/lib/ratatui_ruby/text/span.rb +158 -0
- data/lib/ratatui_ruby/text.rb +99 -0
- data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
- data/lib/ratatui_ruby/tui/core.rb +67 -0
- data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
- data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
- data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
- data/lib/ratatui_ruby/tui.rb +106 -0
- data/lib/ratatui_ruby/version.rb +12 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
- data/lib/ratatui_ruby/widgets/block.rb +266 -0
- data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
- data/lib/ratatui_ruby/widgets/cell.rb +59 -0
- data/lib/ratatui_ruby/widgets/center.rb +71 -0
- data/lib/ratatui_ruby/widgets/chart.rb +172 -0
- data/lib/ratatui_ruby/widgets/clear.rb +66 -0
- data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
- data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
- data/lib/ratatui_ruby/widgets/list.rb +252 -0
- data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
- data/lib/ratatui_ruby/widgets/row.rb +123 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
- data/lib/ratatui_ruby/widgets/table.rb +213 -0
- data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
- data/lib/ratatui_ruby/widgets.rb +43 -0
- data/lib/ratatui_ruby.rb +555 -0
- data/sig/examples/app_all_events/app.rbs +11 -0
- data/sig/examples/app_all_events/model/app_model.rbs +23 -0
- data/sig/examples/app_all_events/model/event_entry.rbs +23 -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 +14 -0
- data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
- data/sig/examples/app_color_picker/app.rbs +17 -0
- data/sig/examples/app_external_editor/app.rbs +12 -0
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_stateful_interaction/app.rbs +39 -0
- data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
- data/sig/examples/verify_readme_usage/app.rbs +17 -0
- data/sig/examples/widget_block_demo/app.rbs +38 -0
- data/sig/examples/widget_box_demo/app.rbs +17 -0
- data/sig/examples/widget_calendar_demo/app.rbs +17 -0
- data/sig/examples/widget_cell_demo/app.rbs +17 -0
- data/sig/examples/widget_chart_demo/app.rbs +17 -0
- data/sig/examples/widget_gauge_demo/app.rbs +17 -0
- data/sig/examples/widget_layout_split/app.rbs +16 -0
- data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
- data/sig/examples/widget_list_demo/app.rbs +17 -0
- data/sig/examples/widget_map_demo/app.rbs +17 -0
- data/sig/examples/widget_popup_demo/app.rbs +17 -0
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
- data/sig/examples/widget_rect/app.rbs +18 -0
- data/sig/examples/widget_render/app.rbs +16 -0
- data/sig/examples/widget_rich_text/app.rbs +17 -0
- data/sig/examples/widget_scroll_text/app.rbs +17 -0
- data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
- data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
- data/sig/examples/widget_style_colors/app.rbs +20 -0
- data/sig/examples/widget_table_demo/app.rbs +17 -0
- data/sig/examples/widget_text_width/app.rbs +16 -0
- data/sig/generated/event_key_predicates.rbs +1348 -0
- data/sig/manifest.yaml +5 -0
- data/sig/patches/data.rbs +26 -0
- data/sig/patches/debugger__.rbs +8 -0
- data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
- data/sig/ratatui_ruby/backend.rbs +12 -0
- data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
- data/sig/ratatui_ruby/buffer.rbs +18 -0
- data/sig/ratatui_ruby/cell.rbs +44 -0
- data/sig/ratatui_ruby/clear.rbs +18 -0
- data/sig/ratatui_ruby/constraint.rbs +26 -0
- data/sig/ratatui_ruby/debug.rbs +45 -0
- data/sig/ratatui_ruby/draw.rbs +30 -0
- data/sig/ratatui_ruby/event.rbs +249 -0
- data/sig/ratatui_ruby/frame.rbs +23 -0
- data/sig/ratatui_ruby/interfaces.rbs +25 -0
- data/sig/ratatui_ruby/labs.rbs +90 -0
- data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
- data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
- data/sig/ratatui_ruby/layout/layout.rbs +45 -0
- data/sig/ratatui_ruby/layout/position.rbs +18 -0
- data/sig/ratatui_ruby/layout/rect.rbs +64 -0
- data/sig/ratatui_ruby/layout/size.rbs +18 -0
- data/sig/ratatui_ruby/list_state.rbs +23 -0
- data/sig/ratatui_ruby/output_guard.rbs +23 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
- data/sig/ratatui_ruby/rect.rbs +17 -0
- data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
- data/sig/ratatui_ruby/session.rbs +93 -0
- data/sig/ratatui_ruby/style/color.rbs +22 -0
- data/sig/ratatui_ruby/style/style.rbs +29 -0
- data/sig/ratatui_ruby/symbols.rbs +141 -0
- data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
- data/sig/ratatui_ruby/table_state.rbs +27 -0
- data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
- data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
- data/sig/ratatui_ruby/test_helper.rbs +18 -0
- data/sig/ratatui_ruby/text/line.rbs +27 -0
- data/sig/ratatui_ruby/text/span.rbs +23 -0
- data/sig/ratatui_ruby/text.rbs +12 -0
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
- data/sig/ratatui_ruby/tui/core.rbs +23 -0
- data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
- data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
- data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
- data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
- data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
- data/sig/ratatui_ruby/tui.rbs +25 -0
- data/sig/ratatui_ruby/version.rbs +12 -0
- data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
- data/sig/ratatui_ruby/widgets/block.rbs +51 -0
- data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
- data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
- data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
- data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
- data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
- data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
- data/sig/ratatui_ruby/widgets/list.rbs +63 -0
- data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
- data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
- data/sig/ratatui_ruby/widgets/row.rbs +43 -0
- data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
- data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
- data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
- data/sig/ratatui_ruby/widgets/table.rbs +78 -0
- data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
- data/sig/ratatui_ruby/widgets.rbs +16 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +729 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
class Event
|
|
10
|
+
# Synthetic event for synchronizing async operations in tests.
|
|
11
|
+
#
|
|
12
|
+
# Testing async behavior is tricky. You inject an event, but results arrive
|
|
13
|
+
# later. By the time you assert, the async work may not have completed.
|
|
14
|
+
#
|
|
15
|
+
# When a runtime (Tea, Kit) encounters this event, it should wait for all
|
|
16
|
+
# pending async operations to complete before processing the next event.
|
|
17
|
+
# This enables deterministic testing without changing production code paths.
|
|
18
|
+
#
|
|
19
|
+
# Inject this event between user actions and assertions to ensure async
|
|
20
|
+
# results have been processed:
|
|
21
|
+
#
|
|
22
|
+
# === Example
|
|
23
|
+
#
|
|
24
|
+
#--
|
|
25
|
+
# SPDX-SnippetBegin
|
|
26
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
27
|
+
# SPDX-License-Identifier: MIT-0
|
|
28
|
+
#++
|
|
29
|
+
# inject_key("s") # Triggers async command
|
|
30
|
+
# inject_sync # Wait for command to complete
|
|
31
|
+
# inject_key(:q) # Quit after seeing results
|
|
32
|
+
# Tea.run(...)
|
|
33
|
+
# assert_snapshots("after_s_with_results")
|
|
34
|
+
#
|
|
35
|
+
#--
|
|
36
|
+
# SPDX-SnippetEnd
|
|
37
|
+
#++
|
|
38
|
+
# This is not "test mode"—it's a real event that runtimes handle.
|
|
39
|
+
# Production apps could use it too (e.g., "ensure saves complete before quit").
|
|
40
|
+
class Sync < Event
|
|
41
|
+
# Returns true for Sync events.
|
|
42
|
+
def sync?
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Deconstructs the event for pattern matching.
|
|
47
|
+
def deconstruct_keys(keys)
|
|
48
|
+
{ type: :sync }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
# Base class for all RatatuiRuby events.
|
|
10
|
+
#
|
|
11
|
+
# Events represent terminal input: keyboard, mouse, resize, paste, focus changes.
|
|
12
|
+
# Returned by RatatuiRuby.poll_event. All events support Ruby 3.0+ pattern matching.
|
|
13
|
+
#
|
|
14
|
+
# == Event Types
|
|
15
|
+
#
|
|
16
|
+
# * <tt>Key</tt> — keyboard input
|
|
17
|
+
# * <tt>Mouse</tt> — mouse clicks, movement, wheel
|
|
18
|
+
# * <tt>Resize</tt> — terminal resized
|
|
19
|
+
# * <tt>Paste</tt> — clipboard paste
|
|
20
|
+
# * <tt>FocusGained</tt> — terminal gained focus
|
|
21
|
+
# * <tt>FocusLost</tt> — terminal lost focus
|
|
22
|
+
# * <tt>None</tt> — no event available (Null Object)
|
|
23
|
+
#
|
|
24
|
+
# == Pattern Matching (Exhaustive)
|
|
25
|
+
#
|
|
26
|
+
# Use <tt>case...in</tt> to dispatch on every possible event type. This ensures
|
|
27
|
+
# you handle every case without needing an +else+ clause:
|
|
28
|
+
#
|
|
29
|
+
#--
|
|
30
|
+
# SPDX-SnippetBegin
|
|
31
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
32
|
+
# SPDX-License-Identifier: MIT-0
|
|
33
|
+
#++
|
|
34
|
+
# case RatatuiRuby.poll_event
|
|
35
|
+
# in { type: :key, code: "q" }
|
|
36
|
+
# break
|
|
37
|
+
# in { type: :key, code: code, modifiers: }
|
|
38
|
+
# handle_key(code, modifiers)
|
|
39
|
+
# in { type: :mouse, kind: "down", x:, y: }
|
|
40
|
+
# handle_click(x, y)
|
|
41
|
+
# in { type: :mouse, kind:, x:, y: }
|
|
42
|
+
# # handle other mouse activities
|
|
43
|
+
# in { type: :resize, width:, height: }
|
|
44
|
+
# handle_resize(width, height)
|
|
45
|
+
# in { type: :paste, content: }
|
|
46
|
+
# handle_paste(content)
|
|
47
|
+
# in { type: :focus_gained }
|
|
48
|
+
# handle_focus_gain
|
|
49
|
+
# in { type: :focus_lost }
|
|
50
|
+
# handle_focus_loss
|
|
51
|
+
# in { type: :none }
|
|
52
|
+
# # Idle
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
#--
|
|
56
|
+
# SPDX-SnippetEnd
|
|
57
|
+
#++
|
|
58
|
+
# == Predicates
|
|
59
|
+
#
|
|
60
|
+
# Check event types with predicates without pattern matching:
|
|
61
|
+
#
|
|
62
|
+
#--
|
|
63
|
+
# SPDX-SnippetBegin
|
|
64
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
65
|
+
# SPDX-License-Identifier: MIT-0
|
|
66
|
+
#++
|
|
67
|
+
# event = RatatuiRuby.poll_event
|
|
68
|
+
# if event.key?
|
|
69
|
+
# puts "Key pressed"
|
|
70
|
+
# elsif event.none?
|
|
71
|
+
# # Idle
|
|
72
|
+
# elsif event.mouse?
|
|
73
|
+
# puts "Mouse event"
|
|
74
|
+
# end
|
|
75
|
+
#--
|
|
76
|
+
# SPDX-SnippetEnd
|
|
77
|
+
#++
|
|
78
|
+
class Event
|
|
79
|
+
# Returns true if this is a None event.
|
|
80
|
+
def none?
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns true if this is a Key event.
|
|
85
|
+
def key?
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns true if this is a Mouse event.
|
|
90
|
+
def mouse?
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns true if this is a Resize event.
|
|
95
|
+
def resize?
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns true if this is a Paste event.
|
|
100
|
+
def paste?
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns true if this is a FocusGained event.
|
|
105
|
+
def focus_gained?
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns true if this is a FocusLost event.
|
|
110
|
+
def focus_lost?
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns true if this is a Sync event.
|
|
115
|
+
def sync?
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Responds to dynamic predicate methods for key checks.
|
|
120
|
+
# All non-Key events return false for any key predicate.
|
|
121
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
122
|
+
if name.to_s.end_with?("?")
|
|
123
|
+
false
|
|
124
|
+
else
|
|
125
|
+
super
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Declares that this class responds to dynamic predicate methods.
|
|
130
|
+
def respond_to_missing?(name, *args)
|
|
131
|
+
name.to_s.end_with?("?") || super
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Deconstructs the event for pattern matching.
|
|
135
|
+
#
|
|
136
|
+
# Keys argument is unused but required by the protocol.
|
|
137
|
+
#
|
|
138
|
+
#--
|
|
139
|
+
# SPDX-SnippetBegin
|
|
140
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
141
|
+
# SPDX-License-Identifier: MIT-0
|
|
142
|
+
#++
|
|
143
|
+
# case event
|
|
144
|
+
# in type: :key, code:
|
|
145
|
+
# puts "Key: #{code}"
|
|
146
|
+
# end
|
|
147
|
+
#--
|
|
148
|
+
# SPDX-SnippetEnd
|
|
149
|
+
#++
|
|
150
|
+
def deconstruct_keys(keys)
|
|
151
|
+
{}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
require_relative "event/none"
|
|
157
|
+
require_relative "event/key"
|
|
158
|
+
require_relative "event/mouse"
|
|
159
|
+
require_relative "event/resize"
|
|
160
|
+
require_relative "event/paste"
|
|
161
|
+
require_relative "event/focus_gained"
|
|
162
|
+
require_relative "event/focus_lost"
|
|
163
|
+
require_relative "event/sync"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
# Provides access to the terminal buffer for rendering widgets.
|
|
10
|
+
#
|
|
11
|
+
# Rendering in immediate-mode TUIs requires knowing the terminal dimensions and
|
|
12
|
+
# placing widgets at specific positions. Without explicit control, layout
|
|
13
|
+
# calculations become duplicated between rendering and hit testing.
|
|
14
|
+
#
|
|
15
|
+
# This class exposes the terminal frame during a draw call. It provides the
|
|
16
|
+
# current area and methods to render widgets at precise locations.
|
|
17
|
+
#
|
|
18
|
+
# Use it inside a <tt>RatatuiRuby.draw</tt> block to render widgets with full
|
|
19
|
+
# control over placement.
|
|
20
|
+
#
|
|
21
|
+
# == Thread/Ractor Safety
|
|
22
|
+
#
|
|
23
|
+
# Frame is an *I/O handle*, not a data object. It has side effects
|
|
24
|
+
# (render_widget, set_cursor_position) and is intentionally *not*
|
|
25
|
+
# Ractor-shareable. Passing it to helper methods during the draw block is
|
|
26
|
+
# fine. However, do not include it in immutable Models/Messages or pass
|
|
27
|
+
# it to other Ractors. Frame is only valid during the draw block's execution.
|
|
28
|
+
#
|
|
29
|
+
# === Examples
|
|
30
|
+
#
|
|
31
|
+
# Basic usage with a single widget:
|
|
32
|
+
#
|
|
33
|
+
#--
|
|
34
|
+
# SPDX-SnippetBegin
|
|
35
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
36
|
+
# SPDX-License-Identifier: MIT-0
|
|
37
|
+
#++
|
|
38
|
+
# RatatuiRuby.draw do |frame|
|
|
39
|
+
# paragraph = RatatuiRuby::Widgets::Paragraph.new(text: "Hello, world!")
|
|
40
|
+
# frame.render_widget(paragraph, frame.area)
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
#--
|
|
44
|
+
# SPDX-SnippetEnd
|
|
45
|
+
#++
|
|
46
|
+
# Using Layout.split for multi-region layouts:
|
|
47
|
+
#
|
|
48
|
+
#--
|
|
49
|
+
# SPDX-SnippetBegin
|
|
50
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
51
|
+
# SPDX-License-Identifier: MIT-0
|
|
52
|
+
#++
|
|
53
|
+
# RatatuiRuby.draw do |frame|
|
|
54
|
+
# sidebar, main = RatatuiRuby::Layout.split(
|
|
55
|
+
# frame.area,
|
|
56
|
+
# direction: :horizontal,
|
|
57
|
+
# constraints: [
|
|
58
|
+
# RatatuiRuby::Layout::Constraint.length(20),
|
|
59
|
+
# RatatuiRuby::Layout::Constraint.fill(1)
|
|
60
|
+
# ]
|
|
61
|
+
# )
|
|
62
|
+
#
|
|
63
|
+
# frame.render_widget(sidebar_widget, sidebar)
|
|
64
|
+
# frame.render_widget(main_widget, main)
|
|
65
|
+
#
|
|
66
|
+
# # Store rects for hit testing — no duplication!
|
|
67
|
+
# @regions = { sidebar: sidebar, main: main }
|
|
68
|
+
# end
|
|
69
|
+
#--
|
|
70
|
+
# SPDX-SnippetEnd
|
|
71
|
+
#++
|
|
72
|
+
class Frame
|
|
73
|
+
##
|
|
74
|
+
# :method: area
|
|
75
|
+
# :call-seq: area() -> Rect
|
|
76
|
+
#
|
|
77
|
+
# Returns the full terminal area as a Rect.
|
|
78
|
+
#
|
|
79
|
+
# The returned Rect represents the entire drawable area of the terminal.
|
|
80
|
+
# Use it as the starting point for layout calculations.
|
|
81
|
+
#
|
|
82
|
+
# === Example
|
|
83
|
+
#
|
|
84
|
+
#--
|
|
85
|
+
# SPDX-SnippetBegin
|
|
86
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
87
|
+
# SPDX-License-Identifier: MIT-0
|
|
88
|
+
#++
|
|
89
|
+
# RatatuiRuby.draw do |frame|
|
|
90
|
+
# puts "Terminal size: #{frame.area.width}x#{frame.area.height}"
|
|
91
|
+
# end
|
|
92
|
+
#
|
|
93
|
+
#--
|
|
94
|
+
# SPDX-SnippetEnd
|
|
95
|
+
#++
|
|
96
|
+
# (Native method implemented in Rust)
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# :method: render_widget
|
|
100
|
+
# :call-seq: render_widget(widget, area) -> nil
|
|
101
|
+
#
|
|
102
|
+
# Renders a widget at the specified area.
|
|
103
|
+
#
|
|
104
|
+
# Widgets in RatatuiRuby are immutable Data objects. This method takes a
|
|
105
|
+
# widget and a Rect, rendering the widget's content within that region.
|
|
106
|
+
#
|
|
107
|
+
# [widget]
|
|
108
|
+
# The widget to render (Paragraph, Layout, List, Table, etc.).
|
|
109
|
+
# [area]
|
|
110
|
+
#--
|
|
111
|
+
# SPDX-SnippetBegin
|
|
112
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long
|
|
113
|
+
# SPDX-License-Identifier: MIT-0
|
|
114
|
+
#++
|
|
115
|
+
# A Rect specifying where to render the widget.
|
|
116
|
+
#
|
|
117
|
+
#--
|
|
118
|
+
# SPDX-SnippetEnd
|
|
119
|
+
#++
|
|
120
|
+
# === Example
|
|
121
|
+
#
|
|
122
|
+
#--
|
|
123
|
+
# SPDX-SnippetBegin
|
|
124
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
125
|
+
# SPDX-License-Identifier: MIT-0
|
|
126
|
+
#++
|
|
127
|
+
# RatatuiRuby.draw do |frame|
|
|
128
|
+
# para = RatatuiRuby::Widgets::Paragraph.new(text: "Content")
|
|
129
|
+
# frame.render_widget(para, frame.area)
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
#--
|
|
133
|
+
# SPDX-SnippetEnd
|
|
134
|
+
#++
|
|
135
|
+
# (Native method implemented in Rust)
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# :method: render_stateful_widget
|
|
139
|
+
# :call-seq: render_stateful_widget(widget, area, state) -> nil
|
|
140
|
+
#
|
|
141
|
+
# Renders a widget with persistent state.
|
|
142
|
+
#
|
|
143
|
+
# Some UI components (like List or Table) have **runtime status** (Status) that
|
|
144
|
+
# changes during rendering, such as the current scroll offset.
|
|
145
|
+
#
|
|
146
|
+
# Since Widget definitions (Configuration Definition) are immutable inputs,
|
|
147
|
+
# you must pass a separate mutable State object (Output Status) to capture
|
|
148
|
+
# these changes.
|
|
149
|
+
#
|
|
150
|
+
# Note: The Widget configuration is *always* required. The State object is
|
|
151
|
+
# only used for specific widgets that need to persist runtime status.
|
|
152
|
+
#
|
|
153
|
+
#
|
|
154
|
+
# [widget]
|
|
155
|
+
# The immutable widget configuration (Input) (e.g., RatatuiRuby::List).
|
|
156
|
+
# [area]
|
|
157
|
+
# The Rect area to render into.
|
|
158
|
+
# [state]
|
|
159
|
+
#--
|
|
160
|
+
# SPDX-SnippetBegin
|
|
161
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
162
|
+
# SPDX-License-Identifier: MIT-0
|
|
163
|
+
#++
|
|
164
|
+
# The mutable state object (Output) (e.g., RatatuiRuby::ListState).
|
|
165
|
+
#
|
|
166
|
+
#--
|
|
167
|
+
# SPDX-SnippetEnd
|
|
168
|
+
#++
|
|
169
|
+
# === Example
|
|
170
|
+
#
|
|
171
|
+
#--
|
|
172
|
+
# SPDX-SnippetBegin
|
|
173
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
174
|
+
# SPDX-License-Identifier: MIT-0
|
|
175
|
+
#++
|
|
176
|
+
# # Initialize state once (outside the loop)
|
|
177
|
+
# @list_state = RatatuiRuby::ListState.new
|
|
178
|
+
#
|
|
179
|
+
# RatatuiRuby.draw do |frame|
|
|
180
|
+
# list = RatatuiRuby::Widgets::List.new(items: ["A", "B"])
|
|
181
|
+
# frame.render_stateful_widget(list, frame.area, @list_state)
|
|
182
|
+
# end
|
|
183
|
+
#
|
|
184
|
+
# # Read back the offset calculated by Ratatui
|
|
185
|
+
# puts @list_state.offset
|
|
186
|
+
#
|
|
187
|
+
#--
|
|
188
|
+
# SPDX-SnippetEnd
|
|
189
|
+
#++
|
|
190
|
+
# (Native method implemented in Rust)
|
|
191
|
+
|
|
192
|
+
##
|
|
193
|
+
# :method: set_cursor_position
|
|
194
|
+
# :call-seq: set_cursor_position(x, y) -> nil
|
|
195
|
+
#
|
|
196
|
+
# Positions the blinking cursor at the given coordinates.
|
|
197
|
+
#
|
|
198
|
+
# Text input fields show users where typed characters will appear. Without
|
|
199
|
+
# a visible cursor, users cannot tell if the input is focused or where text
|
|
200
|
+
# will insert.
|
|
201
|
+
#
|
|
202
|
+
# This method moves the terminal cursor to a specific cell. Coordinates are
|
|
203
|
+
# 0-indexed from the terminal's top-left corner.
|
|
204
|
+
#
|
|
205
|
+
# Use it when building login forms, search bars, or command palettes.
|
|
206
|
+
#
|
|
207
|
+
# [x]
|
|
208
|
+
# Column position (<tt>0</tt> = leftmost column).
|
|
209
|
+
# [y]
|
|
210
|
+
#--
|
|
211
|
+
# SPDX-SnippetBegin
|
|
212
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
213
|
+
# SPDX-License-Identifier: MIT-0
|
|
214
|
+
#++
|
|
215
|
+
# Row position (<tt>0</tt> = topmost row).
|
|
216
|
+
#
|
|
217
|
+
#--
|
|
218
|
+
# SPDX-SnippetEnd
|
|
219
|
+
#++
|
|
220
|
+
# === Example
|
|
221
|
+
#
|
|
222
|
+
# Position the cursor at the end of typed text in a login form:
|
|
223
|
+
#
|
|
224
|
+
#--
|
|
225
|
+
# SPDX-SnippetBegin
|
|
226
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
227
|
+
# SPDX-License-Identifier: MIT-0
|
|
228
|
+
#++
|
|
229
|
+
# PREFIX = "Username: [ "
|
|
230
|
+
# username = "alice"
|
|
231
|
+
#
|
|
232
|
+
# RatatuiRuby.draw do |frame|
|
|
233
|
+
# # Render the input field
|
|
234
|
+
# prompt = RatatuiRuby::Widgets::Paragraph.new(
|
|
235
|
+
# text: "#{PREFIX}#{username} ]",
|
|
236
|
+
# block: RatatuiRuby::Widgets::Block.new(borders: :all)
|
|
237
|
+
# )
|
|
238
|
+
# frame.render_widget(prompt, frame.area)
|
|
239
|
+
#
|
|
240
|
+
# # Position cursor after the typed text
|
|
241
|
+
# # Account for border (1) + prefix length + username length
|
|
242
|
+
# cursor_x = 1 + PREFIX.length + username.length
|
|
243
|
+
# cursor_y = 1 # First line inside border
|
|
244
|
+
# frame.set_cursor_position(cursor_x, cursor_y)
|
|
245
|
+
# end
|
|
246
|
+
#
|
|
247
|
+
#--
|
|
248
|
+
# SPDX-SnippetEnd
|
|
249
|
+
#++
|
|
250
|
+
# See also:
|
|
251
|
+
# - {Component-based implementation using Frame API}[link:/examples/app_color_picker/app_rb.html]
|
|
252
|
+
# - {Declarative implementation using Tree API}[link:/examples/app_login_form/app_rb.html]
|
|
253
|
+
# - RatatuiRuby::Cursor (Tree API alternative)
|
|
254
|
+
#
|
|
255
|
+
# (Native method implemented in Rust)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
|
|
10
|
+
module RatatuiRuby
|
|
11
|
+
module Labs
|
|
12
|
+
# A11Y lab: exports widget tree as XML.
|
|
13
|
+
#
|
|
14
|
+
# Writes an XML representation of the widget tree to a temporary file
|
|
15
|
+
# every frame when enabled.
|
|
16
|
+
module A11y
|
|
17
|
+
# Path to the XML output file in the system temp directory.
|
|
18
|
+
OUTPUT_PATH = File.join(Dir.tmpdir, "ratatui_ruby_a11y.xml").freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Dumps the widget tree to XML (single widget for tree mode).
|
|
22
|
+
def dump_widget_tree(widget, _area = nil)
|
|
23
|
+
ensure_rexml_loaded
|
|
24
|
+
doc = REXML::Document.new
|
|
25
|
+
doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
|
|
26
|
+
doc.add(build_element(widget))
|
|
27
|
+
write_document(doc)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns startup message for users to see before TUI launches.
|
|
31
|
+
#
|
|
32
|
+
# Since stdout is captured during TUI rendering, users need to know
|
|
33
|
+
# where the XML file will be written before the app starts.
|
|
34
|
+
def startup_message
|
|
35
|
+
<<~MSG
|
|
36
|
+
A11Y Lab enabled! Widget tree will be written to:
|
|
37
|
+
#{OUTPUT_PATH}
|
|
38
|
+
|
|
39
|
+
Press Enter to launch the TUI...
|
|
40
|
+
MSG
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Dumps multiple widgets captured from Frame API mode.
|
|
44
|
+
def dump_widgets(widgets_with_areas)
|
|
45
|
+
ensure_rexml_loaded
|
|
46
|
+
Labs.warn_once!("Labs::A11y (RR_LABS=A11Y)")
|
|
47
|
+
|
|
48
|
+
# Reset counter each frame for stable IDs
|
|
49
|
+
@widget_id_counter = 0
|
|
50
|
+
|
|
51
|
+
doc = REXML::Document.new
|
|
52
|
+
doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
|
|
53
|
+
|
|
54
|
+
frame = REXML::Element.new("RatatuiFrame")
|
|
55
|
+
widgets_with_areas.each do |widget, area|
|
|
56
|
+
frame.add(build_element_with_area(widget, area))
|
|
57
|
+
end
|
|
58
|
+
doc.add(frame)
|
|
59
|
+
|
|
60
|
+
write_document(doc)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private def write_document(doc)
|
|
64
|
+
output = +""
|
|
65
|
+
formatter = REXML::Formatters::Pretty.new(2)
|
|
66
|
+
formatter.compact = true
|
|
67
|
+
formatter.write(doc, output)
|
|
68
|
+
File.write(OUTPUT_PATH, output)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def build_element_with_area(widget, area)
|
|
72
|
+
class_name = widget.class.name&.split("::")&.last || "Unknown"
|
|
73
|
+
element = REXML::Element.new(class_name)
|
|
74
|
+
|
|
75
|
+
# Generate unique id for this widget
|
|
76
|
+
@widget_id_counter ||= 0
|
|
77
|
+
@widget_id_counter += 1
|
|
78
|
+
widget_id = "w#{@widget_id_counter}"
|
|
79
|
+
element.add_attribute("id", widget_id)
|
|
80
|
+
|
|
81
|
+
# Add area attributes
|
|
82
|
+
element.add_attribute("x", area.x.to_s)
|
|
83
|
+
element.add_attribute("y", area.y.to_s)
|
|
84
|
+
element.add_attribute("width", area.width.to_s)
|
|
85
|
+
element.add_attribute("height", area.height.to_s)
|
|
86
|
+
|
|
87
|
+
add_members(element, widget, parent_id: widget_id)
|
|
88
|
+
element
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def build_element(node)
|
|
92
|
+
class_name = node.class.name&.split("::")&.last || "Unknown"
|
|
93
|
+
element = REXML::Element.new(class_name)
|
|
94
|
+
|
|
95
|
+
if node.respond_to?(:to_h) && node.respond_to?(:members)
|
|
96
|
+
add_members(element, node)
|
|
97
|
+
else
|
|
98
|
+
element.text = node.to_s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
element
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def add_members(element, node, parent_id: nil)
|
|
105
|
+
return unless node.respond_to?(:to_h) && node.respond_to?(:members)
|
|
106
|
+
|
|
107
|
+
node.to_h.each do |key, value|
|
|
108
|
+
# Skip nil and empty values entirely (no noise in output)
|
|
109
|
+
next if value.nil?
|
|
110
|
+
next if value.respond_to?(:empty?) && value.empty?
|
|
111
|
+
|
|
112
|
+
# Skip objects where all members are nil/empty (like default Style)
|
|
113
|
+
if value.respond_to?(:to_h) && value.respond_to?(:members)
|
|
114
|
+
attrs = value.to_h.compact
|
|
115
|
+
next if attrs.empty? || attrs.values.all? { |v| v.respond_to?(:empty?) && v.empty? }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Scalar values → XML attributes
|
|
119
|
+
# Exception: 'text' and 'content' accept Text (multi-line capable)
|
|
120
|
+
# Complex values → XML child elements
|
|
121
|
+
multiline_keys = %w[text content]
|
|
122
|
+
if scalar?(value) && !multiline_keys.include?(key.to_s)
|
|
123
|
+
element.add_attribute(key.to_s, value.to_s)
|
|
124
|
+
else
|
|
125
|
+
# Special handling for 'block' wrapper - add ARIA role and id/for
|
|
126
|
+
is_block_wrapper = key.to_s == "block" && parent_id
|
|
127
|
+
child = build_child_element(key, value, is_wrapper: is_block_wrapper, parent_id:)
|
|
128
|
+
element.add(child) if child
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private def scalar?(value)
|
|
134
|
+
case value
|
|
135
|
+
when String, Symbol, Numeric, TrueClass, FalseClass
|
|
136
|
+
true
|
|
137
|
+
else
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private def build_child_element(key, value, is_wrapper: false, parent_id: nil)
|
|
143
|
+
element = REXML::Element.new(key.to_s)
|
|
144
|
+
|
|
145
|
+
# Add ARIA role and id/for association for block wrappers
|
|
146
|
+
if is_wrapper && parent_id
|
|
147
|
+
element.add_attribute("role", "group")
|
|
148
|
+
element.add_attribute("for", parent_id)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
case value
|
|
152
|
+
when Array
|
|
153
|
+
value.each { |item| element.add(build_element(item)) }
|
|
154
|
+
when Hash
|
|
155
|
+
# Plain Hash: serialize keys as attributes
|
|
156
|
+
value.each do |k, v|
|
|
157
|
+
next if v.nil?
|
|
158
|
+
next if v.respond_to?(:empty?) && v.empty?
|
|
159
|
+
element.add_attribute(k.to_s, v.to_s)
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
if value.respond_to?(:to_h) && value.respond_to?(:members)
|
|
163
|
+
add_members(element, value)
|
|
164
|
+
else
|
|
165
|
+
element.text = value.to_s
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
element
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Lazily loads REXML when first needed.
|
|
173
|
+
private def ensure_rexml_loaded
|
|
174
|
+
return if defined?(@rexml_loaded) && @rexml_loaded
|
|
175
|
+
|
|
176
|
+
require "rexml/document"
|
|
177
|
+
@rexml_loaded = true
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
class Frame
|
|
10
|
+
# A11Y Lab Integration
|
|
11
|
+
#
|
|
12
|
+
# When the A11Y lab is enabled, we capture widgets as they are rendered
|
|
13
|
+
# and write the tree to XML when flush_a11y_capture is called.
|
|
14
|
+
module A11yCapture
|
|
15
|
+
# Intercepts render_widget to capture widgets for A11Y export.
|
|
16
|
+
# @param widget [widget] The widget being rendered
|
|
17
|
+
# @param area [Layout::Rect] The area to render into
|
|
18
|
+
def render_widget(widget, area)
|
|
19
|
+
if Labs.enabled?(:a11y)
|
|
20
|
+
widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
|
|
21
|
+
widgets << [widget, area]
|
|
22
|
+
end
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Intercepts render_stateful_widget to capture widgets for A11Y export.
|
|
27
|
+
# @param widget [widget] The widget being rendered
|
|
28
|
+
# @param area [Layout::Rect] The area to render into
|
|
29
|
+
# @param state [Object] The widget state
|
|
30
|
+
def render_stateful_widget(widget, area, state)
|
|
31
|
+
if Labs.enabled?(:a11y)
|
|
32
|
+
widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
|
|
33
|
+
widgets << [widget, area]
|
|
34
|
+
end
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called at end of draw block to flush captured widgets
|
|
39
|
+
def flush_a11y_capture
|
|
40
|
+
widgets = @a11y_widgets
|
|
41
|
+
return unless Labs.enabled?(:a11y) && widgets&.any?
|
|
42
|
+
|
|
43
|
+
Labs::A11y.dump_widgets(widgets)
|
|
44
|
+
@a11y_widgets = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
prepend A11yCapture
|
|
49
|
+
end
|
|
50
|
+
end
|