diff --git a/README.md b/README.md index fb1c8375..f24a55ee 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ continues to build only the Alacritty backend and does not require Zig. | --------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `state` | cwd, size, cursor, window title, last command + exit code, effective timeouts, text snapshot. | | `text [--full]` | Plain text of the viewport (or scrollback). | -| `screenshot [-o file.svg] [--full]` | Terminal text to stdout, or a crisp full-color SVG image (svg-term-style window) to a file. | +| `screenshot [-o file.svg] [--full] [--zoom N]` | Terminal text to stdout, or a full-color SVG scaled without changing its terminal cells. | | `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags). | | `get command\|output\|exit-code\|cwd\|cursor\|size\|title` | Structured getters. | @@ -312,7 +312,13 @@ Colors accept ANSI-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`). ### Screenshots -Screenshots render a snapshot of the session in the current terminal by default, but can render an SVG using the `-o` output flag. Nerd Font icons are embedded as vector paths, so SVGs remain self-contained without changing the font stack for regular text. +Screenshots render a snapshot of the session in the current terminal by +default, but can render an SVG using the `-o` output flag. `--zoom 0.5` +halves the image dimensions while preserving the same rows and columns. Nerd +Font icons are embedded as vector paths, so SVGs remain self-contained without +changing the font stack for regular text. +Rendered screenshots and recordings append `COLSxROWS` to the program title; +when the terminal has no title they use `tui-test capture - COLSxROWS`.

full-color SVG screenshot of a TUI rendered by tui-test @@ -326,25 +332,27 @@ Record a selected part of a session directly to animated APNG (primary), GIF | Command | Description | | --- | --- | -| `record start OUT [--format apng\|gif\|mp4\|cast] [--fps N] [--speed N] [--idle-time-limit SEC]` | Start recording. Format is inferred from `.png`/`.apng`, `.gif`, `.mp4`, or `.cast`. | +| `record start OUT [--format apng\|gif\|mp4\|cast] [--fps N] [--speed N] [--idle-time-limit SEC] [--zoom N]` | Start recording. Format is inferred from `.png`/`.apng`, `.gif`, `.mp4`, or `.cast`. | | `record stop` | Stop recording and finish the output file. | | `get-recording [session]` | Print the separate, always-on session cast to stdout. | ```sh tui-test open -tui-test record start demo.png # lossless animated PNG +tui-test record start demo.png --zoom 0.5 tui-test submit "echo hello" tui-test wait command tui-test record stop ``` APNG keeps full 24/32-bit color. APNG, GIF, and MP4 render at 2x pixel density -for sharper text; GIF additionally uses palette quantization for viewers that -cannot display APNG. MP4 export streams rendered frames to `ffmpeg` using H.264, -and starting an MP4 recording fails immediately unless `ffmpeg` is available on -`PATH`. Defaults are 30 fps, 1x speed, a 5-second idle-gap limit, and a 3-second -final hold. If a process exits before `record stop`, APNG/GIF/MP4 capture remains -beside the target as `OUT.tui-test.cast`. +for sharper text; `--zoom` multiplies those dimensions, so `--zoom 0.5` +produces a 1x-size export with the same terminal cells. GIF additionally uses +palette quantization for viewers that cannot display APNG. MP4 export streams +rendered frames to `ffmpeg` using H.264, and starting an MP4 recording fails +immediately unless `ffmpeg` is available on `PATH`. Defaults are 30 fps, 1x +speed, 1x zoom, a 5-second idle-gap limit, and a 3-second final hold. Zoom does +not apply to cast output. If a process exits before `record stop`, APNG/GIF/MP4 +capture remains beside the target as `OUT.tui-test.cast`. Raster export uses the selected JetBrains Mono bundle tier, when enabled, plus installed system fonts for Unicode fallbacks. The CLI and language bindings @@ -357,6 +365,38 @@ silently substituting unsupported glyphs. animated APNG terminal recording produced by tui-test

+The same 48x10-cell recording rendered at native 100%, 50%, and 25% zoom: + +

+ 100%
+ terminal recording rendered at 100 percent zoom +

+ +

+ 50%
+ terminal recording rendered at 50 percent zoom +

+ +

+ 25%
+ terminal recording rendered at 25 percent zoom +

