ratatui_ruby 1.2.2 → 1.3.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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +54 -0
  3. data/.builds/ruby-3.3.yml +54 -0
  4. data/.builds/ruby-3.4.yml +54 -0
  5. data/.builds/ruby-4.0.0.yml +54 -0
  6. data/.pre-commit-config.yaml +16 -0
  7. data/.rubocop.yml +10 -0
  8. data/AGENTS.md +147 -0
  9. data/CHANGELOG.md +771 -0
  10. data/README.md +187 -0
  11. data/README.rdoc +302 -0
  12. data/Rakefile +11 -0
  13. data/Steepfile +50 -0
  14. data/doc/concepts/application_architecture.md +321 -0
  15. data/doc/concepts/application_testing.md +193 -0
  16. data/doc/concepts/async.md +190 -0
  17. data/doc/concepts/custom_widgets.md +247 -0
  18. data/doc/concepts/debugging.md +401 -0
  19. data/doc/concepts/event_handling.md +162 -0
  20. data/doc/concepts/interactive_design.md +146 -0
  21. data/doc/contributors/auditing/parity.md +239 -0
  22. data/doc/contributors/design/ruby_frontend.md +448 -0
  23. data/doc/contributors/design/rust_backend.md +434 -0
  24. data/doc/contributors/design.md +11 -0
  25. data/doc/contributors/developing_examples.md +400 -0
  26. data/doc/contributors/documentation_style.md +121 -0
  27. data/doc/contributors/index.md +21 -0
  28. data/doc/contributors/releasing.md +215 -0
  29. data/doc/contributors/todo/align/api_completeness_audit-finished.md +381 -0
  30. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +200 -0
  31. data/doc/contributors/todo/align/term.md +351 -0
  32. data/doc/contributors/todo/align/terminal.md +647 -0
  33. data/doc/contributors/todo/future_work.md +169 -0
  34. data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
  35. data/doc/contributors/upstream_requests/tab_rects.md +173 -0
  36. data/doc/contributors/upstream_requests/title_rects.md +132 -0
  37. data/doc/custom.css +22 -0
  38. data/doc/getting_started/quickstart.md +291 -0
  39. data/doc/getting_started/why.md +93 -0
  40. data/doc/images/app_all_events.png +0 -0
  41. data/doc/images/app_cli_rich_moments.gif +0 -0
  42. data/doc/images/app_color_picker.png +0 -0
  43. data/doc/images/app_debugging_showcase.gif +0 -0
  44. data/doc/images/app_debugging_showcase.png +0 -0
  45. data/doc/images/app_external_editor.gif +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 +34 -0
  80. data/doc/troubleshooting/async.md +4 -0
  81. data/doc/troubleshooting/terminal_limitations.md +131 -0
  82. data/doc/troubleshooting/tui_output.md +197 -0
  83. data/examples/app_all_events/README.md +114 -0
  84. data/examples/app_all_events/app.rb +98 -0
  85. data/examples/app_all_events/model/app_model.rb +159 -0
  86. data/examples/app_all_events/model/event_color_cycle.rb +43 -0
  87. data/examples/app_all_events/model/event_entry.rb +94 -0
  88. data/examples/app_all_events/model/msg.rb +39 -0
  89. data/examples/app_all_events/model/timestamp.rb +56 -0
  90. data/examples/app_all_events/update.rb +75 -0
  91. data/examples/app_all_events/view/app_view.rb +80 -0
  92. data/examples/app_all_events/view/controls_view.rb +54 -0
  93. data/examples/app_all_events/view/counts_view.rb +61 -0
  94. data/examples/app_all_events/view/live_view.rb +72 -0
  95. data/examples/app_all_events/view/log_view.rb +57 -0
  96. data/examples/app_all_events/view.rb +9 -0
  97. data/examples/app_cli_rich_moments/README.md +81 -0
  98. data/examples/app_cli_rich_moments/app.rb +189 -0
  99. data/examples/app_color_picker/README.md +156 -0
  100. data/examples/app_color_picker/app.rb +76 -0
  101. data/examples/app_color_picker/clipboard.rb +86 -0
  102. data/examples/app_color_picker/color.rb +193 -0
  103. data/examples/app_color_picker/controls.rb +92 -0
  104. data/examples/app_color_picker/copy_dialog.rb +168 -0
  105. data/examples/app_color_picker/export_pane.rb +128 -0
  106. data/examples/app_color_picker/harmony.rb +58 -0
  107. data/examples/app_color_picker/input.rb +176 -0
  108. data/examples/app_color_picker/main_container.rb +180 -0
  109. data/examples/app_color_picker/palette.rb +111 -0
  110. data/examples/app_debugging_showcase/README.md +119 -0
  111. data/examples/app_debugging_showcase/app.rb +318 -0
  112. data/examples/app_external_editor/README.md +62 -0
  113. data/examples/app_external_editor/app.rb +344 -0
  114. data/examples/app_login_form/README.md +58 -0
  115. data/examples/app_login_form/app.rb +109 -0
  116. data/examples/app_stateful_interaction/README.md +35 -0
  117. data/examples/app_stateful_interaction/app.rb +328 -0
  118. data/examples/timeout_demo.rb +45 -0
  119. data/examples/verify_quickstart_dsl/README.md +55 -0
  120. data/examples/verify_quickstart_dsl/app.rb +49 -0
  121. data/examples/verify_quickstart_layout/README.md +77 -0
  122. data/examples/verify_quickstart_layout/app.rb +73 -0
  123. data/examples/verify_quickstart_lifecycle/README.md +68 -0
  124. data/examples/verify_quickstart_lifecycle/app.rb +62 -0
  125. data/examples/verify_readme_usage/README.md +49 -0
  126. data/examples/verify_readme_usage/app.rb +42 -0
  127. data/examples/verify_website_managed/README.md +48 -0
  128. data/examples/verify_website_managed/app.rb +36 -0
  129. data/examples/verify_website_menu/README.md +60 -0
  130. data/examples/verify_website_menu/app.rb +84 -0
  131. data/examples/verify_website_spinner/README.md +44 -0
  132. data/examples/verify_website_spinner/app.rb +34 -0
  133. data/examples/widget_barchart/README.md +58 -0
  134. data/examples/widget_barchart/app.rb +240 -0
  135. data/examples/widget_block/README.md +44 -0
  136. data/examples/widget_block/app.rb +258 -0
  137. data/examples/widget_box/README.md +54 -0
  138. data/examples/widget_box/app.rb +255 -0
  139. data/examples/widget_calendar/README.md +48 -0
  140. data/examples/widget_calendar/app.rb +115 -0
  141. data/examples/widget_canvas/README.md +31 -0
  142. data/examples/widget_canvas/app.rb +130 -0
  143. data/examples/widget_cell/README.md +45 -0
  144. data/examples/widget_cell/app.rb +112 -0
  145. data/examples/widget_center/README.md +33 -0
  146. data/examples/widget_center/app.rb +118 -0
  147. data/examples/widget_chart/README.md +50 -0
  148. data/examples/widget_chart/app.rb +220 -0
  149. data/examples/widget_gauge/README.md +50 -0
  150. data/examples/widget_gauge/app.rb +229 -0
  151. data/examples/widget_layout_split/README.md +53 -0
  152. data/examples/widget_layout_split/app.rb +260 -0
  153. data/examples/widget_line_gauge/README.md +50 -0
  154. data/examples/widget_line_gauge/app.rb +219 -0
  155. data/examples/widget_list/README.md +58 -0
  156. data/examples/widget_list/app.rb +382 -0
  157. data/examples/widget_map/README.md +48 -0
  158. data/examples/widget_map/app.rb +95 -0
  159. data/examples/widget_overlay/README.md +45 -0
  160. data/examples/widget_overlay/app.rb +250 -0
  161. data/examples/widget_popup/README.md +45 -0
  162. data/examples/widget_popup/app.rb +106 -0
  163. data/examples/widget_ratatui_logo/README.md +43 -0
  164. data/examples/widget_ratatui_logo/app.rb +104 -0
  165. data/examples/widget_ratatui_mascot/README.md +43 -0
  166. data/examples/widget_ratatui_mascot/app.rb +95 -0
  167. data/examples/widget_rect/README.md +53 -0
  168. data/examples/widget_rect/app.rb +222 -0
  169. data/examples/widget_render/README.md +46 -0
  170. data/examples/widget_render/app.rb +186 -0
  171. data/examples/widget_render/app.rbs +41 -0
  172. data/examples/widget_rich_text/README.md +44 -0
  173. data/examples/widget_rich_text/app.rb +193 -0
  174. data/examples/widget_scroll_text/README.md +46 -0
  175. data/examples/widget_scroll_text/app.rb +109 -0
  176. data/examples/widget_scrollbar/README.md +46 -0
  177. data/examples/widget_scrollbar/app.rb +155 -0
  178. data/examples/widget_sparkline/README.md +51 -0
  179. data/examples/widget_sparkline/app.rb +277 -0
  180. data/examples/widget_style_colors/README.md +43 -0
  181. data/examples/widget_style_colors/app.rb +83 -0
  182. data/examples/widget_table/README.md +57 -0
  183. data/examples/widget_table/app.rb +285 -0
  184. data/examples/widget_tabs/README.md +50 -0
  185. data/examples/widget_tabs/app.rb +183 -0
  186. data/examples/widget_text_width/README.md +44 -0
  187. data/examples/widget_text_width/app.rb +117 -0
  188. data/ext/ratatui_ruby/Cargo.lock +1 -2
  189. data/ext/ratatui_ruby/Cargo.toml +1 -2
  190. data/ext/ratatui_ruby/src/events.rs +18 -157
  191. data/lib/ratatui_ruby/event/focus_gained.rb +50 -0
  192. data/lib/ratatui_ruby/event/focus_lost.rb +51 -0
  193. data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
  194. data/lib/ratatui_ruby/event/key/modifier.rb +2 -0
  195. data/lib/ratatui_ruby/event/key.rb +9 -0
  196. data/lib/ratatui_ruby/event/mouse.rb +33 -0
  197. data/lib/ratatui_ruby/event/paste.rb +25 -0
  198. data/lib/ratatui_ruby/event/resize.rb +65 -0
  199. data/lib/ratatui_ruby/version.rb +1 -1
  200. data/migrate_to_buffer.rb +145 -0
  201. data/mise.toml +8 -0
  202. data/sig/ratatui_ruby/event.rbs +97 -0
  203. data/tasks/autodoc/examples.rb +87 -0
  204. data/tasks/autodoc/member.rb +58 -0
  205. data/tasks/autodoc/name.rb +21 -0
  206. data/tasks/autodoc.rake +21 -0
  207. data/tasks/bump/bump_workflow.rb +49 -0
  208. data/tasks/bump/cargo_lockfile.rb +21 -0
  209. data/tasks/bump/changelog.rb +104 -0
  210. data/tasks/bump/header.rb +32 -0
  211. data/tasks/bump/history.rb +32 -0
  212. data/tasks/bump/links.rb +69 -0
  213. data/tasks/bump/manifest.rb +33 -0
  214. data/tasks/bump/patch_release.rb +19 -0
  215. data/tasks/bump/release_branch.rb +17 -0
  216. data/tasks/bump/release_from_trunk.rb +49 -0
  217. data/tasks/bump/repository.rb +54 -0
  218. data/tasks/bump/ruby_gem.rb +29 -0
  219. data/tasks/bump/sem_ver.rb +44 -0
  220. data/tasks/bump/unreleased_section.rb +73 -0
  221. data/tasks/bump.rake +61 -0
  222. data/tasks/doc/documentation.rb +59 -0
  223. data/tasks/doc/link/file_url.rb +30 -0
  224. data/tasks/doc/link/relative_path.rb +61 -0
  225. data/tasks/doc/link/web_url.rb +55 -0
  226. data/tasks/doc/link.rb +52 -0
  227. data/tasks/doc/link_audit.rb +116 -0
  228. data/tasks/doc/problem.rb +40 -0
  229. data/tasks/doc/source_file.rb +93 -0
  230. data/tasks/doc.rake +905 -0
  231. data/tasks/example_viewer.html.erb +172 -0
  232. data/tasks/extension.rake +14 -0
  233. data/tasks/license/headers_md.rb +223 -0
  234. data/tasks/license/headers_rb.rb +210 -0
  235. data/tasks/license/license_utils.rb +130 -0
  236. data/tasks/license/snippets_md.rb +315 -0
  237. data/tasks/license/snippets_rdoc.rb +150 -0
  238. data/tasks/license.rake +91 -0
  239. data/tasks/lint.rake +170 -0
  240. data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
  241. data/tasks/rbs_predicates/predicate_tests.rb +124 -0
  242. data/tasks/rbs_predicates/rbs_signature.rb +63 -0
  243. data/tasks/rbs_predicates.rake +31 -0
  244. data/tasks/rdoc_config.rb +29 -0
  245. data/tasks/resources/build.yml.erb +60 -0
  246. data/tasks/resources/index.html.erb +141 -0
  247. data/tasks/resources/rubies.yml +7 -0
  248. data/tasks/sourcehut.rake +122 -0
  249. data/tasks/steep.rake +11 -0
  250. data/tasks/terminal_preview/app_screenshot.rb +45 -0
  251. data/tasks/terminal_preview/crash_report.rb +54 -0
  252. data/tasks/terminal_preview/example_app.rb +27 -0
  253. data/tasks/terminal_preview/launcher_script.rb +48 -0
  254. data/tasks/terminal_preview/preview_collection.rb +60 -0
  255. data/tasks/terminal_preview/preview_timing.rb +24 -0
  256. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  257. data/tasks/terminal_preview/saved_screenshot.rb +56 -0
  258. data/tasks/terminal_preview/system_appearance.rb +13 -0
  259. data/tasks/terminal_preview/terminal_window.rb +138 -0
  260. data/tasks/terminal_preview/window_id.rb +16 -0
  261. data/tasks/terminal_preview.rake +30 -0
  262. data/tasks/test.rake +36 -0
  263. data/tasks/website/index_page.rb +30 -0
  264. data/tasks/website/version.rb +122 -0
  265. data/tasks/website/version_menu.rb +68 -0
  266. data/tasks/website/versioned_documentation.rb +83 -0
  267. data/tasks/website/website.rb +53 -0
  268. data/tasks/website.rake +28 -0
  269. metadata +256 -1
