ratatui_ruby 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) 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/test_helper/snapshot.rb +58 -10
  6. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  7. data/lib/ratatui_ruby/test_helper.rb +2 -0
  8. data/lib/ratatui_ruby/version.rb +1 -1
  9. metadata +17 -270
  10. data/.builds/ruby-3.2.yml +0 -54
  11. data/.builds/ruby-3.3.yml +0 -54
  12. data/.builds/ruby-3.4.yml +0 -54
  13. data/.builds/ruby-4.0.0.yml +0 -54
  14. data/.pre-commit-config.yaml +0 -16
  15. data/.rubocop.yml +0 -10
  16. data/AGENTS.md +0 -147
  17. data/CHANGELOG.md +0 -771
  18. data/README.md +0 -187
  19. data/README.rdoc +0 -302
  20. data/Rakefile +0 -11
  21. data/Steepfile +0 -50
  22. data/doc/concepts/application_architecture.md +0 -321
  23. data/doc/concepts/application_testing.md +0 -193
  24. data/doc/concepts/async.md +0 -190
  25. data/doc/concepts/custom_widgets.md +0 -247
  26. data/doc/concepts/debugging.md +0 -401
  27. data/doc/concepts/event_handling.md +0 -162
  28. data/doc/concepts/interactive_design.md +0 -146
  29. data/doc/contributors/auditing/parity.md +0 -239
  30. data/doc/contributors/design/ruby_frontend.md +0 -448
  31. data/doc/contributors/design/rust_backend.md +0 -434
  32. data/doc/contributors/design.md +0 -11
  33. data/doc/contributors/developing_examples.md +0 -400
  34. data/doc/contributors/documentation_style.md +0 -121
  35. data/doc/contributors/index.md +0 -21
  36. data/doc/contributors/releasing.md +0 -215
  37. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  38. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  39. data/doc/contributors/todo/align/term.md +0 -351
  40. data/doc/contributors/todo/align/terminal.md +0 -647
  41. data/doc/contributors/todo/future_work.md +0 -169
  42. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  43. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  44. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  45. data/doc/custom.css +0 -22
  46. data/doc/getting_started/quickstart.md +0 -291
  47. data/doc/getting_started/why.md +0 -93
  48. data/doc/images/app_all_events.png +0 -0
  49. data/doc/images/app_cli_rich_moments.gif +0 -0
  50. data/doc/images/app_color_picker.png +0 -0
  51. data/doc/images/app_debugging_showcase.gif +0 -0
  52. data/doc/images/app_debugging_showcase.png +0 -0
  53. data/doc/images/app_external_editor.gif +0 -0
  54. data/doc/images/app_login_form.png +0 -0
  55. data/doc/images/app_stateful_interaction.png +0 -0
  56. data/doc/images/verify_quickstart_dsl.png +0 -0
  57. data/doc/images/verify_quickstart_layout.png +0 -0
  58. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  59. data/doc/images/verify_readme_usage.png +0 -0
  60. data/doc/images/widget_barchart.png +0 -0
  61. data/doc/images/widget_block.png +0 -0
  62. data/doc/images/widget_box.png +0 -0
  63. data/doc/images/widget_calendar.png +0 -0
  64. data/doc/images/widget_canvas.png +0 -0
  65. data/doc/images/widget_cell.png +0 -0
  66. data/doc/images/widget_center.png +0 -0
  67. data/doc/images/widget_chart.png +0 -0
  68. data/doc/images/widget_gauge.png +0 -0
  69. data/doc/images/widget_layout_split.png +0 -0
  70. data/doc/images/widget_line_gauge.png +0 -0
  71. data/doc/images/widget_list.png +0 -0
  72. data/doc/images/widget_map.png +0 -0
  73. data/doc/images/widget_overlay.png +0 -0
  74. data/doc/images/widget_popup.png +0 -0
  75. data/doc/images/widget_ratatui_logo.png +0 -0
  76. data/doc/images/widget_ratatui_mascot.png +0 -0
  77. data/doc/images/widget_rect.png +0 -0
  78. data/doc/images/widget_render.png +0 -0
  79. data/doc/images/widget_rich_text.png +0 -0
  80. data/doc/images/widget_scroll_text.png +0 -0
  81. data/doc/images/widget_scrollbar.png +0 -0
  82. data/doc/images/widget_sparkline.png +0 -0
  83. data/doc/images/widget_style_colors.png +0 -0
  84. data/doc/images/widget_table.png +0 -0
  85. data/doc/images/widget_tabs.png +0 -0
  86. data/doc/images/widget_text_width.png +0 -0
  87. data/doc/index.md +0 -34
  88. data/doc/troubleshooting/async.md +0 -4
  89. data/doc/troubleshooting/terminal_limitations.md +0 -131
  90. data/doc/troubleshooting/tui_output.md +0 -197
  91. data/examples/app_all_events/README.md +0 -114
  92. data/examples/app_all_events/app.rb +0 -98
  93. data/examples/app_all_events/model/app_model.rb +0 -159
  94. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  95. data/examples/app_all_events/model/event_entry.rb +0 -94
  96. data/examples/app_all_events/model/msg.rb +0 -39
  97. data/examples/app_all_events/model/timestamp.rb +0 -56
  98. data/examples/app_all_events/update.rb +0 -75
  99. data/examples/app_all_events/view/app_view.rb +0 -80
  100. data/examples/app_all_events/view/controls_view.rb +0 -54
  101. data/examples/app_all_events/view/counts_view.rb +0 -61
  102. data/examples/app_all_events/view/live_view.rb +0 -72
  103. data/examples/app_all_events/view/log_view.rb +0 -57
  104. data/examples/app_all_events/view.rb +0 -9
  105. data/examples/app_cli_rich_moments/README.md +0 -81
  106. data/examples/app_cli_rich_moments/app.rb +0 -189
  107. data/examples/app_color_picker/README.md +0 -156
  108. data/examples/app_color_picker/app.rb +0 -76
  109. data/examples/app_color_picker/clipboard.rb +0 -86
  110. data/examples/app_color_picker/color.rb +0 -193
  111. data/examples/app_color_picker/controls.rb +0 -92
  112. data/examples/app_color_picker/copy_dialog.rb +0 -168
  113. data/examples/app_color_picker/export_pane.rb +0 -128
  114. data/examples/app_color_picker/harmony.rb +0 -58
  115. data/examples/app_color_picker/input.rb +0 -176
  116. data/examples/app_color_picker/main_container.rb +0 -180
  117. data/examples/app_color_picker/palette.rb +0 -111
  118. data/examples/app_debugging_showcase/README.md +0 -119
  119. data/examples/app_debugging_showcase/app.rb +0 -318
  120. data/examples/app_external_editor/README.md +0 -62
  121. data/examples/app_external_editor/app.rb +0 -344
  122. data/examples/app_login_form/README.md +0 -58
  123. data/examples/app_login_form/app.rb +0 -109
  124. data/examples/app_stateful_interaction/README.md +0 -35
  125. data/examples/app_stateful_interaction/app.rb +0 -328
  126. data/examples/timeout_demo.rb +0 -45
  127. data/examples/verify_quickstart_dsl/README.md +0 -55
  128. data/examples/verify_quickstart_dsl/app.rb +0 -49
  129. data/examples/verify_quickstart_layout/README.md +0 -77
  130. data/examples/verify_quickstart_layout/app.rb +0 -73
  131. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  132. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  133. data/examples/verify_readme_usage/README.md +0 -49
  134. data/examples/verify_readme_usage/app.rb +0 -42
  135. data/examples/verify_website_managed/README.md +0 -48
  136. data/examples/verify_website_managed/app.rb +0 -36
  137. data/examples/verify_website_menu/README.md +0 -60
  138. data/examples/verify_website_menu/app.rb +0 -84
  139. data/examples/verify_website_spinner/README.md +0 -44
  140. data/examples/verify_website_spinner/app.rb +0 -34
  141. data/examples/widget_barchart/README.md +0 -58
  142. data/examples/widget_barchart/app.rb +0 -240
  143. data/examples/widget_block/README.md +0 -44
  144. data/examples/widget_block/app.rb +0 -258
  145. data/examples/widget_box/README.md +0 -54
  146. data/examples/widget_box/app.rb +0 -255
  147. data/examples/widget_calendar/README.md +0 -48
  148. data/examples/widget_calendar/app.rb +0 -115
  149. data/examples/widget_canvas/README.md +0 -31
  150. data/examples/widget_canvas/app.rb +0 -130
  151. data/examples/widget_cell/README.md +0 -45
  152. data/examples/widget_cell/app.rb +0 -112
  153. data/examples/widget_center/README.md +0 -33
  154. data/examples/widget_center/app.rb +0 -118
  155. data/examples/widget_chart/README.md +0 -50
  156. data/examples/widget_chart/app.rb +0 -220
  157. data/examples/widget_gauge/README.md +0 -50
  158. data/examples/widget_gauge/app.rb +0 -229
  159. data/examples/widget_layout_split/README.md +0 -53
  160. data/examples/widget_layout_split/app.rb +0 -260
  161. data/examples/widget_line_gauge/README.md +0 -50
  162. data/examples/widget_line_gauge/app.rb +0 -219
  163. data/examples/widget_list/README.md +0 -58
  164. data/examples/widget_list/app.rb +0 -382
  165. data/examples/widget_map/README.md +0 -48
  166. data/examples/widget_map/app.rb +0 -95
  167. data/examples/widget_overlay/README.md +0 -45
  168. data/examples/widget_overlay/app.rb +0 -250
  169. data/examples/widget_popup/README.md +0 -45
  170. data/examples/widget_popup/app.rb +0 -106
  171. data/examples/widget_ratatui_logo/README.md +0 -43
  172. data/examples/widget_ratatui_logo/app.rb +0 -104
  173. data/examples/widget_ratatui_mascot/README.md +0 -43
  174. data/examples/widget_ratatui_mascot/app.rb +0 -95
  175. data/examples/widget_rect/README.md +0 -53
  176. data/examples/widget_rect/app.rb +0 -222
  177. data/examples/widget_render/README.md +0 -46
  178. data/examples/widget_render/app.rb +0 -186
  179. data/examples/widget_render/app.rbs +0 -41
  180. data/examples/widget_rich_text/README.md +0 -44
  181. data/examples/widget_rich_text/app.rb +0 -193
  182. data/examples/widget_scroll_text/README.md +0 -46
  183. data/examples/widget_scroll_text/app.rb +0 -109
  184. data/examples/widget_scrollbar/README.md +0 -46
  185. data/examples/widget_scrollbar/app.rb +0 -155
  186. data/examples/widget_sparkline/README.md +0 -51
  187. data/examples/widget_sparkline/app.rb +0 -277
  188. data/examples/widget_style_colors/README.md +0 -43
  189. data/examples/widget_style_colors/app.rb +0 -83
  190. data/examples/widget_table/README.md +0 -57
  191. data/examples/widget_table/app.rb +0 -285
  192. data/examples/widget_tabs/README.md +0 -50
  193. data/examples/widget_tabs/app.rb +0 -183
  194. data/examples/widget_text_width/README.md +0 -44
  195. data/examples/widget_text_width/app.rb +0 -117
  196. data/migrate_to_buffer.rb +0 -145
  197. data/mise.toml +0 -8
  198. data/tasks/autodoc/examples.rb +0 -87
  199. data/tasks/autodoc/member.rb +0 -58
  200. data/tasks/autodoc/name.rb +0 -21
  201. data/tasks/autodoc.rake +0 -21
  202. data/tasks/bump/bump_workflow.rb +0 -49
  203. data/tasks/bump/cargo_lockfile.rb +0 -21
  204. data/tasks/bump/changelog.rb +0 -104
  205. data/tasks/bump/header.rb +0 -32
  206. data/tasks/bump/history.rb +0 -32
  207. data/tasks/bump/links.rb +0 -69
  208. data/tasks/bump/manifest.rb +0 -33
  209. data/tasks/bump/patch_release.rb +0 -19
  210. data/tasks/bump/release_branch.rb +0 -17
  211. data/tasks/bump/release_from_trunk.rb +0 -49
  212. data/tasks/bump/repository.rb +0 -54
  213. data/tasks/bump/ruby_gem.rb +0 -29
  214. data/tasks/bump/sem_ver.rb +0 -44
  215. data/tasks/bump/unreleased_section.rb +0 -73
  216. data/tasks/bump.rake +0 -61
  217. data/tasks/doc/documentation.rb +0 -59
  218. data/tasks/doc/link/file_url.rb +0 -30
  219. data/tasks/doc/link/relative_path.rb +0 -61
  220. data/tasks/doc/link/web_url.rb +0 -55
  221. data/tasks/doc/link.rb +0 -52
  222. data/tasks/doc/link_audit.rb +0 -116
  223. data/tasks/doc/problem.rb +0 -40
  224. data/tasks/doc/source_file.rb +0 -93
  225. data/tasks/doc.rake +0 -905
  226. data/tasks/example_viewer.html.erb +0 -172
  227. data/tasks/extension.rake +0 -14
  228. data/tasks/license/headers_md.rb +0 -223
  229. data/tasks/license/headers_rb.rb +0 -210
  230. data/tasks/license/license_utils.rb +0 -130
  231. data/tasks/license/snippets_md.rb +0 -315
  232. data/tasks/license/snippets_rdoc.rb +0 -150
  233. data/tasks/license.rake +0 -91
  234. data/tasks/lint.rake +0 -170
  235. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  236. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  237. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  238. data/tasks/rbs_predicates.rake +0 -31
  239. data/tasks/rdoc_config.rb +0 -29
  240. data/tasks/resources/build.yml.erb +0 -60
  241. data/tasks/resources/index.html.erb +0 -141
  242. data/tasks/resources/rubies.yml +0 -7
  243. data/tasks/sourcehut.rake +0 -122
  244. data/tasks/steep.rake +0 -11
  245. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  246. data/tasks/terminal_preview/crash_report.rb +0 -54
  247. data/tasks/terminal_preview/example_app.rb +0 -27
  248. data/tasks/terminal_preview/launcher_script.rb +0 -48
  249. data/tasks/terminal_preview/preview_collection.rb +0 -60
  250. data/tasks/terminal_preview/preview_timing.rb +0 -24
  251. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  252. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  253. data/tasks/terminal_preview/system_appearance.rb +0 -13
  254. data/tasks/terminal_preview/terminal_window.rb +0 -138
  255. data/tasks/terminal_preview/window_id.rb +0 -16
  256. data/tasks/terminal_preview.rake +0 -30
  257. data/tasks/test.rake +0 -36
  258. data/tasks/website/index_page.rb +0 -30
  259. data/tasks/website/version.rb +0 -122
  260. data/tasks/website/version_menu.rb +0 -68
  261. data/tasks/website/versioned_documentation.rb +0 -83
  262. data/tasks/website/website.rb +0 -53
  263. data/tasks/website.rake +0 -28