+ +Resize events keep the encoded canvas stable while existing terminal content +reflows as the window grows and shrinks in place: + +

+ animated GIF showing a centered terminal window resizing +

+ +Regenerate the checked-in SVG, APNG, GIF, Nerd Font, and resize examples with: + +```sh +bash scripts/regenerate-static-media.sh +``` + +The manually captured `static/tui-test-demo.mp4` is intentionally left unchanged. + Every session also records automatically from open in `.cast` format. Export it with `tui-test get-recording > demo.cast` for the wider asciicast ecosystem. This interoperability is implemented directly from the public asciicast v2 diff --git a/SKILL.md b/SKILL.md index d8903437..84f838b2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -74,7 +74,7 @@ without parsing text: | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `state` | cwd, size, cursor, last command + exit code, timeouts, and a text snapshot. | | `text [--full]` | Rendered viewport text, or full scrollback with `--full`. | -| `screenshot [PATH] [-o FILE] [--full]` | Terminal text to stdout, or a full-color SVG image (crisp at any zoom, svg-term-style window) when a path is given. | +| `screenshot [PATH] [-o FILE] [--full] [--zoom N]` | Terminal text to stdout, or a full-color SVG scaled without changing its terminal cells. | | `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags) for a region. | | `get command\|output\|exit-code\|cwd\|cursor\|size\|title` | One structured field. | @@ -125,7 +125,7 @@ Colors accept ansi-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`). | Command | Description | | ----------------------------------- | ---------------------------------------------------------------------------- | -| `record start OUT [options]` | Start APNG, GIF, or asciicast recording (format inferred from extension). | +| `record start OUT [options]` | Start APNG, GIF, MP4, or asciicast recording; `--zoom N` scales image/video output. | | `record stop` | Finish the active recording. | | `get-recording [session]` | Print the always-on asciinema v2 cast (works even after the session stopped).| | `monitor` | Watch the session live, full-color, in another terminal. | @@ -213,20 +213,23 @@ tui-test get-recording > demo.cast # current session's recording to stdout tui-test get-recording work > w.cast # a specific session by name (even if stopped) ``` -Record a selected span directly to APNG (primary), GIF (fallback), or cast: +Record a selected span directly to APNG, GIF, MP4, or cast: ```sh -tui-test record start demo.png +tui-test record start demo.png --zoom 0.5 tui-test submit "echo hello" tui-test wait command tui-test record stop ``` -APNG and GIF render at 2x pixel density. Use `--fps`, `--speed`, and -`--idle-time-limit` to tune playback. `.cast` output interoperates with the -asciicast ecosystem without adding any GPL dependency to tui-test. If a -process exits before `record stop`, an APNG/GIF capture remains beside the -target as `OUT.tui-test.cast`. +APNG, GIF, and MP4 render at 2x pixel density. `--zoom` multiplies the output +dimensions without changing the rows or columns; `--zoom 0.5` produces a 1x +export. Resize events change the terminal window size inside a centered, +opaque canvas sized for the recording's largest frame. Use `--fps`, `--speed`, +and `--idle-time-limit` to tune playback. `.cast` output does not use zoom and +interoperates with the asciicast ecosystem without adding any GPL dependency +to tui-test. If a process exits before `record stop`, an APNG/GIF/MP4 capture +remains beside the target as `OUT.tui-test.cast`. ## Live monitor diff --git a/bindings/js/README.md b/bindings/js/README.md index 6f561dd8..55fcf5ad 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -108,7 +108,7 @@ Cancelling a promise does not cancel the underlying Rust operation. Operations f Closing a session removes it from `sessions()`, but keeps its recording. `getRecording()` can read that recording for the rest of the process. The 1024 most recently closed sessions have their recordings retained. ```js -await su.startRecording("demo.png", { fps: 30, speed: 1 }); +await su.startRecording("demo.png", { fps: 30, speed: 1, zoom: 0.5 }); await su.submit("echo hello"); await su.waitCommand(); const path = await su.stopRecording(); @@ -116,7 +116,9 @@ const path = await su.stopRecording(); `.png`/`.apng` selects lossless APNG, `.gif` selects GIF, `.mp4` selects MP4, and `.cast` selects asciicast v2. The `format` option can override extension -inference. MP4 recording requires `ffmpeg` to be available on `PATH`. +inference. `zoom` scales SVG screenshots and image/video recordings without +changing terminal rows or columns. MP4 recording requires `ffmpeg` to be +available on `PATH`. ## Configuration diff --git a/bindings/python/README.md b/bindings/python/README.md index 1ae49c7e..0fd824eb 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -113,7 +113,7 @@ Cancelling a task does not cancel the underlying Rust operation. Operations for Closing a session removes it from `sessions()`, but keeps its recording. `get_recording()` can read that recording for the rest of the process. The 1024 most recently closed sessions have their recordings retained. ```python -await su.start_recording("demo.png", fps=30, speed=1.0) +await su.start_recording("demo.png", fps=30, speed=1.0, zoom=0.5) await su.submit("echo hello") await su.wait_command() path = await su.stop_recording() @@ -121,7 +121,9 @@ path = await su.stop_recording() `.png`/`.apng` selects lossless APNG, `.gif` selects GIF, `.mp4` selects MP4, and `.cast` selects asciicast v2. Pass `format=` to override extension -inference. MP4 recording requires `ffmpeg` to be available on `PATH`. +inference. `zoom=` scales SVG screenshots and image/video recordings without +changing terminal rows or columns. MP4 recording requires `ffmpeg` to be +available on `PATH`. ## Configuration diff --git a/scripts/regenerate-static-media.sh b/scripts/regenerate-static-media.sh new file mode 100755 index 00000000..ca470dd3 --- /dev/null +++ b/scripts/regenerate-static-media.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/regenerate-static-media.sh [--skip-build] [--cli PATH] + +Regenerates: + static/screen.svg + static/recording.png + static/recording.gif + static/recording-zoom-100.png + static/recording-zoom-50.png + static/recording-zoom-25.png + static/recording-nerd-fonts.png + static/recording-nerd-fonts.gif + static/resize-demo.gif + +static/tui-test-demo.mp4 is a manually captured monitor demo and is not changed. +EOF +} + +skip_build=0 +cli_path="" +while (($#)); do + case "$1" in + --skip-build) + skip_build=1 + shift + ;; + --cli) + if (($# < 2)); then + echo "--cli requires a path" >&2 + exit 2 + fi + cli_path=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd -- "$script_dir/.." && pwd) +static_dir="$repo_root/static" +temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/tui-test-static-media.XXXXXX") +session_prefix="static-media-$$" +sessions=() + +close_session() { + local session=$1 + local exit_code + + if [[ -z ${cli_path:-} || ! -f $cli_path ]]; then + return + fi + + set +e + "$cli_path" --session "$session" close >/dev/null 2>&1 + exit_code=$? + set -e + if ((exit_code != 0 && exit_code != 3)); then + echo "warning: could not close tui-test session '$session'" >&2 + fi +} + +cleanup() { + local session + for session in "${sessions[@]}"; do + close_session "$session" + done + rm -rf -- "$temp_dir" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +tui() { + local session=$1 + shift + "$cli_path" --session "$session" "$@" +} + +register_session() { + local session=$1 + sessions+=("$session") + close_session "$session" +} + +shell_command() { + local script_path=$1 + printf 'bash %q' "$script_path" +} + +prepare_recording_session() { + local session=$1 + + tui "$session" submit "stty -echo; clear" >/dev/null + tui "$session" wait command --timeout 10000 >/dev/null +} + +record_set() { + local session_suffix=$1 + local cols=$2 + local rows=$3 + local demo_script=$4 + shift 4 + local outputs=("$@") + local session="$session_prefix-$session_suffix" + local command + local output + + command=$(shell_command "$demo_script") + register_session "$session" + tui "$session" open --shell bash --cols "$cols" --rows "$rows" >/dev/null + + for output in "${outputs[@]}"; do + prepare_recording_session "$session" + tui "$session" record start "$static_dir/$output" --fps 20 >/dev/null + tui "$session" submit "$command" >/dev/null + tui "$session" wait text "done" --timeout 10000 >/dev/null + tui "$session" wait command --timeout 10000 >/dev/null + tui "$session" record stop >/dev/null + done + + close_session "$session" +} + +record_zoom_set() { + local session="$session_prefix-recording-zoom" + local command + local spec + local output + local zoom + + command=$(shell_command "$recording_script") + register_session "$session" + tui "$session" open --shell bash --cols 48 --rows 10 >/dev/null + + for spec in \ + "recording-zoom-100.png:1" \ + "recording-zoom-50.png:0.5" \ + "recording-zoom-25.png:0.25"; do + output=${spec%%:*} + zoom=${spec##*:} + prepare_recording_session "$session" + tui "$session" record start "$static_dir/$output" --fps 20 --zoom "$zoom" >/dev/null + tui "$session" submit "$command" >/dev/null + tui "$session" wait text "done" --timeout 10000 >/dev/null + tui "$session" wait command --timeout 10000 >/dev/null + tui "$session" record stop >/dev/null + done + + close_session "$session" +} + +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + binary_name=tui-test.exe + ;; + *) + binary_name=tui-test + ;; +esac + +cd -- "$repo_root" + +if [[ -z "$cli_path" ]]; then + if ((skip_build == 0)); then + echo "Building tui-test..." + cargo build -p tui-test-cli + fi + + target_dir=${CARGO_TARGET_DIR:-"$repo_root/target"} + if [[ "$target_dir" != /* ]]; then + target_dir="$repo_root/$target_dir" + fi + cli_path="$target_dir/debug/$binary_name" +elif [[ "$cli_path" != /* ]]; then + cli_path="$repo_root/$cli_path" +fi + +if [[ ! -f "$cli_path" ]]; then + echo "tui-test binary not found: $cli_path" >&2 + echo "Run without --skip-build or pass --cli PATH." >&2 + exit 1 +fi + +mkdir -p -- "$static_dir" + +screen_script="$temp_dir/screen.sh" +cat >"$screen_script" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +escape=$'\033' +bell=$'\a' +printf '%s]2;tui-test - terminal automation%s' "$escape" "$bell" +printf '%s[1;36mtui-test>%s[0m open\r\n' "$escape" "$escape" +printf '%s[32m[ok]%s[0m session ready %s[90mBash - 60x20%s[0m\r\n\r\n' \ + "$escape" "$escape" "$escape" "$escape" +printf '%s[1;36mtui-test>%s[0m record start demo.png\r\n' "$escape" "$escape" +printf '%s[33m[rec]%s[0m recording APNG at 2x density\r\n\r\n' "$escape" "$escape" +printf '%s[1;36mtui-test>%s[0m expect text ready\r\n' "$escape" "$escape" +printf '%s[32m[ok] matched%s[0m ready\r\n' "$escape" "$escape" +sleep 30 +EOF +chmod +x "$screen_script" + +recording_script="$temp_dir/recording.sh" +cat >"$recording_script" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +clear +printf '\033]2;tui-test recording\a' +sleep 0.75 +printf '\033[36mtui-test> \033[0m' +sleep 0.25 +printf '\033[33mdemo\033[0m\n' +sleep 0.25 +printf '\033[36m tui-test terminal\033[0m\n' +sleep 0.25 +printf ' record APNG / GIF / MP4\n' +sleep 0.25 +printf '\033[32m done\033[0m\n' +EOF +chmod +x "$recording_script" + +nerd_font_script="$temp_dir/nerd-fonts.sh" +cat >"$nerd_font_script" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +clear +printf '\033]2;tui-test Nerd Font recording\a' +sleep 0.75 +folder=$'\uf115' +powerline=$'\ue0b0' +printf '\033[36mtui-test> \033[0m' +sleep 0.25 +printf '\033[33micons\033[0m\n' +sleep 0.25 +printf '\033[36m %s Nerd Font glyphs\033[0m\n' "$folder" +sleep 0.25 +printf ' %s Powerline rendering\n' "$powerline" +sleep 0.25 +printf '\033[32m done\033[0m\n' +EOF +chmod +x "$nerd_font_script" + +resize_script="$temp_dir/resize.sh" +cat >"$resize_script" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +clear +printf '\033]2;tui-test resize and reflow demo\a' +sleep 0.6 +printf '\033[36mtui-test resize and reflow demo\033[0m\n\n' +printf '\033[33mThe terminal keeps this paragraph intact while narrower widths reflow it ' +printf 'across more rows and wider widths pull it back together.\033[0m\n\n' +printf '\033[32mWatch the same words wrap, unwrap, and return to their original layout.\033[0m\n' +sleep 8 +EOF +chmod +x "$resize_script" + +echo "Regenerating static/screen.svg..." +screen_session="$session_prefix-screen" +register_session "$screen_session" +tui "$screen_session" run --cols 60 --rows 20 -- bash --noprofile --norc "$screen_script" >/dev/null +tui "$screen_session" wait text "recording APNG" --timeout 10000 >/dev/null +tui "$screen_session" screenshot --out "$static_dir/screen.svg" >/dev/null +close_session "$screen_session" + +echo "Regenerating APNG and GIF examples..." +record_set recording 48 10 "$recording_script" recording.png recording.gif + +echo "Regenerating zoom comparison examples..." +record_zoom_set + +echo "Regenerating Nerd Font examples..." +record_set nerd-fonts 54 12 "$nerd_font_script" \ + recording-nerd-fonts.png recording-nerd-fonts.gif + +echo "Regenerating resize demo..." +resize_session="$session_prefix-resize" +register_session "$resize_session" +tui "$resize_session" open --shell bash --cols 60 --rows 16 >/dev/null +prepare_recording_session "$resize_session" +tui "$resize_session" record start "$static_dir/resize-demo.gif" --fps 20 >/dev/null +tui "$resize_session" submit "$(shell_command "$resize_script")" >/dev/null +tui "$resize_session" wait text "Watch the same words" --timeout 10000 >/dev/null +sleep 0.3 +tui "$resize_session" resize 42 10 >/dev/null +sleep 0.9 +tui "$resize_session" resize 30 7 >/dev/null +sleep 0.9 +tui "$resize_session" resize 50 12 >/dev/null +sleep 0.9 +tui "$resize_session" resize 60 16 >/dev/null +tui "$resize_session" wait command --timeout 10000 >/dev/null +tui "$resize_session" record stop >/dev/null +close_session "$resize_session" + +echo "Static media regenerated in $static_dir" +echo "static/tui-test-demo.mp4 is a manually captured monitor demo and was not modified." diff --git a/static/recording-nerd-fonts.gif b/static/recording-nerd-fonts.gif index 66664278..445459e3 100644 Binary files a/static/recording-nerd-fonts.gif and b/static/recording-nerd-fonts.gif differ diff --git a/static/recording-nerd-fonts.png b/static/recording-nerd-fonts.png index 507a4144..bd04d8b3 100644 Binary files a/static/recording-nerd-fonts.png and b/static/recording-nerd-fonts.png differ diff --git a/static/recording-zoom-100.png b/static/recording-zoom-100.png new file mode 100644 index 00000000..ef05469a Binary files /dev/null and b/static/recording-zoom-100.png differ diff --git a/static/recording-zoom-25.png b/static/recording-zoom-25.png new file mode 100644 index 00000000..fa916270 Binary files /dev/null and b/static/recording-zoom-25.png differ diff --git a/static/recording-zoom-50.png b/static/recording-zoom-50.png new file mode 100644 index 00000000..a57de914 Binary files /dev/null and b/static/recording-zoom-50.png differ diff --git a/static/recording.gif b/static/recording.gif index aa7fd300..a0d003a1 100644 Binary files a/static/recording.gif and b/static/recording.gif differ diff --git a/static/recording.png b/static/recording.png index 08fe68e8..5a08dbad 100644 Binary files a/static/recording.png and b/static/recording.png differ diff --git a/static/resize-demo.gif b/static/resize-demo.gif new file mode 100644 index 00000000..779eeb43 Binary files /dev/null and b/static/resize-demo.gif differ diff --git a/static/screen.svg b/static/screen.svg index c389ff28..9c271045 100644 --- a/static/screen.svg +++ b/static/screen.svg @@ -1 +1 @@ -deleted: test/e2e.test.tsdeleted: tsconfig.jsondeleted: tui-test.config.tsChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: README.mdUntracked files: (use "git add <file>..." to include in what will be committed) .github/skills/SKILL.mdscreen.pngv.svg> sleep 2 > \ No newline at end of file +tui-test - terminal automation - 60x20tui-test> open [ok] session ready Bash - 60x20tui-test> record start demo.png [rec] recording APNG at 2x density tui-test> expect text ready [ok] matched ready \ No newline at end of file