@@ -253,6 +253,39 @@ module RatatuiRuby
253
253
  else false
254
254
  end
255
255
  end
256
+
257
+ alias wheel_up? scroll_up?
258
+ alias wheel_down? scroll_down?
259
+
260
+ # Returns true for any scroll event.
261
+ #
262
+ # event.scroll? # => true for scroll_up or scroll_down
263
+ def scroll?
264
+ scroll_up? || scroll_down?
265
+ end
266
+
267
+ alias primary? left?
268
+ alias secondary? right?
269
+ alias context_menu? right?
270
+ alias aux? middle?
271
+ alias auxiliary? aux?
272
+
273
+ # Returns true for mouse movement without button press.
274
+ #
275
+ # event.moved? # => true for moved (no button)
276
+ def moved?
277
+ @kind == "moved"
278
+ end
279
+
280
+ alias hover? moved?
281
+ alias hovering? moved?
282
+ alias move? moved?
283
+ alias dragging? drag?
284
+
285
+ alias release? up?
286
+ alias released? up?
287
+ alias press? down?
288
+ alias pressed? down?
256
289
  end
257
290
  end
258
291
  end
@@ -67,6 +67,9 @@ module RatatuiRuby
67
67
  def paste?
68
68
  true
69
69
  end
70
+ alias clipboard? paste?
71
+ alias pasteboard? paste?
72
+ alias pasted? paste?
70
73
 
