ratatui_ruby 1.1.0 → 1.1.1
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/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/lib/ratatui_ruby/version.rb +1 -1
- metadata +1 -255
- data/.builds/ruby-3.2.yml +0 -54
- data/.builds/ruby-3.3.yml +0 -54
- data/.builds/ruby-3.4.yml +0 -54
- data/.builds/ruby-4.0.0.yml +0 -54
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -10
- data/AGENTS.md +0 -147
- data/CHANGELOG.md +0 -736
- data/README.md +0 -187
- data/README.rdoc +0 -302
- data/Rakefile +0 -11
- data/Steepfile +0 -50
- data/doc/concepts/application_architecture.md +0 -321
- data/doc/concepts/application_testing.md +0 -193
- data/doc/concepts/async.md +0 -190
- data/doc/concepts/custom_widgets.md +0 -247
- data/doc/concepts/debugging.md +0 -401
- data/doc/concepts/event_handling.md +0 -162
- data/doc/concepts/interactive_design.md +0 -146
- data/doc/contributors/auditing/parity.md +0 -239
- data/doc/contributors/design/ruby_frontend.md +0 -448
- data/doc/contributors/design/rust_backend.md +0 -434
- data/doc/contributors/design.md +0 -11
- data/doc/contributors/developing_examples.md +0 -400
- data/doc/contributors/documentation_style.md +0 -121
- data/doc/contributors/index.md +0 -21
- data/doc/contributors/releasing.md +0 -215
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
- data/doc/contributors/todo/align/term.md +0 -351
- data/doc/contributors/todo/align/terminal.md +0 -647
- data/doc/contributors/todo/future_work.md +0 -169
- data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
- data/doc/contributors/upstream_requests/tab_rects.md +0 -173
- data/doc/contributors/upstream_requests/title_rects.md +0 -132
- data/doc/custom.css +0 -22
- data/doc/getting_started/quickstart.md +0 -291
- data/doc/getting_started/why.md +0 -93
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_external_editor.gif +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.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.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.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.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/index.md +0 -34
- data/doc/troubleshooting/async.md +0 -4
- data/doc/troubleshooting/terminal_limitations.md +0 -131
- data/doc/troubleshooting/tui_output.md +0 -197
- data/examples/app_all_events/README.md +0 -114
- data/examples/app_all_events/app.rb +0 -98
- data/examples/app_all_events/model/app_model.rb +0 -159
- data/examples/app_all_events/model/event_color_cycle.rb +0 -43
- data/examples/app_all_events/model/event_entry.rb +0 -94
- data/examples/app_all_events/model/msg.rb +0 -39
- data/examples/app_all_events/model/timestamp.rb +0 -56
- data/examples/app_all_events/update.rb +0 -75
- data/examples/app_all_events/view/app_view.rb +0 -80
- data/examples/app_all_events/view/controls_view.rb +0 -54
- data/examples/app_all_events/view/counts_view.rb +0 -61
- data/examples/app_all_events/view/live_view.rb +0 -72
- data/examples/app_all_events/view/log_view.rb +0 -57
- data/examples/app_all_events/view.rb +0 -9
- data/examples/app_cli_rich_moments/README.md +0 -81
- data/examples/app_cli_rich_moments/app.rb +0 -189
- data/examples/app_color_picker/README.md +0 -156
- data/examples/app_color_picker/app.rb +0 -76
- data/examples/app_color_picker/clipboard.rb +0 -86
- data/examples/app_color_picker/color.rb +0 -193
- data/examples/app_color_picker/controls.rb +0 -92
- data/examples/app_color_picker/copy_dialog.rb +0 -168
- data/examples/app_color_picker/export_pane.rb +0 -128
- data/examples/app_color_picker/harmony.rb +0 -58
- data/examples/app_color_picker/input.rb +0 -176
- data/examples/app_color_picker/main_container.rb +0 -180
- data/examples/app_color_picker/palette.rb +0 -111
- data/examples/app_debugging_showcase/README.md +0 -119
- data/examples/app_debugging_showcase/app.rb +0 -318
- data/examples/app_external_editor/README.md +0 -62
- data/examples/app_external_editor/app.rb +0 -344
- data/examples/app_login_form/README.md +0 -58
- data/examples/app_login_form/app.rb +0 -109
- data/examples/app_stateful_interaction/README.md +0 -35
- data/examples/app_stateful_interaction/app.rb +0 -328
- data/examples/timeout_demo.rb +0 -45
- data/examples/verify_quickstart_dsl/README.md +0 -55
- data/examples/verify_quickstart_dsl/app.rb +0 -49
- data/examples/verify_quickstart_layout/README.md +0 -77
- data/examples/verify_quickstart_layout/app.rb +0 -73
- data/examples/verify_quickstart_lifecycle/README.md +0 -68
- data/examples/verify_quickstart_lifecycle/app.rb +0 -62
- data/examples/verify_readme_usage/README.md +0 -49
- data/examples/verify_readme_usage/app.rb +0 -42
- data/examples/verify_website_managed/README.md +0 -48
- data/examples/verify_website_managed/app.rb +0 -36
- data/examples/verify_website_menu/README.md +0 -60
- data/examples/verify_website_menu/app.rb +0 -84
- data/examples/verify_website_spinner/README.md +0 -44
- data/examples/verify_website_spinner/app.rb +0 -34
- data/examples/widget_barchart/README.md +0 -58
- data/examples/widget_barchart/app.rb +0 -240
- data/examples/widget_block/README.md +0 -44
- data/examples/widget_block/app.rb +0 -258
- data/examples/widget_box/README.md +0 -54
- data/examples/widget_box/app.rb +0 -255
- data/examples/widget_calendar/README.md +0 -48
- data/examples/widget_calendar/app.rb +0 -115
- data/examples/widget_canvas/README.md +0 -31
- data/examples/widget_canvas/app.rb +0 -130
- data/examples/widget_cell/README.md +0 -45
- data/examples/widget_cell/app.rb +0 -112
- data/examples/widget_center/README.md +0 -33
- data/examples/widget_center/app.rb +0 -118
- data/examples/widget_chart/README.md +0 -50
- data/examples/widget_chart/app.rb +0 -220
- data/examples/widget_gauge/README.md +0 -50
- data/examples/widget_gauge/app.rb +0 -229
- data/examples/widget_layout_split/README.md +0 -53
- data/examples/widget_layout_split/app.rb +0 -260
- data/examples/widget_line_gauge/README.md +0 -50
- data/examples/widget_line_gauge/app.rb +0 -219
- data/examples/widget_list/README.md +0 -58
- data/examples/widget_list/app.rb +0 -382
- data/examples/widget_map/README.md +0 -48
- data/examples/widget_map/app.rb +0 -95
- data/examples/widget_overlay/README.md +0 -45
- data/examples/widget_overlay/app.rb +0 -250
- data/examples/widget_popup/README.md +0 -45
- data/examples/widget_popup/app.rb +0 -106
- data/examples/widget_ratatui_logo/README.md +0 -43
- data/examples/widget_ratatui_logo/app.rb +0 -104
- data/examples/widget_ratatui_mascot/README.md +0 -43
- data/examples/widget_ratatui_mascot/app.rb +0 -95
- data/examples/widget_rect/README.md +0 -53
- data/examples/widget_rect/app.rb +0 -222
- data/examples/widget_render/README.md +0 -46
- data/examples/widget_render/app.rb +0 -186
- data/examples/widget_render/app.rbs +0 -41
- data/examples/widget_rich_text/README.md +0 -44
- data/examples/widget_rich_text/app.rb +0 -193
- data/examples/widget_scroll_text/README.md +0 -46
- data/examples/widget_scroll_text/app.rb +0 -109
- data/examples/widget_scrollbar/README.md +0 -46
- data/examples/widget_scrollbar/app.rb +0 -155
- data/examples/widget_sparkline/README.md +0 -51
- data/examples/widget_sparkline/app.rb +0 -277
- data/examples/widget_style_colors/README.md +0 -43
- data/examples/widget_style_colors/app.rb +0 -83
- data/examples/widget_table/README.md +0 -57
- data/examples/widget_table/app.rb +0 -285
- data/examples/widget_tabs/README.md +0 -50
- data/examples/widget_tabs/app.rb +0 -183
- data/examples/widget_text_width/README.md +0 -44
- data/examples/widget_text_width/app.rb +0 -117
- data/migrate_to_buffer.rb +0 -145
- data/mise.toml +0 -8
- data/tasks/autodoc/examples.rb +0 -87
- data/tasks/autodoc/member.rb +0 -58
- data/tasks/autodoc/name.rb +0 -21
- data/tasks/autodoc.rake +0 -21
- data/tasks/bump/bump_workflow.rb +0 -49
- data/tasks/bump/cargo_lockfile.rb +0 -21
- data/tasks/bump/changelog.rb +0 -104
- data/tasks/bump/header.rb +0 -32
- data/tasks/bump/history.rb +0 -32
- data/tasks/bump/links.rb +0 -69
- data/tasks/bump/manifest.rb +0 -33
- data/tasks/bump/patch_release.rb +0 -19
- data/tasks/bump/release_branch.rb +0 -17
- data/tasks/bump/release_from_trunk.rb +0 -49
- data/tasks/bump/repository.rb +0 -54
- data/tasks/bump/ruby_gem.rb +0 -29
- data/tasks/bump/sem_ver.rb +0 -44
- data/tasks/bump/unreleased_section.rb +0 -73
- data/tasks/bump.rake +0 -61
- data/tasks/doc/documentation.rb +0 -59
- data/tasks/doc/link/file_url.rb +0 -30
- data/tasks/doc/link/relative_path.rb +0 -61
- data/tasks/doc/link/web_url.rb +0 -55
- data/tasks/doc/link.rb +0 -52
- data/tasks/doc/link_audit.rb +0 -116
- data/tasks/doc/problem.rb +0 -40
- data/tasks/doc/source_file.rb +0 -93
- data/tasks/doc.rake +0 -905
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/extension.rake +0 -14
- data/tasks/license/headers_md.rb +0 -223
- data/tasks/license/headers_rb.rb +0 -210
- data/tasks/license/license_utils.rb +0 -130
- data/tasks/license/snippets_md.rb +0 -315
- data/tasks/license/snippets_rdoc.rb +0 -150
- data/tasks/license.rake +0 -91
- data/tasks/lint.rake +0 -170
- data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
- data/tasks/rbs_predicates/predicate_tests.rb +0 -124
- data/tasks/rbs_predicates/rbs_signature.rb +0 -63
- data/tasks/rbs_predicates.rake +0 -31
- data/tasks/rdoc_config.rb +0 -29
- data/tasks/resources/build.yml.erb +0 -60
- data/tasks/resources/index.html.erb +0 -141
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/sourcehut.rake +0 -110
- data/tasks/steep.rake +0 -11
- data/tasks/terminal_preview/app_screenshot.rb +0 -45
- data/tasks/terminal_preview/crash_report.rb +0 -54
- data/tasks/terminal_preview/example_app.rb +0 -27
- data/tasks/terminal_preview/launcher_script.rb +0 -48
- data/tasks/terminal_preview/preview_collection.rb +0 -60
- data/tasks/terminal_preview/preview_timing.rb +0 -24
- data/tasks/terminal_preview/safety_confirmation.rb +0 -58
- data/tasks/terminal_preview/saved_screenshot.rb +0 -56
- data/tasks/terminal_preview/system_appearance.rb +0 -13
- data/tasks/terminal_preview/terminal_window.rb +0 -138
- data/tasks/terminal_preview/window_id.rb +0 -16
- data/tasks/terminal_preview.rake +0 -30
- data/tasks/test.rake +0 -36
- data/tasks/website/index_page.rb +0 -30
- data/tasks/website/version.rb +0 -122
- data/tasks/website/version_menu.rb +0 -68
- data/tasks/website/versioned_documentation.rb +0 -83
- data/tasks/website/website.rb +0 -53
- data/tasks/website.rake +0 -28
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# Manages system clipboard interaction with transient feedback.
|
|
9
|
-
#
|
|
10
|
-
# Apps need to copy data to the clipboard. Users need feedback: "Did it work?"
|
|
11
|
-
# Manual clipboard handling and feedback timers scattered through app logic is
|
|
12
|
-
# messy.
|
|
13
|
-
#
|
|
14
|
-
# This object handles clipboard writes to all platforms (pbcopy, xclip, xsel).
|
|
15
|
-
# It manages a feedback message and countdown timer.
|
|
16
|
-
#
|
|
17
|
-
# Use it to provide copy-to-clipboard functionality with user feedback.
|
|
18
|
-
#
|
|
19
|
-
# === Example
|
|
20
|
-
#
|
|
21
|
-
# clipboard = Clipboard.new
|
|
22
|
-
# clipboard.copy("#FF0000")
|
|
23
|
-
# puts clipboard.message # => "Copied!"
|
|
24
|
-
#
|
|
25
|
-
# # In render loop:
|
|
26
|
-
# clipboard.tick # Decrement timer
|
|
27
|
-
# puts clipboard.message # => "" (after 60 frames)
|
|
28
|
-
class Clipboard
|
|
29
|
-
def initialize
|
|
30
|
-
@message = ""
|
|
31
|
-
@timer = 0
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Writes text to the system clipboard.
|
|
35
|
-
#
|
|
36
|
-
# Tries pbcopy (macOS), xclip (Linux), then xsel (Linux fallback). Sets the
|
|
37
|
-
# feedback message to <tt>"Copied!"</tt> and starts a 60-frame timer.
|
|
38
|
-
#
|
|
39
|
-
# [text] String to copy
|
|
40
|
-
#
|
|
41
|
-
# === Example
|
|
42
|
-
#
|
|
43
|
-
# clipboard = Clipboard.new
|
|
44
|
-
# clipboard.copy("#FF0000")
|
|
45
|
-
# clipboard.message # => "Copied!"
|
|
46
|
-
def copy(text)
|
|
47
|
-
if `which pbcopy 2>/dev/null`.strip.length > 0
|
|
48
|
-
IO.popen("pbcopy", "w") { |io| io.write(text) }
|
|
49
|
-
elsif `which xclip 2>/dev/null`.strip.length > 0
|
|
50
|
-
IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
|
|
51
|
-
elsif `which xsel 2>/dev/null`.strip.length > 0
|
|
52
|
-
IO.popen("xsel --clipboard --input", "w") { |io| io.write(text) }
|
|
53
|
-
end
|
|
54
|
-
@message = "Copied!"
|
|
55
|
-
@timer = 60
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Decrements the feedback timer by one frame.
|
|
59
|
-
#
|
|
60
|
-
# Call this once per render cycle. The message disappears when the timer
|
|
61
|
-
# reaches zero.
|
|
62
|
-
#
|
|
63
|
-
# === Example
|
|
64
|
-
#
|
|
65
|
-
# clipboard.copy("text") # timer = 60
|
|
66
|
-
# clipboard.tick # timer = 59
|
|
67
|
-
# 60.times { clipboard.tick } # message becomes ""
|
|
68
|
-
def tick
|
|
69
|
-
@timer -= 1 if @timer > 0
|
|
70
|
-
@message = "" if @timer <= 0
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Current feedback message.
|
|
74
|
-
#
|
|
75
|
-
# Empty string when no active message. <tt>"Copied!"</tt> after a successful
|
|
76
|
-
# copy, fading after 60 frames.
|
|
77
|
-
#
|
|
78
|
-
# === Example
|
|
79
|
-
#
|
|
80
|
-
# clipboard.message # => ""
|
|
81
|
-
# clipboard.copy("x")
|
|
82
|
-
# clipboard.message # => "Copied!"
|
|
83
|
-
def message
|
|
84
|
-
@message
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
require "chroma"
|
|
9
|
-
require "wcag_color_contrast"
|
|
10
|
-
require_relative "harmony"
|
|
11
|
-
|
|
12
|
-
# Represents a single color with format conversion and harmony generation.
|
|
13
|
-
#
|
|
14
|
-
# Colors are central to visual design. Users need to work with colors in multiple
|
|
15
|
-
# formats: hex, RGB, HSL. They also need to generate color schemes: shades, tints,
|
|
16
|
-
# and complementary colors. Managing these conversions and relationships manually
|
|
17
|
-
# is tedious and error-prone.
|
|
18
|
-
#
|
|
19
|
-
# This object wraps a Chroma color. It exposes format conversions. It generates
|
|
20
|
-
# color harmonies. It calculates contrast ratios to choose readable text colors.
|
|
21
|
-
#
|
|
22
|
-
# Use it to parse user input, transform colors, and build color palettes.
|
|
23
|
-
#
|
|
24
|
-
# === Example
|
|
25
|
-
#
|
|
26
|
-
# color = Color.parse("#FF0000")
|
|
27
|
-
# puts color.hex # => "#FF0000"
|
|
28
|
-
# puts color.rgb # => "rgb(255, 0, 0)"
|
|
29
|
-
# puts color.hsl_string # => "hsl(0, 100%, 50%)"
|
|
30
|
-
#
|
|
31
|
-
# # Generate harmonies
|
|
32
|
-
# harmonies = color.harmonies # => [main, shade, tint, complement, ...]
|
|
33
|
-
#
|
|
34
|
-
# # Transform colors
|
|
35
|
-
# lighter = color.tint(5)
|
|
36
|
-
# darker = color.shade(3)
|
|
37
|
-
# rotated = color.spin(180)
|
|
38
|
-
class Color
|
|
39
|
-
def initialize(chroma_color)
|
|
40
|
-
@chroma = chroma_color
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Parses a color string and returns a Color, or nil if the string is invalid.
|
|
44
|
-
#
|
|
45
|
-
# Accepts hex, RGB, HSL, and named colors. Trims whitespace and handles
|
|
46
|
-
# empty strings gracefully.
|
|
47
|
-
#
|
|
48
|
-
# [input_str] String in any format Chroma supports (e.g., <tt>"#FF0000"</tt>, <tt>"red"</tt>, <tt>"rgb(255,0,0)"</tt>)
|
|
49
|
-
#
|
|
50
|
-
# === Example
|
|
51
|
-
#
|
|
52
|
-
# Color.parse("#FF0000") # => Color
|
|
53
|
-
# Color.parse("red") # => Color
|
|
54
|
-
# Color.parse("invalid") # => nil
|
|
55
|
-
# Color.parse("") # => nil
|
|
56
|
-
def self.parse(input_str)
|
|
57
|
-
input_str = input_str.to_s.strip
|
|
58
|
-
return nil if input_str.empty?
|
|
59
|
-
|
|
60
|
-
new(Chroma.paint(input_str.dup))
|
|
61
|
-
rescue
|
|
62
|
-
nil
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Hex color code (uppercase).
|
|
66
|
-
#
|
|
67
|
-
# === Example
|
|
68
|
-
#
|
|
69
|
-
# color = Color.parse("red")
|
|
70
|
-
# color.hex # => "#FF0000"
|
|
71
|
-
def hex
|
|
72
|
-
@chroma.to_hex.upcase
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# RGB color code.
|
|
76
|
-
#
|
|
77
|
-
# === Example
|
|
78
|
-
#
|
|
79
|
-
# color = Color.parse("red")
|
|
80
|
-
# color.rgb # => "rgb(255, 0, 0)"
|
|
81
|
-
def rgb
|
|
82
|
-
@chroma.to_rgb
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# HSL color string with percentage formatting.
|
|
86
|
-
#
|
|
87
|
-
# === Example
|
|
88
|
-
#
|
|
89
|
-
# color = Color.parse("red")
|
|
90
|
-
# color.hsl_string # => "hsl(0, 100%, 50%)"
|
|
91
|
-
def hsl_string
|
|
92
|
-
hsl_obj = @chroma.hsl
|
|
93
|
-
h = hsl_obj.h
|
|
94
|
-
s = hsl_obj.s
|
|
95
|
-
l = hsl_obj.l
|
|
96
|
-
format("hsl(%.0f, %.1f%%, %.1f%%)", h, s * 100, l * 100)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Darkens the color. Returns a new Color.
|
|
100
|
-
#
|
|
101
|
-
# [amount] Integer amount to darken (default: 3)
|
|
102
|
-
#
|
|
103
|
-
# === Example
|
|
104
|
-
#
|
|
105
|
-
# color = Color.parse("red")
|
|
106
|
-
# color.shade(5).hex # => darker red
|
|
107
|
-
def shade(amount = 3)
|
|
108
|
-
Color.new(@chroma.darken(amount))
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Lightens the color. Returns a new Color.
|
|
112
|
-
#
|
|
113
|
-
# [amount] Integer amount to lighten (default: 3)
|
|
114
|
-
#
|
|
115
|
-
# === Example
|
|
116
|
-
#
|
|
117
|
-
# color = Color.parse("red")
|
|
118
|
-
# color.tint(5).hex # => lighter red
|
|
119
|
-
def tint(amount = 3)
|
|
120
|
-
Color.new(@chroma.lighten(amount))
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Rotates the hue. Returns a new Color.
|
|
124
|
-
#
|
|
125
|
-
# [degrees] Integer degrees to rotate (0-360)
|
|
126
|
-
#
|
|
127
|
-
# === Example
|
|
128
|
-
#
|
|
129
|
-
# color = Color.parse("red")
|
|
130
|
-
# color.spin(180).hex # => cyan
|
|
131
|
-
def spin(degrees)
|
|
132
|
-
Color.new(@chroma.spin(degrees))
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Determines optimal text color (:white or :black) for maximum contrast.
|
|
136
|
-
#
|
|
137
|
-
# Uses WCAG contrast ratio calculation. Returns <tt>:white</tt> if white has
|
|
138
|
-
# higher contrast; <tt>:black</tt> otherwise.
|
|
139
|
-
#
|
|
140
|
-
# === Example
|
|
141
|
-
#
|
|
142
|
-
# Color.parse("yellow").contrasting_text_color # => :black
|
|
143
|
-
# Color.parse("navy").contrasting_text_color # => :white
|
|
144
|
-
def contrasting_text_color
|
|
145
|
-
white_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "ffffff")
|
|
146
|
-
black_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "000000")
|
|
147
|
-
(white_contrast > black_contrast) ? :white : :black
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Background color for rendering this color as a swatch.
|
|
151
|
-
#
|
|
152
|
-
# Returns <tt>"#000000"</tt> if text should be white; <tt>"#ffffff"</tt> if black.
|
|
153
|
-
# Used to frame color swatches with contrasting borders.
|
|
154
|
-
#
|
|
155
|
-
# === Example
|
|
156
|
-
#
|
|
157
|
-
# Color.parse("yellow").frame_color # => "#000000"
|
|
158
|
-
def frame_color
|
|
159
|
-
(contrasting_text_color == :white) ? "#000000" : "#ffffff"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Seven-color harmony: main, shade, tint, complement, split 1, split 2, split-complement.
|
|
163
|
-
#
|
|
164
|
-
# Generates a complete color scheme for UI design. Each harmony is a Harmony
|
|
165
|
-
# value object with label, hex, and styling information.
|
|
166
|
-
#
|
|
167
|
-
# === Example
|
|
168
|
-
#
|
|
169
|
-
# color = Color.parse("red")
|
|
170
|
-
# harmonies = color.harmonies
|
|
171
|
-
# harmonies.first.label # => "Main"
|
|
172
|
-
# harmonies.size # => 7
|
|
173
|
-
def harmonies
|
|
174
|
-
[
|
|
175
|
-
harmony_with_label("Main"),
|
|
176
|
-
shade.harmony_with_label("Shade"),
|
|
177
|
-
tint.harmony_with_label("Tint"),
|
|
178
|
-
spin(180).harmony_with_label("Comp"),
|
|
179
|
-
spin(150).harmony_with_label("Split 1"),
|
|
180
|
-
spin(210).harmony_with_label("Split 2"),
|
|
181
|
-
spin(30).harmony_with_label("S.Comp"),
|
|
182
|
-
]
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def harmony_with_label(label)
|
|
186
|
-
Harmony.new(
|
|
187
|
-
label:,
|
|
188
|
-
hex:,
|
|
189
|
-
text_color: contrasting_text_color,
|
|
190
|
-
frame_color:,
|
|
191
|
-
)
|
|
192
|
-
end
|
|
193
|
-
end
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# A display-only component showing keyboard shortcuts and clipboard feedback.
|
|
9
|
-
#
|
|
10
|
-
# Users need to know what keys are available. They also need feedback when
|
|
11
|
-
# they copy a color. This component renders the controls section.
|
|
12
|
-
#
|
|
13
|
-
# === Component Contract
|
|
14
|
-
#
|
|
15
|
-
# - `render(tui, frame, area, clipboard:)`: Draws the controls; stores `area`
|
|
16
|
-
# - `handle_event(event) -> nil`: Display-only, always returns nil
|
|
17
|
-
# - `tick`: Delegates to clipboard for time-based feedback updates
|
|
18
|
-
#
|
|
19
|
-
# === Example
|
|
20
|
-
#
|
|
21
|
-
# controls = Controls.new
|
|
22
|
-
# controls.render(tui, frame, area, clipboard: clipboard)
|
|
23
|
-
# controls.tick(clipboard)
|
|
24
|
-
class Controls
|
|
25
|
-
def initialize
|
|
26
|
-
@area = nil
|
|
27
|
-
@hotkey_style = nil
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# The cached render area.
|
|
31
|
-
attr_reader :area
|
|
32
|
-
|
|
33
|
-
# Renders the controls section into the given area.
|
|
34
|
-
#
|
|
35
|
-
# Shows keyboard shortcuts and clipboard feedback message if one is active.
|
|
36
|
-
#
|
|
37
|
-
# [tui] Session or TUI factory object
|
|
38
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
39
|
-
# [area] Rect area to draw into
|
|
40
|
-
# [clipboard] Clipboard object for feedback message
|
|
41
|
-
#
|
|
42
|
-
# === Example
|
|
43
|
-
#
|
|
44
|
-
# controls.render(tui, frame, control_area, clipboard: clipboard)
|
|
45
|
-
def render(tui, frame, area, clipboard:)
|
|
46
|
-
@area = area
|
|
47
|
-
@hotkey_style ||= tui.style(modifiers: [:bold, :underlined])
|
|
48
|
-
widget = build_widget(tui, clipboard)
|
|
49
|
-
frame.render_widget(widget, area)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Display-only component; always returns nil.
|
|
53
|
-
def handle_event(_event)
|
|
54
|
-
nil
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Delegates tick to the clipboard for time-based updates.
|
|
58
|
-
#
|
|
59
|
-
# [clipboard] Clipboard object to tick
|
|
60
|
-
def tick(clipboard)
|
|
61
|
-
clipboard.tick
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private def build_widget(tui, clipboard)
|
|
65
|
-
control_lines = [
|
|
66
|
-
tui.text_line(spans: [
|
|
67
|
-
tui.text_span(content: "a-z/0-9", style: @hotkey_style),
|
|
68
|
-
tui.text_span(content: ": Type "),
|
|
69
|
-
tui.text_span(content: "enter", style: @hotkey_style),
|
|
70
|
-
tui.text_span(content: ": Parse "),
|
|
71
|
-
tui.text_span(content: "bksp", style: @hotkey_style),
|
|
72
|
-
tui.text_span(content: ": Erase "),
|
|
73
|
-
tui.text_span(content: "esc", style: @hotkey_style),
|
|
74
|
-
tui.text_span(content: ": Quit"),
|
|
75
|
-
]),
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
unless clipboard.message.empty?
|
|
79
|
-
control_lines << tui.text_line(spans: [
|
|
80
|
-
tui.text_span(content: clipboard.message, style: tui.style(fg: :green, modifiers: [:bold])),
|
|
81
|
-
])
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
tui.block(
|
|
85
|
-
title: "Controls",
|
|
86
|
-
borders: [:all],
|
|
87
|
-
children: [
|
|
88
|
-
tui.paragraph(text: control_lines),
|
|
89
|
-
]
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
require_relative "clipboard"
|
|
9
|
-
|
|
10
|
-
# A self-contained modal dialog component for copying text to the clipboard.
|
|
11
|
-
#
|
|
12
|
-
# Users click on content they want to copy. The app needs to confirm: "Are you
|
|
13
|
-
# sure?" This component owns dialog state, renders itself, and handles keyboard
|
|
14
|
-
# input.
|
|
15
|
-
#
|
|
16
|
-
# === Component Contract
|
|
17
|
-
#
|
|
18
|
-
# - `render(tui, frame, area)`: Draws the dialog; stores `area`
|
|
19
|
-
# - `handle_event(event) -> Symbol | nil`: Returns `:consumed` when handled
|
|
20
|
-
# - `open(text)`: Opens the dialog with the text to copy
|
|
21
|
-
# - `close`: Closes the dialog
|
|
22
|
-
# - `active?`: True if the dialog is visible
|
|
23
|
-
#
|
|
24
|
-
# === Example
|
|
25
|
-
#
|
|
26
|
-
# dialog = CopyDialog.new(clipboard)
|
|
27
|
-
# dialog.open("#FF0000")
|
|
28
|
-
#
|
|
29
|
-
# result = dialog.handle_event(event)
|
|
30
|
-
# # result == :consumed when dialog handled the event
|
|
31
|
-
#
|
|
32
|
-
# dialog.render(tui, frame, center_area)
|
|
33
|
-
class CopyDialog
|
|
34
|
-
def initialize(clipboard)
|
|
35
|
-
@clipboard = clipboard
|
|
36
|
-
@text = ""
|
|
37
|
-
@selected = :yes
|
|
38
|
-
@active = false
|
|
39
|
-
@area = nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# The cached render area.
|
|
43
|
-
attr_reader :area
|
|
44
|
-
|
|
45
|
-
# Opens the dialog with text to copy.
|
|
46
|
-
#
|
|
47
|
-
# Initializes selection to <tt>:yes</tt> and sets active to true.
|
|
48
|
-
#
|
|
49
|
-
# [text] String text to show and copy
|
|
50
|
-
#
|
|
51
|
-
# === Example
|
|
52
|
-
#
|
|
53
|
-
# dialog.open("#FF0000")
|
|
54
|
-
# dialog.active? # => true
|
|
55
|
-
def open(text)
|
|
56
|
-
@text = text
|
|
57
|
-
@selected = :yes
|
|
58
|
-
@active = true
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Closes the dialog and deactivates it.
|
|
62
|
-
def close
|
|
63
|
-
@active = false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# True if the dialog is currently open and visible.
|
|
67
|
-
def active?
|
|
68
|
-
@active
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Renders the dialog into the given area.
|
|
72
|
-
#
|
|
73
|
-
# Shows the text to copy, Yes/No buttons with current selection highlighted,
|
|
74
|
-
# and keyboard instructions.
|
|
75
|
-
#
|
|
76
|
-
# [tui] Session or TUI factory object
|
|
77
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
78
|
-
# [area] Rect area to draw into
|
|
79
|
-
#
|
|
80
|
-
# === Example
|
|
81
|
-
#
|
|
82
|
-
# dialog.render(tui, frame, center_area)
|
|
83
|
-
def render(tui, frame, area)
|
|
84
|
-
@area = area
|
|
85
|
-
widget = build_widget(tui)
|
|
86
|
-
frame.render_widget(widget, area)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Processes a keyboard event and updates selection or closes the dialog.
|
|
90
|
-
#
|
|
91
|
-
# Returns:
|
|
92
|
-
# - `:consumed` when the event was handled
|
|
93
|
-
# - `nil` when the event was ignored or dialog is inactive
|
|
94
|
-
#
|
|
95
|
-
# [event] Event from RatatuiRuby.poll_event
|
|
96
|
-
#
|
|
97
|
-
# === Example
|
|
98
|
-
#
|
|
99
|
-
# result = dialog.handle_event(event)
|
|
100
|
-
def handle_event(event)
|
|
101
|
-
return nil unless @active
|
|
102
|
-
|
|
103
|
-
case event
|
|
104
|
-
in { type: :key, code: "left" } | { type: :key, code: "h" }
|
|
105
|
-
@selected = :yes
|
|
106
|
-
:consumed
|
|
107
|
-
in { type: :key, code: "right" } | { type: :key, code: "l" }
|
|
108
|
-
@selected = :no
|
|
109
|
-
:consumed
|
|
110
|
-
in { type: :key, code: "enter" }
|
|
111
|
-
if @selected == :yes
|
|
112
|
-
@clipboard.copy(@text)
|
|
113
|
-
end
|
|
114
|
-
@active = false
|
|
115
|
-
:consumed
|
|
116
|
-
in { type: :key, code: "y" }
|
|
117
|
-
@clipboard.copy(@text)
|
|
118
|
-
@active = false
|
|
119
|
-
:consumed
|
|
120
|
-
in { type: :key, code: "n" }
|
|
121
|
-
@active = false
|
|
122
|
-
:consumed
|
|
123
|
-
else
|
|
124
|
-
nil
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private def build_widget(tui)
|
|
129
|
-
yes_style = if @selected == :yes
|
|
130
|
-
tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
|
|
131
|
-
else
|
|
132
|
-
tui.style(fg: :gray)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
no_style = if @selected == :no
|
|
136
|
-
tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
|
|
137
|
-
else
|
|
138
|
-
tui.style(fg: :gray)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
tui.block(
|
|
142
|
-
title: "Copy to Clipboard",
|
|
143
|
-
borders: [:all],
|
|
144
|
-
border_type: :rounded,
|
|
145
|
-
style: tui.style(bg: :black, fg: :white),
|
|
146
|
-
children: [
|
|
147
|
-
tui.paragraph(
|
|
148
|
-
text: [
|
|
149
|
-
tui.text_line(spans: [
|
|
150
|
-
tui.text_span(content: "Copy #{@text}?", style: tui.style(fg: :white)),
|
|
151
|
-
]),
|
|
152
|
-
tui.text_line(spans: []),
|
|
153
|
-
tui.text_line(spans: [
|
|
154
|
-
tui.text_span(content: "[", style: tui.style(fg: :white)),
|
|
155
|
-
tui.text_span(content: "Yes", style: yes_style),
|
|
156
|
-
tui.text_span(content: "] [", style: tui.style(fg: :white)),
|
|
157
|
-
tui.text_span(content: "No", style: no_style),
|
|
158
|
-
tui.text_span(content: "]", style: tui.style(fg: :white)),
|
|
159
|
-
]),
|
|
160
|
-
tui.text_line(spans: [
|
|
161
|
-
tui.text_span(content: "Use ←/→ or h/l to select, Enter to confirm", style: tui.style(fg: :gray, modifiers: [:italic])),
|
|
162
|
-
]),
|
|
163
|
-
]
|
|
164
|
-
),
|
|
165
|
-
]
|
|
166
|
-
)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# A self-contained component displaying export formats for a color.
|
|
9
|
-
#
|
|
10
|
-
# Users need to copy color values in different formats (HEX, RGB, HSL).
|
|
11
|
-
# This component renders the export section and detects clicks on itself.
|
|
12
|
-
#
|
|
13
|
-
# === Component Contract
|
|
14
|
-
#
|
|
15
|
-
# - `render(tui, frame, area, palette:)`: Draws the export formats; stores `area` for hit testing
|
|
16
|
-
# - `handle_event(event) -> Symbol | nil`: Returns `:copy_requested` when clicked
|
|
17
|
-
#
|
|
18
|
-
# === Example
|
|
19
|
-
#
|
|
20
|
-
# export_pane = ExportPane.new
|
|
21
|
-
# export_pane.render(tui, frame, area, palette: palette)
|
|
22
|
-
#
|
|
23
|
-
# result = export_pane.handle_event(event)
|
|
24
|
-
# if result == :copy_requested && palette.main
|
|
25
|
-
# dialog.open(palette.main.hex)
|
|
26
|
-
# end
|
|
27
|
-
class ExportPane
|
|
28
|
-
def initialize
|
|
29
|
-
@area = nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# The cached render area, for hit testing.
|
|
33
|
-
attr_reader :area
|
|
34
|
-
|
|
35
|
-
# Renders the export formats section into the given area.
|
|
36
|
-
#
|
|
37
|
-
# Shows HEX, RGB, and HSL values for the current color. If no color is set,
|
|
38
|
-
# displays a placeholder message.
|
|
39
|
-
#
|
|
40
|
-
# [tui] Session or TUI factory object
|
|
41
|
-
# [frame] Frame object from RatatuiRuby.draw block
|
|
42
|
-
# [area] Rect area to draw into
|
|
43
|
-
# [palette] Palette object containing the color to display
|
|
44
|
-
#
|
|
45
|
-
# === Example
|
|
46
|
-
#
|
|
47
|
-
# export_pane.render(tui, frame, export_area, palette: palette)
|
|
48
|
-
def render(tui, frame, area, palette:)
|
|
49
|
-
@area = area
|
|
50
|
-
widget = build_widget(tui, palette)
|
|
51
|
-
frame.render_widget(widget, area)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Processes a mouse event and returns a signal if clicked.
|
|
55
|
-
#
|
|
56
|
-
# Returns:
|
|
57
|
-
# - `:copy_requested` when the pane is clicked (caller should open copy dialog)
|
|
58
|
-
# - `nil` when the event was ignored or outside the area
|
|
59
|
-
#
|
|
60
|
-
# [event] Event from RatatuiRuby.poll_event
|
|
61
|
-
#
|
|
62
|
-
# === Example
|
|
63
|
-
#
|
|
64
|
-
# result = export_pane.handle_event(event)
|
|
65
|
-
# if result == :copy_requested
|
|
66
|
-
# dialog.open(palette.main.hex)
|
|
67
|
-
# end
|
|
68
|
-
def handle_event(event)
|
|
69
|
-
case event
|
|
70
|
-
in { type: :mouse, kind: "down", button: "left", x:, y: }
|
|
71
|
-
if @area&.contains?(x, y)
|
|
72
|
-
:copy_requested
|
|
73
|
-
end
|
|
74
|
-
else
|
|
75
|
-
nil
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private def build_widget(tui, palette)
|
|
80
|
-
if palette.main.nil?
|
|
81
|
-
tui.block(
|
|
82
|
-
title: "Export Formats",
|
|
83
|
-
borders: [:all],
|
|
84
|
-
children: [
|
|
85
|
-
tui.paragraph(
|
|
86
|
-
text: tui.text_line(spans: [
|
|
87
|
-
tui.text_span(content: "Enter a color to see formats"),
|
|
88
|
-
])
|
|
89
|
-
),
|
|
90
|
-
]
|
|
91
|
-
)
|
|
92
|
-
else
|
|
93
|
-
build_color_widget(tui, palette.main)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
private def build_color_widget(tui, color)
|
|
98
|
-
hex = color.hex
|
|
99
|
-
rgb = color.rgb
|
|
100
|
-
hsl = color.hsl_string
|
|
101
|
-
text_color = color.contrasting_text_color
|
|
102
|
-
bg_style = tui.style(bg: hex, fg: text_color)
|
|
103
|
-
|
|
104
|
-
tui.block(
|
|
105
|
-
title: "Export Formats",
|
|
106
|
-
borders: [:all],
|
|
107
|
-
style: bg_style,
|
|
108
|
-
children: [
|
|
109
|
-
tui.paragraph(
|
|
110
|
-
text: [
|
|
111
|
-
tui.text_line(spans: [
|
|
112
|
-
tui.text_span(content: "HEX: ", style: bg_style),
|
|
113
|
-
tui.text_span(content: hex, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
114
|
-
]),
|
|
115
|
-
tui.text_line(spans: [
|
|
116
|
-
tui.text_span(content: "RGB: ", style: bg_style),
|
|
117
|
-
tui.text_span(content: rgb, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
118
|
-
]),
|
|
119
|
-
tui.text_line(spans: [
|
|
120
|
-
tui.text_span(content: "HSL: ", style: bg_style),
|
|
121
|
-
tui.text_span(content: hsl, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
|
|
122
|
-
]),
|
|
123
|
-
]
|
|
124
|
-
),
|
|
125
|
-
]
|
|
126
|
-
)
|
|
127
|
-
end
|
|
128
|
-
end
|