ratatui_ruby 1.0.0 → 1.0.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 (236) 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 -232
  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 -146
  13. data/CHANGELOG.md +0 -710
  14. data/README.md +0 -187
  15. data/README.rdoc +0 -302
  16. data/Rakefile +0 -11
  17. data/Steepfile +0 -49
  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 -420
  27. data/doc/contributors/design/rust_backend.md +0 -422
  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/todo/align/api_completeness_audit-finished.md +0 -375
  33. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -206
  34. data/doc/contributors/todo/align/terminal.md +0 -647
  35. data/doc/contributors/todo/future_work.md +0 -169
  36. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  37. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  38. data/doc/custom.css +0 -22
  39. data/doc/getting_started/quickstart.md +0 -291
  40. data/doc/getting_started/why.md +0 -93
  41. data/doc/images/app_all_events.png +0 -0
  42. data/doc/images/app_cli_rich_moments.gif +0 -0
  43. data/doc/images/app_color_picker.png +0 -0
  44. data/doc/images/app_debugging_showcase.gif +0 -0
  45. data/doc/images/app_debugging_showcase.png +0 -0
  46. data/doc/images/app_login_form.png +0 -0
  47. data/doc/images/app_stateful_interaction.png +0 -0
  48. data/doc/images/verify_quickstart_dsl.png +0 -0
  49. data/doc/images/verify_quickstart_layout.png +0 -0
  50. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  51. data/doc/images/verify_readme_usage.png +0 -0
  52. data/doc/images/widget_barchart.png +0 -0
  53. data/doc/images/widget_block.png +0 -0
  54. data/doc/images/widget_box.png +0 -0
  55. data/doc/images/widget_calendar.png +0 -0
  56. data/doc/images/widget_canvas.png +0 -0
  57. data/doc/images/widget_cell.png +0 -0
  58. data/doc/images/widget_center.png +0 -0
  59. data/doc/images/widget_chart.png +0 -0
  60. data/doc/images/widget_gauge.png +0 -0
  61. data/doc/images/widget_layout_split.png +0 -0
  62. data/doc/images/widget_line_gauge.png +0 -0
  63. data/doc/images/widget_list.png +0 -0
  64. data/doc/images/widget_map.png +0 -0
  65. data/doc/images/widget_overlay.png +0 -0
  66. data/doc/images/widget_popup.png +0 -0
  67. data/doc/images/widget_ratatui_logo.png +0 -0
  68. data/doc/images/widget_ratatui_mascot.png +0 -0
  69. data/doc/images/widget_rect.png +0 -0
  70. data/doc/images/widget_render.png +0 -0
  71. data/doc/images/widget_rich_text.png +0 -0
  72. data/doc/images/widget_scroll_text.png +0 -0
  73. data/doc/images/widget_scrollbar.png +0 -0
  74. data/doc/images/widget_sparkline.png +0 -0
  75. data/doc/images/widget_style_colors.png +0 -0
  76. data/doc/images/widget_table.png +0 -0
  77. data/doc/images/widget_tabs.png +0 -0
  78. data/doc/images/widget_text_width.png +0 -0
  79. data/doc/index.md +0 -39
  80. data/doc/troubleshooting/async.md +0 -4
  81. data/doc/troubleshooting/terminal_limitations.md +0 -131
  82. data/doc/troubleshooting/tui_output.md +0 -197
  83. data/examples/app_all_events/README.md +0 -114
  84. data/examples/app_all_events/app.rb +0 -98
  85. data/examples/app_all_events/model/app_model.rb +0 -159
  86. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  87. data/examples/app_all_events/model/event_entry.rb +0 -94
  88. data/examples/app_all_events/model/msg.rb +0 -39
  89. data/examples/app_all_events/model/timestamp.rb +0 -56
  90. data/examples/app_all_events/update.rb +0 -75
  91. data/examples/app_all_events/view/app_view.rb +0 -80
  92. data/examples/app_all_events/view/controls_view.rb +0 -54
  93. data/examples/app_all_events/view/counts_view.rb +0 -61
  94. data/examples/app_all_events/view/live_view.rb +0 -72
  95. data/examples/app_all_events/view/log_view.rb +0 -57
  96. data/examples/app_all_events/view.rb +0 -9
  97. data/examples/app_cli_rich_moments/README.md +0 -81
  98. data/examples/app_cli_rich_moments/app.rb +0 -189
  99. data/examples/app_color_picker/README.md +0 -156
  100. data/examples/app_color_picker/app.rb +0 -76
  101. data/examples/app_color_picker/clipboard.rb +0 -86
  102. data/examples/app_color_picker/color.rb +0 -193
  103. data/examples/app_color_picker/controls.rb +0 -92
  104. data/examples/app_color_picker/copy_dialog.rb +0 -168
  105. data/examples/app_color_picker/export_pane.rb +0 -128
  106. data/examples/app_color_picker/harmony.rb +0 -58
  107. data/examples/app_color_picker/input.rb +0 -176
  108. data/examples/app_color_picker/main_container.rb +0 -180
  109. data/examples/app_color_picker/palette.rb +0 -111
  110. data/examples/app_debugging_showcase/README.md +0 -119
  111. data/examples/app_debugging_showcase/app.rb +0 -318
  112. data/examples/app_login_form/README.md +0 -58
  113. data/examples/app_login_form/app.rb +0 -109
  114. data/examples/app_stateful_interaction/README.md +0 -35
  115. data/examples/app_stateful_interaction/app.rb +0 -328
  116. data/examples/timeout_demo.rb +0 -45
  117. data/examples/verify_quickstart_dsl/README.md +0 -55
  118. data/examples/verify_quickstart_dsl/app.rb +0 -49
  119. data/examples/verify_quickstart_layout/README.md +0 -77
  120. data/examples/verify_quickstart_layout/app.rb +0 -73
  121. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  122. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  123. data/examples/verify_readme_usage/README.md +0 -49
  124. data/examples/verify_readme_usage/app.rb +0 -42
  125. data/examples/verify_website_managed/README.md +0 -48
  126. data/examples/verify_website_managed/app.rb +0 -36
  127. data/examples/verify_website_menu/README.md +0 -60
  128. data/examples/verify_website_menu/app.rb +0 -84
  129. data/examples/verify_website_spinner/README.md +0 -44
  130. data/examples/verify_website_spinner/app.rb +0 -34
  131. data/examples/widget_barchart/README.md +0 -58
  132. data/examples/widget_barchart/app.rb +0 -240
  133. data/examples/widget_block/README.md +0 -44
  134. data/examples/widget_block/app.rb +0 -258
  135. data/examples/widget_box/README.md +0 -54
  136. data/examples/widget_box/app.rb +0 -255
  137. data/examples/widget_calendar/README.md +0 -48
  138. data/examples/widget_calendar/app.rb +0 -115
  139. data/examples/widget_canvas/README.md +0 -31
  140. data/examples/widget_canvas/app.rb +0 -130
  141. data/examples/widget_cell/README.md +0 -45
  142. data/examples/widget_cell/app.rb +0 -112
  143. data/examples/widget_center/README.md +0 -33
  144. data/examples/widget_center/app.rb +0 -118
  145. data/examples/widget_chart/README.md +0 -50
  146. data/examples/widget_chart/app.rb +0 -220
  147. data/examples/widget_gauge/README.md +0 -50
  148. data/examples/widget_gauge/app.rb +0 -229
  149. data/examples/widget_layout_split/README.md +0 -53
  150. data/examples/widget_layout_split/app.rb +0 -260
  151. data/examples/widget_line_gauge/README.md +0 -50
  152. data/examples/widget_line_gauge/app.rb +0 -219
  153. data/examples/widget_list/README.md +0 -58
  154. data/examples/widget_list/app.rb +0 -384
  155. data/examples/widget_map/README.md +0 -48
  156. data/examples/widget_map/app.rb +0 -95
  157. data/examples/widget_overlay/README.md +0 -45
  158. data/examples/widget_overlay/app.rb +0 -250
  159. data/examples/widget_popup/README.md +0 -45
  160. data/examples/widget_popup/app.rb +0 -106
  161. data/examples/widget_ratatui_logo/README.md +0 -43
  162. data/examples/widget_ratatui_logo/app.rb +0 -104
  163. data/examples/widget_ratatui_mascot/README.md +0 -43
  164. data/examples/widget_ratatui_mascot/app.rb +0 -95
  165. data/examples/widget_rect/README.md +0 -53
  166. data/examples/widget_rect/app.rb +0 -222
  167. data/examples/widget_render/README.md +0 -46
  168. data/examples/widget_render/app.rb +0 -186
  169. data/examples/widget_render/app.rbs +0 -41
  170. data/examples/widget_rich_text/README.md +0 -44
  171. data/examples/widget_rich_text/app.rb +0 -193
  172. data/examples/widget_scroll_text/README.md +0 -46
  173. data/examples/widget_scroll_text/app.rb +0 -109
  174. data/examples/widget_scrollbar/README.md +0 -46
  175. data/examples/widget_scrollbar/app.rb +0 -155
  176. data/examples/widget_sparkline/README.md +0 -51
  177. data/examples/widget_sparkline/app.rb +0 -277
  178. data/examples/widget_style_colors/README.md +0 -43
  179. data/examples/widget_style_colors/app.rb +0 -83
  180. data/examples/widget_table/README.md +0 -57
  181. data/examples/widget_table/app.rb +0 -279
  182. data/examples/widget_tabs/README.md +0 -50
  183. data/examples/widget_tabs/app.rb +0 -183
  184. data/examples/widget_text_width/README.md +0 -44
  185. data/examples/widget_text_width/app.rb +0 -117
  186. data/migrate_to_buffer.rb +0 -145
  187. data/mise.toml +0 -8
  188. data/tasks/autodoc/examples.rb +0 -87
  189. data/tasks/autodoc/member.rb +0 -58
  190. data/tasks/autodoc/name.rb +0 -21
  191. data/tasks/autodoc.rake +0 -21
  192. data/tasks/bump/cargo_lockfile.rb +0 -21
  193. data/tasks/bump/changelog.rb +0 -47
  194. data/tasks/bump/header.rb +0 -32
  195. data/tasks/bump/history.rb +0 -32
  196. data/tasks/bump/links.rb +0 -69
  197. data/tasks/bump/manifest.rb +0 -33
  198. data/tasks/bump/ruby_gem.rb +0 -49
  199. data/tasks/bump/sem_ver.rb +0 -40
  200. data/tasks/bump/unreleased_section.rb +0 -56
  201. data/tasks/bump.rake +0 -51
  202. data/tasks/doc.rake +0 -887
  203. data/tasks/example_viewer.html.erb +0 -172
  204. data/tasks/extension.rake +0 -14
  205. data/tasks/license/headers_md.rb +0 -223
  206. data/tasks/license/headers_rb.rb +0 -210
  207. data/tasks/license/license_utils.rb +0 -130
  208. data/tasks/license/snippets_md.rb +0 -315
  209. data/tasks/license/snippets_rdoc.rb +0 -150
  210. data/tasks/license.rake +0 -91
  211. data/tasks/lint.rake +0 -170
  212. data/tasks/rdoc_config.rb +0 -29
  213. data/tasks/resources/build.yml.erb +0 -60
  214. data/tasks/resources/index.html.erb +0 -141
  215. data/tasks/resources/rubies.yml +0 -7
  216. data/tasks/sourcehut.rake +0 -110
  217. data/tasks/steep.rake +0 -11
  218. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  219. data/tasks/terminal_preview/crash_report.rb +0 -54
  220. data/tasks/terminal_preview/example_app.rb +0 -27
  221. data/tasks/terminal_preview/launcher_script.rb +0 -48
  222. data/tasks/terminal_preview/preview_collection.rb +0 -60
  223. data/tasks/terminal_preview/preview_timing.rb +0 -24
  224. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  225. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  226. data/tasks/terminal_preview/system_appearance.rb +0 -13
  227. data/tasks/terminal_preview/terminal_window.rb +0 -138
  228. data/tasks/terminal_preview/window_id.rb +0 -16
  229. data/tasks/terminal_preview.rake +0 -30
  230. data/tasks/test.rake +0 -33
  231. data/tasks/website/index_page.rb +0 -30
  232. data/tasks/website/version.rb +0 -127
  233. data/tasks/website/version_menu.rb +0 -68
  234. data/tasks/website/versioned_documentation.rb +0 -83
  235. data/tasks/website/website.rb +0 -53
  236. data/tasks/website.rake +0 -28
