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.
Files changed (259) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +1 -1
  3. data/ext/ratatui_ruby/Cargo.toml +1 -1
  4. data/lib/ratatui_ruby/version.rb +1 -1
  5. metadata +1 -255
  6. data/.builds/ruby-3.2.yml +0 -54
  7. data/.builds/ruby-3.3.yml +0 -54
  8. data/.builds/ruby-3.4.yml +0 -54
  9. data/.builds/ruby-4.0.0.yml +0 -54
  10. data/.pre-commit-config.yaml +0 -16
  11. data/.rubocop.yml +0 -10
  12. data/AGENTS.md +0 -147
  13. data/CHANGELOG.md +0 -736
  14. data/README.md +0 -187
  15. data/README.rdoc +0 -302
  16. data/Rakefile +0 -11
  17. data/Steepfile +0 -50
  18. data/doc/concepts/application_architecture.md +0 -321
  19. data/doc/concepts/application_testing.md +0 -193
  20. data/doc/concepts/async.md +0 -190
  21. data/doc/concepts/custom_widgets.md +0 -247
  22. data/doc/concepts/debugging.md +0 -401
  23. data/doc/concepts/event_handling.md +0 -162
  24. data/doc/concepts/interactive_design.md +0 -146
  25. data/doc/contributors/auditing/parity.md +0 -239
  26. data/doc/contributors/design/ruby_frontend.md +0 -448
  27. data/doc/contributors/design/rust_backend.md +0 -434
  28. data/doc/contributors/design.md +0 -11
  29. data/doc/contributors/developing_examples.md +0 -400
  30. data/doc/contributors/documentation_style.md +0 -121
  31. data/doc/contributors/index.md +0 -21
  32. data/doc/contributors/releasing.md +0 -215
  33. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  34. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  35. data/doc/contributors/todo/align/term.md +0 -351
  36. data/doc/contributors/todo/align/terminal.md +0 -647
  37. data/doc/contributors/todo/future_work.md +0 -169
  38. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  39. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  40. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  41. data/doc/custom.css +0 -22
  42. data/doc/getting_started/quickstart.md +0 -291
  43. data/doc/getting_started/why.md +0 -93
  44. data/doc/images/app_all_events.png +0 -0
  45. data/doc/images/app_cli_rich_moments.gif +0 -0
  46. data/doc/images/app_color_picker.png +0 -0
  47. data/doc/images/app_debugging_showcase.gif +0 -0
  48. data/doc/images/app_debugging_showcase.png +0 -0
  49. data/doc/images/app_external_editor.gif +0 -0
  50. data/doc/images/app_login_form.png +0 -0
  51. data/doc/images/app_stateful_interaction.png +0 -0
  52. data/doc/images/verify_quickstart_dsl.png +0 -0
  53. data/doc/images/verify_quickstart_layout.png +0 -0
  54. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  55. data/doc/images/verify_readme_usage.png +0 -0
  56. data/doc/images/widget_barchart.png +0 -0
  57. data/doc/images/widget_block.png +0 -0
  58. data/doc/images/widget_box.png +0 -0
  59. data/doc/images/widget_calendar.png +0 -0
  60. data/doc/images/widget_canvas.png +0 -0
  61. data/doc/images/widget_cell.png +0 -0
  62. data/doc/images/widget_center.png +0 -0
  63. data/doc/images/widget_chart.png +0 -0
  64. data/doc/images/widget_gauge.png +0 -0
  65. data/doc/images/widget_layout_split.png +0 -0
  66. data/doc/images/widget_line_gauge.png +0 -0
  67. data/doc/images/widget_list.png +0 -0
  68. data/doc/images/widget_map.png +0 -0
  69. data/doc/images/widget_overlay.png +0 -0
  70. data/doc/images/widget_popup.png +0 -0
  71. data/doc/images/widget_ratatui_logo.png +0 -0
  72. data/doc/images/widget_ratatui_mascot.png +0 -0
  73. data/doc/images/widget_rect.png +0 -0
  74. data/doc/images/widget_render.png +0 -0
  75. data/doc/images/widget_rich_text.png +0 -0
  76. data/doc/images/widget_scroll_text.png +0 -0
  77. data/doc/images/widget_scrollbar.png +0 -0
  78. data/doc/images/widget_sparkline.png +0 -0
  79. data/doc/images/widget_style_colors.png +0 -0
  80. data/doc/images/widget_table.png +0 -0
  81. data/doc/images/widget_tabs.png +0 -0
  82. data/doc/images/widget_text_width.png +0 -0
  83. data/doc/index.md +0 -34
  84. data/doc/troubleshooting/async.md +0 -4
  85. data/doc/troubleshooting/terminal_limitations.md +0 -131
  86. data/doc/troubleshooting/tui_output.md +0 -197
  87. data/examples/app_all_events/README.md +0 -114
  88. data/examples/app_all_events/app.rb +0 -98
  89. data/examples/app_all_events/model/app_model.rb +0 -159
  90. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  91. data/examples/app_all_events/model/event_entry.rb +0 -94
  92. data/examples/app_all_events/model/msg.rb +0 -39
  93. data/examples/app_all_events/model/timestamp.rb +0 -56
  94. data/examples/app_all_events/update.rb +0 -75
  95. data/examples/app_all_events/view/app_view.rb +0 -80
  96. data/examples/app_all_events/view/controls_view.rb +0 -54
  97. data/examples/app_all_events/view/counts_view.rb +0 -61
  98. data/examples/app_all_events/view/live_view.rb +0 -72
  99. data/examples/app_all_events/view/log_view.rb +0 -57
  100. data/examples/app_all_events/view.rb +0 -9
  101. data/examples/app_cli_rich_moments/README.md +0 -81
  102. data/examples/app_cli_rich_moments/app.rb +0 -189
  103. data/examples/app_color_picker/README.md +0 -156
  104. data/examples/app_color_picker/app.rb +0 -76
  105. data/examples/app_color_picker/clipboard.rb +0 -86
  106. data/examples/app_color_picker/color.rb +0 -193
  107. data/examples/app_color_picker/controls.rb +0 -92
  108. data/examples/app_color_picker/copy_dialog.rb +0 -168
  109. data/examples/app_color_picker/export_pane.rb +0 -128
  110. data/examples/app_color_picker/harmony.rb +0 -58
  111. data/examples/app_color_picker/input.rb +0 -176
  112. data/examples/app_color_picker/main_container.rb +0 -180
  113. data/examples/app_color_picker/palette.rb +0 -111
  114. data/examples/app_debugging_showcase/README.md +0 -119
  115. data/examples/app_debugging_showcase/app.rb +0 -318
  116. data/examples/app_external_editor/README.md +0 -62
  117. data/examples/app_external_editor/app.rb +0 -344
  118. data/examples/app_login_form/README.md +0 -58
  119. data/examples/app_login_form/app.rb +0 -109
  120. data/examples/app_stateful_interaction/README.md +0 -35
  121. data/examples/app_stateful_interaction/app.rb +0 -328
  122. data/examples/timeout_demo.rb +0 -45
  123. data/examples/verify_quickstart_dsl/README.md +0 -55
  124. data/examples/verify_quickstart_dsl/app.rb +0 -49
  125. data/examples/verify_quickstart_layout/README.md +0 -77
  126. data/examples/verify_quickstart_layout/app.rb +0 -73
  127. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  128. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  129. data/examples/verify_readme_usage/README.md +0 -49
  130. data/examples/verify_readme_usage/app.rb +0 -42
  131. data/examples/verify_website_managed/README.md +0 -48
  132. data/examples/verify_website_managed/app.rb +0 -36
  133. data/examples/verify_website_menu/README.md +0 -60
  134. data/examples/verify_website_menu/app.rb +0 -84
  135. data/examples/verify_website_spinner/README.md +0 -44
  136. data/examples/verify_website_spinner/app.rb +0 -34
  137. data/examples/widget_barchart/README.md +0 -58
  138. data/examples/widget_barchart/app.rb +0 -240
  139. data/examples/widget_block/README.md +0 -44
  140. data/examples/widget_block/app.rb +0 -258
  141. data/examples/widget_box/README.md +0 -54
  142. data/examples/widget_box/app.rb +0 -255
  143. data/examples/widget_calendar/README.md +0 -48
  144. data/examples/widget_calendar/app.rb +0 -115
  145. data/examples/widget_canvas/README.md +0 -31
  146. data/examples/widget_canvas/app.rb +0 -130
  147. data/examples/widget_cell/README.md +0 -45
  148. data/examples/widget_cell/app.rb +0 -112
  149. data/examples/widget_center/README.md +0 -33
  150. data/examples/widget_center/app.rb +0 -118
  151. data/examples/widget_chart/README.md +0 -50
  152. data/examples/widget_chart/app.rb +0 -220
  153. data/examples/widget_gauge/README.md +0 -50
  154. data/examples/widget_gauge/app.rb +0 -229
  155. data/examples/widget_layout_split/README.md +0 -53
  156. data/examples/widget_layout_split/app.rb +0 -260
  157. data/examples/widget_line_gauge/README.md +0 -50
  158. data/examples/widget_line_gauge/app.rb +0 -219
  159. data/examples/widget_list/README.md +0 -58
  160. data/examples/widget_list/app.rb +0 -382
  161. data/examples/widget_map/README.md +0 -48
  162. data/examples/widget_map/app.rb +0 -95
  163. data/examples/widget_overlay/README.md +0 -45
  164. data/examples/widget_overlay/app.rb +0 -250
  165. data/examples/widget_popup/README.md +0 -45
  166. data/examples/widget_popup/app.rb +0 -106
  167. data/examples/widget_ratatui_logo/README.md +0 -43
  168. data/examples/widget_ratatui_logo/app.rb +0 -104
  169. data/examples/widget_ratatui_mascot/README.md +0 -43
  170. data/examples/widget_ratatui_mascot/app.rb +0 -95
  171. data/examples/widget_rect/README.md +0 -53
  172. data/examples/widget_rect/app.rb +0 -222
  173. data/examples/widget_render/README.md +0 -46
  174. data/examples/widget_render/app.rb +0 -186
  175. data/examples/widget_render/app.rbs +0 -41
  176. data/examples/widget_rich_text/README.md +0 -44
  177. data/examples/widget_rich_text/app.rb +0 -193
  178. data/examples/widget_scroll_text/README.md +0 -46
  179. data/examples/widget_scroll_text/app.rb +0 -109
  180. data/examples/widget_scrollbar/README.md +0 -46
  181. data/examples/widget_scrollbar/app.rb +0 -155
  182. data/examples/widget_sparkline/README.md +0 -51
  183. data/examples/widget_sparkline/app.rb +0 -277
  184. data/examples/widget_style_colors/README.md +0 -43
  185. data/examples/widget_style_colors/app.rb +0 -83
  186. data/examples/widget_table/README.md +0 -57
  187. data/examples/widget_table/app.rb +0 -285
  188. data/examples/widget_tabs/README.md +0 -50
  189. data/examples/widget_tabs/app.rb +0 -183
  190. data/examples/widget_text_width/README.md +0 -44
  191. data/examples/widget_text_width/app.rb +0 -117
  192. data/migrate_to_buffer.rb +0 -145
  193. data/mise.toml +0 -8
  194. data/tasks/autodoc/examples.rb +0 -87
  195. data/tasks/autodoc/member.rb +0 -58
  196. data/tasks/autodoc/name.rb +0 -21
  197. data/tasks/autodoc.rake +0 -21
  198. data/tasks/bump/bump_workflow.rb +0 -49
  199. data/tasks/bump/cargo_lockfile.rb +0 -21
  200. data/tasks/bump/changelog.rb +0 -104
  201. data/tasks/bump/header.rb +0 -32
  202. data/tasks/bump/history.rb +0 -32
  203. data/tasks/bump/links.rb +0 -69
  204. data/tasks/bump/manifest.rb +0 -33
  205. data/tasks/bump/patch_release.rb +0 -19
  206. data/tasks/bump/release_branch.rb +0 -17
  207. data/tasks/bump/release_from_trunk.rb +0 -49
  208. data/tasks/bump/repository.rb +0 -54
  209. data/tasks/bump/ruby_gem.rb +0 -29
  210. data/tasks/bump/sem_ver.rb +0 -44
  211. data/tasks/bump/unreleased_section.rb +0 -73
  212. data/tasks/bump.rake +0 -61
  213. data/tasks/doc/documentation.rb +0 -59
  214. data/tasks/doc/link/file_url.rb +0 -30
  215. data/tasks/doc/link/relative_path.rb +0 -61
  216. data/tasks/doc/link/web_url.rb +0 -55
  217. data/tasks/doc/link.rb +0 -52
  218. data/tasks/doc/link_audit.rb +0 -116
  219. data/tasks/doc/problem.rb +0 -40
  220. data/tasks/doc/source_file.rb +0 -93
  221. data/tasks/doc.rake +0 -905
  222. data/tasks/example_viewer.html.erb +0 -172
  223. data/tasks/extension.rake +0 -14
  224. data/tasks/license/headers_md.rb +0 -223
  225. data/tasks/license/headers_rb.rb +0 -210
  226. data/tasks/license/license_utils.rb +0 -130
  227. data/tasks/license/snippets_md.rb +0 -315
  228. data/tasks/license/snippets_rdoc.rb +0 -150
  229. data/tasks/license.rake +0 -91
  230. data/tasks/lint.rake +0 -170
  231. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  232. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  233. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  234. data/tasks/rbs_predicates.rake +0 -31
  235. data/tasks/rdoc_config.rb +0 -29
  236. data/tasks/resources/build.yml.erb +0 -60
  237. data/tasks/resources/index.html.erb +0 -141
  238. data/tasks/resources/rubies.yml +0 -7
  239. data/tasks/sourcehut.rake +0 -110
  240. data/tasks/steep.rake +0 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  242. data/tasks/terminal_preview/crash_report.rb +0 -54
  243. data/tasks/terminal_preview/example_app.rb +0 -27
  244. data/tasks/terminal_preview/launcher_script.rb +0 -48
  245. data/tasks/terminal_preview/preview_collection.rb +0 -60
  246. data/tasks/terminal_preview/preview_timing.rb +0 -24
  247. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  248. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  249. data/tasks/terminal_preview/system_appearance.rb +0 -13
  250. data/tasks/terminal_preview/terminal_window.rb +0 -138
  251. data/tasks/terminal_preview/window_id.rb +0 -16
  252. data/tasks/terminal_preview.rake +0 -30
  253. data/tasks/test.rake +0 -36
  254. data/tasks/website/index_page.rb +0 -30
  255. data/tasks/website/version.rb +0 -122
  256. data/tasks/website/version_menu.rb +0 -68
  257. data/tasks/website/versioned_documentation.rb +0 -83
  258. data/tasks/website/website.rb +0 -53
  259. 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