From a9f870c2efc017dc296173e25d8d62dc6c25c688 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:19:02 -0700 Subject: [PATCH 1/7] Add zsh prompt redraw regression test --- ...tty_zsh_prompt_redraw_uses_prompt_start.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py diff --git a/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py b/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py new file mode 100644 index 00000000..7827265d --- /dev/null +++ b/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Regression: zsh prompt redraws should not replay fresh-line OSC 133;A markers. + +Prompt themes with async redraws (such as Prezto-like setups) can call +`zle reset-prompt` after the prompt is already visible. Ghostty's zsh shell +integration should emit a single fresh prompt mark for the actual prompt, then +use OSC 133;P for redraws so redraws stay in place instead of looking like +extra prompt lines. +""" + +from __future__ import annotations + +import os +import pty +import select +import shutil +import subprocess +import tempfile +import time +from pathlib import Path + + +FRESH_PROMPT = b"\x1b]133;A;cl=line\x07" +PROMPT_START = b"\x1b]133;P;k=i\x07" +END_COMMAND = b"\x1b]133;D\x07" +START_OUTPUT = b"\x1b]133;C\x07" + + +def _write_redrawing_zshrc(path: Path) -> None: + path.write_text( + """ +autoload -Uz add-zsh-hook + +setopt prompt_cr prompt_percent prompt_sp prompt_subst +PROMPT='%F{4}%1~%f %# ' +RPROMPT='' + +typeset -gi _cmux_redraw_done=0 +typeset -g _cmux_redraw_fd='' + +_cmux_redraw_precmd() { + _cmux_redraw_done=0 +} + +_cmux_redraw_ready() { + emulate -L zsh + local fd="${1:-$_cmux_redraw_fd}" + if [[ -n "$fd" ]]; then + zle -F "$fd" + exec {fd}<&- + fi + _cmux_redraw_fd='' + (( _cmux_redraw_done )) && return 0 + _cmux_redraw_done=1 + zle reset-prompt +} + +_cmux_redraw_line_init() { + if (( !_cmux_redraw_done )) && [[ -z "$_cmux_redraw_fd" ]]; then + exec {_cmux_redraw_fd}< <( + sleep 0.05 + printf 'ready\\n' + ) + zle -F "$_cmux_redraw_fd" _cmux_redraw_ready + fi +} + +add-zsh-hook precmd _cmux_redraw_precmd +zle -N zle-line-init _cmux_redraw_line_init +""".lstrip(), + encoding="utf-8", + ) + + +def _capture_session(env: dict[str, str]) -> bytes: + master, slave = pty.openpty() + proc = subprocess.Popen( + ["zsh", "-d", "-i"], + stdin=slave, + stdout=slave, + stderr=slave, + env=env, + close_fds=True, + ) + os.close(slave) + + output = bytearray() + start = time.time() + phase = 0 + try: + while time.time() - start < 5: + readable, _, _ = select.select([master], [], [], 0.2) + if master in readable: + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + output.extend(chunk) + + elapsed = time.time() - start + if phase == 0 and elapsed > 1.0: + os.write(master, b"\n") + phase = 1 + elif phase == 1 and elapsed > 2.5: + os.write(master, b"exit\n") + phase = 2 + finally: + try: + proc.wait(timeout=5) + finally: + os.close(master) + + return bytes(output) + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + wrapper_dir = root / "ghostty" / "src" / "shell-integration" / "zsh" + if not (wrapper_dir / ".zshenv").exists(): + print(f"SKIP: missing Ghostty zsh wrapper at {wrapper_dir}") + return 0 + + if shutil.which("zsh") is None: + print("SKIP: zsh not installed") + return 0 + + base = Path(tempfile.mkdtemp(prefix="cmux_ghostty_prompt_redraw_")) + try: + home = base / "home" + home.mkdir(parents=True, exist_ok=True) + _write_redrawing_zshrc(home / ".zshrc") + + env = dict(os.environ) + env["HOME"] = str(home) + env["ZDOTDIR"] = str(wrapper_dir) + env["GHOSTTY_ZSH_ZDOTDIR"] = str(home) + env["GHOSTTY_RESOURCES_DIR"] = str(root / "ghostty" / "src") + env.pop("GHOSTTY_SHELL_FEATURES", None) + env.pop("GHOSTTY_BIN_DIR", None) + + output = _capture_session(env) + + marker = output.find(END_COMMAND) + if marker == -1: + print("FAIL: did not observe OSC 133;D for the empty command prompt cycle") + return 1 + + end = output.find(START_OUTPUT, marker + len(END_COMMAND)) + if end == -1: + end = len(output) + + prompt_cycle = output[marker:end] + fresh_count = prompt_cycle.count(FRESH_PROMPT) + prompt_start_count = prompt_cycle.count(PROMPT_START) + + if fresh_count != 1: + print(f"FAIL: expected exactly 1 fresh prompt marker after redraw, saw {fresh_count}") + return 1 + + if prompt_start_count < 1: + print("FAIL: expected redraw path to emit OSC 133;P prompt-start markers") + return 1 + + print("PASS: zsh prompt redraws keep a single fresh prompt marker and reuse OSC 133;P") + return 0 + finally: + shutil.rmtree(base, ignore_errors=True) + + +if __name__ == "__main__": + raise SystemExit(main()) From abf1deed8fa0062a4190719c408e9cd375b9336a Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:19:25 -0700 Subject: [PATCH 2/7] Fix zsh prompt redraw markers --- docs/ghostty-fork.md | 17 ++++++++++++++++- ghostty | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/ghostty-fork.md b/docs/ghostty-fork.md index c98a76c3..dd3d6dc5 100644 --- a/docs/ghostty-fork.md +++ b/docs/ghostty-fork.md @@ -12,7 +12,7 @@ When we change the fork, update this document and the parent submodule SHA. ## Current fork changes -Fork rebased onto upstream `v1.3.0` plus newer `main` commits as of March 9, 2026. +Fork rebased onto upstream `v1.3.0` plus newer `main` commits as of March 12, 2026. ### 1) OSC 99 (kitty) notification parser @@ -62,6 +62,17 @@ section 3 copy-mode commit, even though the section 4 resize commits were applie - Replays the last rendered frame during resize and keeps its geometry anchored correctly. - Reduces transient blank or scaled frames while a macOS window is being resized. +### 5) zsh prompt redraw markers use OSC 133 P + +- Commit: `8ade43ce5` (zsh: use OSC 133 P for prompt redraws) +- Files: + - `src/shell-integration/zsh/ghostty-integration` +- Summary: + - Emits one `OSC 133;A` fresh-prompt mark for real prompt transitions. + - Uses `OSC 133;P` markers for prompt redraws so async zsh themes do not look like extra prompt lines. + +The fork branch HEAD is now the section 5 zsh redraw commit. + ## Upstreamed fork changes ### cursor-click-to-move respects OSC 133 click-to-move @@ -80,4 +91,8 @@ These files change frequently upstream; be careful when rebasing the fork: - `src/terminal/osc.zig` - OSC dispatch logic moves often. Re-check the integration points for the OSC 99 parser. +- `src/shell-integration/zsh/ghostty-integration` + - Prompt marker handling is easy to regress when upstream adjusts zsh redraw behavior. Keep the + `OSC 133;A` vs `OSC 133;P` split intact for redraw-heavy themes. + If you resolve a conflict, update this doc with what changed. diff --git a/ghostty b/ghostty index a50579bd..8ade43ce 160000 --- a/ghostty +++ b/ghostty @@ -1 +1 @@ -Subproject commit a50579bd5ddec81c6244b9b349d4bf781f667cec +Subproject commit 8ade43ce52cda470ffe07e963ab7722c38380792 From 9b529a128b3fe3419edc3f3b74f8c30e2e87fac7 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:39:03 -0700 Subject: [PATCH 3/7] test: cover Pure-style zsh prompt redraws --- .../test_ghostty_zsh_pure_preprompt_redraw.py | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tests/test_ghostty_zsh_pure_preprompt_redraw.py diff --git a/tests/test_ghostty_zsh_pure_preprompt_redraw.py b/tests/test_ghostty_zsh_pure_preprompt_redraw.py new file mode 100644 index 00000000..76efa13f --- /dev/null +++ b/tests/test_ghostty_zsh_pure_preprompt_redraw.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Regression: Ghostty's zsh integration must not leave stale Pure-style preprompt +lines behind after an async redraw. + +Pure does not render its top path/git line as a static multiline PS1. Instead, +it rewrites PROMPT with a special newline sequence and later calls +`zle .reset-prompt` when async git info arrives. Plain zsh redraws that cleanly. +The Ghostty integration currently leaves stale copies of the old top line behind. + +This test uses a minimal Pure-like prompt implementation as a control: +- plain zsh must redraw without stale preprompt lines +- Ghostty-integrated zsh must match that behavior +""" + +from __future__ import annotations + +import os +import pty +import re +import select +import shutil +import subprocess +import tempfile +import time +from pathlib import Path + + +_MINIMAL_PURE_ZSHRC = r""" +setopt promptsubst nopromptcr nopromptsp +prompt_newline=$'\n%{\r%}' + +typeset -g CMUX_TOP='%F{4}%~%f' +typeset -g CMUX_LAST_PROMPT='' +typeset -gi CMUX_ASYNC_DONE=0 +typeset -g CMUX_ASYNC_FD='' + +cmux_render_prompt() { + local cleaned_ps1=$PROMPT + if [[ $PROMPT = *$prompt_newline* ]]; then + cleaned_ps1=${PROMPT##*${prompt_newline}} + fi + + PROMPT="${CMUX_TOP}${prompt_newline}${cleaned_ps1:-%F{5}❯%f }" + + local expanded_prompt="${(S%%)PROMPT}" + if [[ ${1:-} == precmd ]]; then + print + elif [[ $CMUX_LAST_PROMPT != $expanded_prompt ]]; then + zle && zle .reset-prompt + fi + typeset -g CMUX_LAST_PROMPT=$expanded_prompt +} + +cmux_async_ready() { + emulate -L zsh + local fd="${1:-$CMUX_ASYNC_FD}" + if [[ -n $fd ]]; then + zle -F "$fd" + exec {fd}<&- + fi + CMUX_ASYNC_FD='' + + (( CMUX_ASYNC_DONE )) && return + CMUX_ASYNC_DONE=1 + CMUX_TOP='%F{4}%~%f %F{242}main%f%F{218}*%f %F{6}⇣⇡%f' + cmux_render_prompt async +} + +precmd() { + CMUX_ASYNC_DONE=0 + cmux_render_prompt precmd +} + +cmux_line_init() { + if (( !CMUX_ASYNC_DONE )) && [[ -z $CMUX_ASYNC_FD ]]; then + exec {CMUX_ASYNC_FD}< <( + sleep 0.05 + printf 'ready\n' + ) + zle -F "$CMUX_ASYNC_FD" cmux_async_ready + fi +} + +zle -N zle-line-init cmux_line_init +PROMPT='%F{5}❯%f ' +""".lstrip() + +_ANSI_RE = re.compile(rb"\x1b\][^\x07]*\x07|\x1b\[[0-9;?]*[ -/]*[@-~]|\r") + + +def _capture_session(*, use_ghostty: bool, wrapper_dir: Path, resources_dir: Path, workdir: Path) -> str: + base = Path(tempfile.mkdtemp(prefix="cmux_ghostty_pure_preprompt_")) + try: + home = base / "home" + home.mkdir(parents=True, exist_ok=True) + (home / ".zshrc").write_text(_MINIMAL_PURE_ZSHRC, encoding="utf-8") + + env = dict(os.environ) + env["HOME"] = str(home) + env["TERM"] = "xterm-256color" + env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + if use_ghostty: + env["ZDOTDIR"] = str(wrapper_dir) + env["GHOSTTY_ZSH_ZDOTDIR"] = str(home) + env["GHOSTTY_RESOURCES_DIR"] = str(resources_dir) + else: + env["ZDOTDIR"] = str(home) + env.pop("GHOSTTY_ZSH_ZDOTDIR", None) + env.pop("GHOSTTY_RESOURCES_DIR", None) + + master, slave = pty.openpty() + proc = subprocess.Popen( + ["zsh", "-d", "-i"], + cwd=str(workdir), + stdin=slave, + stdout=slave, + stderr=slave, + env=env, + close_fds=True, + ) + os.close(slave) + + output = bytearray() + start = time.time() + phase = 0 + try: + while time.time() - start < 4.5: + readable, _, _ = select.select([master], [], [], 0.2) + if master in readable: + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + output.extend(chunk) + + elapsed = time.time() - start + if phase == 0 and elapsed > 1.2: + os.write(master, b"\n") + phase = 1 + elif phase == 1 and elapsed > 2.8: + os.write(master, b"exit\n") + phase = 2 + finally: + try: + proc.wait(timeout=5) + finally: + os.close(master) + + cleaned = _ANSI_RE.sub(b"", bytes(output)).decode("utf-8", errors="replace") + return cleaned + finally: + shutil.rmtree(base, ignore_errors=True) + + +def _stale_preprompt_lines(cleaned: str, path_line: str, async_line: str) -> tuple[int, int]: + marker = cleaned.find(async_line) + if marker == -1: + return (-1, -1) + + tail = cleaned[marker + len(async_line) :] + return (tail.count(path_line), tail.count(async_line)) + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + wrapper_dir = root / "ghostty" / "src" / "shell-integration" / "zsh" + resources_dir = root / "ghostty" / "src" + workdir = root + + if not (wrapper_dir / ".zshenv").exists(): + print(f"SKIP: missing Ghostty zsh wrapper at {wrapper_dir}") + return 0 + if shutil.which("zsh") is None: + print("SKIP: zsh not installed") + return 0 + + path_line = f"{workdir}\n" + async_line = f"{workdir} main* ⇣⇡" + + plain = _capture_session( + use_ghostty=False, + wrapper_dir=wrapper_dir, + resources_dir=resources_dir, + workdir=workdir, + ) + ghostty = _capture_session( + use_ghostty=True, + wrapper_dir=wrapper_dir, + resources_dir=resources_dir, + workdir=workdir, + ) + + plain_stale, plain_async = _stale_preprompt_lines(plain, path_line, async_line) + ghostty_stale, ghostty_async = _stale_preprompt_lines(ghostty, path_line, async_line) + + if plain_stale < 0: + print("FAIL: plain zsh control never rendered the async preprompt line") + return 1 + if ghostty_stale < 0: + print("FAIL: Ghostty zsh integration never rendered the async preprompt line") + return 1 + + if plain_stale != 0: + print(f"FAIL: plain zsh control left stale preprompt lines behind ({plain_stale})") + return 1 + + if ghostty_stale != plain_stale: + print( + "FAIL: Ghostty zsh integration left stale preprompt lines behind " + f"(ghostty={ghostty_stale}, plain={plain_stale}, async_renders={ghostty_async})" + ) + return 1 + + print("PASS: Ghostty zsh integration redraws Pure-style preprompts without stale lines") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3c58e8c5ca3714eeae1ca0fcee678f01505415f6 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:39:17 -0700 Subject: [PATCH 4/7] ghostty: fix Pure-style multiline prompt redraws --- ghostty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghostty b/ghostty index 8ade43ce..0cf55958 160000 --- a/ghostty +++ b/ghostty @@ -1 +1 @@ -Subproject commit 8ade43ce52cda470ffe07e963ab7722c38380792 +Subproject commit 0cf5595817794466e3a60abe6bf97f8494dedcfe From 473b2a0d1c6f92cc197428bc3d011fb32071a024 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:44:06 -0700 Subject: [PATCH 5/7] docs: clarify fork head and test env setup --- docs/ghostty-fork.md | 14 ++++++++++++-- tests/test_ghostty_zsh_pure_preprompt_redraw.py | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/ghostty-fork.md b/docs/ghostty-fork.md index dd3d6dc5..36189e2d 100644 --- a/docs/ghostty-fork.md +++ b/docs/ghostty-fork.md @@ -71,7 +71,16 @@ section 3 copy-mode commit, even though the section 4 resize commits were applie - Emits one `OSC 133;A` fresh-prompt mark for real prompt transitions. - Uses `OSC 133;P` markers for prompt redraws so async zsh themes do not look like extra prompt lines. -The fork branch HEAD is now the section 5 zsh redraw commit. +### 6) zsh Pure-style multiline prompt redraws + +- Commit: `0cf559581` (zsh: fix Pure-style multiline prompt redraws) +- Files: + - `src/shell-integration/zsh/ghostty-integration` +- Summary: + - Handles multiline prompts that use `\n%{\r%}` to return to column 0 before the visible prompt line. + - Places the continuation marker after Pure's hidden carriage return so async redraws do not leave stale preprompt lines behind. + +The fork branch HEAD is now the section 6 zsh redraw commit. ## Upstreamed fork changes @@ -93,6 +102,7 @@ These files change frequently upstream; be careful when rebasing the fork: - `src/shell-integration/zsh/ghostty-integration` - Prompt marker handling is easy to regress when upstream adjusts zsh redraw behavior. Keep the - `OSC 133;A` vs `OSC 133;P` split intact for redraw-heavy themes. + `OSC 133;A` vs `OSC 133;P` split intact for redraw-heavy themes, and preserve the special + handling for Pure-style `\n%{\r%}` prompt newlines. If you resolve a conflict, update this doc with what changed. diff --git a/tests/test_ghostty_zsh_pure_preprompt_redraw.py b/tests/test_ghostty_zsh_pure_preprompt_redraw.py index 76efa13f..f59fe75e 100644 --- a/tests/test_ghostty_zsh_pure_preprompt_redraw.py +++ b/tests/test_ghostty_zsh_pure_preprompt_redraw.py @@ -99,7 +99,8 @@ def _capture_session(*, use_ghostty: bool, wrapper_dir: Path, resources_dir: Pat env = dict(os.environ) env["HOME"] = str(home) env["TERM"] = "xterm-256color" - env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + env.pop("GHOSTTY_SHELL_FEATURES", None) + env.pop("GHOSTTY_BIN_DIR", None) if use_ghostty: env["ZDOTDIR"] = str(wrapper_dir) env["GHOSTTY_ZSH_ZDOTDIR"] = str(home) From 4d0472360006c93dcacedb57848ba46ad4f09d6f Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:48:14 -0700 Subject: [PATCH 6/7] build: pin GhosttyKit checksum for prompt redraw fix --- scripts/ghosttykit-checksums.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ghosttykit-checksums.txt b/scripts/ghosttykit-checksums.txt index 8ab36d3b..b3818784 100644 --- a/scripts/ghosttykit-checksums.txt +++ b/scripts/ghosttykit-checksums.txt @@ -3,3 +3,4 @@ # Format: 7dd589824d4c9bda8265355718800cccaf7189a0 3915af4256850a0a7bee671c3ba0a47cbfee5dbfc6d71caf952acefdf2ee4207 a50579bd5ddec81c6244b9b349d4bf781f667cec f7e9c0597468a263d6b75eaf815ccecd90c7933f3cf4ae58929569ff23b2666d +0cf5595817794466e3a60abe6bf97f8494dedcfe 1c6ae53ea549740bd45e59fe92714a292fb0d71a41ff915eb6b2e644468152de From 6258fbb48261aeb3efbe1b3c690a6a44969c108c Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:50:53 -0700 Subject: [PATCH 7/7] tests: resolve zsh paths in redraw regressions --- docs/ghostty-fork.md | 5 +++-- ...hostty_zsh_prompt_redraw_uses_prompt_start.py | 9 +++++---- tests/test_ghostty_zsh_pure_preprompt_redraw.py | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/ghostty-fork.md b/docs/ghostty-fork.md index 36189e2d..30ba29bf 100644 --- a/docs/ghostty-fork.md +++ b/docs/ghostty-fork.md @@ -45,8 +45,9 @@ Fork rebased onto upstream `v1.3.0` plus newer `main` commits as of March 12, 20 ### 4) macOS resize stale-frame mitigation -Sections 3 and 4 are grouped by feature, not by commit order. The fork branch HEAD is the -section 3 copy-mode commit, even though the section 4 resize commits were applied earlier. +Sections 3 and 4 are grouped by feature, not by commit order. The section 4 resize commits were +applied earlier than the section 3 copy-mode commit, but they are kept together here because they +touch the same stale-frame mitigation path and tend to conflict in the same files during rebases. - Commits: - `769bbf7a9` (macos: reduce transient blank/scaled frames during resize) diff --git a/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py b/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py index 7827265d..32ff0d64 100644 --- a/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py +++ b/tests/test_ghostty_zsh_prompt_redraw_uses_prompt_start.py @@ -73,10 +73,10 @@ zle -N zle-line-init _cmux_redraw_line_init ) -def _capture_session(env: dict[str, str]) -> bytes: +def _capture_session(env: dict[str, str], zsh_path: str) -> bytes: master, slave = pty.openpty() proc = subprocess.Popen( - ["zsh", "-d", "-i"], + [zsh_path, "-d", "-i"], stdin=slave, stdout=slave, stderr=slave, @@ -123,7 +123,8 @@ def main() -> int: print(f"SKIP: missing Ghostty zsh wrapper at {wrapper_dir}") return 0 - if shutil.which("zsh") is None: + zsh_path = shutil.which("zsh") + if zsh_path is None: print("SKIP: zsh not installed") return 0 @@ -141,7 +142,7 @@ def main() -> int: env.pop("GHOSTTY_SHELL_FEATURES", None) env.pop("GHOSTTY_BIN_DIR", None) - output = _capture_session(env) + output = _capture_session(env, zsh_path) marker = output.find(END_COMMAND) if marker == -1: diff --git a/tests/test_ghostty_zsh_pure_preprompt_redraw.py b/tests/test_ghostty_zsh_pure_preprompt_redraw.py index f59fe75e..ab916fe5 100644 --- a/tests/test_ghostty_zsh_pure_preprompt_redraw.py +++ b/tests/test_ghostty_zsh_pure_preprompt_redraw.py @@ -89,7 +89,14 @@ PROMPT='%F{5}❯%f ' _ANSI_RE = re.compile(rb"\x1b\][^\x07]*\x07|\x1b\[[0-9;?]*[ -/]*[@-~]|\r") -def _capture_session(*, use_ghostty: bool, wrapper_dir: Path, resources_dir: Path, workdir: Path) -> str: +def _capture_session( + *, + use_ghostty: bool, + wrapper_dir: Path, + resources_dir: Path, + workdir: Path, + zsh_path: str, +) -> str: base = Path(tempfile.mkdtemp(prefix="cmux_ghostty_pure_preprompt_")) try: home = base / "home" @@ -112,7 +119,7 @@ def _capture_session(*, use_ghostty: bool, wrapper_dir: Path, resources_dir: Pat master, slave = pty.openpty() proc = subprocess.Popen( - ["zsh", "-d", "-i"], + [zsh_path, "-d", "-i"], cwd=str(workdir), stdin=slave, stdout=slave, @@ -174,7 +181,8 @@ def main() -> int: if not (wrapper_dir / ".zshenv").exists(): print(f"SKIP: missing Ghostty zsh wrapper at {wrapper_dir}") return 0 - if shutil.which("zsh") is None: + zsh_path = shutil.which("zsh") + if zsh_path is None: print("SKIP: zsh not installed") return 0 @@ -186,12 +194,14 @@ def main() -> int: wrapper_dir=wrapper_dir, resources_dir=resources_dir, workdir=workdir, + zsh_path=zsh_path, ) ghostty = _capture_session( use_ghostty=True, wrapper_dir=wrapper_dir, resources_dir=resources_dir, workdir=workdir, + zsh_path=zsh_path, ) plain_stale, plain_async = _stale_preprompt_lines(plain, path_line, async_line)