@@ -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
@@ -1,58 +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 single color variant with label and styling information.
9
- #
10
- # Color palettes need to show individual colors with labels (Main, Shade, Tint,
11
- # Complement). Bundling a color's hex code, text color, and frame color together
12
- # is natural—they're always used as a set.
13
- #
14
- # This value object pairs a color with its metadata and rendering styles.
15
- #
16
- # Use it to represent colors in a palette or harmony.
17
- #
18
- # === Attributes
19
- #
20
- # [label] String label for this color variant
21
- # [hex] String hex color code
22
- # [text_color] Symbol (:white or :black) for readable text
23
- # [frame_color] String background color for the swatch frame
24
- #
25
- # === Example
26
- #
27
- # harmony = Harmony.new(
28
- # label: "Main",
29
- # hex: "#FF0000",
30
- # text_color: :white,
31
- # frame_color: "#000000"
32
- # )
33
- Harmony = Data.define(:label, :hex, :text_color, :frame_color) do
34
- # Renders a 4-line color swatch for display in a TUI Block.
35
- #
36
- # Produces a visual representation: a 7-character-wide box with the color
37
- # centered and the hex code below.
38
- #
39
- # [tui] Session or TUI factory object
40
- #
41
- # === Example
42
- #
43
- # harmony = Harmony.new(...)
44
- # lines = harmony.color_swatch_lines(tui)
45
- # # => [TextLine, TextLine, TextLine, TextLine]
46
- def color_swatch_lines(tui)
47
- [
48
- tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
49
- tui.text_line(spans: [
50
- *Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
51
- tui.text_span(content: " ", style: tui.style(bg: hex, fg: text_color)),
52
- *Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
53
- ]),
54
- tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
55
- tui.text_line(spans: [tui.text_span(content: hex, style: tui.style(fg: :white))]),
56
- ]
57
- end
58
- end
@@ -1,176 +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 "color"
9
-
10
- # A self-contained text input component for color entry.
11
- #
12
- # Users type color values. They make mistakes—typos, invalid formats. The app
13
- # needs to validate their input and show helpful error messages.
14
- #
15
- # This component encapsulates rendering, state, and event handling. It draws
16
- # itself into the provided area, caches that area for hit testing, and handles
17
- # keyboard events internally.
18
- #
19
- # === Component Contract
20
- #
21
- # - `render(tui, frame, area)`: Draws the input field; stores `area` for hit testing
22
- # - `handle_event(event) -> Symbol | nil`: Returns `:consumed`, `:submitted`, or `nil`
23
- #
24
- # === Example
25
- #
26
- # input = Input.new
27
- # input.render(tui, frame, area)
28
- #
29
- # result = input.handle_event(event)
30
- # case result
31
- # when :submitted
32
- # palette.update_color(input.parsed_color)
33
- # end
34
- class Input
35
- PRINTABLE_PATTERN = /[\w#,().\s%]/
36
-
37
- # Creates a new Input with an optional initial value.
38
- #
39
- # [initial_value] String initial color input (default: <tt>"#F96302"</tt>)
40
- def initialize(initial_value = "#F96302")
41
- @value = initial_value
42
- @error = ""
43
- @parsed_color = nil
44
- @area = nil
45
- end
46
-
47
- # Current input string.
48
- attr_reader :value
49
-
50
- # Error message from the last failed parse, or empty string.
51
- attr_reader :error
52
-
53
- # The last successfully parsed Color, or nil.
54
- attr_reader :parsed_color
55
-
56
- # The cached render area, for hit testing.
57
- attr_reader :area
58
-
59
- # Clears the current error message.
60
- def clear_error
61
- @error = ""
62
- end
63
-
64
- # Renders the input widget into the given area.
65
- #
66
- # Caches `area` for hit testing. Shows the current input value and positions
67
- # the terminal's blinking cursor at the end of the text using
68
- # `frame.set_cursor_position`. Displays the error message in red if set.
69
- #
70
- # [tui] Session or TUI factory object
71
- # [frame] Frame object from RatatuiRuby.draw block
72
- # [area] Rect area to draw into
73
- #
74
- # === Example
75
- #
76
- # input.render(tui, frame, input_area)
77
- def render(tui, frame, area)
78
- @area = area
79
- widget = build_widget(tui)
80
- frame.render_widget(widget, area)
81
-
82
- # Position real blinking cursor at end of input text
83
- cursor_x, cursor_y = cursor_position_in(area)
84
- frame.set_cursor_position(cursor_x, cursor_y)
85
- end
86
-
87
- # Processes a keyboard event and updates internal state.
88
- #
89
- # Returns:
90
- # - `:submitted` when Enter is pressed (caller should read `parsed_color`)
91
- # - `:consumed` when the event was handled (typing, backspace)
92
- # - `nil` when the event was ignored
93
- #
94
- # [event] Event from RatatuiRuby.poll_event
95
- #
96
- # === Example
97
- #
98
- # result = input.handle_event(event)
99
- # if result == :submitted
100
- # palette.update_color(input.parsed_color)
101
- # end
102
- def handle_event(event)
103
- case event
104
- in { type: :key, code: "enter" }
105
- parse
106
- :submitted
107
- in { type: :key, code: "backspace" }
108
- delete_char
109
- :consumed
110
- in { type: :paste, content: }
111
- set(content)
112
- parse
113
- :submitted
114
- in { type: :key, code: code }
115
- append_char(code)
116
- :consumed
117
- else
118
- nil
119
- end
120
- end
121
-
122
- private def append_char(char)
123
- @value += char if char.length == 1 && char.match?(PRINTABLE_PATTERN)
124
- end
125
-
126
- private def delete_char
127
- @value = @value[0...-1]
128
- end
129
-
130
- private def set(text)
131
- @value = text
132
- end
133
-
134
- private def parse
135
- color = Color.parse(@value)
136
- if color
137
- clear_error
138
- @parsed_color = color
139
- else
140
- @error = "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
141
- @parsed_color = nil
142
- end
143
- end
144
-
145
- private def build_widget(tui)
146
- input_lines = [
147
- tui.text_line(spans: [
148
- tui.text_span(content: @value),
149
- ]),
150
- ]
151
-
152
- unless @error.empty?
153
- input_lines << tui.text_line(spans: [
154
- tui.text_span(content: @error, style: tui.style(fg: :red)),
155
- ])
156
- end
157
-
158
- tui.block(
159
- title: "Color Input",
160
- borders: [:all],
161
- children: [
162
- tui.paragraph(text: input_lines),
163
- ]
164
- )
165
- end
166
-
167
- # Calculates cursor position within the input area.
168
- #
169
- # Accounts for block border (1 cell) and current text length.
170
- private def cursor_position_in(area)
171
- # Border takes 1 cell on left, cursor goes after last character
172
- x = area.x + 1 + @value.length
173
- y = area.y + 1 # First line inside border
174
- [x, y]
175
- end
176
- end