ratatui_ruby 0.7.1 → 0.7.3

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 (311) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +12 -4
  7. data/CHANGELOG.md +49 -0
  8. data/README.md +7 -7
  9. data/Rakefile +1 -1
  10. data/doc/{application_architecture.md → concepts/application_architecture.md} +30 -0
  11. data/doc/{application_testing.md → concepts/application_testing.md} +4 -2
  12. data/doc/{event_handling.md → concepts/event_handling.md} +1 -1
  13. data/doc/contributors/auditing/parity.md +233 -0
  14. data/doc/contributors/developing_examples.md +10 -10
  15. data/doc/contributors/upstream_requests/tab_rects.md +173 -0
  16. data/doc/contributors/upstream_requests/title_rects.md +132 -0
  17. data/doc/contributors/v1.0.0_blockers.md +54 -747
  18. data/doc/{quickstart.md → getting_started/quickstart.md} +26 -26
  19. data/doc/{why.md → getting_started/why.md} +1 -1
  20. data/doc/index.md +23 -9
  21. data/doc/{terminal_limitations.md → troubleshooting/terminal_limitations.md} +33 -0
  22. data/doc/troubleshooting/tui_output.md +76 -0
  23. data/examples/app_all_events/README.md +1 -0
  24. data/examples/app_all_events/app.rb +2 -0
  25. data/examples/app_all_events/model/app_model.rb +2 -0
  26. data/examples/app_all_events/model/event_color_cycle.rb +2 -0
  27. data/examples/app_all_events/model/event_entry.rb +2 -0
  28. data/examples/app_all_events/model/msg.rb +2 -0
  29. data/examples/app_all_events/model/timestamp.rb +2 -0
  30. data/examples/app_all_events/update.rb +2 -0
  31. data/examples/app_all_events/view/app_view.rb +2 -0
  32. data/examples/app_all_events/view/controls_view.rb +2 -0
  33. data/examples/app_all_events/view/counts_view.rb +2 -0
  34. data/examples/app_all_events/view/live_view.rb +2 -0
  35. data/examples/app_all_events/view/log_view.rb +2 -0
  36. data/examples/app_all_events/view.rb +2 -0
  37. data/examples/app_color_picker/README.md +2 -0
  38. data/examples/app_color_picker/app.rb +2 -0
  39. data/examples/app_color_picker/clipboard.rb +2 -0
  40. data/examples/app_color_picker/color.rb +2 -0
  41. data/examples/app_color_picker/controls.rb +2 -0
  42. data/examples/app_color_picker/copy_dialog.rb +2 -0
  43. data/examples/app_color_picker/export_pane.rb +2 -0
  44. data/examples/app_color_picker/harmony.rb +2 -0
  45. data/examples/app_color_picker/input.rb +2 -0
  46. data/examples/app_color_picker/main_container.rb +2 -0
  47. data/examples/app_color_picker/palette.rb +2 -0
  48. data/examples/app_login_form/README.md +3 -0
  49. data/examples/app_login_form/app.rb +2 -0
  50. data/examples/app_stateful_interaction/README.md +2 -0
  51. data/examples/app_stateful_interaction/app.rb +2 -0
  52. data/examples/timeout_demo.rb +2 -0
  53. data/examples/verify_quickstart_dsl/README.md +2 -2
  54. data/examples/verify_quickstart_dsl/app.rb +2 -0
  55. data/examples/verify_quickstart_layout/README.md +2 -2
  56. data/examples/verify_quickstart_layout/app.rb +2 -0
  57. data/examples/verify_quickstart_lifecycle/README.md +2 -2
  58. data/examples/verify_quickstart_lifecycle/app.rb +2 -0
  59. data/examples/verify_readme_usage/app.rb +2 -0
  60. data/examples/{widget_barchart_demo → widget_barchart}/README.md +5 -3
  61. data/examples/{widget_barchart_demo → widget_barchart}/app.rb +7 -5
  62. data/examples/{widget_block_demo → widget_block}/README.md +5 -3
  63. data/examples/{widget_block_demo → widget_block}/app.rb +6 -4
  64. data/examples/{widget_box_demo → widget_box}/README.md +7 -4
  65. data/examples/{widget_box_demo → widget_box}/app.rb +7 -5
  66. data/examples/{widget_calendar_demo → widget_calendar}/README.md +6 -3
  67. data/examples/{widget_calendar_demo → widget_calendar}/app.rb +6 -4
  68. data/examples/{widget_canvas_demo → widget_canvas}/README.md +2 -2
  69. data/examples/{widget_canvas_demo → widget_canvas}/app.rb +6 -4
  70. data/examples/{widget_cell_demo → widget_cell}/README.md +6 -3
  71. data/examples/{widget_cell_demo → widget_cell}/app.rb +7 -5
  72. data/examples/{widget_center_demo → widget_center}/README.md +2 -2
  73. data/examples/{widget_center_demo → widget_center}/app.rb +6 -4
  74. data/examples/{widget_chart_demo → widget_chart}/README.md +7 -4
  75. data/examples/{widget_chart_demo → widget_chart}/app.rb +7 -5
  76. data/examples/{widget_gauge_demo → widget_gauge}/README.md +6 -3
  77. data/examples/{widget_gauge_demo → widget_gauge}/app.rb +7 -5
  78. data/examples/widget_layout_split/README.md +5 -2
  79. data/examples/widget_layout_split/app.rb +3 -1
  80. data/examples/{widget_line_gauge_demo → widget_line_gauge}/README.md +6 -3
  81. data/examples/{widget_line_gauge_demo → widget_line_gauge}/app.rb +7 -5
  82. data/examples/{widget_list_demo → widget_list}/README.md +7 -4
  83. data/examples/{widget_list_demo → widget_list}/app.rb +7 -5
  84. data/examples/{widget_map_demo → widget_map}/README.md +7 -4
  85. data/examples/{widget_map_demo → widget_map}/app.rb +4 -2
  86. data/examples/{widget_overlay_demo → widget_overlay}/README.md +6 -3
  87. data/examples/{widget_overlay_demo → widget_overlay}/app.rb +5 -3
  88. data/examples/{widget_popup_demo → widget_popup}/README.md +7 -4
  89. data/examples/{widget_popup_demo → widget_popup}/app.rb +6 -4
  90. data/examples/{widget_ratatui_logo_demo → widget_ratatui_logo}/README.md +6 -3
  91. data/examples/{widget_ratatui_logo_demo → widget_ratatui_logo}/app.rb +8 -6
  92. data/examples/{widget_ratatui_mascot_demo → widget_ratatui_mascot}/README.md +6 -3
  93. data/examples/{widget_ratatui_mascot_demo → widget_ratatui_mascot}/app.rb +6 -4
  94. data/examples/widget_rect/README.md +5 -2
  95. data/examples/widget_rect/app.rb +2 -0
  96. data/examples/widget_render/README.md +4 -1
  97. data/examples/widget_render/app.rb +2 -0
  98. data/examples/widget_rich_text/README.md +4 -1
  99. data/examples/widget_rich_text/app.rb +2 -0
  100. data/examples/widget_scroll_text/README.md +4 -1
  101. data/examples/widget_scroll_text/app.rb +3 -1
  102. data/examples/{widget_scrollbar_demo → widget_scrollbar}/README.md +7 -4
  103. data/examples/{widget_scrollbar_demo → widget_scrollbar}/app.rb +6 -4
  104. data/examples/{widget_sparkline_demo → widget_sparkline}/README.md +6 -3
  105. data/examples/{widget_sparkline_demo → widget_sparkline}/app.rb +7 -5
  106. data/examples/widget_style_colors/README.md +4 -1
  107. data/examples/widget_style_colors/app.rb +2 -0
  108. data/examples/{widget_table_demo → widget_table}/README.md +7 -4
  109. data/examples/{widget_table_demo → widget_table}/app.rb +4 -2
  110. data/examples/{widget_tabs_demo → widget_tabs}/README.md +6 -3
  111. data/examples/{widget_tabs_demo → widget_tabs}/app.rb +7 -5
  112. data/examples/widget_text_width/README.md +5 -2
  113. data/examples/widget_text_width/app.rb +2 -0
  114. data/exe/.gitkeep +0 -0
  115. data/ext/ratatui_ruby/Cargo.lock +1 -1
  116. data/ext/ratatui_ruby/Cargo.toml +1 -1
  117. data/ext/ratatui_ruby/extconf.rb +2 -0
  118. data/ext/ratatui_ruby/src/lib.rs +2 -2
  119. data/ext/ratatui_ruby/src/rendering.rs +9 -0
  120. data/ext/ratatui_ruby/src/style.rs +22 -2
  121. data/ext/ratatui_ruby/src/text.rs +26 -0
  122. data/ext/ratatui_ruby/src/widgets/barchart.rs +8 -6
  123. data/ext/ratatui_ruby/src/widgets/chart.rs +31 -4
  124. data/ext/ratatui_ruby/src/widgets/table.rs +13 -5
  125. data/ext/ratatui_ruby/src/widgets/tabs.rs +49 -9
  126. data/lib/ratatui_ruby/buffer/cell.rb +2 -0
  127. data/lib/ratatui_ruby/buffer.rb +2 -0
  128. data/lib/ratatui_ruby/cell.rb +2 -0
  129. data/lib/ratatui_ruby/event/focus_gained.rb +2 -0
  130. data/lib/ratatui_ruby/event/focus_lost.rb +2 -0
  131. data/lib/ratatui_ruby/event/key/character.rb +2 -0
  132. data/lib/ratatui_ruby/event/key/media.rb +2 -0
  133. data/lib/ratatui_ruby/event/key/modifier.rb +2 -0
  134. data/lib/ratatui_ruby/event/key/navigation.rb +2 -0
  135. data/lib/ratatui_ruby/event/key/system.rb +2 -0
  136. data/lib/ratatui_ruby/event/key.rb +2 -0
  137. data/lib/ratatui_ruby/event/mouse.rb +2 -0
  138. data/lib/ratatui_ruby/event/none.rb +2 -0
  139. data/lib/ratatui_ruby/event/paste.rb +2 -0
  140. data/lib/ratatui_ruby/event/resize.rb +2 -0
  141. data/lib/ratatui_ruby/event.rb +2 -0
  142. data/lib/ratatui_ruby/frame.rb +2 -0
  143. data/lib/ratatui_ruby/layout/constraint.rb +2 -0
  144. data/lib/ratatui_ruby/layout/layout.rb +2 -0
  145. data/lib/ratatui_ruby/layout/rect.rb +2 -0
  146. data/lib/ratatui_ruby/layout.rb +2 -0
  147. data/lib/ratatui_ruby/list_state.rb +2 -0
  148. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -0
  149. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +2 -0
  150. data/lib/ratatui_ruby/schema/bar_chart.rb +4 -2
  151. data/lib/ratatui_ruby/schema/block.rb +4 -2
  152. data/lib/ratatui_ruby/schema/calendar.rb +4 -2
  153. data/lib/ratatui_ruby/schema/canvas.rb +2 -0
  154. data/lib/ratatui_ruby/schema/center.rb +2 -0
  155. data/lib/ratatui_ruby/schema/chart.rb +4 -2
  156. data/lib/ratatui_ruby/schema/clear.rb +2 -0
  157. data/lib/ratatui_ruby/schema/constraint.rb +2 -0
  158. data/lib/ratatui_ruby/schema/cursor.rb +2 -0
  159. data/lib/ratatui_ruby/schema/draw.rb +2 -0
  160. data/lib/ratatui_ruby/schema/gauge.rb +4 -2
  161. data/lib/ratatui_ruby/schema/layout.rb +2 -0
  162. data/lib/ratatui_ruby/schema/line_gauge.rb +4 -2
  163. data/lib/ratatui_ruby/schema/list.rb +3 -1
  164. data/lib/ratatui_ruby/schema/list_item.rb +2 -0
  165. data/lib/ratatui_ruby/schema/overlay.rb +2 -0
  166. data/lib/ratatui_ruby/schema/paragraph.rb +2 -0
  167. data/lib/ratatui_ruby/schema/ratatui_logo.rb +4 -2
  168. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +4 -2
  169. data/lib/ratatui_ruby/schema/rect.rb +2 -0
  170. data/lib/ratatui_ruby/schema/row.rb +2 -0
  171. data/lib/ratatui_ruby/schema/scrollbar.rb +4 -2
  172. data/lib/ratatui_ruby/schema/shape/label.rb +2 -0
  173. data/lib/ratatui_ruby/schema/sparkline.rb +4 -2
  174. data/lib/ratatui_ruby/schema/style.rb +2 -0
  175. data/lib/ratatui_ruby/schema/table.rb +2 -0
  176. data/lib/ratatui_ruby/schema/tabs.rb +4 -2
  177. data/lib/ratatui_ruby/schema/text.rb +2 -0
  178. data/lib/ratatui_ruby/scrollbar_state.rb +2 -0
  179. data/lib/ratatui_ruby/style/style.rb +3 -0
  180. data/lib/ratatui_ruby/style.rb +2 -0
  181. data/lib/ratatui_ruby/table_state.rb +2 -0
  182. data/lib/ratatui_ruby/test_helper/event_injection.rb +2 -0
  183. data/lib/ratatui_ruby/test_helper/snapshot.rb +62 -21
  184. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  185. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  186. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  187. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  188. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  189. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  190. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  191. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  192. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  193. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  194. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  195. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  196. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  197. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  198. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  199. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  200. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  201. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  202. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  203. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  204. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  205. data/lib/ratatui_ruby/test_helper/style_assertions.rb +2 -0
  206. data/lib/ratatui_ruby/test_helper/terminal.rb +5 -0
  207. data/lib/ratatui_ruby/test_helper/test_doubles.rb +2 -0
  208. data/lib/ratatui_ruby/test_helper.rb +6 -4
  209. data/lib/ratatui_ruby/tui/buffer_factories.rb +2 -0
  210. data/lib/ratatui_ruby/tui/canvas_factories.rb +2 -0
  211. data/lib/ratatui_ruby/tui/core.rb +2 -0
  212. data/lib/ratatui_ruby/tui/layout_factories.rb +2 -0
  213. data/lib/ratatui_ruby/tui/state_factories.rb +2 -0
  214. data/lib/ratatui_ruby/tui/style_factories.rb +2 -0
  215. data/lib/ratatui_ruby/tui/text_factories.rb +2 -0
  216. data/lib/ratatui_ruby/tui/widget_factories.rb +2 -0
  217. data/lib/ratatui_ruby/tui.rb +2 -0
  218. data/lib/ratatui_ruby/version.rb +3 -1
  219. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
  220. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
  221. data/lib/ratatui_ruby/widgets/bar_chart.rb +7 -4
  222. data/lib/ratatui_ruby/widgets/block.rb +46 -2
  223. data/lib/ratatui_ruby/widgets/calendar.rb +4 -2
  224. data/lib/ratatui_ruby/widgets/canvas.rb +2 -0
  225. data/lib/ratatui_ruby/widgets/cell.rb +2 -0
  226. data/lib/ratatui_ruby/widgets/center.rb +2 -0
  227. data/lib/ratatui_ruby/widgets/chart.rb +13 -6
  228. data/lib/ratatui_ruby/widgets/clear.rb +2 -0
  229. data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
  230. data/lib/ratatui_ruby/widgets/gauge.rb +4 -2
  231. data/lib/ratatui_ruby/widgets/line_gauge.rb +4 -2
  232. data/lib/ratatui_ruby/widgets/list.rb +3 -1
  233. data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
  234. data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
  235. data/lib/ratatui_ruby/widgets/paragraph.rb +2 -0
  236. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +4 -2
  237. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +4 -2
  238. data/lib/ratatui_ruby/widgets/row.rb +2 -0
  239. data/lib/ratatui_ruby/widgets/scrollbar.rb +4 -2
  240. data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
  241. data/lib/ratatui_ruby/widgets/sparkline.rb +7 -4
  242. data/lib/ratatui_ruby/widgets/table.rb +2 -0
  243. data/lib/ratatui_ruby/widgets/tabs.rb +12 -8
  244. data/lib/ratatui_ruby/widgets.rb +2 -0
  245. data/lib/ratatui_ruby.rb +130 -9
  246. data/tasks/autodoc/examples.rb +2 -0
  247. data/tasks/autodoc/member.rb +2 -0
  248. data/tasks/autodoc/name.rb +2 -0
  249. data/tasks/autodoc.rake +2 -0
  250. data/tasks/bump/cargo_lockfile.rb +2 -0
  251. data/tasks/bump/changelog.rb +2 -0
  252. data/tasks/bump/header.rb +2 -0
  253. data/tasks/bump/history.rb +2 -0
  254. data/tasks/bump/links.rb +2 -0
  255. data/tasks/bump/manifest.rb +2 -0
  256. data/tasks/bump/ruby_gem.rb +2 -0
  257. data/tasks/bump/sem_ver.rb +2 -0
  258. data/tasks/bump/unreleased_section.rb +2 -0
  259. data/tasks/bump.rake +2 -0
  260. data/tasks/doc.rake +268 -0
  261. data/tasks/extension.rake +2 -0
  262. data/tasks/lint.rake +115 -0
  263. data/tasks/rdoc_config.rb +18 -4
  264. data/tasks/sourcehut.rake +2 -0
  265. data/tasks/terminal_preview/app_screenshot.rb +2 -0
  266. data/tasks/terminal_preview/crash_report.rb +2 -0
  267. data/tasks/terminal_preview/example_app.rb +2 -0
  268. data/tasks/terminal_preview/launcher_script.rb +2 -0
  269. data/tasks/terminal_preview/preview_collection.rb +2 -0
  270. data/tasks/terminal_preview/preview_timing.rb +2 -0
  271. data/tasks/terminal_preview/safety_confirmation.rb +2 -0
  272. data/tasks/terminal_preview/saved_screenshot.rb +2 -0
  273. data/tasks/terminal_preview/system_appearance.rb +2 -0
  274. data/tasks/terminal_preview/terminal_window.rb +2 -0
  275. data/tasks/terminal_preview/window_id.rb +2 -0
  276. data/tasks/terminal_preview.rake +2 -0
  277. data/tasks/test.rake +2 -0
  278. data/tasks/website/index_page.rb +2 -0
  279. data/tasks/website/version.rb +12 -2
  280. data/tasks/website/version_menu.rb +2 -0
  281. data/tasks/website/versioned_documentation.rb +2 -0
  282. data/tasks/website/website.rb +2 -0
  283. data/tasks/website.rake +2 -0
  284. metadata +97 -75
  285. data/doc/contributors/architectural_overhaul/chat_conversations.md +0 -4952
  286. data/doc/contributors/architectural_overhaul/implementation_plan.md +0 -60
  287. data/doc/contributors/architectural_overhaul/task.md +0 -37
  288. /data/doc/{async.md → concepts/async.md} +0 -0
  289. /data/doc/{interactive_design.md → concepts/interactive_design.md} +0 -0
  290. /data/doc/images/{widget_barchart_demo.png → widget_barchart.png} +0 -0
  291. /data/doc/images/{widget_block_demo.png → widget_block.png} +0 -0
  292. /data/doc/images/{widget_box_demo.png → widget_box.png} +0 -0
  293. /data/doc/images/{widget_calendar_demo.png → widget_calendar.png} +0 -0
  294. /data/doc/images/{widget_canvas_demo.png → widget_canvas.png} +0 -0
  295. /data/doc/images/{widget_cell_demo.png → widget_cell.png} +0 -0
  296. /data/doc/images/{widget_center_demo.png → widget_center.png} +0 -0
  297. /data/doc/images/{widget_chart_demo.png → widget_chart.png} +0 -0
  298. /data/doc/images/{widget_gauge_demo.png → widget_gauge.png} +0 -0
  299. /data/doc/images/{widget_line_gauge_demo.png → widget_line_gauge.png} +0 -0
  300. /data/doc/images/{widget_list_demo.png → widget_list.png} +0 -0
  301. /data/doc/images/{widget_map_demo.png → widget_map.png} +0 -0
  302. /data/doc/images/{widget_overlay_demo.png → widget_overlay.png} +0 -0
  303. /data/doc/images/{widget_popup_demo.png → widget_popup.png} +0 -0
  304. /data/doc/images/{widget_ratatui_logo_demo.png → widget_ratatui_logo.png} +0 -0
  305. /data/doc/images/{widget_ratatui_mascot_demo.png → widget_ratatui_mascot.png} +0 -0
  306. /data/doc/images/{widget_scrollbar_demo.png → widget_scrollbar.png} +0 -0
  307. /data/doc/images/{widget_sparkline_demo.png → widget_sparkline.png} +0 -0
  308. /data/doc/images/{widget_table_demo.png → widget_table.png} +0 -0
  309. /data/doc/images/{widget_tabs_demo.png → widget_tabs.png} +0 -0
  310. /data/doc/{v0.7.0_migration.md → migration/v0_7_0.md} +0 -0
  311. /data/doc/{debugging.md → troubleshooting/debugging.md} +0 -0