71
74
  # Creates a new Paste event.
72
75
  #
@@ -100,6 +103,28 @@ module RatatuiRuby
100
103
  return false unless other.is_a?(Paste)
101
104
  content == other.content
102
105
  end
106
+
107
+ # Returns true if the pasted content is empty.
108
+ def empty?
109
+ @content.empty?
110
+ end
111
+
112
+ # Returns true if the pasted content is empty or whitespace-only.
113
+ def blank?
114
+ @content.strip.empty?
115
+ end
116
+
117
+ # Returns true if the pasted content spans multiple lines.
118
+ def multiline?
119
+ @content.include?("\n")
120
+ end
121
+ alias multi_line? multiline?
122
+
123
+ # Returns true if the pasted content is a single line.
124
+ def single_line?
125
+ !multiline?
126
+ end
127
+ alias singleline? single_line?
103
128
  end
104
129
  end
105
130
  end
@@ -151,6 +151,71 @@ module RatatuiRuby
151
151
  else false
152
152
  end
153
153
  end
154
+
155
+ # Returns true. Unix <tt>SIGWINCH</tt> triggers terminal resize.
156
+ #
157
+ # event.sigwinch? # => true
158
+ def sigwinch?
159
+ true
160
+ end
161
+
162
+ alias winch? sigwinch?
163
+ alias sig_winch? sigwinch?
164
+
165
+ alias terminal_resize? resize?
166
+ alias window_resize? resize?
167
+ alias window_change? resize?
168
+ alias viewport_resize? resize?
169
+ alias viewport_change? resize?
170
+ alias size_change? resize?
171
+ alias resized? resize?
172
+
173
+ VT100_WIDTH = 80 # :nodoc:
174
+ VT100_HEIGHT = 24 # :nodoc:
175
+
176
+ # Returns true if width exceeds height.
177
+ #
178
+ # Event::Resize.new(width: 120, height: 24).landscape? # => true
179
+ def landscape?
180
+ @width > @height
181
+ end
182
+
183
+ # Returns true if height exceeds or equals width.
184
+ #
185
+ # Event::Resize.new(width: 40, height: 80).portrait? # => true
186
+ def portrait?
187
+ @height >= @width
188
+ end
189
+
190
+ # Returns true if dimensions are exactly 80x24.
191
+ #
192
+ # Event::Resize.new(width: 80, height: 24).vt100? # => true
193
+ def vt100?
194
+ @width == VT100_WIDTH && @height == VT100_HEIGHT
195
+ end
196
+
197
+ # Returns true if both dimensions meet or exceed 80x24.
198
+ #
199
+ # Event::Resize.new(width: 120, height: 40).at_least_vt100? # => true
200
+ def at_least_vt100?
201
+ @width >= VT100_WIDTH && @height >= VT100_HEIGHT
202
+ end
203
+
204
+ # Returns true if both dimensions exceed 80x24.
205
+ #
206
+ # Event::Resize.new(width: 81, height: 25).over_vt100? # => true
207
+ def over_vt100?
208
+ @width > VT100_WIDTH && @height > VT100_HEIGHT
209
+ end
210
+
211
+ # Returns true if either dimension falls below VT100 standard.
212
+ #
213
+ # Event::Resize.new(width: 60, height: 24).cramped? # => true
214
+ def cramped?
215
+ @width < VT100_WIDTH || @height < VT100_HEIGHT
216
+ end
217
+
218
+ alias constrained? cramped?
154
219
  end
