ratatui_ruby 1.1.0 → 1.1.1

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 (259) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +1 -1
  3. data/ext/ratatui_ruby/Cargo.toml +1 -1
  4. data/lib/ratatui_ruby/version.rb +1 -1
  5. metadata +1 -255
  6. data/.builds/ruby-3.2.yml +0 -54
  7. data/.builds/ruby-3.3.yml +0 -54
  8. data/.builds/ruby-3.4.yml +0 -54
  9. data/.builds/ruby-4.0.0.yml +0 -54
  10. data/.pre-commit-config.yaml +0 -16
  11. data/.rubocop.yml +0 -10
  12. data/AGENTS.md +0 -147
  13. data/CHANGELOG.md +0 -736
  14. data/README.md +0 -187
  15. data/README.rdoc +0 -302
  16. data/Rakefile +0 -11
  17. data/Steepfile +0 -50
  18. data/doc/concepts/application_architecture.md +0 -321
  19. data/doc/concepts/application_testing.md +0 -193
  20. data/doc/concepts/async.md +0 -190
  21. data/doc/concepts/custom_widgets.md +0 -247
  22. data/doc/concepts/debugging.md +0 -401
  23. data/doc/concepts/event_handling.md +0 -162
  24. data/doc/concepts/interactive_design.md +0 -146
  25. data/doc/contributors/auditing/parity.md +0 -239
  26. data/doc/contributors/design/ruby_frontend.md +0 -448
  27. data/doc/contributors/design/rust_backend.md +0 -434
  28. data/doc/contributors/design.md +0 -11
  29. data/doc/contributors/developing_examples.md +0 -400
  30. data/doc/contributors/documentation_style.md +0 -121
  31. data/doc/contributors/index.md +0 -21
  32. data/doc/contributors/releasing.md +0 -215
  33. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  34. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  35. data/doc/contributors/todo/align/term.md +0 -351
  36. data/doc/contributors/todo/align/terminal.md +0 -647
  37. data/doc/contributors/todo/future_work.md +0 -169
  38. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  39. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  40. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  41. data/doc/custom.css +0 -22
  42. data/doc/getting_started/quickstart.md +0 -291
  43. data/doc/getting_started/why.md +0 -93
  44. data/doc/images/app_all_events.png +0 -0
  45. data/doc/images/app_cli_rich_moments.gif +0 -0
  46. data/doc/images/app_color_picker.png +0 -0
  47. data/doc/images/app_debugging_showcase.gif +0 -0
  48. data/doc/images/app_debugging_showcase.png +0 -0
  49. data/doc/images/app_external_editor.gif +0 -0
  50. data/doc/images/app_login_form.png +0 -0
  51. data/doc/images/app_stateful_interaction.png +0 -0
  52. data/doc/images/verify_quickstart_dsl.png +0 -0
  53. data/doc/images/verify_quickstart_layout.png +0 -0
  54. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  55. data/doc/images/verify_readme_usage.png +0 -0
  56. data/doc/images/widget_barchart.png +0 -0
  57. data/doc/images/widget_block.png +0 -0
  58. data/doc/images/widget_box.png +0 -0
  59. data/doc/images/widget_calendar.png +0 -0
  60. data/doc/images/widget_canvas.png +0 -0
  61. data/doc/images/widget_cell.png +0 -0
  62. data/doc/images/widget_center.png +0 -0
  63. data/doc/images/widget_chart.png +0 -0
  64. data/doc/images/widget_gauge.png +0 -0
  65. data/doc/images/widget_layout_split.png +0 -0
  66. data/doc/images/widget_line_gauge.png +0 -0
  67. data/doc/images/widget_list.png +0 -0
  68. data/doc/images/widget_map.png +0 -0
  69. data/doc/images/widget_overlay.png +0 -0
  70. data/doc/images/widget_popup.png +0 -0
  71. data/doc/images/widget_ratatui_logo.png +0 -0
  72. data/doc/images/widget_ratatui_mascot.png +0 -0
  73. data/doc/images/widget_rect.png +0 -0
  74. data/doc/images/widget_render.png +0 -0
  75. data/doc/images/widget_rich_text.png +0 -0
  76. data/doc/images/widget_scroll_text.png +0 -0
  77. data/doc/images/widget_scrollbar.png +0 -0
  78. data/doc/images/widget_sparkline.png +0 -0
  79. data/doc/images/widget_style_colors.png +0 -0
  80. data/doc/images/widget_table.png +0 -0
  81. data/doc/images/widget_tabs.png +0 -0
  82. data/doc/images/widget_text_width.png +0 -0
  83. data/doc/index.md +0 -34
  84. data/doc/troubleshooting/async.md +0 -4
  85. data/doc/troubleshooting/terminal_limitations.md +0 -131
  86. data/doc/troubleshooting/tui_output.md +0 -197
  87. data/examples/app_all_events/README.md +0 -114
  88. data/examples/app_all_events/app.rb +0 -98
  89. data/examples/app_all_events/model/app_model.rb +0 -159
  90. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  91. data/examples/app_all_events/model/event_entry.rb +0 -94
  92. data/examples/app_all_events/model/msg.rb +0 -39
  93. data/examples/app_all_events/model/timestamp.rb +0 -56
  94. data/examples/app_all_events/update.rb +0 -75
  95. data/examples/app_all_events/view/app_view.rb +0 -80
  96. data/examples/app_all_events/view/controls_view.rb +0 -54
  97. data/examples/app_all_events/view/counts_view.rb +0 -61
  98. data/examples/app_all_events/view/live_view.rb +0 -72
  99. data/examples/app_all_events/view/log_view.rb +0 -57
  100. data/examples/app_all_events/view.rb +0 -9
  101. data/examples/app_cli_rich_moments/README.md +0 -81
  102. data/examples/app_cli_rich_moments/app.rb +0 -189
  103. data/examples/app_color_picker/README.md +0 -156
  104. data/examples/app_color_picker/app.rb +0 -76
  105. data/examples/app_color_picker/clipboard.rb +0 -86
  106. data/examples/app_color_picker/color.rb +0 -193
  107. data/examples/app_color_picker/controls.rb +0 -92
  108. data/examples/app_color_picker/copy_dialog.rb +0 -168
  109. data/examples/app_color_picker/export_pane.rb +0 -128
  110. data/examples/app_color_picker/harmony.rb +0 -58
  111. data/examples/app_color_picker/input.rb +0 -176
  112. data/examples/app_color_picker/main_container.rb +0 -180
  113. data/examples/app_color_picker/palette.rb +0 -111
  114. data/examples/app_debugging_showcase/README.md +0 -119
  115. data/examples/app_debugging_showcase/app.rb +0 -318
  116. data/examples/app_external_editor/README.md +0 -62
  117. data/examples/app_external_editor/app.rb +0 -344
  118. data/examples/app_login_form/README.md +0 -58
  119. data/examples/app_login_form/app.rb +0 -109
  120. data/examples/app_stateful_interaction/README.md +0 -35
  121. data/examples/app_stateful_interaction/app.rb +0 -328
  122. data/examples/timeout_demo.rb +0 -45
  123. data/examples/verify_quickstart_dsl/README.md +0 -55
  124. data/examples/verify_quickstart_dsl/app.rb +0 -49
  125. data/examples/verify_quickstart_layout/README.md +0 -77
  126. data/examples/verify_quickstart_layout/app.rb +0 -73
  127. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  128. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  129. data/examples/verify_readme_usage/README.md +0 -49
  130. data/examples/verify_readme_usage/app.rb +0 -42
  131. data/examples/verify_website_managed/README.md +0 -48
  132. data/examples/verify_website_managed/app.rb +0 -36
  133. data/examples/verify_website_menu/README.md +0 -60
  134. data/examples/verify_website_menu/app.rb +0 -84
  135. data/examples/verify_website_spinner/README.md +0 -44
  136. data/examples/verify_website_spinner/app.rb +0 -34
  137. data/examples/widget_barchart/README.md +0 -58
  138. data/examples/widget_barchart/app.rb +0 -240
  139. data/examples/widget_block/README.md +0 -44
  140. data/examples/widget_block/app.rb +0 -258
  141. data/examples/widget_box/README.md +0 -54
  142. data/examples/widget_box/app.rb +0 -255
  143. data/examples/widget_calendar/README.md +0 -48
  144. data/examples/widget_calendar/app.rb +0 -115
  145. data/examples/widget_canvas/README.md +0 -31
  146. data/examples/widget_canvas/app.rb +0 -130
  147. data/examples/widget_cell/README.md +0 -45
  148. data/examples/widget_cell/app.rb +0 -112
  149. data/examples/widget_center/README.md +0 -33
  150. data/examples/widget_center/app.rb +0 -118
  151. data/examples/widget_chart/README.md +0 -50
  152. data/examples/widget_chart/app.rb +0 -220
  153. data/examples/widget_gauge/README.md +0 -50
  154. data/examples/widget_gauge/app.rb +0 -229
  155. data/examples/widget_layout_split/README.md +0 -53
  156. data/examples/widget_layout_split/app.rb +0 -260
  157. data/examples/widget_line_gauge/README.md +0 -50
  158. data/examples/widget_line_gauge/app.rb +0 -219
  159. data/examples/widget_list/README.md +0 -58
  160. data/examples/widget_list/app.rb +0 -382
  161. data/examples/widget_map/README.md +0 -48
  162. data/examples/widget_map/app.rb +0 -95
  163. data/examples/widget_overlay/README.md +0 -45
  164. data/examples/widget_overlay/app.rb +0 -250
  165. data/examples/widget_popup/README.md +0 -45
  166. data/examples/widget_popup/app.rb +0 -106
  167. data/examples/widget_ratatui_logo/README.md +0 -43
  168. data/examples/widget_ratatui_logo/app.rb +0 -104
  169. data/examples/widget_ratatui_mascot/README.md +0 -43
  170. data/examples/widget_ratatui_mascot/app.rb +0 -95
  171. data/examples/widget_rect/README.md +0 -53
  172. data/examples/widget_rect/app.rb +0 -222
  173. data/examples/widget_render/README.md +0 -46
  174. data/examples/widget_render/app.rb +0 -186
  175. data/examples/widget_render/app.rbs +0 -41
  176. data/examples/widget_rich_text/README.md +0 -44
  177. data/examples/widget_rich_text/app.rb +0 -193
  178. data/examples/widget_scroll_text/README.md +0 -46
  179. data/examples/widget_scroll_text/app.rb +0 -109
  180. data/examples/widget_scrollbar/README.md +0 -46
  181. data/examples/widget_scrollbar/app.rb +0 -155
  182. data/examples/widget_sparkline/README.md +0 -51
  183. data/examples/widget_sparkline/app.rb +0 -277
  184. data/examples/widget_style_colors/README.md +0 -43
  185. data/examples/widget_style_colors/app.rb +0 -83
  186. data/examples/widget_table/README.md +0 -57
  187. data/examples/widget_table/app.rb +0 -285
  188. data/examples/widget_tabs/README.md +0 -50
  189. data/examples/widget_tabs/app.rb +0 -183
  190. data/examples/widget_text_width/README.md +0 -44
  191. data/examples/widget_text_width/app.rb +0 -117
  192. data/migrate_to_buffer.rb +0 -145
  193. data/mise.toml +0 -8
  194. data/tasks/autodoc/examples.rb +0 -87
  195. data/tasks/autodoc/member.rb +0 -58
  196. data/tasks/autodoc/name.rb +0 -21
  197. data/tasks/autodoc.rake +0 -21
  198. data/tasks/bump/bump_workflow.rb +0 -49
  199. data/tasks/bump/cargo_lockfile.rb +0 -21
  200. data/tasks/bump/changelog.rb +0 -104
  201. data/tasks/bump/header.rb +0 -32
  202. data/tasks/bump/history.rb +0 -32
  203. data/tasks/bump/links.rb +0 -69
  204. data/tasks/bump/manifest.rb +0 -33
  205. data/tasks/bump/patch_release.rb +0 -19
  206. data/tasks/bump/release_branch.rb +0 -17
  207. data/tasks/bump/release_from_trunk.rb +0 -49
  208. data/tasks/bump/repository.rb +0 -54
  209. data/tasks/bump/ruby_gem.rb +0 -29
  210. data/tasks/bump/sem_ver.rb +0 -44
  211. data/tasks/bump/unreleased_section.rb +0 -73
  212. data/tasks/bump.rake +0 -61
  213. data/tasks/doc/documentation.rb +0 -59
  214. data/tasks/doc/link/file_url.rb +0 -30
  215. data/tasks/doc/link/relative_path.rb +0 -61
  216. data/tasks/doc/link/web_url.rb +0 -55
  217. data/tasks/doc/link.rb +0 -52
  218. data/tasks/doc/link_audit.rb +0 -116
  219. data/tasks/doc/problem.rb +0 -40
  220. data/tasks/doc/source_file.rb +0 -93
  221. data/tasks/doc.rake +0 -905
  222. data/tasks/example_viewer.html.erb +0 -172
  223. data/tasks/extension.rake +0 -14
  224. data/tasks/license/headers_md.rb +0 -223
  225. data/tasks/license/headers_rb.rb +0 -210
  226. data/tasks/license/license_utils.rb +0 -130
  227. data/tasks/license/snippets_md.rb +0 -315
  228. data/tasks/license/snippets_rdoc.rb +0 -150
  229. data/tasks/license.rake +0 -91
  230. data/tasks/lint.rake +0 -170
  231. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  232. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  233. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  234. data/tasks/rbs_predicates.rake +0 -31
  235. data/tasks/rdoc_config.rb +0 -29
  236. data/tasks/resources/build.yml.erb +0 -60
  237. data/tasks/resources/index.html.erb +0 -141
  238. data/tasks/resources/rubies.yml +0 -7
  239. data/tasks/sourcehut.rake +0 -110
  240. data/tasks/steep.rake +0 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  242. data/tasks/terminal_preview/crash_report.rb +0 -54
  243. data/tasks/terminal_preview/example_app.rb +0 -27
  244. data/tasks/terminal_preview/launcher_script.rb +0 -48
  245. data/tasks/terminal_preview/preview_collection.rb +0 -60
  246. data/tasks/terminal_preview/preview_timing.rb +0 -24
  247. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  248. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  249. data/tasks/terminal_preview/system_appearance.rb +0 -13
  250. data/tasks/terminal_preview/terminal_window.rb +0 -138
  251. data/tasks/terminal_preview/window_id.rb +0 -16
  252. data/tasks/terminal_preview.rake +0 -30
  253. data/tasks/test.rake +0 -36
  254. data/tasks/website/index_page.rb +0 -30
  255. data/tasks/website/version.rb +0 -122
  256. data/tasks/website/version_menu.rb +0 -68
  257. data/tasks/website/versioned_documentation.rb +0 -83
  258. data/tasks/website/website.rb +0 -53
  259. data/tasks/website.rake +0 -28
