ratatui_ruby 0.3.1 → 0.5.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 (350) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +89 -132
  7. data/CHANGELOG.md +223 -1
  8. data/README.md +23 -16
  9. data/REUSE.toml +20 -0
  10. data/doc/application_architecture.md +176 -0
  11. data/doc/application_testing.md +17 -10
  12. data/doc/contributors/design/ruby_frontend.md +11 -7
  13. data/doc/contributors/developing_examples.md +261 -0
  14. data/doc/contributors/documentation_style.md +104 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/index.md +2 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +125 -0
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_analytics.png +0 -0
  21. data/doc/images/app_color_picker.png +0 -0
  22. data/doc/images/app_custom_widget.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_map_demo.png +0 -0
  25. data/doc/images/app_mouse_events.png +0 -0
  26. data/doc/images/app_table_select.png +0 -0
  27. data/doc/images/verify_quickstart_dsl.png +0 -0
  28. data/doc/images/verify_quickstart_layout.png +0 -0
  29. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  30. data/doc/images/verify_readme_usage.png +0 -0
  31. data/doc/images/widget_barchart_demo.png +0 -0
  32. data/doc/images/widget_block_padding.png +0 -0
  33. data/doc/images/widget_block_titles.png +0 -0
  34. data/doc/images/widget_box_demo.png +0 -0
  35. data/doc/images/widget_calendar_demo.png +0 -0
  36. data/doc/images/widget_cell_demo.png +0 -0
  37. data/doc/images/widget_chart_demo.png +0 -0
  38. data/doc/images/widget_gauge_demo.png +0 -0
  39. data/doc/images/widget_layout_split.png +0 -0
  40. data/doc/images/widget_line_gauge_demo.png +0 -0
  41. data/doc/images/widget_list_demo.png +0 -0
  42. data/doc/images/widget_list_styles.png +0 -0
  43. data/doc/images/widget_popup_demo.png +0 -0
  44. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  45. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  46. data/doc/images/widget_rect.png +0 -0
  47. data/doc/images/widget_render.png +0 -0
  48. data/doc/images/widget_rich_text.png +0 -0
  49. data/doc/images/widget_scroll_text.png +0 -0
  50. data/doc/images/widget_scrollbar_demo.png +0 -0
  51. data/doc/images/widget_sparkline_demo.png +0 -0
  52. data/doc/images/widget_style_colors.png +0 -0
  53. data/doc/images/widget_table_flex.png +0 -0
  54. data/doc/images/widget_tabs_demo.png +0 -0
  55. data/doc/index.md +1 -0
  56. data/doc/interactive_design.md +116 -0
  57. data/doc/quickstart.md +186 -84
  58. data/examples/app_all_events/README.md +81 -0
  59. data/examples/app_all_events/app.rb +93 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +75 -0
  62. data/examples/app_all_events/model/events.rb +180 -0
  63. data/examples/app_all_events/model/highlight.rb +57 -0
  64. data/examples/app_all_events/model/timestamp.rb +54 -0
  65. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +24 -0
  66. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +24 -0
  67. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +24 -0
  68. data/examples/app_all_events/test/snapshots/after_key_a.txt +24 -0
  69. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +24 -0
  70. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +24 -0
  71. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +24 -0
  72. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +24 -0
  73. data/examples/app_all_events/test/snapshots/after_paste.txt +24 -0
  74. data/examples/app_all_events/test/snapshots/after_resize.txt +24 -0
  75. data/examples/app_all_events/test/snapshots/after_right_click.txt +24 -0
  76. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +24 -0
  77. data/examples/app_all_events/test/snapshots/initial_state.txt +24 -0
  78. data/examples/app_all_events/view/app_view.rb +78 -0
  79. data/examples/app_all_events/view/controls_view.rb +50 -0
  80. data/examples/app_all_events/view/counts_view.rb +55 -0
  81. data/examples/app_all_events/view/live_view.rb +69 -0
  82. data/examples/app_all_events/view/log_view.rb +60 -0
  83. data/{lib/ratatui_ruby/output.rb → examples/app_all_events/view.rb} +1 -1
  84. data/examples/app_all_events/view_state.rb +42 -0
  85. data/examples/app_color_picker/README.md +94 -0
  86. data/examples/app_color_picker/app.rb +112 -0
  87. data/examples/app_color_picker/clipboard.rb +84 -0
  88. data/examples/app_color_picker/color.rb +191 -0
  89. data/examples/app_color_picker/copy_dialog.rb +170 -0
  90. data/examples/app_color_picker/harmony.rb +56 -0
  91. data/examples/app_color_picker/input.rb +142 -0
  92. data/examples/app_color_picker/palette.rb +80 -0
  93. data/examples/app_color_picker/scene.rb +201 -0
  94. data/examples/app_login_form/app.rb +108 -0
  95. data/examples/app_map_demo/app.rb +93 -0
  96. data/examples/app_table_select/app.rb +201 -0
  97. data/examples/verify_quickstart_dsl/app.rb +45 -0
  98. data/examples/verify_quickstart_layout/app.rb +69 -0
  99. data/examples/verify_quickstart_lifecycle/app.rb +48 -0
  100. data/examples/verify_readme_usage/app.rb +34 -0
  101. data/examples/widget_barchart_demo/app.rb +238 -0
  102. data/examples/widget_block_padding/app.rb +67 -0
  103. data/examples/widget_block_titles/app.rb +69 -0
  104. data/examples/widget_box_demo/app.rb +250 -0
  105. data/examples/widget_calendar_demo/app.rb +109 -0
  106. data/examples/widget_cell_demo/app.rb +104 -0
  107. data/examples/widget_chart_demo/app.rb +213 -0
  108. data/examples/widget_gauge_demo/app.rb +212 -0
  109. data/examples/widget_layout_split/app.rb +246 -0
  110. data/examples/widget_line_gauge_demo/app.rb +217 -0
  111. data/examples/widget_list_demo/app.rb +382 -0
  112. data/examples/widget_list_styles/app.rb +141 -0
  113. data/examples/widget_popup_demo/app.rb +104 -0
  114. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  115. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  116. data/examples/widget_rect/app.rb +205 -0
  117. data/examples/widget_render/app.rb +184 -0
  118. data/examples/widget_rich_text/app.rb +137 -0
  119. data/examples/widget_scroll_text/app.rb +108 -0
  120. data/examples/widget_scrollbar_demo/app.rb +153 -0
  121. data/examples/widget_sparkline_demo/app.rb +274 -0
  122. data/examples/widget_style_colors/app.rb +102 -0
  123. data/examples/widget_table_flex/app.rb +95 -0
  124. data/examples/widget_tabs_demo/app.rb +167 -0
  125. data/ext/ratatui_ruby/Cargo.lock +889 -115
  126. data/ext/ratatui_ruby/Cargo.toml +4 -3
  127. data/ext/ratatui_ruby/clippy.toml +7 -0
  128. data/ext/ratatui_ruby/extconf.rb +7 -0
  129. data/ext/ratatui_ruby/src/events.rs +293 -219
  130. data/ext/ratatui_ruby/src/frame.rs +115 -0
  131. data/ext/ratatui_ruby/src/lib.rs +105 -24
  132. data/ext/ratatui_ruby/src/rendering.rs +94 -10
  133. data/ext/ratatui_ruby/src/style.rs +357 -93
  134. data/ext/ratatui_ruby/src/terminal.rs +121 -31
  135. data/ext/ratatui_ruby/src/text.rs +178 -0
  136. data/ext/ratatui_ruby/src/widgets/barchart.rs +99 -24
  137. data/ext/ratatui_ruby/src/widgets/block.rs +32 -3
  138. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  139. data/ext/ratatui_ruby/src/widgets/canvas.rs +44 -9
  140. data/ext/ratatui_ruby/src/widgets/chart.rs +79 -27
  141. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  142. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  143. data/ext/ratatui_ruby/src/widgets/layout.rs +223 -15
  144. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  145. data/ext/ratatui_ruby/src/widgets/list.rs +114 -11
  146. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  147. data/ext/ratatui_ruby/src/widgets/overlay.rs +4 -2
  148. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  149. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  150. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +51 -0
  151. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +61 -7
  152. data/ext/ratatui_ruby/src/widgets/sparkline.rs +73 -6
  153. data/ext/ratatui_ruby/src/widgets/table.rs +177 -64
  154. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  155. data/lib/ratatui_ruby/cell.rb +166 -0
  156. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  157. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  158. data/lib/ratatui_ruby/event/key.rb +211 -0
  159. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  160. data/lib/ratatui_ruby/event/none.rb +43 -0
  161. data/lib/ratatui_ruby/event/paste.rb +71 -0
  162. data/lib/ratatui_ruby/event/resize.rb +80 -0
  163. data/lib/ratatui_ruby/event.rb +131 -0
  164. data/lib/ratatui_ruby/frame.rb +87 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  166. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +23 -0
  167. data/lib/ratatui_ruby/schema/bar_chart.rb +226 -17
  168. data/lib/ratatui_ruby/schema/block.rb +178 -11
  169. data/lib/ratatui_ruby/schema/calendar.rb +70 -14
  170. data/lib/ratatui_ruby/schema/canvas.rb +213 -46
  171. data/lib/ratatui_ruby/schema/center.rb +46 -8
  172. data/lib/ratatui_ruby/schema/chart.rb +134 -32
  173. data/lib/ratatui_ruby/schema/clear.rb +22 -53
  174. data/lib/ratatui_ruby/schema/constraint.rb +72 -12
  175. data/lib/ratatui_ruby/schema/cursor.rb +23 -5
  176. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  177. data/lib/ratatui_ruby/schema/gauge.rb +56 -12
  178. data/lib/ratatui_ruby/schema/layout.rb +91 -9
  179. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  180. data/lib/ratatui_ruby/schema/list.rb +92 -16
  181. data/lib/ratatui_ruby/schema/overlay.rb +29 -3
  182. data/lib/ratatui_ruby/schema/paragraph.rb +82 -25
  183. data/lib/ratatui_ruby/schema/ratatui_logo.rb +29 -0
  184. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +34 -0
  185. data/lib/ratatui_ruby/schema/rect.rb +59 -10
  186. data/lib/ratatui_ruby/schema/scrollbar.rb +127 -19
  187. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  188. data/lib/ratatui_ruby/schema/sparkline.rb +120 -12
  189. data/lib/ratatui_ruby/schema/style.rb +39 -11
  190. data/lib/ratatui_ruby/schema/table.rb +109 -18
  191. data/lib/ratatui_ruby/schema/tabs.rb +71 -10
  192. data/lib/ratatui_ruby/schema/text.rb +90 -0
  193. data/lib/ratatui_ruby/session/autodoc.rb +417 -0
  194. data/lib/ratatui_ruby/session.rb +163 -0
  195. data/lib/ratatui_ruby/test_helper.rb +322 -13
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby.rb +184 -38
  198. data/sig/examples/app_all_events/app.rbs +11 -0
  199. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  200. data/sig/examples/app_all_events/model/events.rbs +15 -0
  201. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  202. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  203. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  204. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  205. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  206. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  207. data/sig/examples/app_all_events/view.rbs +8 -0
  208. data/sig/examples/app_all_events/view_state.rbs +15 -0
  209. data/sig/examples/app_color_picker/app.rbs +12 -0
  210. data/sig/examples/app_login_form/app.rbs +11 -0
  211. data/sig/examples/app_map_demo/app.rbs +11 -0
  212. data/sig/examples/app_table_select/app.rbs +11 -0
  213. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  214. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  215. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  216. data/sig/examples/widget_block_padding/app.rbs +11 -0
  217. data/sig/examples/widget_block_titles/app.rbs +11 -0
  218. data/sig/examples/widget_box_demo/app.rbs +11 -0
  219. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  220. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  221. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  222. data/sig/examples/widget_gauge_demo/app.rbs +11 -0
  223. data/sig/examples/widget_layout_split/app.rbs +10 -0
  224. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  225. data/sig/examples/widget_list_demo/app.rbs +12 -0
  226. data/sig/examples/widget_list_styles/app.rbs +11 -0
  227. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  228. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  229. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  230. data/sig/examples/widget_rect/app.rbs +12 -0
  231. data/sig/examples/widget_render/app.rbs +10 -0
  232. data/sig/examples/widget_rich_text/app.rbs +11 -0
  233. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  234. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  235. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  236. data/sig/examples/widget_style_colors/app.rbs +14 -0
  237. data/sig/examples/widget_table_flex/app.rbs +11 -0
  238. data/sig/ratatui_ruby/event.rbs +69 -0
  239. data/sig/ratatui_ruby/frame.rbs +9 -0
  240. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -3
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  244. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  245. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  246. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  247. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  248. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  249. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  250. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  251. data/sig/ratatui_ruby/schema/draw.rbs +27 -0
  252. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  253. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  255. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  256. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  257. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +8 -0
  258. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  259. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  260. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  261. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  262. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  263. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  264. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  265. data/sig/ratatui_ruby/session.rbs +94 -0
  266. data/tasks/autodoc/inventory.rb +61 -0
  267. data/tasks/autodoc/member.rb +56 -0
  268. data/tasks/autodoc/name.rb +19 -0
  269. data/tasks/autodoc/notice.rb +26 -0
  270. data/tasks/autodoc/rbs.rb +38 -0
  271. data/tasks/autodoc/rdoc.rb +45 -0
  272. data/tasks/autodoc.rake +47 -0
  273. data/tasks/bump/history.rb +2 -2
  274. data/tasks/doc.rake +600 -6
  275. data/tasks/example_viewer.html.erb +172 -0
  276. data/tasks/lint.rake +8 -4
  277. data/tasks/resources/build.yml.erb +13 -11
  278. data/tasks/resources/index.html.erb +6 -0
  279. data/tasks/sourcehut.rake +4 -4
  280. data/tasks/terminal_preview/app_screenshot.rb +33 -0
  281. data/tasks/terminal_preview/crash_report.rb +52 -0
  282. data/tasks/terminal_preview/example_app.rb +25 -0
  283. data/tasks/terminal_preview/launcher_script.rb +46 -0
  284. data/tasks/terminal_preview/preview_collection.rb +58 -0
  285. data/tasks/terminal_preview/preview_timing.rb +22 -0
  286. data/tasks/terminal_preview/safety_confirmation.rb +56 -0
  287. data/tasks/terminal_preview/saved_screenshot.rb +53 -0
  288. data/tasks/terminal_preview/system_appearance.rb +11 -0
  289. data/tasks/terminal_preview/terminal_window.rb +136 -0
  290. data/tasks/terminal_preview/window_id.rb +14 -0
  291. data/tasks/terminal_preview.rake +28 -0
  292. data/tasks/test.rake +2 -2
  293. data/tasks/website/index_page.rb +3 -3
  294. data/tasks/website/version.rb +10 -10
  295. data/tasks/website/version_menu.rb +10 -12
  296. data/tasks/website/versioned_documentation.rb +49 -17
  297. data/tasks/website/website.rb +6 -8
  298. data/tasks/website.rake +4 -4
  299. metadata +206 -54
  300. data/LICENSES/BSD-2-Clause.txt +0 -9
  301. data/doc/images/examples-analytics.rb.png +0 -0
  302. data/doc/images/examples-box_demo.rb.png +0 -0
  303. data/doc/images/examples-calendar_demo.rb.png +0 -0
  304. data/doc/images/examples-chart_demo.rb.png +0 -0
  305. data/doc/images/examples-custom_widget.rb.png +0 -0
  306. data/doc/images/examples-dashboard.rb.png +0 -0
  307. data/doc/images/examples-list_styles.rb.png +0 -0
  308. data/doc/images/examples-login_form.rb.png +0 -0
  309. data/doc/images/examples-map_demo.rb.png +0 -0
  310. data/doc/images/examples-mouse_events.rb.png +0 -0
  311. data/doc/images/examples-popup_demo.rb.gif +0 -0
  312. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  313. data/doc/images/examples-scroll_text.rb.png +0 -0
  314. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  315. data/doc/images/examples-stock_ticker.rb.png +0 -0
  316. data/doc/images/examples-system_monitor.rb.png +0 -0
  317. data/doc/images/examples-table_select.rb.png +0 -0
  318. data/examples/analytics.rb +0 -88
  319. data/examples/box_demo.rb +0 -71
  320. data/examples/calendar_demo.rb +0 -55
  321. data/examples/chart_demo.rb +0 -84
  322. data/examples/custom_widget.rb +0 -43
  323. data/examples/dashboard.rb +0 -72
  324. data/examples/list_styles.rb +0 -66
  325. data/examples/login_form.rb +0 -115
  326. data/examples/map_demo.rb +0 -58
  327. data/examples/mouse_events.rb +0 -95
  328. data/examples/popup_demo.rb +0 -105
  329. data/examples/quickstart_dsl.rb +0 -30
  330. data/examples/quickstart_lifecycle.rb +0 -40
  331. data/examples/readme_usage.rb +0 -21
  332. data/examples/scroll_text.rb +0 -74
  333. data/examples/scrollbar_demo.rb +0 -75
  334. data/examples/stock_ticker.rb +0 -93
  335. data/examples/system_monitor.rb +0 -94
  336. data/examples/table_select.rb +0 -70
  337. data/examples/test_analytics.rb +0 -65
  338. data/examples/test_box_demo.rb +0 -38
  339. data/examples/test_calendar_demo.rb +0 -66
  340. data/examples/test_dashboard.rb +0 -38
  341. data/examples/test_list_styles.rb +0 -61
  342. data/examples/test_login_form.rb +0 -63
  343. data/examples/test_map_demo.rb +0 -100
  344. data/examples/test_popup_demo.rb +0 -62
  345. data/examples/test_scroll_text.rb +0 -130
  346. data/examples/test_stock_ticker.rb +0 -39
  347. data/examples/test_system_monitor.rb +0 -40
  348. data/examples/test_table_select.rb +0 -37
  349. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  350. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "window_id"