data/lib/ratatui_ruby.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require_relative "ratatui_ruby/version"
7
9
 
@@ -40,13 +42,72 @@ end
40
42
  #
41
43
  # Use `RatatuiRuby.run` to start your application.
42
44
  module RatatuiRuby
43
- # Generic error class for RatatuiRuby.
45
+ # Base error class for RatatuiRuby.
46
+ #
47
+ # All library-specific exceptions inherit from this class.
48
+ # Catch this to handle any RatatuiRuby error generically.
49
+ #
50
+ # === Example
51
+ #
52
+ # begin
53
+ # RatatuiRuby.run { |tui| ... }
54
+ # rescue RatatuiRuby::Error => e
55
+ # puts "RatatuiRuby error: #{e.message}"
56
+ # end
44
57
  class Error < StandardError
45
- # Raised when a terminal operation fails (e.g., I/O error, backend failure).
58
+ # Operational failure during terminal I/O.
59
+ #
60
+ # Terminals are finnicky. I/O can fail. Backends can crash.
61
+ # These are runtime problems outside your control.
62
+ #
63
+ # This error signals the terminal operation itself failed.
64
+ # The library tried to do something with the terminal and couldn't.
65
+ #
66
+ # Catch this to handle terminal failures gracefully.
67
+ #
68
+ # === Example
69
+ #
70
+ # begin
71
+ # RatatuiRuby.init_terminal
72
+ # rescue RatatuiRuby::Error::Terminal => e
73
+ # puts "Terminal failed: #{e.message}"
74
+ # end
46
75
  class Terminal < Error; end
