ratatui_ruby 1.2.0 → 1.2.2

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