data/tasks/sourcehut.rake DELETED
@@ -1,110 +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
- puts "Updating stable branch to point to #{tag_name}..."
72
- # Resolve the tag to a commit hash (peel annotated tags)
73
- # This renders a commit SHA that can be pushed to a branch head
74
- commit_sha = `git rev-parse #{tag_name}^{}`.strip
75
-
76
- # Update local stable branch to match
77
- sh "git branch -f stable #{commit_sha}"
78
-
79
- # Push the commit to remote stable branch
80
- # This creates 'stable' if it doesn't exist, or fast-forwards it.
81
- sh "git push origin #{commit_sha}:stable"
82
- end
83
- end
84
-
85
- if Rake::Task.task_defined?("release")
86
- Rake::Task["release"].enhance do
87
- # Replace Bundler's normalized tag with semver-style for prerelease versions.
88
- # Bundler creates tags using normalized Gem::Version (e.g., v1.0.0.pre.beta.1).
89
- # Semver uses hyphens (e.g., v1.0.0-beta.1). We want only the semver tag.
90
- version_content = File.read("lib/ratatui_ruby/version.rb")
91
- version = version_content.match(/VERSION = "(.+?)"/)[1]
92
-
93
- if version.include?("-")
94
- normalized_tag = "v#{Gem::Version.new(version)}"
95
- semver_tag = "v#{version}"
96
-
97
- if normalized_tag != semver_tag
98
- puts "Replacing normalized tag #{normalized_tag} with semver tag #{semver_tag}..."
99
- # Delete the normalized tag locally and remotely
100
- sh "git tag -d #{normalized_tag}"
101
- sh "git push origin :refs/tags/#{normalized_tag}"
102
- # Create the semver tag pointing to the same commit
103
- sh "git tag #{semver_tag} HEAD"
104
- sh "git push origin #{semver_tag}"
105
- end
106
- end
107
-
108
- Rake::Task["sourcehut:update_stable"].invoke
109
- end
110
- 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
@@ -1,30 +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
- require "fileutils"
9
- require_relative "terminal_preview/preview_collection"
10
- require_relative "terminal_preview/example_app"
11
-
12
- namespace :terminal_preview do
13
- desc "Generate native PNG screenshots using Terminal.app"
14
- task :generate do
15
- img_dir = File.expand_path("../doc/images", __dir__)
16
- FileUtils.mkdir_p(img_dir)
17
-
18
- # Create empty placeholder files for any missing images that compile depends on.
19
- # This prevents Rake from trying to build them as dependencies.
20
- ExampleApp.all.each do |app|
21
- image_path = File.join(img_dir, "#{app}.png")
22
- FileUtils.touch(image_path) unless File.exist?(image_path)
23
- end
24
-
25
- Rake::Task["compile"].invoke
26
-
27
- collection = PreviewCollection.new(img_dir)
28
- collection.generate
29
- end
30
- end