47
76
 
48
- # Raised when an API safety contract is violated (e.g., accessing a Frame outside its valid scope).
77
+ # Object lifetime violation.
78
+ #
79
+ # Some objects are only valid during specific scopes.
80
+ # Using them after their scope ends causes undefined behavior.
81
+ #
82
+ # This error prevents use-after-scope bugs.
83
+ # The object you're accessing is no longer valid.
84
+ #
85
+ # To resolve, ensure scoped objects are used only within their
86
+ # valid lifetime (e.g., inside the block where they're created).
87
+ #
88
+ # === Example
89
+ #
90
+ # stored_frame = nil
91
+ # RatatuiRuby.draw { |frame| stored_frame = frame }
92
+ # stored_frame.area # => raises Error::Safety
49
93
  class Safety < Error; end
94
+
95
+ # State invariant violation.
96
+ #
97
+ # The library has rules about valid state transitions.
98
+ # Calling methods in the wrong order or state breaks invariants.
99
+ #
100
+ # This error signals you violated a state machine contract.
101
+ # The program state doesn't allow this operation right now.
102
+ #
103
+ # To resolve, check `terminal_active?` or restructure the
104
+ # code to ensure methods are called in the expected order.
105
+ #
106
+ # === Example
107
+ #
108
+ # RatatuiRuby.init_terminal
109
+ # RatatuiRuby.init_terminal # => raises Error::Invariant
110
+ class Invariant < Error; end
50
111
  end