@@ -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
@@ -1,180 +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 "input"
9
- require_relative "palette"
10
- require_relative "export_pane"
11
- require_relative "controls"
12
- require_relative "clipboard"
13
- require_relative "copy_dialog"
14
-
15
- # The root container that owns all child components and orchestrates the UI.
16
- #
17
- # Building a complete color picker UI involves layout calculation, widget
18
- # composition, event routing, and cross-component communication. The Container
19
- # pattern centralizes this orchestration while keeping components decoupled.
20
- #
21
- # This container:
22
- # - **Layout Phase**: Calculates Rects using tui.layout_split
23
- # - **Delegation Phase**: Calls child.render(tui, frame, area) for each component
24
- # - **Event Routing (Chain of Responsibility)**: Delegates events front-to-back
25
- # - **Mediator Pattern**: Manages cross-component communication via symbolic signals
26
- #
27
- # === Component Contract
28
- #
29
- # - `render(tui, frame, area)`: Lays out and renders all children
30
- # - `handle_event(event) -> Symbol | nil`: Routes events to children
31
- # - `tick`: Delegates lifecycle updates (clipboard timer)
32
- #
33
- # === Example
34
- #
35
- # container = MainContainer.new(tui)
36
- # container.render(tui, frame, frame.area)
37
- # result = container.handle_event(event)
38
- # container.tick
39
- class MainContainer
40
- def initialize(tui)
41
- @tui = tui
42
- @input = Input.new
43
- @palette = Palette.new(@input.parsed_color)
44
- @export_pane = ExportPane.new
45
- @controls = Controls.new
46
- @clipboard = Clipboard.new
47
- @dialog = CopyDialog.new(@clipboard)
48
-
49
- # Parse initial color
50
- initial_result = simulate_initial_parse
51
- @palette.update_color(initial_result) if initial_result
52
- end
53
-
54
- # Renders all child components into the given area.
55
- #
56
- # Calculates layout once per frame. Delegates rendering to each component.
57
- # Renders the dialog overlay last for z-ordering.
58
- #
59
- # [tui] Session or TUI factory object
60
- # [frame] Frame object from RatatuiRuby.draw block
61
- # [area] Rect area to draw into
62
- #
63
- # === Example
64
- #
65
- # tui.draw { |frame| container.render(tui, frame, frame.area) }
66
- def render(tui, frame, area)
67
- # Layout Phase: calculate all areas
68
- input_area, rest = tui.layout_split(
69
- area,
70
- direction: :vertical,
71
- constraints: [
72
- tui.constraint_length(3),
73
- tui.constraint_fill(1),
74
- ]
75
- )
76
-
77
- color_area, control_area = tui.layout_split(
78
- rest,
79
- direction: :vertical,
80
- constraints: [
81
- tui.constraint_length(14),
82
- tui.constraint_fill(1),
83
- ]
84
- )
85
-
86
- harmony_area, export_area = tui.layout_split(
87
- color_area,
88
- direction: :vertical,
89
- constraints: [
90
- tui.constraint_length(7),
91
- tui.constraint_fill(1),
92
- ]
93
- )
94
-
95
- # Delegation Phase: render each component
96
- @input.render(tui, frame, input_area)
97
- @palette.render(tui, frame, harmony_area)
98
- @export_pane.render(tui, frame, export_area, palette: @palette)
99
- @controls.render(tui, frame, control_area, clipboard: @clipboard)
100
-
101
- # Overlay Logic: dialog rendered last for z-ordering
102
- if @dialog.active?
103
- dialog_area = calculate_center_area(area, 40, 8)
104
- frame.render_widget(tui.clear, area)
105
- @dialog.render(tui, frame, dialog_area)
106
- end
107
- end
108
-
109
- # Routes events to child components in visual order (front-to-back).
110
- #
111
- # Implements Chain of Responsibility:
112
- # 1. If dialog is active, offer it the event first
113
- # 2. Then Input, ExportPane (which may trigger dialog)
114
- # 3. Mediator pattern: interprets symbolic signals for cross-component effects
115
- #
116
- # Returns:
117
- # - `:consumed` when any component handled the event
118
- # - `nil` when no component handled the event
119
- #
120
- # [event] Event from RatatuiRuby.poll_event
121
- #
122
- # === Example
123
- #
124
- # result = container.handle_event(event)
125
- def handle_event(event)
126
- # Clear input error when not in dialog mode
127
- @input.clear_error unless @dialog.active?
128
-
129
- # Front-to-back: dialog has priority when active
130
- if @dialog.active?
131
- result = @dialog.handle_event(event)
132
- return :consumed if result == :consumed
133
- end
134
-
135
- # Input component
136
- result = @input.handle_event(event)
137
- case result
138
- when :submitted
139
- # Mediator: sync Input -> Palette
140
- @palette.update_color(@input.parsed_color)
141
- return :consumed
142
- when :consumed
143
- return :consumed
144
- end
145
-
146
- # ExportPane: may request copy dialog
147
- result = @export_pane.handle_event(event)
148
- if result == :copy_requested && @palette.main
149
- @dialog.open(@palette.main.hex)
150
- return :consumed
151
- end
152
-
153
- # Palette and Controls are display-only
154
- nil
155
- end
156
-
157
- # Delegates lifecycle tick to time-sensitive components.
158
- #
159
- # Currently handles clipboard feedback timer.
160
- #
161
- # === Example
162
- #
163
- # container.tick
164
- def tick
165
- @controls.tick(@clipboard)
166
- end
167
-
168
- private def calculate_center_area(parent_area, width, height)
169
- x = (parent_area.width - width) / 2
170
- y = (parent_area.height - height) / 2
171
- @tui.rect(x:, y:, width:, height:)
172
- end
173
-
174
- # Simulates the initial parse that happens when the app starts.
175
- # Input is initialized with a default color, so we need to parse it.
176
- private def simulate_initial_parse
177
- require_relative "color"
178
- Color.parse(@input.value)
179
- end
180
- end
@@ -1,111 +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 component displaying a color palette with harmonies.
11
- #
12
- # Color pickers need to show related colors: shades, tints, complements. This
13
- # component owns a primary color and renders its harmonies.
14
- #
15
- # === Component Contract
16
- #
17
- # - `render(tui, frame, area)`: Draws the harmony blocks; stores `area`
18
- # - `handle_event(event) -> nil`: Display-only, always returns nil
19
- # - `update_color(color)`: Updates the primary color (called by MainContainer)
20
- #
21
- # === Example
22
- #
23
- # palette = Palette.new
24
- # palette.update_color(Color.parse("#FF0000"))
25
- # palette.render(tui, frame, palette_area)
26
- class Palette
27
- def initialize(primary_color = nil)
28
- @primary = primary_color
29
- @area = nil
30
- end
31
-
32
- # The cached render area.
33
- attr_reader :area
34
-
35
- # The primary (main) color, or nil if no color is set.
36
- #
37
- # === Example
38
- #
39
- # palette.main.hex # => "#FF0000"
40
- def main
41
- @primary
42
- end
43
-
44
- # Updates the primary color.
45
- #
46
- # Called by the MainContainer when Input submits a new color.
47
- #
48
- # [color] Color object or nil
49
- def update_color(color)
50
- @primary = color
51
- end
52
-
53
- # All harmonies: main, shade, tint, complement, split 1, split 2, split-complement.
54
- #
55
- # Returns an empty array if no primary color is set.
56
- def all
57
- return [] if @primary.nil?
58
-
59
- @primary.harmonies
60
- end
61
-
62
- # Renders the palette into the given area.
63
- #
64
- # Shows all harmony blocks in a horizontal layout. If no color is set,
65
- # displays a placeholder message.
66
- #
67
- # [tui] Session or TUI factory object
68
- # [frame] Frame object from RatatuiRuby.draw block
69
- # [area] Rect area to draw into
70
- #
71
- # === Example
72
- #
73
- # palette.render(tui, frame, palette_area)
74
- def render(tui, frame, area)
75
- @area = area
76
- widget = build_widget(tui)
77
- frame.render_widget(widget, area)
78
- end
79
-
80
- # Display-only component; always returns nil.
81
- def handle_event(_event)
82
- nil
83
- end
84
-
85
- private def build_widget(tui)
86
- if @primary.nil?
87
- tui.paragraph(text: "No color selected")
88
- else
89
- blocks = as_blocks(tui)
90
- tui.layout(
91
- direction: :horizontal,
92
- constraints: Array.new(blocks.size) { tui.constraint_fill(1) },
93
- children: blocks
94
- )
95
- end
96
- end
97
-
98
- private def as_blocks(tui)
99
- return [] if @primary.nil?
100
-
101
- all.map do |harmony|
102
- tui.block(
103
- title: harmony.label,
104
- borders: [:all],
105
- children: [
106
- tui.paragraph(text: harmony.color_swatch_lines(tui)),
107
- ]
108
- )
109
- end
110
- end
111
- end
@@ -1,119 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Debugging Showcase
7
-
8
- [![Debugging Showcase](../../doc/images/app_debugging_showcase.gif)](app.rb)
9
-
10
- Interactive demonstration of RatatuiRuby's debugging features.
11
-
12
- For comprehensive documentation, see the [Debugging Guide](../../doc/concepts/debugging.md).
13
-
14
- ## What This Example Does
15
-
16
- This app lets you trigger each debugging feature with a hotkey. Test your setup before encountering a real bug.
17
-
18
- | Key | Action |
19
- |-----|--------|
20
- | `d` | Enable `debug_mode!` programmatically |
21
- | `p` | Trigger a Rust panic |
22
- | `t` | Trigger a Rust `TypeError` |
23
- | `b` | Refresh debug status |
24
- | `q` | Quit |
25
-
26
- ## Library Features Showcased
27
-
28
- Reading this code will teach you how to:
29
-
30
- * **Enable Remote Debugging**: Call `RatatuiRuby.debug_mode!` and receive the socket path
31
- * **Detect Debug State**: Query `Debug.enabled?`, `Debug.rust_backtrace_enabled?`, and `Debug.remote_debugging_mode`
32
- * **Trigger Test Panics**: Use `Debug.test_panic!` to verify Rust backtrace visibility
33
- * **See Improved Error Messages**: Bypass DWIM coercion to see `TypeError` messages with value context
34
-
35
- **Note:** Rust backtraces only appear for panics (`[p]`). Exceptions (`[t]`) show Ruby backtraces with improved error messages from Rust.
36
-
37
- ## Environment Variables
38
-
39
- The example behaves differently depending on which environment variables you set.
40
-
41
- ### No Environment Variables
42
-
43
- <!-- SPDX-SnippetBegin -->
44
- <!--
45
- SPDX-FileCopyrightText: 2026 Kerrick Long
46
- SPDX-License-Identifier: MIT-0
47
- -->
48
- ```bash
49
- ruby examples/app_debugging_showcase/app.rb
50
- ```
51
- <!-- SPDX-SnippetEnd -->
52
-
53
- The TUI starts normally. Rust backtraces are disabled. Press `[d]` to enable remote debugging mid-session.
54
-
55
- ### `RUST_BACKTRACE=1`
56
-
57
- <!-- SPDX-SnippetBegin -->
58
- <!--
59
- SPDX-FileCopyrightText: 2026 Kerrick Long
60
- SPDX-License-Identifier: MIT-0
61
- -->
62
- ```bash
63
- RUST_BACKTRACE=1 ruby examples/app_debugging_showcase/app.rb
64
- ```
65
- <!-- SPDX-SnippetEnd -->
66
-
67
- Enables Rust backtraces. Press `[p]` to see the full Rust stack trace with file names and line numbers.
68
-
69
- ### `RR_DEBUG=1`
70
-
71
- <!-- SPDX-SnippetBegin -->
72
- <!--
73
- SPDX-FileCopyrightText: 2026 Kerrick Long
74
- SPDX-License-Identifier: MIT-0
75
- -->
76
- ```bash
77
- RR_DEBUG=1 ruby examples/app_debugging_showcase/app.rb
78
- ```
79
- <!-- SPDX-SnippetEnd -->
80
-
81
- Full debug mode at startup. The debugger opens a socket and **waits for a connection** before the TUI starts. You'll see the socket path printed before the app runs.
82
-
83
- In another terminal:
84
-
85
- <!-- SPDX-SnippetBegin -->
86
- <!--
87
- SPDX-FileCopyrightText: 2026 Kerrick Long
88
- SPDX-License-Identifier: MIT-0
89
- -->
90
- ```bash
91
- rdbg --attach
92
- ```
93
- <!-- SPDX-SnippetEnd -->
94
-
95
- When you attach, you'll see "Stop by SIGURG" — that's normal! SIGURG is how the debug gem signals the app to pause. Type `continue` to resume.
96
-
97
- This uses the [ruby/debug](https://github.com/ruby/debug?tab=readme-ov-file#readme) gem for remote debugging.
98
-
99
- ### When to Use Which
100
-
101
- `RR_DEBUG=1` includes Rust backtraces automatically (it calls `enable_rust_backtrace!` internally).
102
-
103
- Use `RUST_BACKTRACE=1` alone when you want backtraces but **don't** want the debugger to stop and wait for a connection at startup.
104
-
105
- ## What Problems Does This Solve?
106
-
107
- ### "Is my Rust backtrace setup working?"
108
-
109
- Press `[p]`. If you see a stack trace with file names and line numbers, you're good. If not, you need `RUST_BACKTRACE=1`.
110
-
111
- ### "How do I attach a debugger to a TUI?"
112
-
113
- TUIs run in raw mode — you can't type into a REPL. Press `[d]` to enable remote debugging. The socket path appears in the UI. Run `rdbg --attach` from another terminal.
114
-
115
- ### "What does a Rust TypeError look like?"
116
-
117
- Press `[t]`. The error message shows the expected type and the actual value you passed (e.g., `expected array for rows, got 42`). Note: This shows Ruby's backtrace — only panics (`[p]`) show Rust backtraces.
118
-
119
- [Read the source code →](app.rb)