ratatui_ruby 0.8.0 → 0.9.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 (352) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +2 -2
  3. data/.builds/ruby-3.3.yml +2 -2
  4. data/.builds/ruby-3.4.yml +2 -2
  5. data/.builds/ruby-4.0.0.yml +2 -2
  6. data/.pre-commit-config.yaml +1 -1
  7. data/AGENTS.md +3 -3
  8. data/CHANGELOG.md +53 -1
  9. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  10. data/LICENSES/MIT-0.txt +16 -0
  11. data/README.md +33 -5
  12. data/Rakefile +1 -1
  13. data/doc/concepts/application_architecture.md +44 -3
  14. data/doc/concepts/application_testing.md +43 -1
  15. data/doc/concepts/async.md +32 -2
  16. data/doc/concepts/custom_widgets.md +247 -0
  17. data/doc/concepts/event_handling.md +32 -3
  18. data/doc/concepts/interactive_design.md +32 -2
  19. data/doc/contributors/auditing/parity.md +7 -1
  20. data/doc/contributors/design/ruby_frontend.md +85 -1
  21. data/doc/contributors/design/rust_backend.md +67 -1
  22. data/doc/contributors/developing_examples.md +56 -2
  23. data/doc/contributors/documentation_style.md +20 -3
  24. data/doc/contributors/future_work.md +169 -0
  25. data/doc/contributors/index.md +1 -1
  26. data/doc/contributors/v1.0.0_blockers.md +15 -175
  27. data/doc/getting_started/quickstart.md +22 -4
  28. data/doc/getting_started/why.md +1 -1
  29. data/doc/index.md +2 -1
  30. data/doc/troubleshooting/debugging.md +32 -2
  31. data/doc/troubleshooting/terminal_limitations.md +8 -2
  32. data/doc/troubleshooting/tui_output.md +42 -0
  33. data/examples/app_all_events/README.md +14 -2
  34. data/examples/app_all_events/app.rb +1 -1
  35. data/examples/app_all_events/model/app_model.rb +1 -1
  36. data/examples/app_all_events/model/event_color_cycle.rb +1 -1
  37. data/examples/app_all_events/model/event_entry.rb +1 -1
  38. data/examples/app_all_events/model/msg.rb +1 -1
  39. data/examples/app_all_events/model/timestamp.rb +1 -1
  40. data/examples/app_all_events/update.rb +1 -1
  41. data/examples/app_all_events/view/app_view.rb +1 -1
  42. data/examples/app_all_events/view/controls_view.rb +1 -1
  43. data/examples/app_all_events/view/counts_view.rb +1 -1
  44. data/examples/app_all_events/view/live_view.rb +1 -1
  45. data/examples/app_all_events/view/log_view.rb +1 -1
  46. data/examples/app_all_events/view.rb +1 -1
  47. data/examples/app_color_picker/README.md +20 -2
  48. data/examples/app_color_picker/app.rb +1 -1
  49. data/examples/app_color_picker/clipboard.rb +1 -1
  50. data/examples/app_color_picker/color.rb +1 -1
  51. data/examples/app_color_picker/controls.rb +1 -1
  52. data/examples/app_color_picker/copy_dialog.rb +1 -1
  53. data/examples/app_color_picker/export_pane.rb +1 -1
  54. data/examples/app_color_picker/harmony.rb +1 -1
  55. data/examples/app_color_picker/input.rb +1 -1
  56. data/examples/app_color_picker/main_container.rb +1 -1
  57. data/examples/app_color_picker/palette.rb +1 -1
  58. data/examples/app_login_form/README.md +8 -2
  59. data/examples/app_login_form/app.rb +1 -1
  60. data/examples/app_stateful_interaction/README.md +2 -2
  61. data/examples/app_stateful_interaction/app.rb +71 -17
  62. data/examples/timeout_demo.rb +1 -1
  63. data/examples/verify_quickstart_dsl/README.md +6 -0
  64. data/examples/verify_quickstart_dsl/app.rb +3 -3
  65. data/examples/verify_quickstart_layout/README.md +6 -0
  66. data/examples/verify_quickstart_layout/app.rb +3 -3
  67. data/examples/verify_quickstart_lifecycle/README.md +6 -0
  68. data/examples/verify_quickstart_lifecycle/app.rb +3 -3
  69. data/examples/verify_readme_usage/README.md +6 -0
  70. data/examples/verify_readme_usage/app.rb +3 -3
  71. data/examples/widget_barchart/README.md +6 -0
  72. data/examples/widget_barchart/app.rb +2 -2
  73. data/examples/widget_block/README.md +7 -1
  74. data/examples/widget_block/app.rb +2 -2
  75. data/examples/widget_box/README.md +6 -0
  76. data/examples/widget_box/app.rb +9 -6
  77. data/examples/widget_calendar/README.md +6 -0
  78. data/examples/widget_calendar/app.rb +2 -2
  79. data/examples/widget_canvas/README.md +4 -0
  80. data/examples/widget_canvas/app.rb +2 -2
  81. data/examples/widget_cell/README.md +6 -0
  82. data/examples/widget_cell/app.rb +2 -3
  83. data/examples/widget_center/README.md +4 -0
  84. data/examples/widget_center/app.rb +2 -2
  85. data/examples/widget_chart/README.md +6 -0
  86. data/examples/widget_chart/app.rb +2 -2
  87. data/examples/widget_gauge/README.md +6 -0
  88. data/examples/widget_gauge/app.rb +2 -2
  89. data/examples/widget_layout_split/README.md +6 -0
  90. data/examples/widget_layout_split/app.rb +3 -3
  91. data/examples/widget_line_gauge/README.md +6 -0
  92. data/examples/widget_line_gauge/app.rb +2 -2
  93. data/examples/widget_list/README.md +6 -0
  94. data/examples/widget_list/app.rb +2 -2
  95. data/examples/widget_map/README.md +8 -2
  96. data/examples/widget_map/app.rb +2 -2
  97. data/examples/widget_overlay/README.md +7 -1
  98. data/examples/widget_overlay/app.rb +2 -2
  99. data/examples/widget_popup/README.md +6 -0
  100. data/examples/widget_popup/app.rb +2 -2
  101. data/examples/widget_ratatui_logo/README.md +6 -0
  102. data/examples/widget_ratatui_logo/app.rb +2 -3
  103. data/examples/widget_ratatui_mascot/README.md +6 -0
  104. data/examples/widget_ratatui_mascot/app.rb +2 -2
  105. data/examples/widget_rect/README.md +12 -0
  106. data/examples/widget_rect/app.rb +40 -26
  107. data/examples/widget_render/README.md +6 -0
  108. data/examples/widget_render/app.rb +2 -2
  109. data/examples/widget_render/app.rbs +41 -0
  110. data/examples/widget_rich_text/README.md +6 -0
  111. data/examples/widget_rich_text/app.rb +2 -2
  112. data/examples/widget_scroll_text/README.md +6 -0
  113. data/examples/widget_scroll_text/app.rb +2 -2
  114. data/examples/widget_scrollbar/README.md +6 -0
  115. data/examples/widget_scrollbar/app.rb +2 -2
  116. data/examples/widget_sparkline/README.md +6 -0
  117. data/examples/widget_sparkline/app.rb +2 -2
  118. data/examples/widget_style_colors/README.md +6 -0
  119. data/examples/widget_style_colors/app.rb +2 -2
  120. data/examples/widget_table/README.md +8 -2
  121. data/examples/widget_table/app.rb +2 -2
  122. data/examples/widget_tabs/README.md +6 -0
  123. data/examples/widget_tabs/app.rb +2 -2
  124. data/examples/widget_text_width/README.md +6 -0
  125. data/examples/widget_text_width/app.rb +4 -4
  126. data/ext/ratatui_ruby/Cargo.lock +1 -1
  127. data/ext/ratatui_ruby/Cargo.toml +1 -1
  128. data/ext/ratatui_ruby/extconf.rb +2 -2
  129. data/ext/ratatui_ruby/src/rendering.rs +1 -1
  130. data/ext/ratatui_ruby/src/style.rs +0 -8
  131. data/ext/ratatui_ruby/src/widgets/chart.rs +0 -118
  132. data/ext/ratatui_ruby/src/widgets/list_state.rs +36 -0
  133. data/lib/ratatui_ruby/buffer/cell.rb +34 -2
  134. data/lib/ratatui_ruby/buffer.rb +2 -2
  135. data/lib/ratatui_ruby/cell.rb +34 -2
  136. data/lib/ratatui_ruby/event/focus_gained.rb +26 -2
  137. data/lib/ratatui_ruby/event/focus_lost.rb +26 -2
  138. data/lib/ratatui_ruby/event/key/character.rb +18 -2
  139. data/lib/ratatui_ruby/event/key/media.rb +2 -2
  140. data/lib/ratatui_ruby/event/key/modifier.rb +10 -2
  141. data/lib/ratatui_ruby/event/key/navigation.rb +2 -2
  142. data/lib/ratatui_ruby/event/key/system.rb +2 -2
  143. data/lib/ratatui_ruby/event/key.rb +114 -2
  144. data/lib/ratatui_ruby/event/mouse.rb +42 -2
  145. data/lib/ratatui_ruby/event/none.rb +10 -2
  146. data/lib/ratatui_ruby/event/paste.rb +34 -2
  147. data/lib/ratatui_ruby/event/resize.rb +34 -2
  148. data/lib/ratatui_ruby/event.rb +26 -2
  149. data/lib/ratatui_ruby/frame.rb +74 -2
  150. data/lib/ratatui_ruby/layout/constraint.rb +58 -2
  151. data/lib/ratatui_ruby/layout/layout.rb +47 -2
  152. data/lib/ratatui_ruby/layout/rect.rb +403 -2
  153. data/lib/ratatui_ruby/layout.rb +2 -2
  154. data/lib/ratatui_ruby/list_state.rb +113 -2
  155. data/lib/ratatui_ruby/output_guard.rb +26 -3
  156. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  157. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +2 -2
  158. data/lib/ratatui_ruby/schema/bar_chart.rb +50 -2
  159. data/lib/ratatui_ruby/schema/block.rb +21 -15
  160. data/lib/ratatui_ruby/schema/calendar.rb +2 -2
  161. data/lib/ratatui_ruby/schema/canvas.rb +10 -2
  162. data/lib/ratatui_ruby/schema/center.rb +10 -2
  163. data/lib/ratatui_ruby/schema/chart.rb +2 -28
  164. data/lib/ratatui_ruby/schema/clear.rb +10 -2
  165. data/lib/ratatui_ruby/schema/constraint.rb +58 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +10 -2
  167. data/lib/ratatui_ruby/schema/draw.rb +10 -2
  168. data/lib/ratatui_ruby/schema/gauge.rb +2 -2
  169. data/lib/ratatui_ruby/schema/layout.rb +18 -2
  170. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  171. data/lib/ratatui_ruby/schema/list.rb +10 -2
  172. data/lib/ratatui_ruby/schema/list_item.rb +10 -2
  173. data/lib/ratatui_ruby/schema/overlay.rb +10 -2
  174. data/lib/ratatui_ruby/schema/paragraph.rb +10 -2
  175. data/lib/ratatui_ruby/schema/ratatui_logo.rb +2 -2
  176. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +2 -2
  177. data/lib/ratatui_ruby/schema/rect.rb +58 -2
  178. data/lib/ratatui_ruby/schema/row.rb +10 -2
  179. data/lib/ratatui_ruby/schema/scrollbar.rb +2 -2
  180. data/lib/ratatui_ruby/schema/shape/label.rb +10 -2
  181. data/lib/ratatui_ruby/schema/sparkline.rb +10 -2
  182. data/lib/ratatui_ruby/schema/style.rb +18 -2
  183. data/lib/ratatui_ruby/schema/table.rb +2 -2
  184. data/lib/ratatui_ruby/schema/tabs.rb +2 -2
  185. data/lib/ratatui_ruby/schema/text.rb +34 -2
  186. data/lib/ratatui_ruby/scrollbar_state.rb +10 -2
  187. data/lib/ratatui_ruby/style/style.rb +18 -2
  188. data/lib/ratatui_ruby/style.rb +2 -2
  189. data/lib/ratatui_ruby/table_state.rb +10 -2
  190. data/lib/ratatui_ruby/terminal_lifecycle.rb +18 -3
  191. data/lib/ratatui_ruby/test_helper/event_injection.rb +34 -2
  192. data/lib/ratatui_ruby/test_helper/snapshot.rb +74 -9
  193. data/lib/ratatui_ruby/test_helper/style_assertions.rb +98 -2
  194. data/lib/ratatui_ruby/test_helper/terminal.rb +50 -2
  195. data/lib/ratatui_ruby/test_helper/test_doubles.rb +18 -2
  196. data/lib/ratatui_ruby/test_helper.rb +10 -2
  197. data/lib/ratatui_ruby/tui/buffer_factories.rb +2 -2
  198. data/lib/ratatui_ruby/tui/canvas_factories.rb +2 -2
  199. data/lib/ratatui_ruby/tui/core.rb +2 -2
  200. data/lib/ratatui_ruby/tui/layout_factories.rb +32 -2
  201. data/lib/ratatui_ruby/tui/state_factories.rb +2 -2
  202. data/lib/ratatui_ruby/tui/style_factories.rb +2 -2
  203. data/lib/ratatui_ruby/tui/text_factories.rb +2 -2
  204. data/lib/ratatui_ruby/tui/widget_factories.rb +2 -2
  205. data/lib/ratatui_ruby/tui.rb +11 -3
  206. data/lib/ratatui_ruby/version.rb +3 -3
  207. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -2
  208. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -2
  209. data/lib/ratatui_ruby/widgets/bar_chart.rb +58 -2
  210. data/lib/ratatui_ruby/widgets/block.rb +37 -15
  211. data/lib/ratatui_ruby/widgets/calendar.rb +2 -2
  212. data/lib/ratatui_ruby/widgets/canvas.rb +10 -2
  213. data/lib/ratatui_ruby/widgets/cell.rb +10 -2
  214. data/lib/ratatui_ruby/widgets/center.rb +10 -2
  215. data/lib/ratatui_ruby/widgets/chart.rb +2 -28
  216. data/lib/ratatui_ruby/widgets/clear.rb +10 -2
  217. data/lib/ratatui_ruby/widgets/cursor.rb +10 -2
  218. data/lib/ratatui_ruby/widgets/gauge.rb +16 -2
  219. data/lib/ratatui_ruby/widgets/line_gauge.rb +16 -2
  220. data/lib/ratatui_ruby/widgets/list.rb +41 -2
  221. data/lib/ratatui_ruby/widgets/list_item.rb +10 -2
  222. data/lib/ratatui_ruby/widgets/overlay.rb +10 -2
  223. data/lib/ratatui_ruby/widgets/paragraph.rb +10 -2
  224. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -2
  225. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -2
  226. data/lib/ratatui_ruby/widgets/row.rb +10 -2
  227. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -2
  228. data/lib/ratatui_ruby/widgets/shape/label.rb +10 -2
  229. data/lib/ratatui_ruby/widgets/sparkline.rb +10 -2
  230. data/lib/ratatui_ruby/widgets/table.rb +62 -2
  231. data/lib/ratatui_ruby/widgets/tabs.rb +2 -2
  232. data/lib/ratatui_ruby/widgets.rb +2 -2
  233. data/lib/ratatui_ruby.rb +90 -2
  234. data/sig/examples/app_all_events/view.rbs +7 -1
  235. data/sig/examples/app_all_events/view_state.rbs +7 -1
  236. data/sig/examples/app_color_picker/app.rbs +5 -0
  237. data/sig/examples/app_stateful_interaction/app.rbs +7 -1
  238. data/sig/examples/verify_quickstart_dsl/app.rbs +7 -1
  239. data/sig/examples/verify_quickstart_lifecycle/app.rbs +7 -1
  240. data/sig/examples/verify_readme_usage/app.rbs +7 -1
  241. data/sig/examples/widget_block_demo/app.rbs +6 -0
  242. data/sig/examples/widget_box_demo/app.rbs +7 -1
  243. data/sig/examples/widget_calendar_demo/app.rbs +7 -1
  244. data/sig/examples/widget_cell_demo/app.rbs +7 -1
  245. data/sig/examples/widget_chart_demo/app.rbs +7 -1
  246. data/sig/examples/widget_gauge_demo/app.rbs +7 -1
  247. data/sig/examples/widget_layout_split/app.rbs +7 -1
  248. data/sig/examples/widget_line_gauge_demo/app.rbs +7 -1
  249. data/sig/examples/widget_list_demo/app.rbs +5 -0
  250. data/sig/examples/widget_map_demo/app.rbs +7 -1
  251. data/sig/examples/widget_popup_demo/app.rbs +7 -1
  252. data/sig/examples/widget_ratatui_logo_demo/app.rbs +7 -1
  253. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +7 -1
  254. data/sig/examples/widget_rect/app.rbs +7 -1
  255. data/sig/examples/widget_render/app.rbs +7 -1
  256. data/sig/examples/widget_rich_text/app.rbs +7 -1
  257. data/sig/examples/widget_scroll_text/app.rbs +7 -1
  258. data/sig/examples/widget_scrollbar_demo/app.rbs +7 -1
  259. data/sig/examples/widget_sparkline_demo/app.rbs +7 -1
  260. data/sig/examples/widget_style_colors/app.rbs +7 -1
  261. data/sig/examples/widget_table_demo/app.rbs +7 -1
  262. data/sig/examples/widget_text_width/app.rbs +7 -1
  263. data/sig/ratatui_ruby/event.rbs +7 -1
  264. data/sig/ratatui_ruby/frame.rbs +15 -3
  265. data/sig/ratatui_ruby/list_state.rbs +11 -1
  266. data/sig/ratatui_ruby/ratatui_ruby.rbs +8 -2
  267. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +7 -1
  268. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +6 -0
  269. data/sig/ratatui_ruby/schema/bar_chart.rbs +6 -0
  270. data/sig/ratatui_ruby/schema/block.rbs +7 -1
  271. data/sig/ratatui_ruby/schema/calendar.rbs +6 -0
  272. data/sig/ratatui_ruby/schema/canvas.rbs +6 -0
  273. data/sig/ratatui_ruby/schema/center.rbs +6 -0
  274. data/sig/ratatui_ruby/schema/chart.rbs +6 -9
  275. data/sig/ratatui_ruby/schema/constraint.rbs +6 -0
  276. data/sig/ratatui_ruby/schema/cursor.rbs +6 -0
  277. data/sig/ratatui_ruby/schema/draw.rbs +6 -0
  278. data/sig/ratatui_ruby/schema/gauge.rbs +9 -1
  279. data/sig/ratatui_ruby/schema/layout.rbs +6 -0
  280. data/sig/ratatui_ruby/schema/line_gauge.rbs +9 -1
  281. data/sig/ratatui_ruby/schema/list.rbs +9 -1
  282. data/sig/ratatui_ruby/schema/list_item.rbs +7 -1
  283. data/sig/ratatui_ruby/schema/overlay.rbs +6 -0
  284. data/sig/ratatui_ruby/schema/paragraph.rbs +6 -0
  285. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +6 -0
  286. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +5 -0
  287. data/sig/ratatui_ruby/schema/rect.rbs +30 -0
  288. data/sig/ratatui_ruby/schema/row.rbs +7 -1
  289. data/sig/ratatui_ruby/schema/scrollbar.rbs +6 -0
  290. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -0
  291. data/sig/ratatui_ruby/schema/style.rbs +7 -1
  292. data/sig/ratatui_ruby/schema/table.rbs +11 -1
  293. data/sig/ratatui_ruby/schema/tabs.rbs +6 -0
  294. data/sig/ratatui_ruby/schema/text.rbs +7 -1
  295. data/sig/ratatui_ruby/scrollbar_state.rbs +7 -1
  296. data/sig/ratatui_ruby/session.rbs +7 -1
  297. data/sig/ratatui_ruby/table_state.rbs +7 -1
  298. data/sig/ratatui_ruby/test_helper/event_injection.rbs +7 -1
  299. data/sig/ratatui_ruby/test_helper/snapshot.rbs +7 -1
  300. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +7 -1
  301. data/sig/ratatui_ruby/test_helper/terminal.rbs +7 -1
  302. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -1
  303. data/sig/ratatui_ruby/test_helper.rbs +7 -1
  304. data/sig/ratatui_ruby/tui/buffer_factories.rbs +7 -1
  305. data/sig/ratatui_ruby/tui/canvas_factories.rbs +7 -1
  306. data/sig/ratatui_ruby/tui/core.rbs +7 -1
  307. data/sig/ratatui_ruby/tui/layout_factories.rbs +7 -1
  308. data/sig/ratatui_ruby/tui/state_factories.rbs +7 -1
  309. data/sig/ratatui_ruby/tui/style_factories.rbs +7 -1
  310. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -1
  311. data/sig/ratatui_ruby/tui/widget_factories.rbs +7 -1
  312. data/sig/ratatui_ruby/tui.rbs +7 -1
  313. data/sig/ratatui_ruby/version.rbs +6 -0
  314. data/tasks/autodoc/examples.rb +1 -1
  315. data/tasks/autodoc/member.rb +1 -1
  316. data/tasks/autodoc/name.rb +1 -1
  317. data/tasks/bump/cargo_lockfile.rb +1 -1
  318. data/tasks/bump/changelog.rb +1 -1
  319. data/tasks/bump/header.rb +1 -1
  320. data/tasks/bump/history.rb +1 -1
  321. data/tasks/bump/links.rb +1 -1
  322. data/tasks/bump/manifest.rb +1 -1
  323. data/tasks/bump/ruby_gem.rb +1 -1
  324. data/tasks/bump/sem_ver.rb +1 -1
  325. data/tasks/bump/unreleased_section.rb +1 -1
  326. data/tasks/license/headers_md.rb +223 -0
  327. data/tasks/license/headers_rb.rb +210 -0
  328. data/tasks/license/license_utils.rb +130 -0
  329. data/tasks/license/snippets_md.rb +315 -0
  330. data/tasks/license/snippets_rdoc.rb +150 -0
  331. data/tasks/license.rake +91 -0
  332. data/tasks/rdoc_config.rb +1 -1
  333. data/tasks/resources/build.yml.erb +13 -7
  334. data/tasks/sourcehut.rake +3 -1
  335. data/tasks/terminal_preview/app_screenshot.rb +1 -1
  336. data/tasks/terminal_preview/crash_report.rb +1 -1
  337. data/tasks/terminal_preview/example_app.rb +1 -1
  338. data/tasks/terminal_preview/launcher_script.rb +1 -1
  339. data/tasks/terminal_preview/preview_collection.rb +1 -1
  340. data/tasks/terminal_preview/preview_timing.rb +1 -1
  341. data/tasks/terminal_preview/safety_confirmation.rb +1 -1
  342. data/tasks/terminal_preview/saved_screenshot.rb +1 -1
  343. data/tasks/terminal_preview/system_appearance.rb +1 -1
  344. data/tasks/terminal_preview/terminal_window.rb +1 -1
  345. data/tasks/terminal_preview/window_id.rb +1 -1
  346. data/tasks/website/index_page.rb +1 -1
  347. data/tasks/website/version.rb +1 -1
  348. data/tasks/website/version_menu.rb +1 -1
  349. data/tasks/website/versioned_documentation.rb +1 -1
  350. data/tasks/website/website.rb +1 -1
  351. metadata +13 -3
  352. data/doc/migration/v0_7_0.md +0 -236
