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`.
@@ -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.
+The same 48x10-cell recording rendered at native 100%, 50%, and 25% zoom:
+
+
+ 100%
+
+
+
+
+ 50%
+
+
+
+
+ 25%
+
+
+
+Resize events keep the encoded canvas stable while existing terminal content
+reflows as the window grows and shrinks in place:
+
+
+
+
+
+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 @@
-
\ No newline at end of file
+
\ No newline at end of file