7
+ require_relative "preview_timing"
8
+
9
+ class TerminalWindow
10
+ CTRL_C = "ASCII character 3"
11
+
12
+ def initialize(launcher_script_path, pid_file)
13
+ @launcher_script_path = launcher_script_path
14
+ @pid_file = pid_file
15
+ @window_id = nil
16
+ end
17
+
18
+ def open
19
+ setup_script = <<~APPLESCRIPT
20
+ tell application "Terminal"
21
+ set newTab to do script "#{@launcher_script_path}"
22
+ set currentWindow to window 1
23
+
24
+ set number of rows of currentWindow to 24
25
+ set number of columns of currentWindow to 80
26
+ set position of currentWindow to {100, 100}
27
+ set frontmost of currentWindow to true
28
+
29
+ return id of currentWindow
30
+ end tell
31
+ APPLESCRIPT
32
+
33
+ @window_id = WindowID.new(`osascript -e '#{setup_script}'`.strip)
34
+ wait_for_startup
35
+ yield self
36
+ ensure
37
+ close if @window_id
38
+ end
39
+
40
+ def window_id
41
+ @window_id
42
+ end
43
+
44
+ private def close
45
+ try_graceful_shutdown
46
+ kill_process if process_still_alive?
47
+
48
+ delay_script = <<~APPLESCRIPT
49
+ tell application "Terminal"
50
+ delay #{PreviewTiming.close_delay}
51
+
52
+ try
53
+ close window id #{@window_id}
54
+ end try
55
+ end tell
56
+ APPLESCRIPT
57
+
58
+ system("osascript", "-e", delay_script, out: File::NULL, err: File::NULL)
59
+ end
60
+
61
+ private def wait_for_startup
62
+ sleep PreviewTiming.window_startup
63
+
64
+ unless @window_id.valid?
65
+ raise "Failed to open terminal window"
66
+ end
67
+
68
+ unless process_running?
69
+ error_output = contents
70
+ raise error_output
71
+ end
72
+ end
73
+
74
+ private def try_graceful_shutdown
75
+ shutdown_script = <<~APPLESCRIPT
76
+ tell application "Terminal"
77
+ try
78
+ do script (#{CTRL_C}) in window id #{@window_id}
79
+ end try
80
+ end tell
81
+ APPLESCRIPT
82
+
83
+ system("osascript", "-e", shutdown_script, out: File::NULL, err: File::NULL)
84
+ sleep 0.2
85
+ end
86
+
87
+ private def process_still_alive?
88
+ return false unless @pid_file && File.exist?(@pid_file)
89
+
90
+ pid = File.read(@pid_file).strip.to_i
91
+ Process.kill(0, pid)
92
+ true
93
+ rescue Errno::ESRCH, Errno::ENOENT
94
+ false
95
+ end
96
+
97
+ private def kill_process
98
+ return unless @pid_file && File.exist?(@pid_file)
99
+
100
+ pid = File.read(@pid_file).strip.to_i
101
+ Process.kill("TERM", pid)
102
+ rescue Errno::ESRCH, Errno::ENOENT
103
+ # Process already gone or PID file doesn't exist
104
+ end
105
+
106
+ private def process_running?
107
+ check_script = <<~APPLESCRIPT
108
+ tell application "Terminal"
109
+ try
110
+ set theWindow to window id #{@window_id}
111
+ return busy of theWindow
112
+ on error
113
+ return false
114
+ end try
115
+ end tell
116
+ APPLESCRIPT
117
+
118
+ result = `osascript -e '#{check_script}'`.strip
119
+ result == "true"
120
+ end
121
+
122
+ private def contents
123
+ read_script = <<~APPLESCRIPT
124
+ tell application "Terminal"
125
+ try
126
+ set theWindow to window id #{@window_id}
127
+ return contents of selected tab of theWindow
128
+ on error
129
+ return ""
130
+ end try
131
+ end tell
132
+ APPLESCRIPT
133
+
134
+ `osascript -e '#{read_script}'`
135
+ end
136
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ class WindowID < Data.define(:value)
7
+ def valid?
8
+ !value.empty? && value.match?(/^\d+$/)
9
+ end
10
+
11
+ def to_s
12
+ value
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "fileutils"
7
+ require_relative "terminal_preview/preview_collection"
8
+ require_relative "terminal_preview/example_app"
9
+
10
+ namespace :terminal_preview do
11
+ desc "Generate native PNG screenshots using Terminal.app"
12
+ task :generate do
13
+ img_dir = File.expand_path("../doc/images", __dir__)
14
+ FileUtils.mkdir_p(img_dir)
15
+
16
+ # Create empty placeholder files for any missing images that compile depends on.
17
+ # This prevents Rake from trying to build them as dependencies.
18
+ ExampleApp.all.each do |app|
19
+ image_path = File.join(img_dir, "#{app}.png")
20
+ FileUtils.touch(image_path) unless File.exist?(image_path)
21
+ end
22
+
23
+ Rake::Task["compile"].invoke
24
+
25
+ collection = PreviewCollection.new(img_dir)
26
+ collection.generate
27
+ end
28
+ end
data/tasks/test.rake CHANGED
@@ -16,7 +16,7 @@ end
16
16
  Rake::Task["test"].clear if Rake::Task.task_defined?("test")
17
17
 
18
18
  desc "Run all tests (Ruby and Rust)"
19
- task test: %w[test:ruby test:rust]
19
+ task test: %w[compile test:ruby test:rust]
20
20
 
21
21
  namespace :test do
22
22
  desc "Run Rust tests"
@@ -26,6 +26,6 @@ namespace :test do
26
26
 
27
27
  # Create a specific Minitest task for Ruby tests
28
28
  Minitest::TestTask.create(:ruby) do |t|
29
- t.test_globs = ["test/**/test_*.rb", "examples/**/test_*.rb"]
29
+ t.test_globs = ["test/**/*.rb", "examples/**/test_*.rb"]
30
30
  end
31
31
  end
@@ -8,17 +8,17 @@ require "erb"
8
8
  class IndexPage
9
9
  def initialize(versions)
10
10
  @versions = versions
11
-
11
+
12
12
  latest_version = @versions.find { |v| v.is_a?(Tagged) }
13
13
  latest_version.is_latest = true if latest_version
14
14
  end
15
15
 
16
16
  def publish_to(path, project_name:)
17
17
  puts "Generating index page..."
18
-
18
+
19
19
  template_path = File.expand_path("../resources/index.html.erb", __dir__)
20
20
  template = File.read(template_path)
21
-
21
+
22
22
  versions = @versions
23
23
  # project_name is used in the ERB
24
24
  html_content = ERB.new(template).result(binding)
@@ -10,9 +10,9 @@ class Version
10
10
  def self.all
11
11
  tags = `git tag`.split.grep(/^v\d/)
12
12
  sorted_versions = tags.map { |t| Tagged.new(t) }
13
- .sort_by { |v| v.semver }
14
- .reverse
15
-
13
+ .sort_by(&:semver)
14
+ .reverse
15
+
16
16
  [Edge.new] + sorted_versions
17
17
  end
18
18
 
@@ -31,11 +31,11 @@ class Version
31
31
  def checkout(globs:, &block)
32
32
  raise NotImplementedError
33
33
  end
34
-
34
+
35
35
  def latest?
36
36
  false
37
37
  end
38
-
38
+
39
39
  def edge?
40
40
  false
41
41
  end
@@ -53,7 +53,7 @@ class Edge < Version
53
53
  def type
54
54
  :edge
55
55
  end
56
-
56
+
57
57
  def edge?
58
58
  true
59
59
  end
@@ -64,14 +64,14 @@ class Edge < Version
64
64
  files = `git ls-files`.split("\n").select do |f|
65
65
  globs.any? { |glob| File.fnmatch(glob, f, File::FNM_PATHNAME) }
66
66
  end
67
-
67
+
68
68
  files.each do |file|
69
69
  dest = File.join(path, file)
70
70
  next unless File.exist?(file) # Skip files that are in the index but deleted in the working tree
71
71
  FileUtils.mkdir_p(File.dirname(dest))
72
72
  FileUtils.cp(file, dest)
73
73
  end
74
-
74
+
75
75
  yield path
76
76
  end
77
77
  end
@@ -99,9 +99,9 @@ class Tagged < Version
99
99
  def semver
100
100
  Gem::Version.new(@tag.sub(/^v/, ""))
101
101
  end
102
-
102
+
103
103
  attr_accessor :is_latest
104
-
104
+
105
105
  def latest?
106
106
  @is_latest
107
107
  end
@@ -11,21 +11,19 @@ class VersionMenu
11
11
 
12
12
  def run
13
13
  puts "Injecting version menu into generated HTML..."
14
-
14
+
15
15
  # Process all HTML files in the output directory
16
16
  Dir.glob(File.join(@root, "**/*.html")).each do |file|
17
17
  inject_menu(file)
18
18
  end
19
19
  end
20
20
 
21
- private
22
-
23
- def inject_menu(file)
21
+ private def inject_menu(file)
24
22
  content = File.read(file)
25
-
23
+
26
24
  # Find the injection point (before the theme toggle button)
27
25
  pattern = /(<button[^>]*id="theme-toggle"[^>]*>)/mi
28
-
26
+
29
27
  unless content.match?(pattern)
30
28
  # warn "Could not find theme-toggle in #{file}"
31
29
  return
@@ -35,11 +33,11 @@ class VersionMenu
35
33
  file_dir = File.dirname(file)
36
34
  relative_path_to_root = Pathname.new(@root).relative_path_from(Pathname.new(file_dir)).to_s
37
35
  relative_path_to_root += "/" unless relative_path_to_root.end_with?("/")
38
-
36
+
39
37
  # Determine current version from file path
40
38
  relative_path_from_root = Pathname.new(file).relative_path_from(Pathname.new(@root)).to_s
41
39
  current_version_slug = relative_path_from_root.split("/").first
42
-
40
+
43
41
  # Build options
44
42
  options = @versions.map do |version|
45
43
  value = "#{relative_path_to_root}#{version.slug}/index.html"
@@ -47,10 +45,10 @@ class VersionMenu
47
45
  display_name = version.name
48
46
  display_name += " (latest)" if version.respond_to?(:latest?) && version.latest?
49
47
  display_name += " (dev)" if version.edge?
50
-
48
+
51
49
  %Q{<option value="#{value}" #{selected}>#{display_name}</option>}
52
50
  end.join("\n")
53
-
51
+
54
52
  # margin-left: auto pushes it to the right
55
53
  # margin-right: 1rem spacing from the theme toggle
56
54
  switcher_html = <<~HTML
@@ -59,10 +57,10 @@ class VersionMenu
59
57
  <option value="#{relative_path_to_root}index.html">All Versions</option>
60
58
  </select>
61
59
  HTML
62
-
60
+
63
61
  # Inject before the button
64
62
  new_content = content.sub(pattern, "#{switcher_html}\n\\1")
65
-
63
+
66
64
  File.write(file, new_content)
67
65
  end
68
66
  end
@@ -12,38 +12,70 @@ class VersionedDocumentation
12
12
 
13
13
  def publish_to(path, project_name:, globs:, assets: [])
14
14
  puts "Building documentation for #{@version.name}..."
15
-
15
+
16
16
  absolute_path = File.absolute_path(path)
17
17
  gemfile_path = File.absolute_path("Gemfile")
18
18
  custom_css_path = File.absolute_path("doc/custom.css")
19
+ rakefile_path = File.absolute_path("Rakefile")
20
+ tasks_dir_path = File.absolute_path("tasks")
21
+
22
+ @version.checkout(globs:) do |source_path|
23
+ # Copy current Rakefile and tasks into the temp directory
24
+ # This ensures all versions use the latest example generation logic
25
+ FileUtils.cp(rakefile_path, File.join(source_path, "Rakefile"))
26
+ FileUtils.cp_r(tasks_dir_path, File.join(source_path, "tasks"))
19
27
 
20
- @version.checkout(globs: globs) do |source_path|
21
28
  Dir.chdir(source_path) do
22
29
  title = "#{project_name} #{@version.name}"
23
30
  title = "#{project_name} (main)" if @version.edge?
24
-
25
- # We need to expand globs relative to the source path
26
- files = globs.flat_map { |glob| Dir[glob] }.uniq
27
-
28
- system(
29
- { "BUNDLE_GEMFILE" => gemfile_path },
30
- "bundle exec rdoc -o #{absolute_path} --main #{RDocConfig::MAIN} --title '#{title}' --template-stylesheets \"#{custom_css_path}\" #{files.join(' ')}"
31
+
32
+ # Use rake rerdoc to ensure copy_examples runs
33
+ # Set environment variables to override rdoc settings
34
+ success = system(
35
+ {
36
+ "BUNDLE_GEMFILE" => gemfile_path,
37
+ "RDOC_OUTPUT" => absolute_path,
38
+ "RDOC_TITLE" => title,
39
+ "RDOC_CUSTOM_CSS" => custom_css_path,
40
+ },
41
+ "bundle exec rake rerdoc"
31
42
  )
32
43
 
44
+ # Fall back to direct rdoc call if rake fails for any reason
45
+ unless success
46
+ puts " Rake task failed, falling back to direct rdoc call..."
47
+ files = globs.flat_map { |glob| Dir[glob] }.uniq
48
+ system(
49
+ { "BUNDLE_GEMFILE" => gemfile_path },
50
+ "bundle exec rdoc -o #{absolute_path} --main #{RDocConfig::MAIN} --title '#{title}' --template-stylesheets \"#{custom_css_path}\" #{files.join(' ')}"
51
+ )
52
+ end
53
+
54
+ # Copy generated documentation to target path if it was generated elsewhere
55
+ # This handles cases where RDOC_OUTPUT wasn't respected (evaluated at load time)
56
+ temp_output_paths = ["tmp/rdoc", "doc"]
57
+ temp_output_paths.each do |temp_path|
58
+ # Check if this looks like generated rdoc (has index.html)
59
+ if Dir.exist?(temp_path) && !Dir.empty?(temp_path) && File.exist?(File.join(temp_path, "index.html"))
60
+ puts " Copying generated docs from #{temp_path} to #{absolute_path}..."
61
+ FileUtils.mkdir_p(absolute_path)
62
+ FileUtils.cp_r Dir["#{temp_path}/*"], absolute_path
63
+ break
64
+ end
65
+ end
66
+
33
67
  copy_assets_to(absolute_path, assets)
34
68
  end
35
69
  end
36
70
  end
37
71
 
38
- private
39
-
40
- def copy_assets_to(path, assets)
72
+ private def copy_assets_to(path, assets)
41
73
  assets.each do |asset_dir|
42
- if Dir.exist?(asset_dir)
43
- destination = File.join(path, asset_dir)
44
- FileUtils.mkdir_p(destination)
45
- FileUtils.cp_r Dir["#{asset_dir}/*"], destination
46
- end
74
+ if Dir.exist?(asset_dir)
75
+ destination = File.join(path, asset_dir)
76
+ FileUtils.mkdir_p(destination)
77
+ FileUtils.cp_r Dir["#{asset_dir}/*"], destination
78
+ end
47
79
  end
48
80
  end
49
81
  end
@@ -19,11 +19,11 @@ class Website
19
19
 
20
20
  def build
21
21
  clean
22
-
22
+
23
23
  versions.each do |version|
24
24
  VersionedDocumentation.new(version).publish_to(
25
- join(version.slug),
26
- project_name: @project_name,
25
+ join(version.slug),
26
+ project_name: @project_name,
27
27
  globs: @globs,
28
28
  assets: @assets
29
29
  )
@@ -31,7 +31,7 @@ class Website
31
31
 
32
32
  IndexPage.new(versions).publish_to(join("index.html"), project_name: @project_name)
33
33
 
34
- VersionMenu.new(root: @destination, versions: versions).run
34
+ VersionMenu.new(root: @destination, versions:).run
35
35
 
36
36
  puts "Website built in #{@destination}/"
37
37
  end
@@ -40,13 +40,11 @@ class Website
40
40
  @versions ||= Version.all
41
41
  end
42
42
 
43
- private
44
-
45
- def join(path)
43
+ private def join(path)
46
44
  File.join(@destination, path)
47
45
  end
48
46
 
49
- def clean
47
+ private def clean
50
48
  FileUtils.rm_rf(@destination)
51
49
  FileUtils.mkdir_p(@destination)
52
50
  end
data/tasks/website.rake CHANGED
@@ -12,14 +12,14 @@ namespace :website do
12
12
  desc "Build documentation for main (current dir) and all git tags"
13
13
  task :build do
14
14
  require_relative "website/website"
15
-
15
+
16
16
  spec = Gem::Specification.load(Dir["*.gemspec"].first)
17
- globs = RDocConfig::RDOC_FILES + ["*.gemspec", "doc/images/**/*"]
18
-
17
+ globs = RDocConfig::RDOC_FILES + ["*.gemspec", "doc/images/**/*", "examples/**/*"]
18
+
19
19
  Website.new(
20
20
  at: "www",
21
21
  project_name: spec.name,
22
- globs: globs,
22
+ globs:,
23
23
  assets: ["doc/images"] # directories to copy
24
24
  ).build
25
25
  end