ratatui_ruby 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +14 -12
- data/.builds/ruby-3.3.yml +14 -12
- data/.builds/ruby-3.4.yml +14 -12
- data/.builds/ruby-4.0.0.yml +14 -12
- data/AGENTS.md +54 -13
- data/CHANGELOG.md +186 -1
- data/README.md +17 -15
- data/doc/application_architecture.md +116 -0
- data/doc/application_testing.md +12 -7
- data/doc/contributors/better_dx.md +543 -0
- data/doc/contributors/design/ruby_frontend.md +1 -1
- data/doc/contributors/developing_examples.md +203 -0
- data/doc/contributors/documentation_style.md +97 -0
- data/doc/contributors/dwim_dx.md +366 -0
- data/doc/contributors/example_analysis.md +82 -0
- data/doc/custom.css +14 -0
- data/doc/event_handling.md +119 -0
- data/doc/images/all_events.png +0 -0
- data/doc/images/analytics.png +0 -0
- data/doc/images/block_padding.png +0 -0
- data/doc/images/block_titles.png +0 -0
- data/doc/images/box_demo.png +0 -0
- data/doc/images/calendar_demo.png +0 -0
- data/doc/images/cell_demo.png +0 -0
- data/doc/images/chart_demo.png +0 -0
- data/doc/images/custom_widget.png +0 -0
- data/doc/images/flex_layout.png +0 -0
- data/doc/images/gauge_demo.png +0 -0
- data/doc/images/hit_test.png +0 -0
- data/doc/images/line_gauge_demo.png +0 -0
- data/doc/images/list_demo.png +0 -0
- data/doc/images/list_styles.png +0 -0
- data/doc/images/login_form.png +0 -0
- data/doc/images/map_demo.png +0 -0
- data/doc/images/mouse_events.png +0 -0
- data/doc/images/popup_demo.png +0 -0
- data/doc/images/quickstart_dsl.png +0 -0
- data/doc/images/quickstart_lifecycle.png +0 -0
- data/doc/images/ratatui_logo_demo.png +0 -0
- data/doc/images/readme_usage.png +0 -0
- data/doc/images/rich_text.png +0 -0
- data/doc/images/scroll_text.png +0 -0
- data/doc/images/scrollbar_demo.png +0 -0
- data/doc/images/sparkline_demo.png +0 -0
- data/doc/images/table_flex.png +0 -0
- data/doc/images/table_select.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/index.md +1 -0
- data/doc/interactive_design.md +121 -0
- data/doc/quickstart.md +147 -72
- data/examples/all_events/app.rb +169 -0
- data/examples/all_events/app.rbs +7 -0
- data/examples/all_events/test_app.rb +139 -0
- data/examples/analytics/app.rb +258 -0
- data/examples/analytics/app.rbs +7 -0
- data/examples/analytics/test_app.rb +132 -0
- data/examples/block_padding/app.rb +63 -0
- data/examples/block_padding/app.rbs +7 -0
- data/examples/block_padding/test_app.rb +31 -0
- data/examples/block_titles/app.rb +61 -0
- data/examples/block_titles/app.rbs +7 -0
- data/examples/block_titles/test_app.rb +34 -0
- data/examples/box_demo/app.rb +216 -0
- data/examples/box_demo/app.rbs +7 -0
- data/examples/box_demo/test_app.rb +88 -0
- data/examples/calendar_demo/app.rb +101 -0
- data/examples/calendar_demo/app.rbs +7 -0
- data/examples/calendar_demo/test_app.rb +108 -0
- data/examples/cell_demo/app.rb +108 -0
- data/examples/cell_demo/app.rbs +7 -0
- data/examples/cell_demo/test_app.rb +36 -0
- data/examples/chart_demo/app.rb +203 -0
- data/examples/chart_demo/app.rbs +7 -0
- data/examples/chart_demo/test_app.rb +102 -0
- data/examples/custom_widget/app.rb +51 -0
- data/examples/custom_widget/app.rbs +7 -0
- data/examples/custom_widget/test_app.rb +30 -0
- data/examples/flex_layout/app.rb +156 -0
- data/examples/flex_layout/app.rbs +7 -0
- data/examples/flex_layout/test_app.rb +65 -0
- data/examples/gauge_demo/app.rb +182 -0
- data/examples/gauge_demo/app.rbs +7 -0
- data/examples/gauge_demo/test_app.rb +120 -0
- data/examples/hit_test/app.rb +175 -0
- data/examples/hit_test/app.rbs +7 -0
- data/examples/hit_test/test_app.rb +102 -0
- data/examples/line_gauge_demo/app.rb +190 -0
- data/examples/line_gauge_demo/app.rbs +7 -0
- data/examples/line_gauge_demo/test_app.rb +129 -0
- data/examples/list_demo/app.rb +253 -0
- data/examples/list_demo/app.rbs +12 -0
- data/examples/list_demo/test_app.rb +237 -0
- data/examples/list_styles/app.rb +140 -0
- data/examples/list_styles/app.rbs +7 -0
- data/examples/list_styles/test_app.rb +157 -0
- data/examples/{login_form.rb → login_form/app.rb} +12 -16
- data/examples/login_form/app.rbs +7 -0
- data/examples/login_form/test_app.rb +51 -0
- data/examples/map_demo/app.rb +90 -0
- data/examples/map_demo/app.rbs +7 -0
- data/examples/map_demo/test_app.rb +149 -0
- data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
- data/examples/mouse_events/app.rbs +7 -0
- data/examples/mouse_events/test_app.rb +53 -0
- data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
- data/examples/popup_demo/app.rbs +7 -0
- data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
- data/examples/quickstart_dsl/app.rb +36 -0
- data/examples/quickstart_dsl/app.rbs +7 -0
- data/examples/quickstart_dsl/test_app.rb +29 -0
- data/examples/quickstart_lifecycle/app.rb +39 -0
- data/examples/quickstart_lifecycle/app.rbs +7 -0
- data/examples/quickstart_lifecycle/test_app.rb +29 -0
- data/examples/ratatui_logo_demo/app.rb +79 -0
- data/examples/ratatui_logo_demo/app.rbs +7 -0
- data/examples/ratatui_logo_demo/test_app.rb +51 -0
- data/examples/ratatui_mascot_demo/app.rb +84 -0
- data/examples/ratatui_mascot_demo/app.rbs +7 -0
- data/examples/ratatui_mascot_demo/test_app.rb +47 -0
- data/examples/readme_usage/app.rb +29 -0
- data/examples/readme_usage/app.rbs +7 -0
- data/examples/readme_usage/test_app.rb +29 -0
- data/examples/rich_text/app.rb +141 -0
- data/examples/rich_text/app.rbs +7 -0
- data/examples/rich_text/test_app.rb +166 -0
- data/examples/scroll_text/app.rb +103 -0
- data/examples/scroll_text/app.rbs +7 -0
- data/examples/scroll_text/test_app.rb +110 -0
- data/examples/scrollbar_demo/app.rb +143 -0
- data/examples/scrollbar_demo/app.rbs +7 -0
- data/examples/scrollbar_demo/test_app.rb +77 -0
- data/examples/sparkline_demo/app.rb +240 -0
- data/examples/sparkline_demo/app.rbs +10 -0
- data/examples/sparkline_demo/test_app.rb +107 -0
- data/examples/table_flex/app.rb +65 -0
- data/examples/table_flex/app.rbs +7 -0
- data/examples/table_flex/test_app.rb +36 -0
- data/examples/table_select/app.rb +198 -0
- data/examples/table_select/app.rbs +7 -0
- data/examples/table_select/test_app.rb +180 -0
- data/examples/widget_style_colors/app.rb +104 -0
- data/examples/widget_style_colors/app.rbs +14 -0
- data/examples/widget_style_colors/test_app.rb +48 -0
- data/ext/ratatui_ruby/Cargo.lock +889 -115
- data/ext/ratatui_ruby/Cargo.toml +4 -3
- data/ext/ratatui_ruby/clippy.toml +7 -0
- data/ext/ratatui_ruby/extconf.rb +7 -0
- data/ext/ratatui_ruby/src/events.rs +218 -229
- data/ext/ratatui_ruby/src/lib.rs +38 -10
- data/ext/ratatui_ruby/src/rendering.rs +90 -10
- data/ext/ratatui_ruby/src/style.rs +281 -98
- data/ext/ratatui_ruby/src/terminal.rs +119 -25
- data/ext/ratatui_ruby/src/text.rs +171 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
- data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
- data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
- data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
- data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
- data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
- data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
- data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
- data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
- data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
- data/lib/ratatui_ruby/cell.rb +166 -0
- data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
- data/lib/ratatui_ruby/event/key.rb +211 -0
- data/lib/ratatui_ruby/event/mouse.rb +124 -0
- data/lib/ratatui_ruby/event/paste.rb +71 -0
- data/lib/ratatui_ruby/event/resize.rb +80 -0
- data/lib/ratatui_ruby/event.rb +79 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
- data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
- data/lib/ratatui_ruby/schema/block.rb +186 -14
- data/lib/ratatui_ruby/schema/calendar.rb +74 -17
- data/lib/ratatui_ruby/schema/canvas.rb +215 -48
- data/lib/ratatui_ruby/schema/center.rb +49 -11
- data/lib/ratatui_ruby/schema/chart.rb +151 -41
- data/lib/ratatui_ruby/schema/clear.rb +41 -72
- data/lib/ratatui_ruby/schema/constraint.rb +82 -22
- data/lib/ratatui_ruby/schema/cursor.rb +27 -9
- data/lib/ratatui_ruby/schema/draw.rb +53 -0
- data/lib/ratatui_ruby/schema/gauge.rb +59 -15
- data/lib/ratatui_ruby/schema/layout.rb +95 -13
- data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
- data/lib/ratatui_ruby/schema/list.rb +93 -19
- data/lib/ratatui_ruby/schema/overlay.rb +34 -8
- data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
- data/lib/ratatui_ruby/schema/rect.rb +64 -15
- data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
- data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
- data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
- data/lib/ratatui_ruby/schema/style.rb +49 -21
- data/lib/ratatui_ruby/schema/table.rb +119 -21
- data/lib/ratatui_ruby/schema/tabs.rb +75 -13
- data/lib/ratatui_ruby/schema/text.rb +90 -0
- data/lib/ratatui_ruby/session.rb +146 -0
- data/lib/ratatui_ruby/test_helper.rb +156 -13
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +143 -23
- data/sig/ratatui_ruby/event.rbs +69 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
- data/sig/ratatui_ruby/schema/block.rbs +5 -4
- data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
- data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
- data/sig/ratatui_ruby/schema/center.rbs +3 -3
- data/sig/ratatui_ruby/schema/chart.rbs +8 -5
- data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
- data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
- data/sig/ratatui_ruby/schema/draw.rbs +23 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
- data/sig/ratatui_ruby/schema/layout.rbs +11 -1
- data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
- data/sig/ratatui_ruby/schema/list.rbs +5 -1
- data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
- data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
- data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
- data/sig/ratatui_ruby/schema/rect.rbs +2 -1
- data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
- data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
- data/sig/ratatui_ruby/schema/table.rbs +8 -1
- data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
- data/sig/ratatui_ruby/schema/text.rbs +22 -0
- data/tasks/resources/build.yml.erb +13 -11
- data/tasks/terminal_preview/app_screenshot.rb +35 -0
- data/tasks/terminal_preview/crash_report.rb +54 -0
- data/tasks/terminal_preview/example_app.rb +25 -0
- data/tasks/terminal_preview/launcher_script.rb +48 -0
- data/tasks/terminal_preview/preview_collection.rb +60 -0
- data/tasks/terminal_preview/preview_timing.rb +22 -0
- data/tasks/terminal_preview/safety_confirmation.rb +58 -0
- data/tasks/terminal_preview/saved_screenshot.rb +55 -0
- data/tasks/terminal_preview/system_appearance.rb +11 -0
- data/tasks/terminal_preview/terminal_window.rb +138 -0
- data/tasks/terminal_preview/window_id.rb +14 -0
- data/tasks/terminal_preview.rake +28 -0
- data/tasks/test.rake +1 -1
- metadata +174 -53
- data/doc/images/examples-analytics.rb.png +0 -0
- data/doc/images/examples-box_demo.rb.png +0 -0
- data/doc/images/examples-calendar_demo.rb.png +0 -0
- data/doc/images/examples-chart_demo.rb.png +0 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-dashboard.rb.png +0 -0
- data/doc/images/examples-list_styles.rb.png +0 -0
- data/doc/images/examples-login_form.rb.png +0 -0
- data/doc/images/examples-map_demo.rb.png +0 -0
- data/doc/images/examples-mouse_events.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-scrollbar_demo.rb.png +0 -0
- data/doc/images/examples-stock_ticker.rb.png +0 -0
- data/doc/images/examples-system_monitor.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/examples/analytics.rb +0 -88
- data/examples/box_demo.rb +0 -71
- data/examples/calendar_demo.rb +0 -55
- data/examples/chart_demo.rb +0 -84
- data/examples/custom_widget.rb +0 -43
- data/examples/dashboard.rb +0 -72
- data/examples/list_styles.rb +0 -66
- data/examples/map_demo.rb +0 -58
- data/examples/quickstart_dsl.rb +0 -30
- data/examples/quickstart_lifecycle.rb +0 -40
- data/examples/readme_usage.rb +0 -21
- data/examples/scroll_text.rb +0 -74
- data/examples/scrollbar_demo.rb +0 -75
- data/examples/stock_ticker.rb +0 -93
- data/examples/system_monitor.rb +0 -94
- data/examples/table_select.rb +0 -70
- data/examples/test_analytics.rb +0 -65
- data/examples/test_box_demo.rb +0 -38
- data/examples/test_calendar_demo.rb +0 -66
- data/examples/test_dashboard.rb +0 -38
- data/examples/test_list_styles.rb +0 -61
- data/examples/test_login_form.rb +0 -63
- data/examples/test_map_demo.rb +0 -100
- data/examples/test_scroll_text.rb +0 -130
- data/examples/test_stock_ticker.rb +0 -39
- data/examples/test_system_monitor.rb +0 -40
- data/examples/test_table_select.rb +0 -37
- data/ext/ratatui_ruby/src/buffer.rs +0 -54
- data/lib/ratatui_ruby/dsl.rb +0 -64
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
2
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
|
-
image:
|
|
4
|
+
image: archlinux
|
|
5
5
|
packages:
|
|
6
6
|
- bash
|
|
7
|
-
-
|
|
7
|
+
- base-devel
|
|
8
8
|
- curl
|
|
9
|
-
- openssl
|
|
10
|
-
-
|
|
11
|
-
- zlib
|
|
12
|
-
- readline
|
|
13
|
-
- gdbm
|
|
14
|
-
- ncurses
|
|
15
|
-
- libffi
|
|
16
|
-
- clang
|
|
9
|
+
- openssl
|
|
10
|
+
- libyaml
|
|
11
|
+
- zlib
|
|
12
|
+
- readline
|
|
13
|
+
- gdbm
|
|
14
|
+
- ncurses
|
|
15
|
+
- libffi
|
|
16
|
+
- clang
|
|
17
17
|
- git
|
|
18
18
|
artifacts:
|
|
19
19
|
- ratatui_ruby/pkg/<%= gem_filename %>
|
|
@@ -24,8 +24,10 @@ tasks:
|
|
|
24
24
|
curl https://mise.jdx.dev/install.sh | sh
|
|
25
25
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.buildenv
|
|
26
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
27
|
+
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
28
|
+
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
29
|
+
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
27
30
|
. ~/.buildenv
|
|
28
|
-
export RUSTFLAGS="-C target-feature=-crt-static"
|
|
29
31
|
export CI="true"
|
|
30
32
|
cd ratatui_ruby
|
|
31
33
|
sed -i 's/ruby = .*/ruby = "<%= ruby_version %>"/' mise.toml
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require_relative "launcher_script"
|
|
8
|
+
require_relative "terminal_window"
|
|
9
|
+
require_relative "crash_report"
|
|
10
|
+
|
|
11
|
+
class AppScreenshot < Data.define(:app, :output_path)
|
|
12
|
+
def capture
|
|
13
|
+
print " 📸 #{app}..."
|
|
14
|
+
|
|
15
|
+
LauncherScript.new(app.app_path, Dir.pwd).run do |launcher|
|
|
16
|
+
TerminalWindow.new(launcher.path, launcher.pid_file).open do |window|
|
|
17
|
+
take_snapshot(window.window_id)
|
|
18
|
+
puts " done."
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
rescue => e
|
|
23
|
+
puts " FAILED"
|
|
24
|
+
puts
|
|
25
|
+
puts CrashReport.new(app, e, "Program crashed before screenshot could be taken:")
|
|
26
|
+
puts
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def take_snapshot(window_id)
|
|
33
|
+
system("screencapture -l #{window_id} -o -x '#{output_path}'")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class CrashReport < Data.define(:app, :error, :preamble)
|
|
7
|
+
def self.new(app, error, preamble = nil)
|
|
8
|
+
# Allow preamble to be optional while Data.define requires all fields
|
|
9
|
+
super(app:, error:, preamble:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
output = error.message.strip
|
|
14
|
+
formatted_error = output.split("\n").map { |line| format_line(line) }.join("\n")
|
|
15
|
+
preamble_section = preamble ? <<~PREAMBLE.chomp : ""
|
|
16
|
+
#{box_top}
|
|
17
|
+
#{format_line(preamble)}
|
|
18
|
+
#{box_bottom}
|
|
19
|
+
PREAMBLE
|
|
20
|
+
|
|
21
|
+
<<~TEXT
|
|
22
|
+
#{preamble_section}
|
|
23
|
+
#{border_top(app.to_s)}
|
|
24
|
+
#{formatted_error}
|
|
25
|
+
#{box_bottom}
|
|
26
|
+
TEXT
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def box_top
|
|
32
|
+
"┌" + "─" * (width - 2) + "┐"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def box_bottom
|
|
36
|
+
"└" + "─" * (width - 2) + "┘"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def border_top(title)
|
|
40
|
+
left = "┌─ #{title} "
|
|
41
|
+
right = "┐"
|
|
42
|
+
dashes = "─" * (width - left.length - right.length)
|
|
43
|
+
left + dashes + right
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_line(line)
|
|
47
|
+
truncated = line[0...(width - 4)]
|
|
48
|
+
"│ #{truncated.ljust(width - 4)} │"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def width
|
|
52
|
+
80
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class ExampleApp < Data.define(:directory)
|
|
7
|
+
def self.all
|
|
8
|
+
examples_dir = File.expand_path("../../examples", __dir__)
|
|
9
|
+
Dir.glob("#{examples_dir}/*/app.rb").map do |path|
|
|
10
|
+
new(File.basename(File.dirname(path)))
|
|
11
|
+
end.sort_by(&:directory)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def app_path
|
|
15
|
+
"examples/#{directory}/app.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def screenshot_filename
|
|
19
|
+
"#{directory}.png"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
directory
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
class LauncherScript < Data.define(:app_path, :repo_root)
|
|
10
|
+
def initialize(app_path:, repo_root:)
|
|
11
|
+
super
|
|
12
|
+
write
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
yield self
|
|
17
|
+
ensure
|
|
18
|
+
cleanup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def path
|
|
22
|
+
File.join(Dir.tmpdir, "preview_launcher.sh")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pid_file
|
|
26
|
+
File.join(Dir.tmpdir, "preview_launcher.pid")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def cleanup
|
|
32
|
+
File.delete(path) if File.exist?(path)
|
|
33
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
|
34
|
+
rescue Errno::ENOENT
|
|
35
|
+
# Already deleted
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "example_app"
|
|
8
|
+
require_relative "app_screenshot"
|
|
9
|
+
require_relative "crash_report"
|
|
10
|
+
require_relative "preview_timing"
|
|
11
|
+
require_relative "safety_confirmation"
|
|
12
|
+
require_relative "saved_screenshot"
|
|
13
|
+
|
|
14
|
+
class PreviewCollection
|
|
15
|
+
def initialize(output_dir)
|
|
16
|
+
@output_dir = output_dir
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate
|
|
20
|
+
abort "Error: This task requires macOS." unless RUBY_PLATFORM.match?(/darwin/)
|
|
21
|
+
|
|
22
|
+
apps = ExampleApp.all
|
|
23
|
+
stale_count = count_stale_apps(apps)
|
|
24
|
+
|
|
25
|
+
if stale_count.zero?
|
|
26
|
+
puts "\n✨ All #{apps.count} screenshots are up to date."
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
SafetyConfirmation.new(stale_count, apps.count).request
|
|
31
|
+
|
|
32
|
+
puts "\nHere we go!"
|
|
33
|
+
failures = apps.count { |app| !capture_app(app) }
|
|
34
|
+
|
|
35
|
+
if failures.zero?
|
|
36
|
+
puts "\n✨ All captures complete. Check doc/images/."
|
|
37
|
+
else
|
|
38
|
+
abort "\n❌ #{failures} capture(s) failed."
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def count_stale_apps(apps)
|
|
45
|
+
apps.count { |app| SavedScreenshot.for(app, @output_dir).stale? }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
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
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class PreviewTiming
|
|
7
|
+
def self.window_startup
|
|
8
|
+
1.5
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.between_captures
|
|
12
|
+
0.2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.close_delay
|
|
16
|
+
1.0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.total
|
|
20
|
+
window_startup + close_delay + between_captures
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require_relative "preview_timing"
|
|
7
|
+
require_relative "system_appearance"
|
|
8
|
+
|
|
9
|
+
class SafetyConfirmation
|
|
10
|
+
def initialize(stale_count, total_count)
|
|
11
|
+
@stale_count = stale_count
|
|
12
|
+
@total_count = total_count
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def request
|
|
16
|
+
print_warning
|
|
17
|
+
wait_for_user
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
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
|
+
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
class SavedScreenshot < Data.define(:app, :path)
|
|
9
|
+
def self.for(app, output_dir)
|
|
10
|
+
new(app, File.join(output_dir, app.screenshot_filename))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stale?
|
|
14
|
+
return true unless exists?
|
|
15
|
+
|
|
16
|
+
app_last_modified > screenshot_last_commit_time
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def exists?
|
|
22
|
+
File.exist?(path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def app_last_modified
|
|
26
|
+
# If the file has staged or unstaged changes, it's definitely stale
|
|
27
|
+
return Time.now.to_i if changed?
|
|
28
|
+
|
|
29
|
+
# Otherwise, compare against the last git commit time
|
|
30
|
+
app_last_commit_time
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def changed?
|
|
34
|
+
system("git diff HEAD --quiet #{app.app_path} 2>/dev/null")
|
|
35
|
+
!$?.success?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def app_last_commit_time
|
|
39
|
+
output = `git log -1 --format=%cI "#{app.app_path}" 2>/dev/null`.strip
|
|
40
|
+
return 0 if output.empty?
|
|
41
|
+
|
|
42
|
+
Time.iso8601(output).to_i
|
|
43
|
+
rescue StandardError
|
|
44
|
+
0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def screenshot_last_commit_time
|
|
48
|
+
output = `git log -1 --format=%cI "#{path}" 2>/dev/null`.strip
|
|
49
|
+
return Time.now.to_i if output.empty?
|
|
50
|
+
|
|
51
|
+
Time.iso8601(output).to_i
|
|
52
|
+
rescue StandardError
|
|
53
|
+
Time.now.to_i
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class SystemAppearance
|
|
7
|
+
def self.dark?
|
|
8
|
+
result = `osascript -e 'tell application "System Events" to tell appearance preferences to get dark mode'`.strip
|
|
9
|
+
result == "true"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require_relative "window_id"
|
|
7
|
+
require_relative "preview_timing"
|
|
8
|
+
|
|
9
|
+
class TerminalWindow
|
|
10
|
+
CTRL_C = "ASCII character 3"
|
|
11
|
+
|
|
12
|
+
def initialize(launcher_script_path, pid_file)
|
|
13
|
+
@launcher_script_path = launcher_script_path
|
|
14
|
+
@pid_file = pid_file
|
|
15
|
+
@window_id = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def open
|
|
19
|
+
setup_script = <<~APPLESCRIPT
|
|
20
|
+
tell application "Terminal"
|
|
21
|
+
set newTab to do script "#{@launcher_script_path}"
|
|
22
|
+
set currentWindow to window 1
|
|
23
|
+
|
|
24
|
+
set number of rows of currentWindow to 24
|
|
25
|
+
set number of columns of currentWindow to 80
|
|
26
|
+
set position of currentWindow to {100, 100}
|
|
27
|
+
set frontmost of currentWindow to true
|
|
28
|
+
|
|
29
|
+
return id of currentWindow
|
|
30
|
+
end tell
|
|
31
|
+
APPLESCRIPT
|
|
32
|
+
|
|
33
|
+
@window_id = WindowID.new(`osascript -e '#{setup_script}'`.strip)
|
|
34
|
+
wait_for_startup
|
|
35
|
+
yield self
|
|
36
|
+
ensure
|
|
37
|
+
close if @window_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def window_id
|
|
41
|
+
@window_id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class WindowID < Data.define(:value)
|
|
7
|
+
def valid?
|
|
8
|
+
!value.empty? && value.match?(/^\d+$/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "terminal_preview/preview_collection"
|
|
8
|
+
require_relative "terminal_preview/example_app"
|
|
9
|
+
|
|
10
|
+
namespace :terminal_preview do
|
|
11
|
+
desc "Generate native PNG screenshots using Terminal.app"
|
|
12
|
+
task :generate do
|
|
13
|
+
img_dir = File.expand_path("../doc/images", __dir__)
|
|
14
|
+
FileUtils.mkdir_p(img_dir)
|
|
15
|
+
|
|
16
|
+
# Create empty placeholder files for any missing images that compile depends on.
|
|
17
|
+
# This prevents Rake from trying to build them as dependencies.
|
|
18
|
+
ExampleApp.all.each do |app|
|
|
19
|
+
image_path = File.join(img_dir, "#{app}.png")
|
|
20
|
+
FileUtils.touch(image_path) unless File.exist?(image_path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Rake::Task["compile"].invoke
|
|
24
|
+
|
|
25
|
+
collection = PreviewCollection.new(img_dir)
|
|
26
|
+
collection.generate
|
|
27
|
+
end
|
|
28
|
+
end
|
data/tasks/test.rake
CHANGED