155
220
  end
156
221
  end
@@ -8,5 +8,5 @@
8
8
  module RatatuiRuby
9
9
  # The version of the ratatui_ruby gem.
10
10
  # See https://semver.org/spec/v2.0.0.html
11
- VERSION = "1.2.2"
11
+ VERSION = "1.3.0"
12
12
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ #
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+ #++
8
+
9
+ # !/usr/bin/env ruby
10
+
11
+ # Migration script to refactor widget renderers from Frame to Buffer
12
+
13
+ require "fileutils"
14
+
15
+ WIDGETS_DIR = "/Users/kerrick/Developer/ratatui_ruby/ext/ratatui_ruby/src/widgets"
16
+
17
+ # Files to update (excluding block.rs which is already done)
18
+ WIDGET_FILES = %w[
19
+ barchart.rs
20
+ calendar.rs
21
+ canvas.rs
22
+ center.rs
23
+ chart.rs
24
+ clear.rs
25
+ cursor.rs
26
+ gauge.rs
27
+ layout.rs
28
+ line_gauge.rs
29
+ list.rs
30
+ overlay.rs
31
+ paragraph.rs
32
+ ratatui_logo.rs
33
+ ratatui_mascot.rs
34
+ scrollbar.rs
35
+ sparkline.rs
36
+ table.rs
37
+ tabs.rs
38
+ ].freeze
39
+
40
+ def migrate_file(filepath)
41
+ puts "Migrating #{File.basename(filepath)}..."
42
+
43
+ content = File.read(filepath)
44
+ original_content = content.dup
45
+
46
+ # 1. Update function signature
47
+ content.gsub!("pub fn render(frame: &mut Frame,", "pub fn render(buffer: &mut Buffer,")
48
+ content.gsub!("pub fn render_ratatui_mascot(frame: &mut Frame,", "pub fn render_ratatui_mascot(buffer: &mut Buffer,")
49
+
50
+ # 2. Update imports - Add Buffer, remove Frame
51
+ # Handle various import patterns
52
+ content.gsub!(/use ratatui::\{([^}]*),\s*Frame\s*\};/, 'use ratatui::{\1};')
53
+ content.gsub!(/use ratatui::\{Frame,\s*([^}]*)\};/, 'use ratatui::{\1};')
54
+ content.gsub!("use ratatui::Frame;", "")
55
+
56
+ # Add Buffer import if not present
57
+ unless content.match?(/use ratatui::(?:\{[^}]*)?buffer::Buffer/)
58
+ # Find the ratatui use statement and add buffer::Buffer
59
+ content.gsub!("use ratatui::{", "use ratatui::{buffer::Buffer, ")
60
+ content.gsub!("use ratatui::layout::Rect;", "use ratatui::{buffer::Buffer, layout::Rect};")
61
+ end
62
+
63
+ # 3. Update widget.render calls
64
+ # frame.render_widget(widget, area) → widget.render(area, buffer)
65
+ content.gsub!(/frame\.render_widget\(([^,]+),\s*([^)]+)\)/, '\1.render(\2, buffer)')
66
+
67
+ # 4. Update direct buffer access
68
+ content.gsub!("frame.buffer_mut()", "buffer")
69
+
70
+ # 5. Update recursive render_node calls
71
+ content.gsub!("render_node(frame,", "render_node(buffer,")
72
+
73
+ # 5.5. Update stateful widget rendering
74
+ # frame.render_stateful_widget(widget, area, state) → StatefulWidget::render(widget, area, buffer, state)
75
+ content.gsub!(/frame\.render_stateful_widget\(([^,]+),\s*([^,]+),\s*([^)]+)\)/, 'StatefulWidget::render(\1, \2, buffer, \3)')
76
+
77
+ # Add StatefulWidget import if stateful widgets are used
78
+ if content.match?(/StatefulWidget::render/) && !content.match?(/use ratatui::widgets::StatefulWidget/) && content.match?(/use ratatui::/)
79
+ content.sub!("use ratatui::{", "use ratatui::{widgets::StatefulWidget, ")
80
+ end
81
+
82
+ # 6. Clean up any double-added Buffer imports and duplicate Widget imports
83
+ content.gsub!(/buffer::Buffer,\s*buffer::Buffer,/, "buffer::Buffer,")
84
+ content.gsub!(/widgets::Widget,\s*widgets::Widget/, "widgets::Widget")
85
+ content.gsub!(/(use ratatui::\{[^}]*widgets::Widget[^}]*),\s*widgets::Widget/, '\1')
86
+
87
+ # 7. Fix Widget trait usage - make sure Widget is imported where .render is called
88
+ # Add Widget to imports if calling .render on widgets
89
+ if content.match?(/\.render\(area,\s*buffer\)/) && !content.match?(/use ratatui::widgets::Widget/) && content.match?(/use ratatui::\{/) && !content.match?(/widgets::Widget/)
90
+ content.sub!("use ratatui::{", "use ratatui::{widgets::Widget, ")
91
+ end
92
+
93
+ # 8. For files that don't have proper Buffer imports yet, add them
94
+ if !content.match?(/use ratatui::\{[^}]*buffer::Buffer/) && content.match?(/use ratatui::/)
95
+ # Try to add to first ratatui import
96
+ content.sub!("use ratatui::", "use ratatui::{buffer::Buffer};\nuse ratatui::")
97
+ end
98
+
99
+ # 9. Special case: cursor.rs uses frame.set_cursor_position which doesn't exist on Buffer
100
+ # Cursor widget needs special handling - it can't work with just Buffer
101
+ if File.basename(filepath) == "cursor.rs"
102
+ puts " ⚠ cursor.rs requires special handling - skipping set_cursor_position"
103
+ # This widget can't be fully migrated as set_cursor_position is Frame-only
104
+ # Will need manual fix or different approach
105
+ end
106
+ # Only write if content changed
107
+ if content != original_content
108
+ File.write(filepath, content)
109
+ puts " ✓ Updated #{File.basename(filepath)}"
110
+ true
111
+ else
112
+ puts " - No changes needed for #{File.basename(filepath)}"
113
+ false
114
+ end
115
+ end
116
+
117
+ def main
118
+ puts "Starting widget renderer migration..."
119
+ puts "=" * 60
120
+
121
+ updated_count = 0
122
+
123
+ WIDGET_FILES.each do |filename|
124
+ filepath = File.join(WIDGETS_DIR, filename)
125
+
126
+ unless File.exist?(filepath)
127
+ puts " ⚠ File not found: #{filename}"
128
+ next
129
+ end
130
+
131
+ # Create backup
132
+ backup_path = "#{filepath}.premigration"
133
+ FileUtils.cp(filepath, backup_path) unless File.exist?(backup_path)
134
+
135
+ updated_count += 1 if migrate_file(filepath)
136
+ end
137
+
138
+ puts "=" * 60
139
+ puts "Migration complete!"
140
+ puts " Files updated: #{updated_count}/#{WIDGET_FILES.length}"
141
+ puts "\nBackups saved with .premigration extension"
142
+ puts "Run 'bundle exec rake compile' to verify the changes"
143
+ end
144
+
145
+ main if __FILE__ == $PROGRAM_NAME
data/mise.toml ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [tools]
5
+ ruby = "4.0.0"
6
+ rust = "1.91.1"
7
+ python = "3.12"
8
+ pre-commit = "latest"
@@ -67,11 +67,46 @@ module RatatuiRuby
67
67
  private def match_system_dwim?: (String key_name, Symbol key_sym) -> bool
68
68
  end
69
69
 
70
+ # DWIM predicates for common key patterns
71
+ module Dwim
72
+ NAVIGATION_KEYS: Array[String]
73
+ ARROW_KEYS: Array[String]
74
+ VIM_MOVEMENT_KEYS: Array[String]
75
+ PUNCTUATION_NAMES: Hash[Symbol, String]
76
+
77
+ def space?: () -> bool
78
+ def cr?: () -> bool
79
+ def letter?: () -> bool
80
+ def digit?: () -> bool
81
+ def alphanumeric?: () -> bool
82
+ def punctuation?: () -> bool
83
+ def whitespace?: () -> bool
84
+ def interrupt?: () -> bool
85
+ def eof?: () -> bool
86
+ def cancel?: () -> bool
87
+ def sigint?: () -> bool
88
+ def suspend?: () -> bool
89
+ def quit?: () -> bool
90
+ def navigation?: () -> bool
91
+ def arrow?: () -> bool
92
+ def vim?: () -> bool
93
+ def vim_left?: () -> bool
94
+ def vim_down?: () -> bool
95
+ def vim_up?: () -> bool
96
+ def vim_right?: () -> bool
97
+ def vim_word_forward?: () -> bool
98
+ def vim_word_backward?: () -> bool
99
+ def vim_top?: () -> bool
100
+ def vim_bottom?: () -> bool
101
+ def quote?: () -> bool
102
+ end
103
+
70
104
  include Character
71
105
  include Modifier
72
106
  include Media
73
107
  include Navigation
74
108
  include System
109
+ include Dwim
75
110
 
76
111
  attr_reader code: String
77
112
  attr_reader modifiers: Array[String]
@@ -114,6 +149,23 @@ module RatatuiRuby
114
149
  def left?: () -> bool
115
150
  def right?: () -> bool
116
151
  def middle?: () -> bool
152
+ def scroll?: () -> bool
153
+ def moved?: () -> bool
154
+ def hover?: () -> bool
155
+ def hovering?: () -> bool
156
+ def move?: () -> bool
157
+ def primary?: () -> bool
158
+ def secondary?: () -> bool
159
+ def context_menu?: () -> bool
160
+ def aux?: () -> bool
161
+ def auxiliary?: () -> bool
162
+ def wheel_up?: () -> bool
163
+ def wheel_down?: () -> bool
164
+ def release?: () -> bool
165
+ def released?: () -> bool
166
+ def press?: () -> bool
167
+ def pressed?: () -> bool
168
+ def dragging?: () -> bool
117
169
  def to_sym: () -> Symbol
118
170
  def ==: (top other) -> bool
119
171
  def deconstruct_keys: (Array[Symbol]?) -> { type: :mouse, kind: String, x: Integer, y: Integer, button: String, modifiers: Array[String] }
@@ -127,6 +179,20 @@ module RatatuiRuby
127
179
  def to_sym: () -> Symbol
128
180
  def ==: (top other) -> bool
129
181
  def deconstruct_keys: (Array[Symbol]?) -> { type: :resize, width: Integer, height: Integer }
182
+
183
+ # DWIM predicates
184
+ VT100_WIDTH: Integer
185
+ VT100_HEIGHT: Integer
186
+ def sigwinch?: () -> bool
187
+ def winch?: () -> bool
188
+ def sig_winch?: () -> bool
189
+ def landscape?: () -> bool
190
+ def portrait?: () -> bool
191
+ def vt100?: () -> bool
192
+ def at_least_vt100?: () -> bool
193
+ def over_vt100?: () -> bool
194
+ def cramped?: () -> bool
195
+ def constrained?: () -> bool
130
196
  end
131
197
 
132
198
  class Paste < Event
@@ -134,14 +200,45 @@ module RatatuiRuby
134
200
 
135
201
  def initialize: (content: String) -> void
136
202
  def deconstruct_keys: (Array[Symbol]?) -> { type: :paste, content: String }
203
+
204
+ # DWIM predicates
205
+ def clipboard?: () -> bool
206
+ def pasteboard?: () -> bool
207
+ def pasted?: () -> bool
208
+ def empty?: () -> bool
209
+ def blank?: () -> bool
210
+ def multiline?: () -> bool
211
+ def multi_line?: () -> bool
212
+ def single_line?: () -> bool
213
+ def singleline?: () -> bool
137
214
  end
138
215
 
139
216
  class FocusGained < Event
140
217
  def deconstruct_keys: (Array[Symbol]?) -> { type: :focus_gained }
218
+
219
+ # DWIM predicates
220
+ def focus?: () -> bool
221
+ def gained?: () -> bool
222
+ def lost?: () -> bool
223
+ def blur?: () -> bool
224
+ def active?: () -> bool
225
+ def inactive?: () -> bool
226
+ def foreground?: () -> bool
227
+ def background?: () -> bool
141
228
  end
142
229
 
143
230
  class FocusLost < Event
144
231
  def deconstruct_keys: (Array[Symbol]?) -> { type: :focus_lost }
232
+
233
+ # DWIM predicates
234
+ def focus?: () -> bool
235
+ def gained?: () -> bool
236
+ def lost?: () -> bool
237
+ def blur?: () -> bool
238
+ def active?: () -> bool
239
+ def inactive?: () -> bool
240
+ def foreground?: () -> bool
241
+ def background?: () -> bool
145
242
  end
146
243
 
147
244
  class None < Event
@@ -0,0 +1,87 @@
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
+ module Autodoc
9
+ class Examples
10
+ def self.sync
11
+ new.sync
12
+ end
13
+
14
+ def sync
15
+ Dir.glob("{README.md,doc/**/*.md,examples/*/README.md}").each do |readme_path|
16
+ sync_readme(readme_path)
17
+ end
18
+ end
19
+
20
+ private def sync_readme(readme_path)
21
+ content = File.read(readme_path)
22
+ dir = File.dirname(readme_path)
23
+
24
+ new_content = content.gsub(/<!-- SYNC:START:([^ ]+) -->.*?<!-- SYNC:END -->/m) do
25
+ marker_info = $1
26
+ source_rel_path, segment_id = marker_info.split(":")
27
+
28
+ # Support both repo-root-relative paths (no leading ./) and file-relative paths
29
+ source_path = if source_rel_path.start_with?("./", "../")
30
+ File.join(dir, source_rel_path)
31
+ else
32
+ source_rel_path # Already relative to repo root
33
+ end
34
+
35
+ unless File.exist?(source_path)
36
+ warn "Warning: Source file not found: #{source_path}"
37
+ next $&
38
+ end
39
+
40
+ source_content = File.read(source_path)
41
+ extracted_content = if segment_id
42
+ extract_segment(source_content, segment_id, source_path)
43
+ else
44
+ source_content
45
+ end
46
+
47
+ # Detect language from extension
48
+ ext = File.extname(source_path).delete(".")
49
+ lang = (ext == "rb") ? "ruby" : ext
50
+
51
+ # Build replacement
52
+ "<!-- SYNC:START:#{marker_info} -->\n```#{lang}\n#{extracted_content}```\n<!-- SYNC:END -->"
53
+ end
54
+
55
+ if new_content != content
56
+ puts "Syncing #{readme_path}..."
57
+ File.write(readme_path, new_content)
58
+ end
59
+ end
60
+
61
+ def extract_segment(content, segment_id, source_path)
62
+ start_marker = /#\s*\[SYNC:START:#{segment_id}\]/
63
+ end_marker = /#\s*\[SYNC:END:#{segment_id}\]/
64
+
65
+ lines = content.lines
66
+ start_idx = lines.find_index { |l| l =~ start_marker }
67
+ end_idx = lines.find_index { |l| l =~ end_marker }
68
+
69
+ if start_idx && end_idx
70
+ "#{unindent(lines[(start_idx + 1)...end_idx].join).strip}\n"
71
+ else
72
+ warn "Warning: Segment '#{segment_id}' not found in #{source_path}"
73
+ content # Fallback to full content or error? Let's fallback to original for now.
74
+ end
75
+ end
76
+
77
+ def unindent(text)
78
+ lines = text.lines
79
+ # Don't unindent if empty or just one line
80
+ return text if lines.empty?
81
+
82
+ # Find common leading whitespace
83
+ indentation = lines.grep(/\S/).map { |l| l[/^\s*/].length }.min || 0
84
+ lines.map { |l| (l.length > indentation) ? l[indentation..-1] : "#{l.strip}\n" }.join
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
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
+ module Autodoc
9
+ module Member
10
+ class Delegate < Data.define(:name)
11
+ def rbs
12
+ " def #{name}: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped"
13
+ end
14
+
15
+ def rdoc
16
+ [
17
+ " # :method: #{name}",
18
+ " # :call-seq: #{name}(*args, **kwargs, &block)",
19
+ " #",
20
+ " # Delegates to RatatuiRuby.#{name}.",
21
+ " #",
22
+ ]
23
+ end
24
+ end
25
+
26
+ class Factory < Data.define(:name, :const_name)
27
+ def rbs
28
+ " def #{name}: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped"
29
+ end
30
+
31
+ def rdoc
32
+ [
33
+ " # :method: #{name}",
34
+ " # :call-seq: #{name}(*args, **kwargs, &block)",
35
+ " #",
36
+ " # Factory for RatatuiRuby::#{const_name}.new.",
37
+ " #",
38
+ ]
39
+ end
40
+ end
41
+
42
+ class Helper < Data.define(:name, :class_method, :const_name)
43
+ def rbs
44
+ " def #{name}: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped"
45
+ end
46
+
47
+ def rdoc
48
+ [
49
+ " # :method: #{name}",
50
+ " # :call-seq: #{name}(*args, **kwargs, &block)",
51
+ " #",
52
+ " # Helper for RatatuiRuby::#{const_name}.#{class_method}.",
53
+ " #",
54
+ ]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
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
+ module Autodoc
9
+ class Name < Data.define(:string)
10
+ def snake
11
+ string.to_s
12
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
13
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
14
+ .downcase
15
+ end
16
+
17
+ def to_s
18
+ string.to_s
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ require_relative "autodoc/examples"
9
+
10
+ namespace :autodoc do
11
+ desc "Update all automatically generated documentation"
12
+ task all: [:examples]
13
+
14
+ desc "Sync code snippets in example READMEs with source files"
15
+ task :examples do
16
+ Autodoc::Examples.sync
17
+ end
18
+ end
19
+
20
+ desc "Update all automatically generated documentation"
21
+ task autodoc: "autodoc:all"
@@ -0,0 +1,49 @@
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 "repository"
9
+ require_relative "changelog"
10
+
11
+ # Base class for version bump workflows.
12
+ # Subclasses implement the template methods: prepare, release_on_branch, finalize.
13
+ class BumpWorkflow
14
+ def initialize(gem:, repository: Repository.new)
15
+ @gem = gem
16
+ @repository = repository
17
+ end
18
+
19
+ def call(segment)
20
+ @repository.assert_can_bump!(segment)
21
+ @target = @gem.version.next(segment)
22
+
23
+ prepare(segment)
24
+ release_on_branch
25
+ finalize
26
+ end
27
+
28
+ attr_reader :target
29
+
30
+ private def release_on_branch
31
+ changelog = Changelog.new
32
+ @commit_message = changelog.commit_message(target)
33
+ changelog.release(target)
34
+ @gem.update_version(target)
35
+ generate_ci_manifests
36
+ @repository.commit_all(@commit_message)
37
+ end
38
+
39
+ private def generate_ci_manifests
40
+ Rake::Task["sourcehut:build:manifest"].reenable
41
+ Rake::Task["sourcehut:build"].reenable
42
+ Rake::Task["sourcehut"].reenable
43
+ Rake::Task["sourcehut"].invoke
44
+ end
45
+
46
+ # Template methods for subclasses
47
+ private def prepare(segment) = nil
48
+ private def finalize = nil
49
+ end