51
112
 
52
113
  ##
@@ -56,23 +117,64 @@ module RatatuiRuby
56
117
  # [focus_events] whether to enable focus gain/loss events (default: true).
57
118
  # [bracketed_paste] whether to enable bracketed paste mode (default: true).
58
119
  def self.init_terminal(focus_events: true, bracketed_paste: true)
120
+ if @tui_session_active
121
+ raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
122
+ end
123
+ @tui_session_active = true
59
124
  _init_terminal(focus_events, bracketed_paste)
60
125
  end
61
126
 
62
127
  @experimental_warnings = true
128
+ @tui_session_active = false
129
+ @deferred_warnings = []
130
+
131
+ ##
132
+ # Whether a TUI session is currently active.
133
+ #
134
+ # Writing to stdout/stderr during a TUI session corrupts the display.
135
+ # Use this to defer logging, warnings, or debug output until
136
+ # after the session ends.
137
+ #
138
+ # === Example
139
+ #
140
+ # def log(message)
141
+ # if RatatuiRuby.terminal_active?
142
+ # @deferred_logs << message
143
+ # else
144
+ # puts message
145
+ # end
146
+ # end
147
+ def self.terminal_active?
148
+ @tui_session_active
149
+ end
150
+
63
151
  class << self
64
152
  ##