@@ -0,0 +1,315 @@
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
+ # Script to add SPDX snippet headers to fenced code blocks in markdown files.
9
+ #
10
+ # Usage: ruby tasks/license/snippets_md.rb [path...]
11
+ #
12
+ # If no paths are given, processes all .md files via git ls-files.
13
+ #
14
+ # Rules:
15
+ # - Wraps all fenced code blocks (``` or ````) with SPDX snippet headers (MIT-0)
16
+ # - For SYNC:START/SYNC:END blocks, wraps AROUND the sync markers (not inside)
17
+ # - Uses git blame to determine the latest edit year for the code lines
18
+ # - Skips blocks that are already properly wrapped with MIT-0 and Kerrick Long
19
+ # - Removes malformed existing SPDX-Snippet blocks and replaces with correct ones
20
+
21
+ require "open3"
22
+ require "date"
23
+
24
+ COPYRIGHT_HOLDER = "Kerrick Long"
25
+ LICENSE = "MIT-0"
26
+
27
+ # Files to skip entirely (relative paths from repo root)
28
+ EXCLUDED_FILES = [
29
+ "doc/contributors/v1.0.0_blockers.md",
30
+ "doc/contributors/upstream_requests/tab_rects.md",
31
+ "doc/contributors/upstream_requests/title_rects.md",
32
+ ].freeze
33
+
34
+ def get_latest_git_year(file, start_line, end_line)
35
+ cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
36
+ output, _status = Open3.capture2(*cmd)
37
+ years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
38
+ years.empty? ? Date.today.year : years.max
39
+ end
40
+
41
+ def is_our_snippet_header?(lines, idx)
42
+ # Check if the current SPDX-SnippetBegin block already has our copyright/license
43
+ i = idx + 1
44
+ has_our_copyright = false
45
+ has_mit0 = false
46
+
47
+ while i < lines.length && !lines[i].include?("-->")
48
+ line = lines[i]
49
+ has_our_copyright = true if line.include?(COPYRIGHT_HOLDER) && line.include?("SPDX-FileCopyrightText")
50
+ has_mit0 = true if line.include?("MIT-0") && line.include?("SPDX-License-Identifier")
51
+ i += 1
52
+ end
53
+
54
+ has_our_copyright && has_mit0
55
+ end
56
+
57
+ def find_snippet_end(lines, start_idx)
58
+ i = start_idx
59
+ while i < lines.length
60
+ return i if lines[i].include?("SPDX-SnippetEnd")
61
+ i += 1
62
+ end
63
+ nil
64
+ end
65
+
66
+ def process_file(filepath)
67
+ # Skip excluded files
68
+ return if EXCLUDED_FILES.any? { |excluded| filepath.end_with?(excluded) }
69
+
70
+ content = File.read(filepath)
71
+ lines = content.lines
72
+
73
+ # Track code block ranges (to exclude from file header year calculation)
74
+ code_block_ranges = []
75
+ changes = []
76
+ removals = [] # existing malformed SPDX snippet blocks to remove
77
+ i = 0
78
+
79
+ while i < lines.length
80
+ line = lines[i]
81
+
82
+ # Check if we're at an existing SPDX-SnippetBegin
83
+ if line.include?("SPDX-SnippetBegin")
84
+ snippet_start = i
85
+ snippet_end = find_snippet_end(lines, i)
86
+
87
+ if snippet_end
88
+ # Check if this is already our proper snippet
89
+ if is_our_snippet_header?(lines, i)
90
+ # Skip this block entirely - it's already correct
91
+ i = snippet_end + 1
92
+ next
93
+ else
94
+ # Mark for removal - we'll re-wrap the inner content
95
+ removals << { start: snippet_start, end: snippet_end }
96
+ i = snippet_end + 1
97
+ next
98
+ end
99
+ end
100
+ end
101
+
102
+ # Check for SYNC:START pattern
103
+ if line =~ /<!--\s*SYNC:START/
104
+ sync_start_line = i
105
+ j = i + 1
106
+ code_start = nil
107
+ code_end = nil
108
+ sync_end_line = nil
109
+
110
+ while j < lines.length
111
+ if lines[j] =~ /^(````*)(\w*)$/
112
+ if code_start.nil?
113
+ code_start = j
114
+ else
115
+ code_end = j
116
+ end
117
+ elsif lines[j] =~ /<!--\s*SYNC:END/
118
+ sync_end_line = j
119
+ break
120
+ end
121
+ j += 1
122
+ end
123
+
124
+ if code_start && code_end && sync_end_line
125
+ year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
126
+ changes << {
127
+ type: :sync_block,
128
+ start: sync_start_line,
129
+ end: sync_end_line,
130
+ year:,
131
+ }
132
+ code_block_ranges << (code_start..code_end)
133
+ i = sync_end_line + 1
134
+ next
135
+ end
136
+ end
137
+
138
+ # Check for standalone fenced code block
139
+ if line =~ /^(````*)(\w*)$/
140
+ fence_marker = $1
141
+ fence_start = i
142
+ re_end = /^#{Regexp.escape(fence_marker)}$/
143
+
144
+ j = i + 1
145
+ fence_end = nil
146
+ while j < lines.length
147
+ if lines[j] =~ re_end
148
+ fence_end = j
149
+ break
150
+ end
151
+ j += 1
152
+ end
153
+
154
+ if fence_end
155
+ year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
156
+ changes << {
157
+ type: :code_block,
158
+ start: fence_start,
159
+ end: fence_end,
160
+ year:,
161
+ }
162
+ code_block_ranges << (fence_start..fence_end)
163
+ i = fence_end + 1
164
+ next
165
+ end
166
+ end
167
+
168
+ i += 1
169
+ end
170
+
171
+ # Handle removals and additions
172
+ has_changes = !changes.empty? || !removals.empty?
173
+
174
+ # Remove existing malformed SPDX blocks (in reverse order)
175
+ removals.sort_by { |r| -r[:start] }.each do |removal|
176
+ # Remove the SnippetEnd line
177
+ lines.delete_at(removal[:end])
178
+ # Remove lines from SnippetBegin through the --> closing the comment
179
+ close_idx = removal[:start]
180
+ while close_idx < lines.length && !lines[close_idx].include?("-->")
181
+ close_idx += 1
182
+ end
183
+ # Remove from start to close_idx inclusive
184
+ (close_idx - removal[:start] + 1).times { lines.delete_at(removal[:start]) }
185
+ end
186
+
187
+ # Recalculate content after removals
188
+ content = lines.join
189
+ lines = content.lines
190
+
191
+ # Re-scan for code blocks that need wrapping
192
+ changes = []
193
+ i = 0
194
+
195
+ while i < lines.length
196
+ line = lines[i]
197
+
198
+ # Skip if already inside an SPDX-SnippetBegin block
199
+ if line.include?("SPDX-SnippetBegin")
200
+ while i < lines.length && !lines[i].include?("SPDX-SnippetEnd")
201
+ i += 1
202
+ end
203
+ i += 1
204
+ next
205
+ end
206
+
207
+ # Check for SYNC:START pattern
208
+ if line =~ /<!--\s*SYNC:START/
209
+ sync_start_line = i
210
+ j = i + 1
211
+ code_start = nil
212
+ code_end = nil
213
+ sync_end_line = nil
214
+
215
+ while j < lines.length
216
+ if lines[j] =~ /^(````*)(\w*)$/
217
+ if code_start.nil?
218
+ code_start = j
219
+ else
220
+ code_end = j
221
+ end
222
+ elsif lines[j] =~ /<!--\s*SYNC:END/
223
+ sync_end_line = j
224
+ break
225
+ end
226
+ j += 1
227
+ end
228
+
229
+ if code_start && code_end && sync_end_line
230
+ year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
231
+ changes << {
232
+ type: :sync_block,
233
+ start: sync_start_line,
234
+ end: sync_end_line,
235
+ year:,
236
+ }
237
+ i = sync_end_line + 1
238
+ next
239
+ end
240
+ end
241
+
242
+ # Check for standalone fenced code block
243
+ if line =~ /^(````*)(\w*)$/
244
+ fence_marker = $1
245
+ fence_start = i
246
+ re_end = /^#{Regexp.escape(fence_marker)}$/
247
+
248
+ j = i + 1
249
+ fence_end = nil
250
+ while j < lines.length
251
+ if lines[j] =~ re_end
252
+ fence_end = j
253
+ break
254
+ end
255
+ j += 1
256
+ end
257
+
258
+ if fence_end
259
+ year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
260
+ changes << {
261
+ type: :code_block,
262
+ start: fence_start,
263
+ end: fence_end,
264
+ year:,
265
+ }
266
+ i = fence_end + 1
267
+ next
268
+ end
269
+ end
270
+
271
+ i += 1
272
+ end
273
+
274
+ return if changes.empty? && !has_changes
275
+
276
+ # Apply changes in reverse order to preserve line numbers
277
+ changes.sort_by { |c| -c[:start] }.each do |change|
278
+ # REUSE-IgnoreStart
279
+ snippet_begin = "<!-- SPDX-SnippetBegin -->\n<!--\n SPDX-FileCopyrightText: #{change[:year]} #{COPYRIGHT_HOLDER}\n SPDX-License-Identifier: #{LICENSE}\n-->\n"
280
+ snippet_end = "<!-- SPDX-SnippetEnd -->\n"
281
+ # REUSE-IgnoreEnd
282
+
283
+ # Insert end marker after the block
284
+ lines.insert(change[:end] + 1, snippet_end)
285
+ # Insert begin marker before the block
286
+ lines.insert(change[:start], snippet_begin)
287
+ end
288
+
289
+ File.write(filepath, lines.join)
290
+ puts "Updated: #{filepath} (#{changes.length} code block(s))"
291
+ end
292
+
293
+ def find_md_files(paths)
294
+ # Use git ls-files to respect .gitignore
295
+ if paths.empty?
296
+ `git ls-files '*.md'`.split("\n")
297
+ else
298
+ paths.flat_map do |path|
299
+ if File.directory?(path)
300
+ `git ls-files '#{path}/**/*.md'`.split("\n")
301
+ else
302
+ path
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ if __FILE__ == $0
309
+ paths = ARGV.empty? ? [] : ARGV
310
+ files = find_md_files(paths)
311
+
312
+ files.each do |file|
313
+ process_file(file)
314
+ end
315
+ end
@@ -0,0 +1,150 @@
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
+ # Script to add SPDX snippet headers to RDoc code examples in Ruby files.
9
+ #
10
+ # Usage: ruby scripts/add_spdx_rdoc_snippets.rb [path...]
11
+ #
12
+ # If no paths are given, processes all .rb files via git ls-files.
13
+ #
14
+ # Rules:
15
+ # - Wraps RDoc code examples (indented comment lines) with SPDX snippet headers
16
+ # - Uses #-- and #++ to hide the SPDX headers from RDoc rendering
17
+ # - Uses git blame to determine the latest edit year for the code lines
18
+ # - Skips blocks that are already wrapped with SPDX-SnippetBegin
19
+
20
+ require "open3"
21
+ require "date"
22
+
23
+ COPYRIGHT_HOLDER = "Kerrick Long"
24
+ LICENSE = "MIT-0"
25
+
26
+ def get_latest_git_year(file, start_line, end_line)
27
+ cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
28
+ output, _status = Open3.capture2(*cmd)
29
+ years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
30
+ years.empty? ? Date.today.year : years.max
31
+ end
32
+
33
+ def find_rdoc_code_blocks(lines)
34
+ # Find all RDoc code blocks (indented comment lines)
35
+ # Returns array of {start:, end:, indent:} where indent is the comment prefix
36
+ blocks = []
37
+ i = 0
38
+
39
+ while i < lines.length
40
+ line = lines[i]
41
+
42
+ # Check if this is an indented code line in a comment
43
+ # Pattern: optional leading whitespace, #, then 3+ spaces (RDoc code indent)
44
+ if line =~ /^(\s*)#( +)(\S.*)$/
45
+ prefix = $1 # leading whitespace before #
46
+ block_start = i
47
+
48
+ # Find the extent of this code block
49
+ j = i
50
+ while j < lines.length
51
+ current = lines[j]
52
+ # Code block continues if line is indented code OR empty comment line
53
+ if current =~ /^#{Regexp.escape(prefix)}#( +|\s*$)/
54
+ j += 1
55
+ else
56
+ break
57
+ end
58
+ end
59
+
60
+ block_end = j - 1
61
+
62
+ # Only count as a block if it has actual code (not just empty lines)
63
+ has_code = (block_start..block_end).any? { |k| lines[k] =~ /^#{Regexp.escape(prefix)}# +\S/ }
64
+
65
+ if has_code && block_end > block_start
66
+ blocks << { start: block_start, end: block_end, prefix: }
67
+ end
68
+
69
+ i = j
70
+ else
71
+ i += 1
72
+ end
73
+ end
74
+
75
+ blocks
76
+ end
77
+
78
+ def is_already_wrapped?(lines, block_start, prefix)
79
+ # Check if the line before the block is #++ (meaning it's already wrapped)
80
+ return false if block_start < 1
81
+
82
+ prev_line = lines[block_start - 1]
83
+ prev_line =~ /^#{Regexp.escape(prefix)}#\+\+\s*$/
84
+ end
85
+
86
+ def process_file(filepath)
87
+ content = File.read(filepath)
88
+ lines = content.lines
89
+
90
+ blocks = find_rdoc_code_blocks(lines)
91
+
92
+ # Filter out already-wrapped blocks
93
+ blocks.reject! { |b| is_already_wrapped?(lines, b[:start], b[:prefix]) }
94
+
95
+ return if blocks.empty?
96
+
97
+ # Apply changes in reverse order to preserve line numbers
98
+ blocks.sort_by { |b| -b[:start] }.each do |block|
99
+ year = get_latest_git_year(filepath, block[:start] + 1, block[:end] + 1)
100
+ prefix = block[:prefix]
101
+
102
+ # Build the wrapper lines
103
+ # REUSE-IgnoreStart
104
+ begin_wrapper = [
105
+ "#{prefix}#--\n",
106
+ "#{prefix}# SPDX-SnippetBegin\n",
107
+ "#{prefix}# SPDX-FileCopyrightText: #{year} #{COPYRIGHT_HOLDER}\n",
108
+ "#{prefix}# SPDX-License-Identifier: #{LICENSE}\n",
109
+ "#{prefix}#++\n",
110
+ ]
111
+
112
+ end_wrapper = [
113
+ "#{prefix}#--\n",
114
+ "#{prefix}# SPDX-SnippetEnd\n",
115
+ "#{prefix}#++\n",
116
+ ]
117
+ # REUSE-IgnoreEnd
118
+
119
+ # Insert end wrapper after the block
120
+ lines.insert(block[:end] + 1, *end_wrapper)
121
+ # Insert begin wrapper before the block
122
+ lines.insert(block[:start], *begin_wrapper)
123
+ end
124
+
125
+ File.write(filepath, lines.join)
126
+ puts "Updated: #{filepath} (#{blocks.length} code block(s))"
127
+ end
128
+
129
+ def find_rb_files(paths)
130
+ if paths.empty?
131
+ `git ls-files '*.rb'`.split("\n")
132
+ else
133
+ paths.flat_map do |path|
134
+ if File.directory?(path)
135
+ `git ls-files '#{path}/**/*.rb'`.split("\n")
136
+ else
137
+ path
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ if __FILE__ == $0
144
+ paths = ARGV.empty? ? [] : ARGV
145
+ files = find_rb_files(paths)
146
+
147
+ files.each do |file|
148
+ process_file(file)
149
+ end
150
+ end
@@ -0,0 +1,91 @@
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
+ namespace :license do
9
+ namespace :headers do
10
+ desc "Ensure markdown files have correct CC-BY-SA-4.0 headers"
11
+ task :md, [:files] do |_t, args|
12
+ files = args[:files] || ""
13
+ ruby "tasks/license/headers_md.rb #{files}"
14
+ end
15
+
16
+ desc "Ensure Ruby files have correct AGPL-3.0-or-later headers"
17
+ task :rb, [:files] do |_t, args|
18
+ files = args[:files] || ""
19
+ ruby "tasks/license/headers_rb.rb #{files}"
20
+ end
21
+
22
+ desc "Ensure all files have correct license headers"
23
+ task :all do
24
+ Rake::Task["license:headers:md"].invoke
25
+ Rake::Task["license:headers:rb"].invoke
26
+ end
27
+ end
28
+
29
+ namespace :snippets do
30
+ desc "Add MIT-0 SPDX snippet headers to markdown fenced code blocks"
31
+ task :md, [:files] do |_t, args|
32
+ files = args[:files] || ""
33
+ ruby "tasks/license/snippets_md.rb #{files}"
34
+ end
35
+
36
+ desc "Add MIT-0 SPDX snippet headers to RDoc code examples in Ruby files"
37
+ task :rdoc, [:files] do |_t, args|
38
+ files = args[:files] || "lib/"
39
+ ruby "tasks/license/snippets_rdoc.rb #{files}"
40
+ end
41
+
42
+ desc "Add MIT-0 SPDX snippet headers to all code examples"
43
+ task :all do
44
+ Rake::Task["license:snippets:md"].invoke
45
+ Rake::Task["license:snippets:rdoc"].invoke
46
+ end
47
+ end
48
+
49
+ desc "Run all license tasks (headers + snippets)"
50
+ task all: ["headers:all", "snippets:all"]
51
+
52
+ desc "Run license tasks on changed files only (staged + unstaged)"
53
+ task :new do
54
+ # Get changed .md and .rb files (staged and unstaged)
55
+ changed_md = `git diff --name-only --diff-filter=ACMR HEAD -- '*.md' 2>/dev/null`.split("\n")
56
+ staged_md = `git diff --name-only --cached --diff-filter=ACMR -- '*.md' 2>/dev/null`.split("\n")
57
+ changed_rb = `git diff --name-only --diff-filter=ACMR HEAD -- '*.rb' 2>/dev/null`.split("\n")
58
+ staged_rb = `git diff --name-only --cached --diff-filter=ACMR -- '*.rb' 2>/dev/null`.split("\n")
59
+
60
+ # Also get untracked new files
61
+ untracked = `git ls-files --others --exclude-standard`.split("\n")
62
+ untracked_md = untracked.select { |f| f.end_with?(".md") }
63
+ untracked_rb = untracked.select { |f| f.end_with?(".rb") }
64
+
65
+ md_files = (changed_md + staged_md + untracked_md).uniq.join(" ")
66
+ rb_files = (changed_rb + staged_rb + untracked_rb).uniq
67
+
68
+ # Filter rb files to only lib/
69
+ lib_rb_files = rb_files.select { |f| f.start_with?("lib/") }.join(" ")
70
+
71
+ if md_files.empty? && lib_rb_files.empty?
72
+ puts "No changed .md or lib/*.rb files to process"
73
+ else
74
+ unless md_files.empty?
75
+ puts "Processing #{md_files.split.count} changed .md file(s)..."
76
+ Rake::Task["license:headers:md"].invoke(md_files)
77
+ Rake::Task["license:headers:md"].reenable
78
+ Rake::Task["license:snippets:md"].invoke(md_files)
79
+ Rake::Task["license:snippets:md"].reenable
80
+ end
81
+
82
+ unless lib_rb_files.empty?
83
+ puts "Processing #{lib_rb_files.split.count} changed lib/*.rb file(s)..."
84
+ Rake::Task["license:headers:rb"].invoke(lib_rb_files)
85
+ Rake::Task["license:headers:rb"].reenable
86
+ Rake::Task["license:snippets:rdoc"].invoke(lib_rb_files)
87
+ Rake::Task["license:snippets:rdoc"].reenable
88
+ end
89
+ end
90
+ end
91
+ end
data/tasks/rdoc_config.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
2
  # SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  image: archlinux
@@ -13,12 +13,14 @@ packages:
13
13
  - gdbm
14
14
  - ncurses
15
15
  - libffi
16
+ <%- if has_rust -%>
16
17
  - clang
18
+ <%- end -%>
17
19
  - git
18
20
  artifacts:
19
- - ratatui_ruby/pkg/<%= gem_filename %>
21
+ - <%= gem_name %>/pkg/<%= gem_filename %>
20
22
  sources:
21
- - https://git.sr.ht/~kerrick/ratatui_ruby
23
+ - https://git.sr.ht/~kerrick/<%= gem_name %>
22
24
  tasks:
23
25
  - setup: |
24
26
  curl https://mise.jdx.dev/install.sh | sh
@@ -26,10 +28,12 @@ tasks:
26
28
  echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
27
29
  echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
28
30
  echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
31
+ <%- if has_rust -%>
29
32
  echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
33
+ <%- end -%>
30
34
  . ~/.buildenv
31
35
  export CI="true"
32
- cd ratatui_ruby
36
+ cd <%= gem_name %>
33
37
  sed -i 's/ruby = .*/ruby = "<%= ruby_version %>"/' mise.toml
34
38
  mise install
35
39
  mise x -- pip install reuse
@@ -37,18 +41,20 @@ tasks:
37
41
  mise reshim
38
42
  mise x -- bundle config set --local frozen 'true'
39
43
  mise x -- bundle install
44
+ <%- if has_rust -%>
40
45
  mise x -- bundle exec rake compile
46
+ <%- end -%>
41
47
  - test: |
42
48
  . ~/.buildenv
43
- cd ratatui_ruby
49
+ cd <%= gem_name %>
44
50
  echo "Testing Ruby <%= ruby_version %>"
45
51
  mise x -- bundle exec rake test
46
52
  - lint: |
47
53
  . ~/.buildenv
48
- cd ratatui_ruby
54
+ cd <%= gem_name %>
49
55
  echo "Linting Ruby <%= ruby_version %>"
50
56
  mise x -- bundle exec rake lint
51
57
  - package: |
52
58
  . ~/.buildenv
53
- cd ratatui_ruby
59
+ cd <%= gem_name %>
54
60
  mise x -- bundle exec rake build
data/tasks/sourcehut.rake CHANGED
@@ -42,7 +42,9 @@ namespace :sourcehut do
42
42
  rubies.each do |ruby_version|
43
43
  filename = ".builds/ruby-#{ruby_version}.yml"
44
44
  puts "Generating #{filename}..."
45
- content = erb.result_with_hash(ruby_version:, gem_filename:, bundler_version:)
45
+ gem_name = spec.name
46
+ has_rust = File.exist?("ext/#{gem_name}/Cargo.toml")
47
+ content = erb.result_with_hash(ruby_version:, gem_name:, gem_filename:, bundler_version:, has_rust:)
46
48
  File.write(filename, content)
47
49
  end
48
50
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
6
  #++
7
7