ratatui_ruby 1.2.0 → 1.2.2

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 (260) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +2 -1
  3. data/ext/ratatui_ruby/Cargo.toml +2 -1
  4. data/ext/ratatui_ruby/src/events.rs +157 -18
  5. data/lib/ratatui_ruby/version.rb +1 -1
  6. metadata +1 -255
  7. data/.builds/ruby-3.2.yml +0 -54
  8. data/.builds/ruby-3.3.yml +0 -54
  9. data/.builds/ruby-3.4.yml +0 -54
  10. data/.builds/ruby-4.0.0.yml +0 -54
  11. data/.pre-commit-config.yaml +0 -16
  12. data/.rubocop.yml +0 -10
  13. data/AGENTS.md +0 -147
  14. data/CHANGELOG.md +0 -751
  15. data/README.md +0 -187
  16. data/README.rdoc +0 -302
  17. data/Rakefile +0 -11
  18. data/Steepfile +0 -50
  19. data/doc/concepts/application_architecture.md +0 -321
  20. data/doc/concepts/application_testing.md +0 -193
  21. data/doc/concepts/async.md +0 -190
  22. data/doc/concepts/custom_widgets.md +0 -247
  23. data/doc/concepts/debugging.md +0 -401
  24. data/doc/concepts/event_handling.md +0 -162
  25. data/doc/concepts/interactive_design.md +0 -146
  26. data/doc/contributors/auditing/parity.md +0 -239
  27. data/doc/contributors/design/ruby_frontend.md +0 -448
  28. data/doc/contributors/design/rust_backend.md +0 -434
  29. data/doc/contributors/design.md +0 -11
  30. data/doc/contributors/developing_examples.md +0 -400
  31. data/doc/contributors/documentation_style.md +0 -121
  32. data/doc/contributors/index.md +0 -21
  33. data/doc/contributors/releasing.md +0 -215
  34. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  35. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  36. data/doc/contributors/todo/align/term.md +0 -351
  37. data/doc/contributors/todo/align/terminal.md +0 -647
  38. data/doc/contributors/todo/future_work.md +0 -169
  39. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  40. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  41. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  42. data/doc/custom.css +0 -22
  43. data/doc/getting_started/quickstart.md +0 -291
  44. data/doc/getting_started/why.md +0 -93
  45. data/doc/images/app_all_events.png +0 -0
  46. data/doc/images/app_cli_rich_moments.gif +0 -0
  47. data/doc/images/app_color_picker.png +0 -0
  48. data/doc/images/app_debugging_showcase.gif +0 -0
  49. data/doc/images/app_debugging_showcase.png +0 -0
  50. data/doc/images/app_external_editor.gif +0 -0
  51. data/doc/images/app_login_form.png +0 -0
  52. data/doc/images/app_stateful_interaction.png +0 -0
  53. data/doc/images/verify_quickstart_dsl.png +0 -0
  54. data/doc/images/verify_quickstart_layout.png +0 -0
  55. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  56. data/doc/images/verify_readme_usage.png +0 -0
  57. data/doc/images/widget_barchart.png +0 -0
  58. data/doc/images/widget_block.png +0 -0
  59. data/doc/images/widget_box.png +0 -0
  60. data/doc/images/widget_calendar.png +0 -0
  61. data/doc/images/widget_canvas.png +0 -0
  62. data/doc/images/widget_cell.png +0 -0
  63. data/doc/images/widget_center.png +0 -0
  64. data/doc/images/widget_chart.png +0 -0
  65. data/doc/images/widget_gauge.png +0 -0
  66. data/doc/images/widget_layout_split.png +0 -0
  67. data/doc/images/widget_line_gauge.png +0 -0
  68. data/doc/images/widget_list.png +0 -0
  69. data/doc/images/widget_map.png +0 -0
  70. data/doc/images/widget_overlay.png +0 -0
  71. data/doc/images/widget_popup.png +0 -0
  72. data/doc/images/widget_ratatui_logo.png +0 -0
  73. data/doc/images/widget_ratatui_mascot.png +0 -0
  74. data/doc/images/widget_rect.png +0 -0
  75. data/doc/images/widget_render.png +0 -0
  76. data/doc/images/widget_rich_text.png +0 -0
  77. data/doc/images/widget_scroll_text.png +0 -0
  78. data/doc/images/widget_scrollbar.png +0 -0
  79. data/doc/images/widget_sparkline.png +0 -0
  80. data/doc/images/widget_style_colors.png +0 -0
  81. data/doc/images/widget_table.png +0 -0
  82. data/doc/images/widget_tabs.png +0 -0
  83. data/doc/images/widget_text_width.png +0 -0
  84. data/doc/index.md +0 -34
  85. data/doc/troubleshooting/async.md +0 -4
  86. data/doc/troubleshooting/terminal_limitations.md +0 -131
  87. data/doc/troubleshooting/tui_output.md +0 -197
  88. data/examples/app_all_events/README.md +0 -114
  89. data/examples/app_all_events/app.rb +0 -98
  90. data/examples/app_all_events/model/app_model.rb +0 -159
  91. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  92. data/examples/app_all_events/model/event_entry.rb +0 -94
  93. data/examples/app_all_events/model/msg.rb +0 -39
  94. data/examples/app_all_events/model/timestamp.rb +0 -56
  95. data/examples/app_all_events/update.rb +0 -75
  96. data/examples/app_all_events/view/app_view.rb +0 -80
  97. data/examples/app_all_events/view/controls_view.rb +0 -54
  98. data/examples/app_all_events/view/counts_view.rb +0 -61
  99. data/examples/app_all_events/view/live_view.rb +0 -72
  100. data/examples/app_all_events/view/log_view.rb +0 -57
  101. data/examples/app_all_events/view.rb +0 -9
  102. data/examples/app_cli_rich_moments/README.md +0 -81
  103. data/examples/app_cli_rich_moments/app.rb +0 -189
  104. data/examples/app_color_picker/README.md +0 -156
  105. data/examples/app_color_picker/app.rb +0 -76
  106. data/examples/app_color_picker/clipboard.rb +0 -86
  107. data/examples/app_color_picker/color.rb +0 -193
  108. data/examples/app_color_picker/controls.rb +0 -92
  109. data/examples/app_color_picker/copy_dialog.rb +0 -168
  110. data/examples/app_color_picker/export_pane.rb +0 -128
  111. data/examples/app_color_picker/harmony.rb +0 -58
  112. data/examples/app_color_picker/input.rb +0 -176
  113. data/examples/app_color_picker/main_container.rb +0 -180
  114. data/examples/app_color_picker/palette.rb +0 -111
  115. data/examples/app_debugging_showcase/README.md +0 -119
  116. data/examples/app_debugging_showcase/app.rb +0 -318
  117. data/examples/app_external_editor/README.md +0 -62
  118. data/examples/app_external_editor/app.rb +0 -344
  119. data/examples/app_login_form/README.md +0 -58
  120. data/examples/app_login_form/app.rb +0 -109
  121. data/examples/app_stateful_interaction/README.md +0 -35
  122. data/examples/app_stateful_interaction/app.rb +0 -328
  123. data/examples/timeout_demo.rb +0 -45
  124. data/examples/verify_quickstart_dsl/README.md +0 -55
  125. data/examples/verify_quickstart_dsl/app.rb +0 -49
  126. data/examples/verify_quickstart_layout/README.md +0 -77
  127. data/examples/verify_quickstart_layout/app.rb +0 -73
  128. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  129. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  130. data/examples/verify_readme_usage/README.md +0 -49
  131. data/examples/verify_readme_usage/app.rb +0 -42
  132. data/examples/verify_website_managed/README.md +0 -48
  133. data/examples/verify_website_managed/app.rb +0 -36
  134. data/examples/verify_website_menu/README.md +0 -60
  135. data/examples/verify_website_menu/app.rb +0 -84
  136. data/examples/verify_website_spinner/README.md +0 -44
  137. data/examples/verify_website_spinner/app.rb +0 -34
  138. data/examples/widget_barchart/README.md +0 -58
  139. data/examples/widget_barchart/app.rb +0 -240
  140. data/examples/widget_block/README.md +0 -44
  141. data/examples/widget_block/app.rb +0 -258
  142. data/examples/widget_box/README.md +0 -54
  143. data/examples/widget_box/app.rb +0 -255
  144. data/examples/widget_calendar/README.md +0 -48
  145. data/examples/widget_calendar/app.rb +0 -115
  146. data/examples/widget_canvas/README.md +0 -31
  147. data/examples/widget_canvas/app.rb +0 -130
  148. data/examples/widget_cell/README.md +0 -45
  149. data/examples/widget_cell/app.rb +0 -112
  150. data/examples/widget_center/README.md +0 -33
  151. data/examples/widget_center/app.rb +0 -118
  152. data/examples/widget_chart/README.md +0 -50
  153. data/examples/widget_chart/app.rb +0 -220
  154. data/examples/widget_gauge/README.md +0 -50
  155. data/examples/widget_gauge/app.rb +0 -229
  156. data/examples/widget_layout_split/README.md +0 -53
  157. data/examples/widget_layout_split/app.rb +0 -260
  158. data/examples/widget_line_gauge/README.md +0 -50
  159. data/examples/widget_line_gauge/app.rb +0 -219
  160. data/examples/widget_list/README.md +0 -58
  161. data/examples/widget_list/app.rb +0 -382
  162. data/examples/widget_map/README.md +0 -48
  163. data/examples/widget_map/app.rb +0 -95
  164. data/examples/widget_overlay/README.md +0 -45
  165. data/examples/widget_overlay/app.rb +0 -250
  166. data/examples/widget_popup/README.md +0 -45
  167. data/examples/widget_popup/app.rb +0 -106
  168. data/examples/widget_ratatui_logo/README.md +0 -43
  169. data/examples/widget_ratatui_logo/app.rb +0 -104
  170. data/examples/widget_ratatui_mascot/README.md +0 -43
  171. data/examples/widget_ratatui_mascot/app.rb +0 -95
  172. data/examples/widget_rect/README.md +0 -53
  173. data/examples/widget_rect/app.rb +0 -222
  174. data/examples/widget_render/README.md +0 -46
  175. data/examples/widget_render/app.rb +0 -186
  176. data/examples/widget_render/app.rbs +0 -41
  177. data/examples/widget_rich_text/README.md +0 -44
  178. data/examples/widget_rich_text/app.rb +0 -193
  179. data/examples/widget_scroll_text/README.md +0 -46
  180. data/examples/widget_scroll_text/app.rb +0 -109
  181. data/examples/widget_scrollbar/README.md +0 -46
  182. data/examples/widget_scrollbar/app.rb +0 -155
  183. data/examples/widget_sparkline/README.md +0 -51
  184. data/examples/widget_sparkline/app.rb +0 -277
  185. data/examples/widget_style_colors/README.md +0 -43
  186. data/examples/widget_style_colors/app.rb +0 -83
  187. data/examples/widget_table/README.md +0 -57
  188. data/examples/widget_table/app.rb +0 -285
  189. data/examples/widget_tabs/README.md +0 -50
  190. data/examples/widget_tabs/app.rb +0 -183
  191. data/examples/widget_text_width/README.md +0 -44
  192. data/examples/widget_text_width/app.rb +0 -117
  193. data/migrate_to_buffer.rb +0 -145
  194. data/mise.toml +0 -8
  195. data/tasks/autodoc/examples.rb +0 -87
  196. data/tasks/autodoc/member.rb +0 -58
  197. data/tasks/autodoc/name.rb +0 -21
  198. data/tasks/autodoc.rake +0 -21
  199. data/tasks/bump/bump_workflow.rb +0 -49
  200. data/tasks/bump/cargo_lockfile.rb +0 -21
  201. data/tasks/bump/changelog.rb +0 -104
  202. data/tasks/bump/header.rb +0 -32
  203. data/tasks/bump/history.rb +0 -32
  204. data/tasks/bump/links.rb +0 -69
  205. data/tasks/bump/manifest.rb +0 -33
  206. data/tasks/bump/patch_release.rb +0 -19
  207. data/tasks/bump/release_branch.rb +0 -17
  208. data/tasks/bump/release_from_trunk.rb +0 -49
  209. data/tasks/bump/repository.rb +0 -54
  210. data/tasks/bump/ruby_gem.rb +0 -29
  211. data/tasks/bump/sem_ver.rb +0 -44
  212. data/tasks/bump/unreleased_section.rb +0 -73
  213. data/tasks/bump.rake +0 -61
  214. data/tasks/doc/documentation.rb +0 -59
  215. data/tasks/doc/link/file_url.rb +0 -30
  216. data/tasks/doc/link/relative_path.rb +0 -61
  217. data/tasks/doc/link/web_url.rb +0 -55
  218. data/tasks/doc/link.rb +0 -52
  219. data/tasks/doc/link_audit.rb +0 -116
  220. data/tasks/doc/problem.rb +0 -40
  221. data/tasks/doc/source_file.rb +0 -93
  222. data/tasks/doc.rake +0 -905
  223. data/tasks/example_viewer.html.erb +0 -172
  224. data/tasks/extension.rake +0 -14
  225. data/tasks/license/headers_md.rb +0 -223
  226. data/tasks/license/headers_rb.rb +0 -210
  227. data/tasks/license/license_utils.rb +0 -130
  228. data/tasks/license/snippets_md.rb +0 -315
  229. data/tasks/license/snippets_rdoc.rb +0 -150
  230. data/tasks/license.rake +0 -91
  231. data/tasks/lint.rake +0 -170
  232. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  233. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  234. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  235. data/tasks/rbs_predicates.rake +0 -31
  236. data/tasks/rdoc_config.rb +0 -29
  237. data/tasks/resources/build.yml.erb +0 -60
  238. data/tasks/resources/index.html.erb +0 -141
  239. data/tasks/resources/rubies.yml +0 -7
  240. data/tasks/sourcehut.rake +0 -122
  241. data/tasks/steep.rake +0 -11
  242. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  243. data/tasks/terminal_preview/crash_report.rb +0 -54
  244. data/tasks/terminal_preview/example_app.rb +0 -27
  245. data/tasks/terminal_preview/launcher_script.rb +0 -48
  246. data/tasks/terminal_preview/preview_collection.rb +0 -60
  247. data/tasks/terminal_preview/preview_timing.rb +0 -24
  248. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  249. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  250. data/tasks/terminal_preview/system_appearance.rb +0 -13
  251. data/tasks/terminal_preview/terminal_window.rb +0 -138
  252. data/tasks/terminal_preview/window_id.rb +0 -16
  253. data/tasks/terminal_preview.rake +0 -30
  254. data/tasks/test.rake +0 -36
  255. data/tasks/website/index_page.rb +0 -30
  256. data/tasks/website/version.rb +0 -122
  257. data/tasks/website/version_menu.rb +0 -68
  258. data/tasks/website/versioned_documentation.rb +0 -83
  259. data/tasks/website/website.rb +0 -53
  260. data/tasks/website.rake +0 -28