65
153
  # :attr_accessor: experimental_warnings
66
154
  # Whether to show warnings when using experimental features (default: true).
67
155
  attr_accessor :experimental_warnings
156
+
157
+ private def queue_warning(message)
158
+ @deferred_warnings << message
159
+ end
160
+
161
+ private def flush_warnings
162
+ return if @deferred_warnings.empty?
163
+ @deferred_warnings.each { |msg| warn msg }
164
+ @deferred_warnings.clear
165
+ end
68
166
  end
69
167
 
70
168
  ##
71
- # :singleton-method: restore_terminal
72
169
  # Restores the terminal to its original state.
73
170
  # Leaves alternate screen and disables raw mode.
74
- #
75
- # (Native method implemented in Rust)
171
+ # Also flushes any deferred warnings that were queued during the session.
172
+ def self.restore_terminal
173
+ _restore_terminal
174
+ ensure
175
+ @tui_session_active = false
176
+ flush_warnings
177
+ end
76
178
 
77
179
  ##
78
180
  # :singleton-method: inject_test_event
@@ -96,12 +198,31 @@ module RatatuiRuby
96
198
  @warned_features ||= {}
97
199
  return if @warned_features[feature_name]
98
200
 
99
- warn "WARNING: #{feature_name} is an experimental feature and may change in future versions. Disable this warning with RatatuiRuby.experimental_warnings = false."
201
+ message = "WARNING: #{feature_name} is an experimental feature and may change in future versions. Disable this warning with RatatuiRuby.experimental_warnings = false."
202
+ if terminal_active?
203
+ queue_warning(message)
204
+ else
205
+ warn message
206
+ end
100
207
  @warned_features[feature_name] = true
