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,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
|
|
@@ -0,0 +1,191 @@
|
|
|
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 "chroma"
|
|
7
|
+
require "wcag_color_contrast"
|
|
8
|
+
require_relative "harmony"
|
|
9
|
+
|
|
10
|
+
# Represents a single color with format conversion and harmony generation.
|
|
11
|
+
#
|
|
12
|
+
# Colors are central to visual design. Users need to work with colors in multiple
|
|
13
|
+
# formats: hex, RGB, HSL. They also need to generate color schemes: shades, tints,
|
|
14
|
+
# and complementary colors. Managing these conversions and relationships manually
|
|
15
|
+
# is tedious and error-prone.
|
|
16
|
+
#
|
|
17
|
+
# This object wraps a Chroma color. It exposes format conversions. It generates
|
|
18
|
+
# color harmonies. It calculates contrast ratios to choose readable text colors.
|
|
19
|
+
#
|
|
20
|
+
# Use it to parse user input, transform colors, and build color palettes.
|
|
21
|
+
#
|
|
22
|
+
# === Example
|
|
23
|
+
#
|
|
24
|
+
# color = Color.parse("#FF0000")
|
|
25
|
+
# puts color.hex # => "#FF0000"
|
|
26
|
+
# puts color.rgb # => "rgb(255, 0, 0)"
|
|
27
|
+
# puts color.hsl_string # => "hsl(0, 100%, 50%)"
|
|
28
|
+
#
|
|
29
|
+
# # Generate harmonies
|
|
30
|
+
# harmonies = color.harmonies # => [main, shade, tint, complement, ...]
|
|
31
|
+
#
|
|
32
|
+
# # Transform colors
|
|
33
|
+
# lighter = color.tint(5)
|
|
34
|
+
# darker = color.shade(3)
|
|
35
|
+
# rotated = color.spin(180)
|
|
36
|
+
class Color
|
|
37
|
+
def initialize(chroma_color)
|
|
38
|
+
@chroma = chroma_color
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Parses a color string and returns a Color, or nil if the string is invalid.
|
|
42
|
+
#
|
|
43
|
+
# Accepts hex, RGB, HSL, and named colors. Trims whitespace and handles
|
|
44
|
+
# empty strings gracefully.
|
|
45
|
+
#
|
|
46
|
+
# [input_str] String in any format Chroma supports (e.g., <tt>"#FF0000"</tt>, <tt>"red"</tt>, <tt>"rgb(255,0,0)"</tt>)
|
|
47
|
+
#
|
|
48
|
+
# === Example
|
|
49
|
+
#
|
|
50
|
+
# Color.parse("#FF0000") # => Color
|
|
51
|
+
# Color.parse("red") # => Color
|
|
52
|
+
# Color.parse("invalid") # => nil
|
|
53
|
+
# Color.parse("") # => nil
|
|
54
|
+
def self.parse(input_str)
|
|
55
|
+
input_str = input_str.to_s.strip
|
|
56
|
+
return nil if input_str.empty?
|
|
57
|
+
|
|
58
|
+
new(Chroma.paint(input_str.dup))
|
|
59
|
+
rescue
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Hex color code (uppercase).
|
|
64
|
+
#
|
|
65
|
+
# === Example
|
|
66
|
+
#
|
|
67
|
+
# color = Color.parse("red")
|
|
68
|
+
# color.hex # => "#FF0000"
|
|
69
|
+
def hex
|
|
70
|
+
@chroma.to_hex.upcase
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# RGB color code.
|
|
74
|
+
#
|
|
75
|
+
# === Example
|
|
76
|
+
#
|
|
77
|
+
# color = Color.parse("red")
|
|
78
|
+
# color.rgb # => "rgb(255, 0, 0)"
|
|
79
|
+
def rgb
|
|
80
|
+
@chroma.to_rgb
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# HSL color string with percentage formatting.
|
|
84
|
+
#
|
|
85
|
+
# === Example
|
|
86
|
+
#
|
|
87
|
+
# color = Color.parse("red")
|
|
88
|
+
# color.hsl_string # => "hsl(0, 100%, 50%)"
|
|
89
|
+
def hsl_string
|
|
90
|
+
hsl_obj = @chroma.hsl
|
|
91
|
+
h = hsl_obj.h
|
|
92
|
+
s = hsl_obj.s
|
|
93
|
+
l = hsl_obj.l
|
|
94
|
+
format("hsl(%.0f, %.1f%%, %.1f%%)", h, s * 100, l * 100)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Darkens the color. Returns a new Color.
|
|
98
|
+
#
|
|
99
|
+
# [amount] Integer amount to darken (default: 3)
|
|
100
|
+
#
|
|
101
|
+
# === Example
|
|
102
|
+
#
|
|
103
|
+
# color = Color.parse("red")
|
|
104
|
+
# color.shade(5).hex # => darker red
|
|
105
|
+
def shade(amount = 3)
|
|
106
|
+
Color.new(@chroma.darken(amount))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Lightens the color. Returns a new Color.
|
|
110
|
+
#
|
|
111
|
+
# [amount] Integer amount to lighten (default: 3)
|
|
112
|
+
#
|
|
113
|
+
# === Example
|
|
114
|
+
#
|
|
115
|
+
# color = Color.parse("red")
|
|
116
|
+
# color.tint(5).hex # => lighter red
|
|
117
|
+
def tint(amount = 3)
|
|
118
|
+
Color.new(@chroma.lighten(amount))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Rotates the hue. Returns a new Color.
|
|
122
|
+
#
|
|
123
|
+
# [degrees] Integer degrees to rotate (0-360)
|
|
124
|
+
#
|
|
125
|
+
# === Example
|
|
126
|
+
#
|
|
127
|
+
# color = Color.parse("red")
|
|
128
|
+
# color.spin(180).hex # => cyan
|
|
129
|
+
def spin(degrees)
|
|
130
|
+
Color.new(@chroma.spin(degrees))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Determines optimal text color (:white or :black) for maximum contrast.
|
|
134
|
+
#
|
|
135
|
+
# Uses WCAG contrast ratio calculation. Returns <tt>:white</tt> if white has
|
|
136
|
+
# higher contrast; <tt>:black</tt> otherwise.
|
|
137
|
+
#
|
|
138
|
+
# === Example
|
|
139
|
+
#
|
|
140
|
+
# Color.parse("yellow").contrasting_text_color # => :black
|
|
141
|
+
# Color.parse("navy").contrasting_text_color # => :white
|
|
142
|
+
def contrasting_text_color
|
|
143
|
+
white_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "ffffff")
|
|
144
|
+
black_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "000000")
|
|
145
|
+
(white_contrast > black_contrast) ? :white : :black
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Background color for rendering this color as a swatch.
|
|
149
|
+
#
|
|
150
|
+
# Returns <tt>"#000000"</tt> if text should be white; <tt>"#ffffff"</tt> if black.
|
|
151
|
+
# Used to frame color swatches with contrasting borders.
|
|
152
|
+
#
|
|
153
|
+
# === Example
|
|
154
|
+
#
|
|
155
|
+
# Color.parse("yellow").frame_color # => "#000000"
|
|
156
|
+
def frame_color
|
|
157
|
+
(contrasting_text_color == :white) ? "#000000" : "#ffffff"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Seven-color harmony: main, shade, tint, complement, split 1, split 2, split-complement.
|
|
161
|
+
#
|
|
162
|
+
# Generates a complete color scheme for UI design. Each harmony is a Harmony
|
|
163
|
+
# value object with label, hex, and styling information.
|
|
164
|
+
#
|
|
165
|
+
# === Example
|
|
166
|
+
#
|
|
167
|
+
# color = Color.parse("red")
|
|
168
|
+
# harmonies = color.harmonies
|
|
169
|
+
# harmonies.first.label # => "Main"
|
|
170
|
+
# harmonies.size # => 7
|
|
171
|
+
def harmonies
|
|
172
|
+
[
|
|
173
|
+
harmony_with_label("Main"),
|
|
174
|
+
shade.harmony_with_label("Shade"),
|
|
175
|
+
tint.harmony_with_label("Tint"),
|
|
176
|
+
spin(180).harmony_with_label("Comp"),
|
|
177
|
+
spin(150).harmony_with_label("Split 1"),
|
|
178
|
+
spin(210).harmony_with_label("Split 2"),
|
|
179
|
+
spin(30).harmony_with_label("S.Comp"),
|
|
180
|
+
]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def harmony_with_label(label)
|
|
184
|
+
Harmony.new(
|
|
185
|
+
label:,
|
|
186
|
+
hex:,
|
|
187
|
+
text_color: contrasting_text_color,
|
|
188
|
+
frame_color:,
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
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 "clipboard"
|
|
7
|
+
|
|
8
|
+
# A confirmation dialog for copying text to the clipboard.
|
|
9
|
+
#
|
|
10
|
+
# Users click on content they want to copy. The app needs to confirm: "Are you
|
|
11
|
+
# sure?" Managing dialog state (visible, selection, active), rendering the
|
|
12
|
+
# dialog, and dispatching keyboard events manually is tedious.
|
|
13
|
+
#
|
|
14
|
+
# This object owns dialog state and lifecycle. It renders itself. It responds
|
|
15
|
+
# to keyboard input. It delegates clipboard operations to a Clipboard.
|
|
16
|
+
#
|
|
17
|
+
# Use it to build copy-on-click interactions with user confirmation.
|
|
18
|
+
#
|
|
19
|
+
# === Example
|
|
20
|
+
#
|
|
21
|
+
# clipboard = Clipboard.new
|
|
22
|
+
# dialog = CopyDialog.new(clipboard)
|
|
23
|
+
#
|
|
24
|
+
# # Open the dialog
|
|
25
|
+
# dialog.open("#FF0000")
|
|
26
|
+
# dialog.active? # => true
|
|
27
|
+
#
|
|
28
|
+
# # Handle input
|
|
29
|
+
# result = dialog.handle_input(event) # Routes to :copied or :cancelled
|
|
30
|
+
#
|
|
31
|
+
# # Render
|
|
32
|
+
# widget = dialog.render(tui, area)
|
|
33
|
+
class CopyDialog
|
|
34
|
+
def initialize(clipboard)
|
|
35
|
+
@clipboard = clipboard
|
|
36
|
+
@text = ""
|
|
37
|
+
@selected = :yes
|
|
38
|
+
@active = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Opens the dialog with text to copy.
|
|
42
|
+
#
|
|
43
|
+
# Initializes selection to <tt>:yes</tt> and sets active to true.
|
|
44
|
+
#
|
|
45
|
+
# [text] String text to show and copy
|
|
46
|
+
#
|
|
47
|
+
# === Example
|
|
48
|
+
#
|
|
49
|
+
# dialog.open("#FF0000")
|
|
50
|
+
# dialog.active? # => true
|
|
51
|
+
# dialog.text # => "#FF0000"
|
|
52
|
+
def open(text)
|
|
53
|
+
@text = text
|
|
54
|
+
@selected = :yes
|
|
55
|
+
@active = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Closes the dialog and deactivates it.
|
|
59
|
+
def close
|
|
60
|
+
@active = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# True if the dialog is currently open and visible.
|
|
64
|
+
#
|
|
65
|
+
# === Example
|
|
66
|
+
#
|
|
67
|
+
# dialog.open("text")
|
|
68
|
+
# dialog.active? # => true
|
|
69
|
+
# dialog.close
|
|
70
|
+
# dialog.active? # => false
|
|
71
|
+
def active?
|
|
72
|
+
@active
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Processes a keyboard event and updates selection or closes the dialog.
|
|
76
|
+
#
|
|
77
|
+
# Left/h moves selection to :yes. Right/l moves to :no. Enter confirms.
|
|
78
|
+
# Y/N hotkeys also work (Y copies immediately, N cancels). Returns nil for
|
|
79
|
+
# all handled events; does nothing if the dialog is inactive.
|
|
80
|
+
#
|
|
81
|
+
# [event] Hash event from RatatuiRuby.poll_event
|
|
82
|
+
#
|
|
83
|
+
# === Example
|
|
84
|
+
#
|
|
85
|
+
# dialog.open("text")
|
|
86
|
+
# dialog.handle_input({ type: :key, code: "left" })
|
|
87
|
+
# dialog.handle_input({ type: :key, code: "enter" })
|
|
88
|
+
# dialog.active? # => false
|
|
89
|
+
def handle_input(event)
|
|
90
|
+
return nil unless @active
|
|
91
|
+
|
|
92
|
+
case event
|
|
93
|
+
in { type: :key, code: "left" } | { type: :key, code: "h" }
|
|
94
|
+
@selected = :yes
|
|
95
|
+
nil
|
|
96
|
+
in { type: :key, code: "right" } | { type: :key, code: "l" }
|
|
97
|
+
@selected = :no
|
|
98
|
+
nil
|
|
99
|
+
in { type: :key, code: "enter" }
|
|
100
|
+
if @selected == :yes
|
|
101
|
+
@clipboard.copy(@text)
|
|
102
|
+
end
|
|
103
|
+
@active = false
|
|
104
|
+
nil
|
|
105
|
+
in { type: :key, code: "y" }
|
|
106
|
+
@clipboard.copy(@text)
|
|
107
|
+
@active = false
|
|
108
|
+
nil
|
|
109
|
+
in { type: :key, code: "n" }
|
|
110
|
+
@active = false
|
|
111
|
+
nil
|
|
112
|
+
else
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Renders the dialog widget for display in a TUI frame.
|
|
118
|
+
#
|
|
119
|
+
# Shows the text to copy, Yes/No buttons with current selection highlighted,
|
|
120
|
+
# and keyboard instructions. Renders only when active.
|
|
121
|
+
#
|
|
122
|
+
# [tui] Session or TUI factory object
|
|
123
|
+
# [area] Rect area for the dialog
|
|
124
|
+
#
|
|
125
|
+
# === Example
|
|
126
|
+
#
|
|
127
|
+
# dialog.open("#FF0000")
|
|
128
|
+
# widget = dialog.render(tui, center_area)
|
|
129
|
+
# frame.render_widget(widget, center_area)
|
|
130
|
+
def render(tui, area)
|
|
131
|
+
yes_style = if @selected == :yes
|
|
132
|
+
tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
|
|
133
|
+
else
|
|
134
|
+
tui.style(fg: :gray)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
no_style = if @selected == :no
|
|
138
|
+
tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
|
|
139
|
+
else
|
|
140
|
+
tui.style(fg: :gray)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
tui.block(
|
|
144
|
+
title: "Copy to Clipboard",
|
|
145
|
+
borders: [:all],
|
|
146
|
+
border_type: :rounded,
|
|
147
|
+
style: tui.style(bg: :black, fg: :white),
|
|
148
|
+
children: [
|
|
149
|
+
tui.paragraph(
|
|
150
|
+
text: [
|
|
151
|
+
tui.text_line(spans: [
|
|
152
|
+
tui.text_span(content: "Copy #{@text}?", style: tui.style(fg: :white)),
|
|
153
|
+
]),
|
|
154
|
+
tui.text_line(spans: []),
|
|
155
|
+
tui.text_line(spans: [
|
|
156
|
+
tui.text_span(content: "[", style: tui.style(fg: :white)),
|
|
157
|
+
tui.text_span(content: "Yes", style: yes_style),
|
|
158
|
+
tui.text_span(content: "] [", style: tui.style(fg: :white)),
|
|
159
|
+
tui.text_span(content: "No", style: no_style),
|
|
160
|
+
tui.text_span(content: "]", style: tui.style(fg: :white)),
|
|
161
|
+
]),
|
|
162
|
+
tui.text_line(spans: [
|
|
163
|
+
tui.text_span(content: "Use ←/→ or h/l to select, Enter to confirm", style: tui.style(fg: :gray, modifiers: [:italic])),
|
|
164
|
+
]),
|
|
165
|
+
]
|
|
166
|
+
),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
# A single color variant with label and styling information.
|
|
7
|
+
#
|
|
8
|
+
# Color palettes need to show individual colors with labels (Main, Shade, Tint,
|
|
9
|
+
# Complement). Bundling a color's hex code, text color, and frame color together
|
|
10
|
+
# is natural—they're always used as a set.
|
|
11
|
+
#
|
|
12
|
+
# This value object pairs a color with its metadata and rendering styles.
|
|
13
|
+
#
|
|
14
|
+
# Use it to represent colors in a palette or harmony.
|
|
15
|
+
#
|
|
16
|
+
# === Attributes
|
|
17
|
+
#
|
|
18
|
+
# [label] String label for this color variant
|
|
19
|
+
# [hex] String hex color code
|
|
20
|
+
# [text_color] Symbol (:white or :black) for readable text
|
|
21
|
+
# [frame_color] String background color for the swatch frame
|
|
22
|
+
#
|
|
23
|
+
# === Example
|
|
24
|
+
#
|
|
25
|
+
# harmony = Harmony.new(
|
|
26
|
+
# label: "Main",
|
|
27
|
+
# hex: "#FF0000",
|
|
28
|
+
# text_color: :white,
|
|
29
|
+
# frame_color: "#000000"
|
|
30
|
+
# )
|
|
31
|
+
Harmony = Data.define(:label, :hex, :text_color, :frame_color) do
|
|
32
|
+
# Renders a 4-line color swatch for display in a TUI Block.
|
|
33
|
+
#
|
|
34
|
+
# Produces a visual representation: a 7-character-wide box with the color
|
|
35
|
+
# centered and the hex code below.
|
|
36
|
+
#
|
|
37
|
+
# [tui] Session or TUI factory object
|
|
38
|
+
#
|
|
39
|
+
# === Example
|
|
40
|
+
#
|
|
41
|
+
# harmony = Harmony.new(...)
|
|
42
|
+
# lines = harmony.color_swatch_lines(tui)
|
|
43
|
+
# # => [TextLine, TextLine, TextLine, TextLine]
|
|
44
|
+
def color_swatch_lines(tui)
|
|
45
|
+
[
|
|
46
|
+
tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
|
|
47
|
+
tui.text_line(spans: [
|
|
48
|
+
*Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
|
|
49
|
+
tui.text_span(content: " ", style: tui.style(bg: hex, fg: text_color)),
|
|
50
|
+
*Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
|
|
51
|
+
]),
|
|
52
|
+
tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
|
|
53
|
+
tui.text_line(spans: [tui.text_span(content: hex, style: tui.style(fg: :white))]),
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
end
|