data/tasks/sourcehut.rake DELETED
@@ -1,122 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- desc "Generate SourceHut build manifests from template"
9
- task sourcehut: "sourcehut:build"
10
-
11
- namespace :sourcehut do
12
- desc "Build SourceHut manifests"
13
- task build: "sourcehut:build:manifest"
14
-
15
- namespace :build do
16
- desc "Generate SourceHut build manifests from template"
17
- task :manifest do
18
- require "erb"
19
- require "yaml"
20
-
21
- spec = Gem::Specification.load("ratatui_ruby.gemspec")
22
-
23
- # Read version directly from file to ensure we get the latest version
24
- # even if it was just bumped in the same Rake execution
25
- version_content = File.read("lib/ratatui_ruby/version.rb")
26
- version = version_content.match(/VERSION = "(.+?)"/)[1]
27
-
28
- # Normalize version using Gem::Version - RubyGems converts hyphens
29
- # (e.g., "1.0.0-beta.1" -> "1.0.0.pre.beta.1")
30
- normalized_version = Gem::Version.new(version).to_s
31
-
32
- gem_filename = "#{spec.name}-#{normalized_version}.gem"
33
-
34
- rubies = YAML.load_file("tasks/resources/rubies.yml")
35
-
36
- bundler_version = File.read("Gemfile.lock").match(/BUNDLED WITH\n\s+([\d.]+)/)[1]
37
-
38
- template = File.read("tasks/resources/build.yml.erb")
39
- erb = ERB.new(template, trim_mode: "-")
40
-
41
- FileUtils.mkdir_p ".builds"
42
-
43
- # Remove old generated files to ensure a clean state
44
- Dir.glob(".builds/*.yml").each { |f| File.delete(f) }
45
-
46
- rubies.each do |ruby_version|
47
- filename = ".builds/ruby-#{ruby_version}.yml"
48
- puts "Generating #{filename}..."
49
- gem_name = spec.name
50
- has_rust = File.exist?("ext/#{gem_name}/Cargo.toml")
51
- content = erb.result_with_hash(ruby_version:, gem_name:, gem_filename:, bundler_version:, has_rust:)
52
- File.write(filename, content)
53
- end
54
- end
55
- end
56
-
57
- desc "Update stable branch to match release and set as default"
58
- task :update_stable do
59
- # Read version to determine tag
60
- version_content = File.read("lib/ratatui_ruby/version.rb")
61
- version = version_content.match(/VERSION = "(.+?)"/)[1]
62
- tag_name = "v#{version}"
63
-
64
- # Verify that the version file matches the actual git tag
65
- # This prevents updating stable to the wrong version if the release failed
66
- latest_tag = `git describe --tags --abbrev=0`.strip
67
- if latest_tag != tag_name
68
- abort "Fatal: Version mismatch! 'lib/ratatui_ruby/version.rb' says #{tag_name}, but the latest git tag is #{latest_tag}."
69
- end
70
-
71
- # Get current stable version (if stable branch exists)
72
- stable_version_str = `git show stable:lib/ratatui_ruby/version.rb 2>/dev/null`.match(/VERSION = "(.+?)"/)&.[](1)
73
- if stable_version_str
74
- stable_version = Gem::Version.new(stable_version_str)
75
- new_version = Gem::Version.new(version)
76
-
77
- if new_version <= stable_version
78
- puts "Skipping stable update: #{version} is not newer than current stable #{stable_version_str}"
79
- next
80
- end
81
- end
82
-
83
- puts "Updating stable branch to point to #{tag_name}..."
84
- # Resolve the tag to a commit hash (peel annotated tags)
85
- # This renders a commit SHA that can be pushed to a branch head
86
- commit_sha = `git rev-parse #{tag_name}^{}`.strip
87
-
88
- # Update local stable branch to match
89
- sh "git branch -f stable #{commit_sha}"
90
-
91
- # Push the commit to remote stable branch
92
- # Force-push because stable is reset to each release, not fast-forwarded.
93
- sh "git push --force origin #{commit_sha}:stable"
94
- end
95
- end
96
-
97
- if Rake::Task.task_defined?("release")
98
- Rake::Task["release"].enhance do
99
- # Replace Bundler's normalized tag with semver-style for prerelease versions.
100
- # Bundler creates tags using normalized Gem::Version (e.g., v1.0.0.pre.beta.1).
101
- # Semver uses hyphens (e.g., v1.0.0-beta.1). We want only the semver tag.
102
- version_content = File.read("lib/ratatui_ruby/version.rb")
103
- version = version_content.match(/VERSION = "(.+?)"/)[1]
104
-
105
- if version.include?("-")
106
- normalized_tag = "v#{Gem::Version.new(version)}"
107
- semver_tag = "v#{version}"
108
-
109
- if normalized_tag != semver_tag
110
- puts "Replacing normalized tag #{normalized_tag} with semver tag #{semver_tag}..."
111
- # Delete the normalized tag locally and remotely
112
- sh "git tag -d #{normalized_tag}"
113
- sh "git push origin :refs/tags/#{normalized_tag}"
114
- # Create the semver tag pointing to the same commit
115
- sh "git tag #{semver_tag} HEAD"
116
- sh "git push origin #{semver_tag}"
117
- end
118
- end
119
-
120
- Rake::Task["sourcehut:update_stable"].invoke
121
- end
122
- end
data/tasks/steep.rake DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- desc "Run Steep type checker"
9
- task :steep do
10
- sh "bundle exec steep check"
11
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require "tmpdir"
9
- require_relative "launcher_script"
10
- require_relative "terminal_window"
11
- require_relative "crash_report"
12
-
13
- class AppScreenshot < Data.define(:app, :output_path)
14
- def capture
15
- print " 📸 #{app}..."
16
-
17
- LauncherScript.new(app.app_path, Dir.pwd).run do |launcher|
18
- TerminalWindow.new(launcher.path, launcher.pid_file).open do |window|
19
- take_snapshot(window.window_id)
20
-
21
- if File.size?(output_path)
22
- puts " done."
23
- true
24
- else
25
- FileUtils.rm_f(output_path)
26
- puts " FAILED"
27
- puts
28
- puts " Window rendered nothing (app may have crashed before drawing)"
29
- puts
30
- false
31
- end
32
- end
33
- end
34
- rescue => e
35
- puts " FAILED"
36
- puts
37
- puts CrashReport.new(app, e, "Program crashed before screenshot could be taken:")
38
- puts
39
- false
40
- end
41
-
42
- private def take_snapshot(window_id)
43
- system("screencapture", "-l", window_id.to_s, "-o", "-x", output_path)
44
- end
45
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- class CrashReport < Data.define(:app, :error, :preamble)
9
- def self.new(app, error, preamble = nil)
10
- # Allow preamble to be optional while Data.define requires all fields
11
- super(app:, error:, preamble:)
12
- end
13
-
14
- def to_s
15
- output = error.message.strip
16
- formatted_error = output.split("\n").map { |line| format_line(line) }.join("\n")
17
- preamble_section = preamble ? <<~PREAMBLE.chomp : ""
18
- #{box_top}
19
- #{format_line(preamble)}
20
- #{box_bottom}
21
- PREAMBLE
22
-
23
- <<~TEXT
24
- #{preamble_section}
25
- #{border_top(app.to_s)}
26
- #{formatted_error}
27
- #{box_bottom}
28
- TEXT
29
- end
30
-
31
- private def box_top
32
- "┌#{'─' * (width - 2)}┐"
33
- end
34
-
35
- private def box_bottom
36
- "└#{'─' * (width - 2)}┘"
37
- end
38
-
39
- private def border_top(title)
40
- left = "┌─ #{title} "
41
- right = "┐"
42
- dashes = "─" * (width - left.length - right.length)
43
- left + dashes + right
44
- end
45
-
46
- private def format_line(line)
47
- truncated = line[0...(width - 4)]
48
- "│ #{truncated.ljust(width - 4)} │"
49
- end
50
-
51
- private def width
52
- 80
53
- end
54
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- class ExampleApp < Data.define(:directory)
9
- def self.all
10
- examples_dir = File.expand_path("../../examples", __dir__)
11
- Dir.glob("#{examples_dir}/*/app.rb").map do |path|
12
- new(File.basename(File.dirname(path)))
13
- end.sort_by(&:directory)
14
- end
15
-
16
- def app_path
17
- "examples/#{directory}/app.rb"
18
- end
19
-
20
- def screenshot_filename
21
- "#{directory}.png"
22
- end
23
-
24
- def to_s
25
- directory
26
- end
27
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require "fileutils"
9
- require "tmpdir"
10
-
11
- class LauncherScript < Data.define(:app_path, :repo_root)
12
- def initialize(app_path:, repo_root:)
13
- super
14
- write
15
- end
16
-
17
- def run
18
- yield self
19
- ensure
20
- cleanup
21
- end
22
-
23
- def path
24
- File.join(Dir.tmpdir, "preview_launcher.sh")
25
- end
26
-
27
- def pid_file
28
- File.join(Dir.tmpdir, "preview_launcher.pid")
29
- end
30
-
31
- private def cleanup
32
- FileUtils.rm_f(path)
33
- FileUtils.rm_f(pid_file)
34
- rescue Errno::ENOENT
35
- # Already deleted
36
- end
37
-
38
- private def write
39
- File.open(path, "w") do |f|
40
- f.puts "#!/bin/zsh"
41
- f.puts "cd '#{repo_root}'"
42
- f.puts "clear"
43
- f.puts "echo $$ > '#{pid_file}'"
44
- f.puts "exec bundle exec ruby '#{app_path}'"
45
- end
46
- FileUtils.chmod(0o755, path)
47
- end
48
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require "fileutils"
9
- require_relative "example_app"
10
- require_relative "app_screenshot"
11
- require_relative "crash_report"
12
- require_relative "preview_timing"
13
- require_relative "safety_confirmation"
14
- require_relative "saved_screenshot"
15
-
16
- class PreviewCollection
17
- def initialize(output_dir)
18
- @output_dir = output_dir
19
- end
20
-
21
- def generate
22
- abort "Error: This task requires macOS." unless RUBY_PLATFORM.match?(/darwin/)
23
-
24
- apps = ExampleApp.all
25
- stale_count = count_stale_apps(apps)
26
-
27
- if stale_count.zero?
28
- puts "\n✨ All #{apps.count} screenshots are up to date."
29
- return
30
- end
31
-
32
- SafetyConfirmation.new(stale_count, apps.count).request
33
-
34
- puts "\nHere we go!"
35
- failures = apps.count { |app| !capture_app(app) }
36
-
37
- if failures.zero?
38
- puts "\n✨ All captures complete. Check doc/images/."
39
- else
40
- abort "\n❌ #{failures} capture(s) failed."
41
- end
42
- end
43
-
44
- private def count_stale_apps(apps)
45
- apps.count { |app| SavedScreenshot.for(app, @output_dir).stale? }
46
- end
47
-
48
- private def capture_app(app)
49
- saved = SavedScreenshot.for(app, @output_dir)
50
-
51
- if saved.stale?
52
- success = AppScreenshot.new(app, saved.path).capture
53
- sleep PreviewTiming.between_captures
54
- success
55
- else
56
- puts " ⏭️ #{app} (unchanged)"
57
- true
58
- end
59
- end
60
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- class PreviewTiming
9
- def self.window_startup
10
- 1.5
11
- end
12
-
13
- def self.between_captures
14
- 0.2
15
- end
16
-
17
- def self.close_delay
18
- 1.0
19
- end
20
-
21
- def self.total
22
- window_startup + close_delay + between_captures
23
- end
24
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "preview_timing"
9
- require_relative "system_appearance"
10
-
11
- class SafetyConfirmation
12
- def initialize(stale_count, total_count)
13
- @stale_count = stale_count
14
- @total_count = total_count
15
- end
16
-
17
- def request
18
- print_warning
19
- wait_for_user
20
- end
21
-
22
- private def print_warning
23
- unchanged_count = @total_count - @stale_count
24
- puts "\n#{'=' * 60}"
25
- puts " 📸 NATIVE TERMINAL CAPTURE 📸"
26
- puts "=" * 60
27
- puts "This task will:"
28
- puts " 1. Take control of your mouse/keyboard focus"
29
- puts " 2. Rapidly open and close Terminal windows"
30
- puts " 3. Capture #{@stale_count} screenshots (#{unchanged_count} unchanged)"
31
- puts
32
- puts "Before starting, be sure Terminal.app has the following permissions"
33
- puts "in System Settings.app -> Privacy & Security:"
34
- puts " - Screen & System Audio Recording"
35
- puts " - Automation -> System Events"
36
- puts
37
- puts "⚠️ PLEASE DO NOT TOUCH YOUR COMPUTER WHILE THIS RUNS."
38
- min_time = (@stale_count * PreviewTiming.total).to_i
39
- max_time = (@stale_count * (PreviewTiming.total + PreviewTiming.close_delay)).to_i
40
- puts " (Estimated time: #{min_time}-#{max_time} seconds)"
41
- puts
42
- end
43
-
44
- private def wait_for_user
45
- loop do
46
- print "Continue? [Y/n]: "
47
- response = $stdin.gets.strip.downcase
48
-
49
- if response.empty? || response == "y"
50
- return if SystemAppearance.dark?
51
- puts "⚠️ Dark Mode is not enabled. Please enable it in System Settings or Control Center before proceeding."
52
- puts
53
- elsif response == "n"
54
- abort "Cancelled."
55
- end
56
- end
57
- end
58
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require "time"
9
-
10
- class SavedScreenshot < Data.define(:app, :path)
11
- def self.for(app, output_dir)
12
- new(app, File.join(output_dir, app.screenshot_filename))
13
- end
14
-
15
- def stale?
16
- return true unless valid?
17
-
18
- app_last_modified > screenshot_last_commit_time
19
- end
20
-
21
- private def valid?
22
- # File must exist and have content (not 0 bytes)
23
- File.size?(path) || false
24
- end
25
-
26
- private def app_last_modified
27
- # If the file has staged or unstaged changes, it's definitely stale
28
- return Time.now.to_i if changed?
29
-
30
- # Otherwise, compare against the last git commit time
31
- app_last_commit_time
32
- end
33
-
34
- private def changed?
35
- system("git diff HEAD --quiet #{app.app_path} 2>/dev/null")
36
- !$?.success?
37
- end
38
-
39
- private def app_last_commit_time
40
- output = `git log -1 --format=%cI "#{app.app_path}" 2>/dev/null`.strip
41
- return 0 if output.empty?
42
-
43
- Time.iso8601(output).to_i
44
- rescue
45
- 0
46
- end
47
-
48
- private def screenshot_last_commit_time
49
- output = `git log -1 --format=%cI "#{path}" 2>/dev/null`.strip
50
- return Time.now.to_i if output.empty?
51
-
52
- Time.iso8601(output).to_i
53
- rescue
54
- Time.now.to_i
55
- end
56
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- class SystemAppearance
9
- def self.dark?
10
- result = `osascript -e 'tell application "System Events" to tell appearance preferences to get dark mode'`.strip
11
- result == "true"
12
- end
13
- end
@@ -1,138 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "window_id"
9
- require_relative "preview_timing"
10
-
11
- class TerminalWindow
12
- CTRL_C = "ASCII character 3"
13
-
14
- def initialize(launcher_script_path, pid_file)
15
- @launcher_script_path = launcher_script_path
16
- @pid_file = pid_file
17
- @window_id = nil
18
- end
19
-
20
- def open
21
- setup_script = <<~APPLESCRIPT
22
- tell application "Terminal"
23
- set newTab to do script "#{@launcher_script_path}"
24
- set currentWindow to window 1
25
-
26
- set number of rows of currentWindow to 24
27
- set number of columns of currentWindow to 80
28
- set position of currentWindow to {100, 100}
29
- set frontmost of currentWindow to true
30
-
31
- return id of currentWindow
32
- end tell
33
- APPLESCRIPT
34
-
35
- @window_id = WindowID.new(`osascript -e '#{setup_script}'`.strip)
36
- wait_for_startup
37
- yield self
38
- ensure
39
- close if @window_id
40
- end
41
-
42
- def window_id
43
- @window_id
44
- end
45
-
46
- private def close
47
- try_graceful_shutdown
48
- kill_process if process_still_alive?
49
-
50
- delay_script = <<~APPLESCRIPT
51
- tell application "Terminal"
52
- delay #{PreviewTiming.close_delay}
53
-
54
- try
55
- close window id #{@window_id}
56
- end try
57
- end tell
58
- APPLESCRIPT
59
-
60
- system("osascript", "-e", delay_script, out: File::NULL, err: File::NULL)
61
- end
62
-
63
- private def wait_for_startup
64
- sleep PreviewTiming.window_startup
65
-
66
- unless @window_id.valid?
67
- raise "Failed to open terminal window"
68
- end
69
-
70
- unless process_running?
71
- error_output = contents
72
- raise error_output
73
- end
74
- end
75
-
76
- private def try_graceful_shutdown
77
- shutdown_script = <<~APPLESCRIPT
78
- tell application "Terminal"
79
- try
80
- do script (#{CTRL_C}) in window id #{@window_id}
81
- end try
82
- end tell
83
- APPLESCRIPT
84
-
85
- system("osascript", "-e", shutdown_script, out: File::NULL, err: File::NULL)
86
- sleep 0.2
87
- end
88
-
89
- private def process_still_alive?
90
- return false unless @pid_file && File.exist?(@pid_file)
91
-
92
- pid = File.read(@pid_file).strip.to_i
93
- Process.kill(0, pid)
94
- true
95
- rescue Errno::ESRCH, Errno::ENOENT
96
- false
97
- end
98
-
99
- private def kill_process
100
- return unless @pid_file && File.exist?(@pid_file)
101
-
102
- pid = File.read(@pid_file).strip.to_i
103
- Process.kill("TERM", pid)
104
- rescue Errno::ESRCH, Errno::ENOENT
105
- # Process already gone or PID file doesn't exist
106
- end
107
-
108
- private def process_running?
109
- check_script = <<~APPLESCRIPT
110
- tell application "Terminal"
111
- try
112
- set theWindow to window id #{@window_id}
113
- return busy of theWindow
114
- on error
115
- return false
116
- end try
117
- end tell
118
- APPLESCRIPT
119
-
120
- result = `osascript -e '#{check_script}'`.strip
121
- result == "true"
122
- end
123
-
124
- private def contents
125
- read_script = <<~APPLESCRIPT
126
- tell application "Terminal"
127
- try
128
- set theWindow to window id #{@window_id}
129
- return contents of selected tab of theWindow
130
- on error
131
- return ""
132
- end try
133
- end tell
134
- APPLESCRIPT
135
-
136
- `osascript -e '#{read_script}'`
137
- end
138
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- class WindowID < Data.define(:value)
9
- def valid?
10
- !value.empty? && value.match?(/^\d+$/)
11
- end
12
-
13
- def to_s
14
- value
15
- end
16
- end