101
208
  end
102
209
 
103
- # (Native method _init_terminal implemented in Rust)
104
- private_class_method :_init_terminal
210
+ ##
211
+ # Initializes a test terminal for unit testing.
212
+ # Sets session active state like init_terminal.
213
+ #
214
+ # [width] Integer width of the test terminal.
215
+ # [height] Integer height of the test terminal.
216
+ def self.init_test_terminal(width, height)
217
+ if @tui_session_active
218
+ raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
219
+ end
220
+ @tui_session_active = true
221
+ _init_test_terminal(width, height)
222
+ end
223
+
224
+ # (Native methods implemented in Rust)
225
+ private_class_method :_init_terminal, :_restore_terminal, :_init_test_terminal
105
226
 
106
227
  ##
107
228
  # Draws the given UI node tree to the terminal.
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  module Autodoc
7
9
  class Examples
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  module Autodoc
7
9
  module Member
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  module Autodoc
7
9
  class Name < Data.define(:string)
data/tasks/autodoc.rake CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require_relative "autodoc/examples"
7
9
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # Lockfiles need to be refreshed by a tool after Manifests are changed.
7
9
  class CargoLockfile < Data.define(:path, :dir, :name)
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require_relative "links"
7
9
  require_relative "unreleased_section"
data/tasks/bump/header.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # Header manages the header section of the changelog.
7
9
  class Header
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # History manages the versioned history of the changelog.
7
9
  class History
data/tasks/bump/links.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # Manages the version comparison links at the botton of the changelog.
7
9
  #
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # Manifests hold a copy of the version number and should be changed manually.
7
9
  # Use Regexp lookarounds in `pattern` to match the version number.
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  class RubyGem
7
9
  def initialize(manifests:, lockfile:, changelog:)
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  # See https://semver.org/spec/v2.0.0.html
7
9
  class SemVer
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require "date"
7
9
  require "rdoc"
data/tasks/bump.rake CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require "rubygems"
7
9
 
data/tasks/doc.rake CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require "rdoc/task"
7
9
 
@@ -611,9 +613,275 @@ end
611
613
  Rake::Task[:rdoc].enhance do
612
614
  Rake::Task[:copy_doc_images].invoke
613
615
  Rake::Task[:copy_examples].invoke
616
+ Rake::Task[:rewrite_examples_link].invoke
614
617
  end
615
618
 
616
619
  Rake::Task[:rerdoc].enhance do
617
620
  Rake::Task[:copy_doc_images].invoke
618
621
  Rake::Task[:copy_examples].invoke
