ratatui_ruby 0.4.0 → 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 +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +87 -171
- data/CHANGELOG.md +38 -1
- data/README.md +8 -3
- data/REUSE.toml +20 -0
- data/doc/application_architecture.md +105 -45
- data/doc/application_testing.md +5 -3
- data/doc/contributors/design/ruby_frontend.md +9 -5
- data/doc/contributors/developing_examples.md +76 -18
- data/doc/contributors/documentation_style.md +7 -0
- data/doc/contributors/index.md +2 -0
- data/doc/event_handling.md +10 -4
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.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_ratatui_logo_demo.png +0 -0
- data/doc/images/widget_ratatui_mascot_demo.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_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/interactive_design.md +25 -30
- data/doc/quickstart.md +147 -120
- 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/examples/app_all_events/view.rb +7 -0
- 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/{login_form → app_login_form}/app.rb +39 -42
- data/examples/{map_demo → app_map_demo}/app.rb +24 -21
- data/examples/{table_select → app_table_select}/app.rb +68 -65
- data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +15 -6
- data/examples/verify_quickstart_layout/app.rb +69 -0
- data/examples/{quickstart_lifecycle → verify_quickstart_lifecycle}/app.rb +19 -10
- data/examples/verify_readme_usage/app.rb +34 -0
- data/examples/widget_barchart_demo/app.rb +238 -0
- data/examples/{block_padding → widget_block_padding}/app.rb +17 -13
- data/examples/{block_titles → widget_block_titles}/app.rb +25 -17
- data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
- 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 +19 -21
- data/examples/widget_table_flex/app.rb +95 -0
- data/examples/widget_tabs_demo/app.rb +167 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/events.rs +121 -36
- data/ext/ratatui_ruby/src/frame.rs +115 -0
- data/ext/ratatui_ruby/src/lib.rs +79 -26
- data/ext/ratatui_ruby/src/rendering.rs +8 -4
- data/ext/ratatui_ruby/src/style.rs +138 -57
- data/ext/ratatui_ruby/src/terminal.rs +5 -9
- data/ext/ratatui_ruby/src/text.rs +13 -6
- data/ext/ratatui_ruby/src/widgets/barchart.rs +56 -54
- data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
- data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
- data/ext/ratatui_ruby/src/widgets/list.rs +32 -9
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +4 -2
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
- data/ext/ratatui_ruby/src/widgets/table.rs +8 -4
- data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
- data/lib/ratatui_ruby/cell.rb +3 -3
- data/lib/ratatui_ruby/event/key.rb +1 -1
- data/lib/ratatui_ruby/event/none.rb +43 -0
- data/lib/ratatui_ruby/event.rb +56 -4
- data/lib/ratatui_ruby/frame.rb +87 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +11 -11
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
- data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
- data/lib/ratatui_ruby/schema/block.rb +163 -168
- data/lib/ratatui_ruby/schema/calendar.rb +66 -67
- data/lib/ratatui_ruby/schema/canvas.rb +63 -63
- data/lib/ratatui_ruby/schema/center.rb +46 -46
- data/lib/ratatui_ruby/schema/chart.rb +135 -143
- data/lib/ratatui_ruby/schema/clear.rb +42 -42
- data/lib/ratatui_ruby/schema/constraint.rb +76 -76
- data/lib/ratatui_ruby/schema/cursor.rb +25 -25
- data/lib/ratatui_ruby/schema/gauge.rb +53 -53
- data/lib/ratatui_ruby/schema/layout.rb +87 -87
- data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
- data/lib/ratatui_ruby/schema/list.rb +86 -84
- data/lib/ratatui_ruby/schema/overlay.rb +31 -31
- data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
- data/lib/ratatui_ruby/schema/rect.rb +60 -60
- data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
- data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
- data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
- data/lib/ratatui_ruby/schema/style.rb +46 -46
- data/lib/ratatui_ruby/schema/table.rb +112 -119
- data/lib/ratatui_ruby/schema/tabs.rb +66 -67
- data/lib/ratatui_ruby/session/autodoc.rb +417 -0
- data/lib/ratatui_ruby/session.rb +40 -23
- data/lib/ratatui_ruby/test_helper.rb +185 -19
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +65 -39
- data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
- data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
- data/sig/examples/app_all_events/model/events.rbs +15 -0
- data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
- data/sig/examples/app_all_events/view/app_view.rbs +8 -0
- data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
- data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
- data/sig/examples/app_all_events/view/live_view.rbs +6 -0
- data/sig/examples/app_all_events/view/log_view.rbs +6 -0
- data/sig/examples/app_all_events/view.rbs +8 -0
- data/sig/examples/app_all_events/view_state.rbs +15 -0
- data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
- data/sig/examples/app_login_form/app.rbs +11 -0
- data/sig/examples/app_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/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
- data/sig/examples/widget_layout_split/app.rbs +10 -0
- data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
- data/sig/examples/widget_list_demo/app.rbs +12 -0
- data/sig/examples/widget_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/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
- data/sig/examples/widget_table_flex/app.rbs +11 -0
- data/sig/ratatui_ruby/frame.rbs +9 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +3 -2
- data/sig/ratatui_ruby/schema/draw.rbs +4 -0
- data/sig/ratatui_ruby/schema/layout.rbs +1 -1
- 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/index.html.erb +6 -0
- data/tasks/sourcehut.rake +4 -4
- data/tasks/terminal_preview/app_screenshot.rb +1 -3
- data/tasks/terminal_preview/crash_report.rb +7 -9
- data/tasks/terminal_preview/launcher_script.rb +4 -6
- data/tasks/terminal_preview/preview_collection.rb +4 -6
- data/tasks/terminal_preview/safety_confirmation.rb +3 -5
- data/tasks/terminal_preview/saved_screenshot.rb +7 -9
- data/tasks/terminal_preview/terminal_window.rb +7 -9
- data/tasks/test.rake +1 -1
- data/tasks/website/index_page.rb +3 -3
- data/tasks/website/version.rb +10 -10
- data/tasks/website/version_menu.rb +10 -12
- data/tasks/website/versioned_documentation.rb +49 -17
- data/tasks/website/website.rb +6 -8
- data/tasks/website.rake +4 -4
- metadata +156 -125
- data/LICENSES/BSD-2-Clause.txt +0 -9
- data/doc/contributors/better_dx.md +0 -543
- data/doc/contributors/example_analysis.md +0 -82
- data/doc/images/all_events.png +0 -0
- data/doc/images/block_padding.png +0 -0
- data/doc/images/block_titles.png +0 -0
- data/doc/images/box_demo.png +0 -0
- data/doc/images/calendar_demo.png +0 -0
- data/doc/images/cell_demo.png +0 -0
- data/doc/images/chart_demo.png +0 -0
- data/doc/images/flex_layout.png +0 -0
- data/doc/images/gauge_demo.png +0 -0
- data/doc/images/line_gauge_demo.png +0 -0
- data/doc/images/list_demo.png +0 -0
- data/doc/images/readme_usage.png +0 -0
- data/doc/images/scrollbar_demo.png +0 -0
- data/doc/images/sparkline_demo.png +0 -0
- data/doc/images/table_flex.png +0 -0
- data/examples/all_events/app.rb +0 -169
- data/examples/all_events/app.rbs +0 -7
- data/examples/all_events/test_app.rb +0 -139
- data/examples/analytics/app.rb +0 -258
- data/examples/analytics/app.rbs +0 -7
- data/examples/analytics/test_app.rb +0 -132
- data/examples/block_padding/app.rbs +0 -7
- data/examples/block_padding/test_app.rb +0 -31
- data/examples/block_titles/app.rbs +0 -7
- data/examples/block_titles/test_app.rb +0 -34
- data/examples/box_demo/app.rbs +0 -7
- data/examples/box_demo/test_app.rb +0 -88
- data/examples/calendar_demo/app.rb +0 -101
- data/examples/calendar_demo/app.rbs +0 -7
- data/examples/calendar_demo/test_app.rb +0 -108
- data/examples/cell_demo/app.rb +0 -108
- data/examples/cell_demo/app.rbs +0 -7
- data/examples/cell_demo/test_app.rb +0 -36
- data/examples/chart_demo/app.rb +0 -203
- data/examples/chart_demo/app.rbs +0 -7
- data/examples/chart_demo/test_app.rb +0 -102
- data/examples/custom_widget/app.rb +0 -51
- data/examples/custom_widget/app.rbs +0 -7
- data/examples/custom_widget/test_app.rb +0 -30
- data/examples/flex_layout/app.rb +0 -156
- data/examples/flex_layout/app.rbs +0 -7
- data/examples/flex_layout/test_app.rb +0 -65
- data/examples/gauge_demo/app.rb +0 -182
- data/examples/gauge_demo/test_app.rb +0 -120
- data/examples/hit_test/app.rb +0 -175
- data/examples/hit_test/app.rbs +0 -7
- data/examples/hit_test/test_app.rb +0 -102
- data/examples/line_gauge_demo/app.rb +0 -190
- data/examples/line_gauge_demo/app.rbs +0 -7
- data/examples/line_gauge_demo/test_app.rb +0 -129
- data/examples/list_demo/app.rb +0 -253
- data/examples/list_demo/test_app.rb +0 -237
- data/examples/list_styles/app.rb +0 -140
- data/examples/list_styles/app.rbs +0 -7
- data/examples/list_styles/test_app.rb +0 -157
- data/examples/login_form/app.rbs +0 -7
- data/examples/login_form/test_app.rb +0 -51
- data/examples/map_demo/app.rbs +0 -7
- data/examples/map_demo/test_app.rb +0 -149
- data/examples/mouse_events/app.rb +0 -97
- data/examples/mouse_events/app.rbs +0 -7
- data/examples/mouse_events/test_app.rb +0 -53
- data/examples/popup_demo/app.rb +0 -103
- data/examples/popup_demo/app.rbs +0 -7
- data/examples/popup_demo/test_app.rb +0 -54
- data/examples/quickstart_dsl/app.rbs +0 -7
- data/examples/quickstart_dsl/test_app.rb +0 -29
- data/examples/quickstart_lifecycle/app.rbs +0 -7
- data/examples/quickstart_lifecycle/test_app.rb +0 -29
- data/examples/ratatui_logo_demo/app.rb +0 -79
- data/examples/ratatui_logo_demo/app.rbs +0 -7
- data/examples/ratatui_logo_demo/test_app.rb +0 -51
- data/examples/ratatui_mascot_demo/app.rb +0 -84
- data/examples/ratatui_mascot_demo/app.rbs +0 -7
- data/examples/ratatui_mascot_demo/test_app.rb +0 -47
- data/examples/readme_usage/app.rb +0 -29
- data/examples/readme_usage/app.rbs +0 -7
- data/examples/readme_usage/test_app.rb +0 -29
- data/examples/rich_text/app.rb +0 -141
- data/examples/rich_text/app.rbs +0 -7
- data/examples/rich_text/test_app.rb +0 -166
- data/examples/scroll_text/app.rb +0 -103
- data/examples/scroll_text/app.rbs +0 -7
- data/examples/scroll_text/test_app.rb +0 -110
- data/examples/scrollbar_demo/app.rb +0 -143
- data/examples/scrollbar_demo/app.rbs +0 -7
- data/examples/scrollbar_demo/test_app.rb +0 -77
- data/examples/sparkline_demo/app.rb +0 -240
- data/examples/sparkline_demo/test_app.rb +0 -107
- data/examples/table_flex/app.rb +0 -65
- data/examples/table_flex/app.rbs +0 -7
- data/examples/table_flex/test_app.rb +0 -36
- data/examples/table_select/app.rbs +0 -7
- data/examples/table_select/test_app.rb +0 -180
- data/examples/widget_style_colors/test_app.rb +0 -48
- /data/doc/images/{analytics.png → app_analytics.png} +0 -0
- /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
- /data/doc/images/{login_form.png → app_login_form.png} +0 -0
- /data/doc/images/{map_demo.png → app_map_demo.png} +0 -0
- /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
- /data/doc/images/{table_select.png → app_table_select.png} +0 -0
- /data/doc/images/{quickstart_dsl.png → verify_quickstart_dsl.png} +0 -0
- /data/doc/images/{ratatui_logo_demo.png → verify_quickstart_layout.png} +0 -0
- /data/doc/images/{quickstart_lifecycle.png → verify_quickstart_lifecycle.png} +0 -0
- /data/doc/images/{list_styles.png → widget_list_styles.png} +0 -0
- /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
- /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
- /data/doc/images/{rich_text.png → widget_rich_text.png} +0 -0
- /data/doc/images/{scroll_text.png → widget_scroll_text.png} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
require_relative "../view"
|
|
7
|
+
require_relative "counts_view"
|
|
8
|
+
require_relative "live_view"
|
|
9
|
+
require_relative "log_view"
|
|
10
|
+
require_relative "controls_view"
|
|
11
|
+
|
|
12
|
+
# Orchestrates the complete UI layout and sub-view composition.
|
|
13
|
+
#
|
|
14
|
+
# Complex applications need a structured way to divide the screen and delegate rendering.
|
|
15
|
+
# Placing all layout logic in one monolithic method makes the code difficult to maintain.
|
|
16
|
+
#
|
|
17
|
+
# This class defines the screen layout using a series of split constraints and delegates to sub-views.
|
|
18
|
+
#
|
|
19
|
+
# Use it as the root view for the All Events example application.
|
|
20
|
+
#
|
|
21
|
+
# === Examples
|
|
22
|
+
#
|
|
23
|
+
# app_view = View::App.new
|
|
24
|
+
# app_view.call(state, tui, frame, area)
|
|
25
|
+
class View::App
|
|
26
|
+
# Creates a new View::App and initializes sub-views.
|
|
27
|
+
def initialize
|
|
28
|
+
@counts_view = View::Counts.new
|
|
29
|
+
@live_view = View::Live.new
|
|
30
|
+
@log_view = View::Log.new
|
|
31
|
+
@controls_view = View::Controls.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Renders the entire application UI to the given area.
|
|
35
|
+
#
|
|
36
|
+
# [state] ViewState containing all application data.
|
|
37
|
+
# [tui] RatatuiRuby instance.
|
|
38
|
+
# [frame] RatatuiRuby::Frame being rendered.
|
|
39
|
+
# [area] RatatuiRuby::Rect defining the total available space.
|
|
40
|
+
#
|
|
41
|
+
# === Example
|
|
42
|
+
#
|
|
43
|
+
# app_view.call(state, tui, frame, area)
|
|
44
|
+
def call(state, tui, frame, area)
|
|
45
|
+
main_area, control_area = tui.layout_split(
|
|
46
|
+
area,
|
|
47
|
+
direction: :vertical,
|
|
48
|
+
constraints: [
|
|
49
|
+
tui.constraint_fill(1),
|
|
50
|
+
tui.constraint_length(3),
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
counts_area, _margin_area, right_area = tui.layout_split(
|
|
55
|
+
main_area,
|
|
56
|
+
direction: :horizontal,
|
|
57
|
+
constraints: [
|
|
58
|
+
tui.constraint_length(20),
|
|
59
|
+
tui.constraint_length(1),
|
|
60
|
+
tui.constraint_fill(1),
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
live_area, log_area = tui.layout_split(
|
|
65
|
+
right_area,
|
|
66
|
+
direction: :vertical,
|
|
67
|
+
constraints: [
|
|
68
|
+
tui.constraint_length(9),
|
|
69
|
+
tui.constraint_fill(1),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@counts_view.call(state, tui, frame, counts_area)
|
|
74
|
+
@live_view.call(state, tui, frame, live_area)
|
|
75
|
+
@log_view.call(state, tui, frame, log_area)
|
|
76
|
+
@controls_view.call(state, tui, frame, control_area)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
require_relative "../view"
|
|
7
|
+
|
|
8
|
+
# Renders the keyboard controls and shortcuts panel.
|
|
9
|
+
#
|
|
10
|
+
# Users need to know how to interact with the application and exit.
|
|
11
|
+
# Hardcoding control descriptions into the main layout makes the code hard to read.
|
|
12
|
+
#
|
|
13
|
+
# This component renders a formatted paragraph listing available global shortcuts.
|
|
14
|
+
#
|
|
15
|
+
# Use it to display help information in a sidebar or dedicated panel.
|
|
16
|
+
#
|
|
17
|
+
# === Examples
|
|
18
|
+
#
|
|
19
|
+
# controls = View::Controls.new
|
|
20
|
+
# controls.call(state, tui, frame, area)
|
|
21
|
+
class View::Controls
|
|
22
|
+
# Renders the controls widget to the given area.
|
|
23
|
+
#
|
|
24
|
+
# [state] ViewState containing style information.
|
|
25
|
+
# [tui] RatatuiRuby instance.
|
|
26
|
+
# [frame] RatatuiRuby::Frame being rendered.
|
|
27
|
+
# [area] RatatuiRuby::Rect defining the widget's bounds.
|
|
28
|
+
#
|
|
29
|
+
# === Example
|
|
30
|
+
#
|
|
31
|
+
# controls.call(state, tui, frame, area)
|
|
32
|
+
def call(state, tui, frame, area)
|
|
33
|
+
widget = tui.paragraph(
|
|
34
|
+
text: [
|
|
35
|
+
tui.text_line(spans: [
|
|
36
|
+
tui.text_span(content: "q", style: state.hotkey_style),
|
|
37
|
+
tui.text_span(content: ": Quit "),
|
|
38
|
+
tui.text_span(content: "Ctrl+C", style: state.hotkey_style),
|
|
39
|
+
tui.text_span(content: ": Quit"),
|
|
40
|
+
]),
|
|
41
|
+
],
|
|
42
|
+
block: tui.block(
|
|
43
|
+
title: "Controls",
|
|
44
|
+
borders: [:all],
|
|
45
|
+
border_style: tui.style(fg: :white)
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
frame.render_widget(widget, area)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
require_relative "../view"
|
|
7
|
+
|
|
8
|
+
# Renders the event statistics dashboard.
|
|
9
|
+
#
|
|
10
|
+
# Developers auditing input need to see real-time counts of various event types.
|
|
11
|
+
#
|
|
12
|
+
# This component displays a list of event types with their total counts.
|
|
13
|
+
#
|
|
14
|
+
# Use it to build an interactive dashboard of application activity.
|
|
15
|
+
class View::Counts
|
|
16
|
+
# Renders the event counts widget to the given area.
|
|
17
|
+
#
|
|
18
|
+
# [state] ViewState containing event data and styles.
|
|
19
|
+
# [tui] RatatuiRuby instance.
|
|
20
|
+
# [frame] RatatuiRuby::Frame being rendered.
|
|
21
|
+
# [area] RatatuiRuby::Rect defining the widget's bounds.
|
|
22
|
+
def call(state, tui, frame, area)
|
|
23
|
+
count_lines = []
|
|
24
|
+
|
|
25
|
+
AppAllEvents::EVENT_TYPES.each do |type|
|
|
26
|
+
count = state.events.count(type)
|
|
27
|
+
label = type.to_s.capitalize
|
|
28
|
+
style = state.events.lit?(type) ? state.lit_style : nil
|
|
29
|
+
|
|
30
|
+
count_lines << tui.text_line(spans: [
|
|
31
|
+
tui.text_span(content: "#{label}: ", style:),
|
|
32
|
+
tui.text_span(content: count.to_s, style: style || tui.style(fg: :yellow)),
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
state.events.sub_counts(type).each do |sub_type, sub_count|
|
|
36
|
+
sub_label = sub_type.to_s.capitalize
|
|
37
|
+
count_lines << tui.text_line(spans: [
|
|
38
|
+
tui.text_span(content: " #{sub_label}: ", style: state.dimmed_style),
|
|
39
|
+
tui.text_span(content: sub_count.to_s, style: state.dimmed_style),
|
|
40
|
+
])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
widget = tui.paragraph(
|
|
45
|
+
text: count_lines,
|
|
46
|
+
scroll: [0, 0],
|
|
47
|
+
block: tui.block(
|
|
48
|
+
title: "Event Counts",
|
|
49
|
+
borders: [:all],
|
|
50
|
+
border_style: tui.style(fg: state.border_color)
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
frame.render_widget(widget, area)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
require_relative "../view"
|
|
7
|
+
|
|
8
|
+
# Renders a real-time summary of the most recent events.
|
|
9
|
+
#
|
|
10
|
+
# Users need to see the immediate result of their actions without digging through a log.
|
|
11
|
+
# Identifying the specific details of the last key press or mouse move at a glance is difficult.
|
|
12
|
+
#
|
|
13
|
+
# This component displays a table showing the latest event of each type with its timestamp and description.
|
|
14
|
+
#
|
|
15
|
+
# Use it to provide instant feedback for user interactions.
|
|
16
|
+
#
|
|
17
|
+
# === Examples
|
|
18
|
+
#
|
|
19
|
+
# live_view = View::Live.new
|
|
20
|
+
# live_view.call(state, tui, frame, area)
|
|
21
|
+
class View::Live
|
|
22
|
+
# Renders the live event table to the given area.
|
|
23
|
+
#
|
|
24
|
+
# [state] ViewState containing event data and styles.
|
|
25
|
+
# [tui] RatatuiRuby instance.
|
|
26
|
+
# [frame] RatatuiRuby::Frame being rendered.
|
|
27
|
+
# [area] RatatuiRuby::Rect defining the widget's bounds.
|
|
28
|
+
#
|
|
29
|
+
# === Example
|
|
30
|
+
#
|
|
31
|
+
# live_view.call(state, tui, frame, area)
|
|
32
|
+
def call(state, tui, frame, area)
|
|
33
|
+
rows = []
|
|
34
|
+
|
|
35
|
+
rows << tui.text_line(spans: [
|
|
36
|
+
tui.text_span(content: "Type".ljust(9), style: tui.style(fg: :gray, modifiers: [:bold])),
|
|
37
|
+
tui.text_span(content: "Time".ljust(10), style: tui.style(fg: :gray, modifiers: [:bold])),
|
|
38
|
+
tui.text_span(content: "Description", style: tui.style(fg: :gray, modifiers: [:bold])),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
(AppAllEvents::EVENT_TYPES - [:none]).each do |type|
|
|
42
|
+
event_data = state.events.live_event(type)
|
|
43
|
+
|
|
44
|
+
class_str = type.to_s.capitalize
|
|
45
|
+
time_str = event_data ? event_data[:time].strftime("%H:%M:%S") : "—"
|
|
46
|
+
desc_str = event_data ? event_data[:description] : "—"
|
|
47
|
+
|
|
48
|
+
is_lit = state.events.lit?(type)
|
|
49
|
+
row_style = is_lit ? tui.style(fg: :black, bg: :green) : nil
|
|
50
|
+
|
|
51
|
+
rows << tui.text_line(spans: [
|
|
52
|
+
tui.text_span(content: class_str.ljust(9), style: row_style || tui.style(fg: :cyan)),
|
|
53
|
+
tui.text_span(content: time_str.ljust(10), style: row_style || tui.style(fg: :white)),
|
|
54
|
+
tui.text_span(content: desc_str, style: row_style),
|
|
55
|
+
])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
widget = tui.paragraph(
|
|
59
|
+
text: rows,
|
|
60
|
+
scroll: [0, 0],
|
|
61
|
+
block: tui.block(
|
|
62
|
+
title: "Live Display",
|
|
63
|
+
borders: [:all],
|
|
64
|
+
border_style: tui.style(fg: state.border_color)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
frame.render_widget(widget, area)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
require_relative "../view"
|
|
7
|
+
|
|
8
|
+
# Renders a detailed, scrollable history of application events.
|
|
9
|
+
#
|
|
10
|
+
# Debugging complex event flows requires a chronological record of raw data.
|
|
11
|
+
# Interpreting raw event objects without formatting is difficult and slow.
|
|
12
|
+
#
|
|
13
|
+
# This component renders event history as a series of formatted, color-coded entries showing raw data.
|
|
14
|
+
#
|
|
15
|
+
# Use it to provide a detailed audit trail of all terminal interactions.
|
|
16
|
+
class View::Log
|
|
17
|
+
# Renders the event log widget to the given area.
|
|
18
|
+
#
|
|
19
|
+
# [state] ViewState containing event data and styles.
|
|
20
|
+
# [tui] RatatuiRuby instance.
|
|
21
|
+
# [frame] RatatuiRuby::Frame being rendered.
|
|
22
|
+
# [area] RatatuiRuby::Rect defining the widget's bounds.
|
|
23
|
+
def call(state, tui, frame, area)
|
|
24
|
+
visible_entries_count = (area.height - 2) / 2
|
|
25
|
+
display_entries = state.events.visible(visible_entries_count)
|
|
26
|
+
|
|
27
|
+
log_lines = []
|
|
28
|
+
if state.events.empty?
|
|
29
|
+
log_lines << tui.text_line(spans: [tui.text_span(content: "No events yet...", style: state.dimmed_style)])
|
|
30
|
+
else
|
|
31
|
+
display_entries.each do |entry|
|
|
32
|
+
entry_style = tui.style(fg: entry.color)
|
|
33
|
+
|
|
34
|
+
# Split description into lines if it's too long, or just let it wrap conceptually (though paragraph wraps by character by default)
|
|
35
|
+
# Using simple inspect output as requested.
|
|
36
|
+
description = entry.description
|
|
37
|
+
|
|
38
|
+
# We want to display it over potentially multiple lines if needed, but the original code did manual 2-line formatting.
|
|
39
|
+
# Let's try to just dump the inspect string. If it's very long, it might be cut off.
|
|
40
|
+
# But the User asked specifically to use inspect.
|
|
41
|
+
|
|
42
|
+
log_lines << tui.text_line(spans: [tui.text_span(content: description, style: entry_style)])
|
|
43
|
+
log_lines << tui.text_line(spans: [tui.text_span(content: "", style: entry_style)]) # Spacer line to match previous 2-line rhythm? Or just compact?
|
|
44
|
+
# Previous view had 2 lines per entry. Let's keep a spacer to make it readable.
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
widget = tui.paragraph(
|
|
49
|
+
text: log_lines,
|
|
50
|
+
scroll: [0, 0],
|
|
51
|
+
wrap: { trim: true },
|
|
52
|
+
block: tui.block(
|
|
53
|
+
title: "Event Log",
|
|
54
|
+
borders: [:all],
|
|
55
|
+
border_style: tui.style(fg: state.border_color)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
frame.render_widget(widget, area)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
# Encapsulates all data required to render the application view.
|
|
7
|
+
#
|
|
8
|
+
# Views need access to models, global settings, and calculated styles.
|
|
9
|
+
# Passing dozens of individual parameters to view components is messy and unmaintainable.
|
|
10
|
+
#
|
|
11
|
+
# This class provides a single, structured object containing all state necessary for rendering.
|
|
12
|
+
#
|
|
13
|
+
# Use it to shuttle data from the main application to the various view components.
|
|
14
|
+
#
|
|
15
|
+
# === Examples
|
|
16
|
+
#
|
|
17
|
+
# state = ViewState.build(events, true, tui, nil)
|
|
18
|
+
# puts state.focused #=> true
|
|
19
|
+
# app_view.call(state, tui, frame, area)
|
|
20
|
+
class ViewState < Data.define(:events, :focused, :hotkey_style, :dimmed_style, :lit_style, :border_color, :area)
|
|
21
|
+
# Builds a new ViewState with calculated styles.
|
|
22
|
+
#
|
|
23
|
+
# [events] Events model instance.
|
|
24
|
+
# [focused] Boolean indicating if the app has focus.
|
|
25
|
+
# [tui] RatatuiRuby instance for style creation.
|
|
26
|
+
# [_resize_sub_counter] Unused parameter (reserved for future use).
|
|
27
|
+
#
|
|
28
|
+
# === Example
|
|
29
|
+
#
|
|
30
|
+
# ViewState.build(events, true, tui, nil) #=> #<ViewState ...>
|
|
31
|
+
def self.build(events, focused, tui, _resize_sub_counter)
|
|
32
|
+
new(
|
|
33
|
+
events:,
|
|
34
|
+
focused:,
|
|
35
|
+
hotkey_style: tui.style(modifiers: [:bold, :underlined]),
|
|
36
|
+
dimmed_style: tui.style(fg: :dark_gray),
|
|
37
|
+
lit_style: tui.style(fg: :green, modifiers: [:bold]),
|
|
38
|
+
border_color: focused ? :green : :gray,
|
|
39
|
+
area: nil
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Color Picker Example
|
|
7
|
+
|
|
8
|
+
This example demonstrates how to build a **Feature-Rich Interactive Application** using `ratatui_ruby`.
|
|
9
|
+
|
|
10
|
+
It goes beyond simple widgets to show a complete, real-world architecture for handling:
|
|
11
|
+
- **Complex State Management** (Input validation, undo/redo prep, clipboard interaction)
|
|
12
|
+
- **Mouse Interaction & Hit Testing**
|
|
13
|
+
- **Dynamic Layouts**
|
|
14
|
+
- **Modal Dialogs**
|
|
15
|
+
|
|
16
|
+
## Architecture: The "Scene-Orchestrated" Pattern
|
|
17
|
+
|
|
18
|
+
This app uses a pattern we call **"Scene-Orchestrated MVC"**.
|
|
19
|
+
|
|
20
|
+
### 1. The App (Controller)
|
|
21
|
+
The main `App` class (`app.rb`) acts as the Controller. It:
|
|
22
|
+
- Holds the source of truth (the State).
|
|
23
|
+
- Runs the Event Loop.
|
|
24
|
+
- Routes input events to the appropriate handler.
|
|
25
|
+
- Initializes the `Scene`.
|
|
26
|
+
|
|
27
|
+
### 2. The Scene (View / Layout Engine)
|
|
28
|
+
The `Scene` class (`scene.rb`) acts as the primary View. Unlike simple examples where the render logic is in the `App` class, here the **Scene owns the Layout**.
|
|
29
|
+
- **Composition**: It takes purely logical objects (`Palette`, `Input`) and decides how to present them.
|
|
30
|
+
- **Hit Testing**: Crucially, the Scene **caches layout rectangles** (like `@export_area_rect`) during the render pass so the Controller knows *where* things are to handle clicks later.
|
|
31
|
+
|
|
32
|
+
### 3. The Logical Models
|
|
33
|
+
The application logic is broken down into small, testable Plain Old Ruby Objects (POROs) that know nothing about the TUI:
|
|
34
|
+
- **`Color`**: Handles hex parsing, contrast calculation, and transformations.
|
|
35
|
+
- **`Palette`**: Generates color harmonies.
|
|
36
|
+
- **`Input`**: Manages the text buffer and validation state.
|
|
37
|
+
- **`Clipboard`**: Wraps system commands.
|
|
38
|
+
|
|
39
|
+
This separation means your **business logic remains pure Ruby**, while the TUI layer focuses solely on presentation.
|
|
40
|
+
|
|
41
|
+
## Key Features Showcased
|
|
42
|
+
|
|
43
|
+
### 🖱️ Mouse Support & Hit Testing
|
|
44
|
+
See `Scene#export_rect` and `App#handle_main_input`.
|
|
45
|
+
The app detects clicks on specific UI elements. This handles the problem: *"How do I know which button the user clicked?"*
|
|
46
|
+
- **Solution**: The rendering layer (Scene) exposes the `Rect` of interactive areas. The event loop checks `rect.contains?(mouse_x, mouse_y)`.
|
|
47
|
+
|
|
48
|
+
### 🔲 Modal Dialogs
|
|
49
|
+
See `CopyDialog`.
|
|
50
|
+
The app implements a modal overlay that intercepts input.
|
|
51
|
+
- **Pattern**: The `App` checks `if dialog.active?`. If true, it routes events *only* to the dialog, effectively "blocking" the main UI.
|
|
52
|
+
|
|
53
|
+
### 🎨 Advanced Styling & Layout
|
|
54
|
+
- **Dynamic Constraints**: Layouts that adapt to content.
|
|
55
|
+
- **Visual Feedback**:
|
|
56
|
+
- Input fields turn red on error.
|
|
57
|
+
- Clipboard messages fade out over time (`Clipboard#tick`).
|
|
58
|
+
- Text colors automatically adjust for contrast (Black text on light backgrounds, White on dark).
|
|
59
|
+
|
|
60
|
+
## Problem Solving: What you can learn
|
|
61
|
+
|
|
62
|
+
Read this example if you are trying to solve:
|
|
63
|
+
1. **"How do I structure a larger app?"** -> Move render logic out of `App` and into a `Scene` or `View` class.
|
|
64
|
+
2. **"How do I handle mouse clicks?"** -> Cache the `Rect` during render.
|
|
65
|
+
3. **"How do I make a popup?"** -> Use a state flag (`active?`) to conditional render on top of everything else (z-ordering) and hijack the input loop.
|
|
66
|
+
4. **"How do I validate input?"** -> Wrap strings in an `Input` object that tracks both keypresses and validation errors.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ruby examples/app_color_picker/app.rb
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Type a hex code (e.g., `#FF0055`) or color name (`cyan`).
|
|
75
|
+
- Press `Enter` to generate the palette.
|
|
76
|
+
- Click on the **Export Formats** box to copy the hex code.
|
|
77
|
+
|
|
78
|
+
## Comparison: Choosing an Architecture
|
|
79
|
+
|
|
80
|
+
Complex applications require structured state habits. This Color Picker and the [App All Events](../app_all_events/README.md) example demonstrate two different approaches.
|
|
81
|
+
|
|
82
|
+
### The Tool Approach (Color Picker)
|
|
83
|
+
|
|
84
|
+
Tools require interaction. Users click buttons and drag sliders. The Controller needs to know where components exist on screen. MVVM hides this layout data.
|
|
85
|
+
|
|
86
|
+
This example uses a "Scene" pattern. The View exposes layout rectangles. The Controller uses these rectangles to handle mouse clicks.
|
|
87
|
+
|
|
88
|
+
Use this pattern for forms, editors, and mouse-driven tools.
|
|
89
|
+
|
|
90
|
+
### The Dashboard Approach (AppAllEvents)
|
|
91
|
+
|
|
92
|
+
Dashboards display data. They rarely require complex mouse interaction. Strict MVVM works best there. The View is a pure function. It accepts a `ViewState` and draws it. It ignores input. This simplifies testing.
|
|
93
|
+
|
|
94
|
+
Use that pattern for logs, monitors, and data viewers.
|
|
@@ -0,0 +1,112 @@
|
|
|
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 "input"
|
|
11
|
+
require_relative "palette"
|
|
12
|
+
require_relative "clipboard"
|
|
13
|
+
require_relative "copy_dialog"
|
|
14
|
+
require_relative "scene"
|
|
15
|
+
|
|
16
|
+
# A terminal-based color picker application.
|
|
17
|
+
#
|
|
18
|
+
# Terminal users often need to select colors for themes or UI components.
|
|
19
|
+
# Manually typing hex codes and guessing how they will look is slow and error-prone.
|
|
20
|
+
#
|
|
21
|
+
# This application solves the problem by providing an interactive interface. It parses hex strings,
|
|
22
|
+
# generates palettes, and displays them visually in the terminal.
|
|
23
|
+
#
|
|
24
|
+
# Use it to experiment with color combinations and quickly find the right hex codes.
|
|
25
|
+
#
|
|
26
|
+
# === Examples
|
|
27
|
+
#
|
|
28
|
+
# AppColorPicker.new.run
|
|
29
|
+
#
|
|
30
|
+
class AppColorPicker
|
|
31
|
+
# Creates a new <tt>AppColorPicker</tt> instance with a default palette and clipboard.
|
|
32
|
+
def initialize
|
|
33
|
+
@input = Input.new
|
|
34
|
+
@palette = Palette.new(@input.parse)
|
|
35
|
+
@clipboard = Clipboard.new
|
|
36
|
+
@dialog = CopyDialog.new(@clipboard)
|
|
37
|
+
@scene = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Starts the terminal session and enters the main event loop.
|
|
41
|
+
#
|
|
42
|
+
# This method initializes the terminal, renders the initial scene, and polls for
|
|
43
|
+
# input until the user quits.
|
|
44
|
+
#
|
|
45
|
+
# === Example
|
|
46
|
+
#
|
|
47
|
+
# app = AppColorPicker.new
|
|
48
|
+
# app.run
|
|
49
|
+
#
|
|
50
|
+
def run
|
|
51
|
+
RatatuiRuby.run do |tui|
|
|
52
|
+
@scene = Scene.new(tui)
|
|
53
|
+
loop do
|
|
54
|
+
render(tui)
|
|
55
|
+
result = handle_input(tui)
|
|
56
|
+
break if result == :quit
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private def render(tui)
|
|
62
|
+
@clipboard.tick
|
|
63
|
+
tui.draw do |frame|
|
|
64
|
+
@scene.render(frame, input: @input, palette: @palette, clipboard: @clipboard, dialog: @dialog)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private def handle_input(tui)
|
|
69
|
+
event = tui.poll_event
|
|
70
|
+
@input.clear_error unless @dialog.active?
|
|
71
|
+
|
|
72
|
+
if @dialog.active?
|
|
73
|
+
handle_dialog_input(event)
|
|
74
|
+
else
|
|
75
|
+
handle_main_input(event)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private def handle_dialog_input(event)
|
|
80
|
+
result = @dialog.handle_input(event)
|
|
81
|
+
case event
|
|
82
|
+
in { type: :key, code: "q" } | { type: :key, code: "esc" } | { type: :key, code: "c", modifiers: ["ctrl"] }
|
|
83
|
+
:quit
|
|
84
|
+
else
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private def handle_main_input(event)
|
|
90
|
+
case event
|
|
91
|
+
in { type: :key, code: "q" } | { type: :key, code: "esc" } | { type: :key, code: "c", modifiers: ["ctrl"] }
|
|
92
|
+
:quit
|
|
93
|
+
in { type: :key, code: "enter" }
|
|
94
|
+
@palette = Palette.new(@input.parse)
|
|
95
|
+
in { type: :key, code: "backspace" }
|
|
96
|
+
@input.delete_char
|
|
97
|
+
in { type: :paste, content: }
|
|
98
|
+
@input.set(content)
|
|
99
|
+
@palette = Palette.new(@input.parse)
|
|
100
|
+
in { type: :key, code: code }
|
|
101
|
+
@input.append_char(code)
|
|
102
|
+
in { type: :mouse, kind: "down", button: "left", x:, y: }
|
|
103
|
+
if @scene && @scene.export_rect&.contains?(x, y) && @palette.main
|
|
104
|
+
@dialog.open(@palette.main.hex)
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
AppColorPicker.new.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
# Manages system clipboard interaction with transient feedback.
|
|
7
|
+
#
|
|
8
|
+
# Apps need to copy data to the clipboard. Users need feedback: "Did it work?"
|
|
9
|
+
# Manual clipboard handling and feedback timers scattered through app logic is
|
|
10
|
+
# messy.
|
|
11
|
+
#
|
|
12
|
+
# This object handles clipboard writes to all platforms (pbcopy, xclip, xsel).
|
|
13
|
+
# It manages a feedback message and countdown timer.
|
|
14
|
+
#
|
|
15
|
+
# Use it to provide copy-to-clipboard functionality with user feedback.
|
|
16
|
+
#
|
|
17
|
+
# === Example
|
|
18
|
+
#
|
|
19
|
+
# clipboard = Clipboard.new
|
|
20
|
+
# clipboard.copy("#FF0000")
|
|
21
|
+
# puts clipboard.message # => "Copied!"
|
|
22
|
+
#
|
|
23
|
+
# # In render loop:
|
|
24
|
+
# clipboard.tick # Decrement timer
|
|
25
|
+
# puts clipboard.message # => "" (after 60 frames)
|
|
26
|
+
class Clipboard
|
|
27
|
+
def initialize
|
|
28
|
+
@message = ""
|
|
29
|
+
@timer = 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Writes text to the system clipboard.
|
|
33
|
+
#
|
|
34
|
+
# Tries pbcopy (macOS), xclip (Linux), then xsel (Linux fallback). Sets the
|
|
35
|
+
# feedback message to <tt>"Copied!"</tt> and starts a 60-frame timer.
|
|
36
|
+
#
|
|
37
|
+
# [text] String to copy
|
|
38
|
+
#
|
|
39
|
+
# === Example
|
|
40
|
+
#
|
|
41
|
+
# clipboard = Clipboard.new
|
|
42
|
+
# clipboard.copy("#FF0000")
|
|
43
|
+
# clipboard.message # => "Copied!"
|
|
44
|
+
def copy(text)
|
|
45
|
+
if `which pbcopy 2>/dev/null`.strip.length > 0
|
|
46
|
+
IO.popen("pbcopy", "w") { |io| io.write(text) }
|
|
47
|
+
elsif `which xclip 2>/dev/null`.strip.length > 0
|
|
48
|
+
IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
|
|
49
|
+
elsif `which xsel 2>/dev/null`.strip.length > 0
|
|
50
|
+
IO.popen("xsel --clipboard --input", "w") { |io| io.write(text) }
|
|
51
|
+
end
|
|
52
|
+
@message = "Copied!"
|
|
53
|
+
@timer = 60
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Decrements the feedback timer by one frame.
|
|
57
|
+
#
|
|
58
|
+
# Call this once per render cycle. The message disappears when the timer
|
|
59
|
+
# reaches zero.
|
|
60
|
+
#
|
|
61
|
+
# === Example
|
|
62
|
+
#
|
|
63
|
+
# clipboard.copy("text") # timer = 60
|
|
64
|
+
# clipboard.tick # timer = 59
|
|
65
|
+
# 60.times { clipboard.tick } # message becomes ""
|
|
66
|
+
def tick
|
|
67
|
+
@timer -= 1 if @timer > 0
|
|
68
|
+
@message = "" if @timer <= 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Current feedback message.
|
|
72
|
+
#
|
|
73
|
+
# Empty string when no active message. <tt>"Copied!"</tt> after a successful
|
|
74
|
+
# copy, fading after 60 frames.
|
|
75
|
+
#
|
|
76
|
+
# === Example
|
|
77
|
+
#
|
|
78
|
+
# clipboard.message # => ""
|
|
79
|
+
# clipboard.copy("x")
|
|
80
|
+
# clipboard.message # => "Copied!"
|
|
81
|
+
def message
|
|
82
|
+
@message
|
|
83
|
+
end
|
|
84
|
+
end
|