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,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
|
|
@@ -0,0 +1,142 @@
|
|
|
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 "color"
|
|
7
|
+
|
|
8
|
+
# Manages text input and color parsing with error feedback.
|
|
9
|
+
#
|
|
10
|
+
# Users type color values. They make mistakes—typos, invalid formats. The app
|
|
11
|
+
# needs to validate their input and show helpful error messages. Manually
|
|
12
|
+
# tracking input state, validation, and error messages across renders is
|
|
13
|
+
# cumbersome and error-prone.
|
|
14
|
+
#
|
|
15
|
+
# This object holds the current input string. It validates by parsing. It stores
|
|
16
|
+
# errors and clears them when appropriate. It provides methods to manipulate
|
|
17
|
+
# the input (append, delete).
|
|
18
|
+
#
|
|
19
|
+
# Use it to build text input forms where validation feedback matters.
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
# input = Input.new
|
|
24
|
+
# input.append_char("#")
|
|
25
|
+
# input.append_char("f")
|
|
26
|
+
# input.append_char("f")
|
|
27
|
+
# color = input.parse # => Color or nil
|
|
28
|
+
# puts input.error # => error message if parse failed
|
|
29
|
+
class Input
|
|
30
|
+
PRINTABLE_PATTERN = /[\w#,().\s%]/
|
|
31
|
+
|
|
32
|
+
# Creates a new Input with an optional initial value.
|
|
33
|
+
#
|
|
34
|
+
# [initial_value] String initial color input (default: <tt>"#F96302"</tt>)
|
|
35
|
+
def initialize(initial_value = "#F96302")
|
|
36
|
+
@value = initial_value
|
|
37
|
+
@error = ""
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Current input string.
|
|
41
|
+
#
|
|
42
|
+
# === Example
|
|
43
|
+
#
|
|
44
|
+
# input = Input.new
|
|
45
|
+
# input.value # => "#F96302"
|
|
46
|
+
def value
|
|
47
|
+
@value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Error message from the last failed parse, or empty string.
|
|
51
|
+
#
|
|
52
|
+
# === Example
|
|
53
|
+
#
|
|
54
|
+
# input.parse # => nil (invalid)
|
|
55
|
+
# input.error # => "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
|
|
56
|
+
def error
|
|
57
|
+
@error
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Clears the current error message.
|
|
61
|
+
def clear_error
|
|
62
|
+
@error = ""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Appends a character to the input if it matches the printable pattern.
|
|
66
|
+
#
|
|
67
|
+
# Silently ignores non-printable characters. Valid characters include
|
|
68
|
+
# letters, digits, hash, comma, parentheses, dot, space, and percent.
|
|
69
|
+
#
|
|
70
|
+
# [char] String single character
|
|
71
|
+
def append_char(char)
|
|
72
|
+
@value += char if char.length == 1 && char.match?(PRINTABLE_PATTERN)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Removes the last character from the input.
|
|
76
|
+
def delete_char
|
|
77
|
+
@value = @value[0...-1]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Replaces the entire input string.
|
|
81
|
+
#
|
|
82
|
+
# [text] String new input value
|
|
83
|
+
def set(text)
|
|
84
|
+
@value = text
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Parses the current input as a Color.
|
|
88
|
+
#
|
|
89
|
+
# Returns a Color if valid; nil otherwise. Sets the error message on failure.
|
|
90
|
+
# Clears the error message on success.
|
|
91
|
+
#
|
|
92
|
+
# === Example
|
|
93
|
+
#
|
|
94
|
+
# input = Input.new("#FF0000")
|
|
95
|
+
# color = input.parse # => Color
|
|
96
|
+
# input.error # => ""
|
|
97
|
+
def parse
|
|
98
|
+
color = Color.parse(@value)
|
|
99
|
+
if color
|
|
100
|
+
clear_error
|
|
101
|
+
color
|
|
102
|
+
else
|
|
103
|
+
@error = "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Renders the input widget for display in a TUI frame.
|
|
109
|
+
#
|
|
110
|
+
# Shows the current input value with a cursor. Displays the error message
|
|
111
|
+
# in red if one is set.
|
|
112
|
+
#
|
|
113
|
+
# [tui] Session or TUI factory object
|
|
114
|
+
#
|
|
115
|
+
# === Example
|
|
116
|
+
#
|
|
117
|
+
# input = Input.new
|
|
118
|
+
# widget = input.render(tui)
|
|
119
|
+
# frame.render_widget(widget, area)
|
|
120
|
+
def render(tui)
|
|
121
|
+
input_lines = [
|
|
122
|
+
tui.text_line(spans: [
|
|
123
|
+
tui.text_span(content: @value),
|
|
124
|
+
tui.text_span(content: "_", style: tui.style(modifiers: [:reversed])),
|
|
125
|
+
]),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
unless @error.empty?
|
|
129
|
+
input_lines << tui.text_line(spans: [
|
|
130
|
+
tui.text_span(content: @error, style: tui.style(fg: :red)),
|
|
131
|
+
])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
tui.block(
|
|
135
|
+
title: "Color Input",
|
|
136
|
+
borders: [:all],
|
|
137
|
+
children: [
|
|
138
|
+
tui.paragraph(text: input_lines),
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
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 "color"
|
|
7
|
+
|
|
8
|
+
# Holds a primary color and its harmonies.
|
|
9
|
+
#
|
|
10
|
+
# Color pickers need to show related colors: shades, tints, complements. Building
|
|
11
|
+
# these relationships repeatedly is redundant. Passing them individually through
|
|
12
|
+
# rendering pipelines is awkward.
|
|
13
|
+
#
|
|
14
|
+
# This object owns a primary color and generates its harmonies on demand. It
|
|
15
|
+
# provides accessor methods and rendering helpers.
|
|
16
|
+
#
|
|
17
|
+
# Use it to organize color data for palette displays.
|
|
18
|
+
#
|
|
19
|
+
# === Example
|
|
20
|
+
#
|
|
21
|
+
# color = Color.parse("#FF0000")
|
|
22
|
+
# palette = Palette.new(color)
|
|
23
|
+
# palette.main # => Color
|
|
24
|
+
# palette.all # => [Harmony, Harmony, ...]
|
|
25
|
+
# blocks = palette.as_blocks(tui) # => [Block, Block, ...]
|
|
26
|
+
class Palette
|
|
27
|
+
def initialize(primary_color)
|
|
28
|
+
@primary = primary_color
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The primary (main) color, or nil if no color is set.
|
|
32
|
+
#
|
|
33
|
+
# === Example
|
|
34
|
+
#
|
|
35
|
+
# palette = Palette.new(color)
|
|
36
|
+
# palette.main.hex # => "#FF0000"
|
|
37
|
+
def main
|
|
38
|
+
@primary
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# All harmonies: main, shade, tint, complement, split 1, split 2, split-complement.
|
|
42
|
+
#
|
|
43
|
+
# Returns an empty array if no primary color is set.
|
|
44
|
+
#
|
|
45
|
+
# === Example
|
|
46
|
+
#
|
|
47
|
+
# palette = Palette.new(color)
|
|
48
|
+
# palette.all.size # => 7
|
|
49
|
+
def all
|
|
50
|
+
return [] if @primary.nil?
|
|
51
|
+
|
|
52
|
+
@primary.harmonies
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Renders all harmonies as TUI Block widgets.
|
|
56
|
+
#
|
|
57
|
+
# Each harmony becomes a titled block showing its color swatch. Returns an empty
|
|
58
|
+
# array if no primary color is set.
|
|
59
|
+
#
|
|
60
|
+
# [tui] Session or TUI factory object
|
|
61
|
+
#
|
|
62
|
+
# === Example
|
|
63
|
+
#
|
|
64
|
+
# palette = Palette.new(color)
|
|
65
|
+
# blocks = palette.as_blocks(tui)
|
|
66
|
+
# # blocks[0] => Block titled "Main" with color swatch
|
|
67
|
+
def as_blocks(tui)
|
|
68
|
+
return [] if @primary.nil?
|
|
69
|
+
|
|
70
|
+
all.map do |harmony|
|
|
71
|
+
tui.block(
|
|
72
|
+
title: harmony.label,
|
|
73
|
+
borders: [:all],
|
|
74
|
+
children: [
|
|
75
|
+
tui.paragraph(text: harmony.color_swatch_lines(tui)),
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|