622
+ Rake::Task[:rewrite_examples_link].invoke
623
+ end
624
+
625
+ task :rewrite_examples_link do
626
+ require "nokogiri"
627
+
628
+ rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
629
+
630
+ # Build a mapping of example READMEs to their H1 titles and categories
631
+ examples_by_category = { "Apps" => [], "Widgets" => [] }
632
+
633
+ Dir.glob("examples/*/README.md").each do |readme_path|
634
+ dir_name = File.dirname(readme_path).sub("examples/", "")
635
+
636
+ # Skip verify examples entirely
637
+ next if dir_name.start_with?("verify_")
638
+
639
+ content = File.read(readme_path)
640
+ if content =~ /^#\s+(.+)$/
641
+ title = $1.strip.sub(/ Example$/, "") # Remove trailing " Example"
642
+ rdoc_path = "examples/#{dir_name}/README_md.html"
643
+
644
+ # Categorize by prefix
645
+ category = if dir_name.start_with?("app_")
646
+ "Apps"
647
+ elsif dir_name.start_with?("widget_")
648
+ title = title.sub(/ Widget$/, "") # Also strip trailing " Widget" for widgets
649
+ "Widgets"
650
+ else
651
+ nil
652
+ end
653
+
654
+ if category
655
+ examples_by_category[category] << { title:, rdoc_path:, dir_name: }
656
+ end
657
+ end
658
+ end
659
+
660
+ # Sort each category alphabetically by title
661
+ examples_by_category.each_value { |list| list.sort_by! { |e| e[:title] } }
662
+
663
+ # Process all HTML files
664
+ Dir.glob("#{rdoc_dir}/**/*.html").each do |file|
665
+ content = File.read(file)
666
+ modified = false
667
+
668
+ doc = Nokogiri::HTML(content)
669
+
670
+ # Find the examples details section to remove from Pages
671
+ examples_detail = doc.css("details summary").find { |s| s.text.strip.downcase == "examples" }&.parent
672
+
673
+ # Find the classindex-section to insert Examples section before it
674
+ classindex_section = doc.at_css("#classindex-section")
675
+
676
+ if examples_detail && classindex_section
677
+ # Remove examples from Pages section
678
+ examples_detail.remove
679
+
680
+ # Build the new Examples section as a top-level nav-section
681
+ current_depth = file.sub("#{rdoc_dir}/", "").count("/")
682
+ prefix = "../" * current_depth
683
+
684
+ examples_section = Nokogiri::XML::Node.new("div", doc)
685
+ examples_section["id"] = "exampleindex-section"
686
+ examples_section["class"] = "nav-section"
687
+
688
+ examples_section.inner_html = <<~HTML
689
+ <details class="nav-section-collapsible" open>
690
+ <summary class="nav-section-header">
691
+ <span class="nav-section-icon">
692
+ <svg><use href="#icon-layers"></use></svg>
693
+ </span>
694
+ <span class="nav-section-title">Examples</span>
695
+ <span class="nav-section-chevron">
696
+ <svg><use href="#icon-chevron"></use></svg>
697
+ </span>
698
+ </summary>
699
+ <ul class="link-list nav-list">
700
+ </ul>
701
+ </details>
702
+ HTML
703
+
704
+ # Build the category structure
705
+ examples_ul = examples_section.at_css("ul.link-list")
706
+
707
+ examples_by_category.each do |category_name, examples|
708
+ next if examples.empty?
709
+
710
+ cat_li = Nokogiri::XML::Node.new("li", doc)
711
+ cat_details = Nokogiri::XML::Node.new("details", doc)
712
+ # Subcategories closed by default
713
+ cat_summary = Nokogiri::XML::Node.new("summary", doc)
714
+ cat_summary.content = category_name
715
+ cat_details.add_child(cat_summary)
716
+
717
+ cat_ul = Nokogiri::XML::Node.new("ul", doc)
718
+ cat_ul["class"] = "link-list nav-list"
719
+
720
+ examples.each do |example|
721
+ li = Nokogiri::XML::Node.new("li", doc)
722
+ a = Nokogiri::XML::Node.new("a", doc)
723
+ a["href"] = "#{prefix}#{example[:rdoc_path]}"
724
+ a.content = example[:title]
725
+ li.add_child(a)
726
+ cat_ul.add_child(li)
727
+ end
728
+
729
+ cat_details.add_child(cat_ul)
730
+ cat_li.add_child(cat_details)
731
+ examples_ul.add_child(cat_li)
732
+ end
733
+
734
+ # Insert Examples section before Classes and Modules
735
+ classindex_section.add_previous_sibling(examples_section)
736
+
737
+ # --- GUIDES SECTION ---
738
+ # Build dynamic hierarchical tree from doc/ folder structure
739
+ guides_tree = build_guides_tree
740
+
741
+ # Find and remove the doc details section from Pages
742
+ doc_detail = doc.css("details summary").find { |s| s.text.strip.downcase == "doc" }&.parent
743
+ doc_detail&.remove
744
+
745
+ # Create the Guides section
746
+ guides_section = Nokogiri::XML::Node.new("div", doc)
747
+ guides_section["id"] = "guidesindex-section"
748
+ guides_section["class"] = "nav-section"
749
+
750
+ guides_section.inner_html = <<~HTML
751
+ <details class="nav-section-collapsible" open>
752
+ <summary class="nav-section-header">
753
+ <span class="nav-section-icon">
754
+ <svg><use href="#icon-file"></use></svg>
755
+ </span>
756
+ <span class="nav-section-title">Guides</span>
757
+ <span class="nav-section-chevron">
758
+ <svg><use href="#icon-chevron"></use></svg>
759
+ </span>
760
+ </summary>
761
+ <ul class="link-list nav-list">
762
+ </ul>
763
+ </details>
764
+ HTML
765
+
766
+ # Get current file path relative to rdoc_dir (e.g. "doc/getting_started/quickstart_md.html")
767
+ current_file_rel = file.sub("#{rdoc_dir}/", "")
768
+
769
+ guides_ul = guides_section.at_css("ul.link-list")
770
+ build_guides_nav(guides_ul, guides_tree, doc, prefix, current_file_rel, "doc")
771
+
772
+ # Insert Guides section before Examples
773
+ examples_section.add_previous_sibling(guides_section)
774
+
775
+ content = doc.to_html
776
+ modified = true
777
+ end
778
+
779
+ # Also rewrite examples_md.html to examples/index.html
780
+ if content.include?("examples_md.html")
781
+ content = content.gsub(/href="([^"]*?)examples_md\.html"/, 'href="\1examples/index.html"')
782
+ modified = true
783
+ end
784
+
785
+ File.write(file, content) if modified
786
+ end
787
+
788
+ # Delete the now-unused examples_md.html
789
+ examples_page = "#{rdoc_dir}/examples_md.html"
790
+ FileUtils.rm_f(examples_page)
791
+
792
+ puts "Created Examples and Guides sections in sidebar"
793
+ end
794
+
795
+ # Build a hierarchical tree structure from doc/**/*.md files
796
+ def build_guides_tree
797
+ tree = { files: [], subdirs: {} }
798
+
799
+ Dir.glob("doc/**/*.md").each do |md_path|
800
+ # Skip images folder
801
+ next if md_path.include?("/images/")
802
+
803
+ relative = md_path.sub("doc/", "")
804
+ parts = relative.split("/")
805
+ filename = parts.pop
806
+
807
+ # Get title from H1
808
+ content = File.read(md_path)
809
+ title = if content =~ /^#\s+(.+)$/
810
+ $1.strip
811
+ else
812
+ filename.sub(/\.md$/, "").tr("_-", " ").split.map(&:capitalize).join(" ")
813
+ end
814
+
815
+ # Convert to RDoc path
816
+ rdoc_path = "doc/#{relative.gsub('.', '_')}.html"
817
+
818
+ # Navigate to correct position in tree
819
+ current = tree
820
+ parts.each do |dir|
821
+ current[:subdirs][dir] ||= { files: [], subdirs: {} }
822
+ current = current[:subdirs][dir]
823
+ end
824
+
825
+ current[:files] << { title:, rdoc_path:, filename: }
826
+ end
827
+
828
+ # Sort files in each level alphabetically by title
829
+ sort_guides_tree(tree)
830
+ tree
831
+ end
832
+
833
+ def sort_guides_tree(node)
834
+ node[:files].sort_by! { |f| f[:title] }
835
+ node[:subdirs].each_value { |subdir| sort_guides_tree(subdir) }
836
+ end
837
+
838
+ # Recursively build navigation elements from the tree
839
+ # current_file_rel: path of current HTML file relative to rdoc_dir (e.g. "doc/getting_started/quickstart_md.html")
840
+ # current_tree_path: path in the tree we're building (e.g. "doc", "doc/getting_started")
841
+ def build_guides_nav(parent_ul, tree, doc, prefix, current_file_rel, current_tree_path)
842
+ # Add files at this level first
843
+ tree[:files].each do |file|
844
+ # Check if this file is the current page
845
+ is_current = (file[:rdoc_path] == current_file_rel)
846
+
847
+ li = Nokogiri::XML::Node.new("li", doc)
848
+ a = Nokogiri::XML::Node.new("a", doc)
849
+ a["href"] = "#{prefix}#{file[:rdoc_path]}"
850
+ if is_current
851
+ a["class"] = "active"
852
+ strong = Nokogiri::XML::Node.new("strong", doc)
853
+ strong.content = file[:title]
854
+ a.add_child(strong)
855
+ else
856
+ a.content = file[:title]
857
+ end
858
+ li.add_child(a)
859
+ parent_ul.add_child(li)
860
+ end
861
+
862
+ # Add subdirectories as collapsible details
863
+ tree[:subdirs].each do |dir_name, subtree|
864
+ subdir_path = "#{current_tree_path}/#{dir_name}"
865
+
866
+ # Check if current file is inside this subdirectory
867
+ # current_file_rel might be "doc/getting_started/quickstart_md.html"
868
+ # subdir_path would be "doc/getting_started"
869
+ is_current_in_subdir = current_file_rel.start_with?("#{subdir_path}/")
870
+
871
+ li = Nokogiri::XML::Node.new("li", doc)
872
+ details = Nokogiri::XML::Node.new("details", doc)
873
+ # Open if current file is inside this subdir
874
+ details["open"] = "open" if is_current_in_subdir
875
+ summary = Nokogiri::XML::Node.new("summary", doc)
876
+ summary.content = dir_name.tr("_-", " ").split.map(&:capitalize).join(" ")
877
+ details.add_child(summary)
878
+
879
+ subdir_ul = Nokogiri::XML::Node.new("ul", doc)
880
+ subdir_ul["class"] = "link-list nav-list"
881
+ build_guides_nav(subdir_ul, subtree, doc, prefix, current_file_rel, subdir_path)
882
+
883
+ details.add_child(subdir_ul)
884
+ li.add_child(details)
885
+ parent_ul.add_child(li)
886
+ end
619
887
  end
data/tasks/extension.rake CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #--
3
4
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
5
  # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
5
7
 
6
8
  require "rake/extensiontask"
7
9