From 23f3cd5f101fedcff6350648f8ba3993e6c55d90 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 11 Mar 2026 10:07:54 -0400 Subject: [PATCH] zsh: improve prompt marking with dynamic themes Replace the strip-in-preexec / re-add-in-precmd pattern for OSC 133 marks with a save/restore approach. Instead of pattern-matching marks out of PS1 (which exposes PS1 in intermediate states to other hooks), we save the original PS1/PS2 before adding marks and then restore them. This also adds dynamic theme detection: if PS1 changed between cycles (e.g., a theme rebuilt it), we skip injecting continuation marks into newlines. This prevents breaking plugins like Pure that use pattern matching to strip/rebuild the prompt. Additionally, move _ghostty_precmd to the end of precmd_functions in _ghostty_deferred_init (instead of substituting in-place) so that the first prompt is properly marked even when other hooks were appended after our auto-injection. There's one scenario that we still don't complete cover: precmd_functions+=(_test_overwrite_ps1) _test_overwrite_ps1() { PS1="test> " } ... which results in the first prompt not printing its prompt marks because _test_overwrite_ps1 becomes the last thing to run, overwriting our marks, but this will be fixed for subsequent prompts when we move our handler back to the last index. Fixes: #11282 --- src/shell-integration/zsh/ghostty-integration | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 7442546f8..dc9bd1605 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -131,32 +131,59 @@ _ghostty_deferred_init() { # SIGCHLD if notify is set. Themes that update prompt # asynchronously from a `zle -F` handler might still remove our # marks. Oh well. + + # Restore PS1/PS2 to their pre-mark state if nothing else has + # modified them since we last added marks. This avoids exposing + # PS1 with our marks to other hooks (which can break themes like + # Pure that use pattern matching to strip/rebuild the prompt). + # If PS1 was modified (by a theme, async update, etc.), we + # keep the modified version, prioritizing the theme's changes. + builtin local ps1_changed=0 + if [[ -n ${_ghostty_saved_ps1+x} ]]; then + if [[ $PS1 == $_ghostty_marked_ps1 ]]; then + PS1=$_ghostty_saved_ps1 + PS2=$_ghostty_saved_ps2 + elif [[ $PS1 != $_ghostty_saved_ps1 ]]; then + ps1_changed=1 + fi + fi + + # Save the clean PS1/PS2 before we add marks. + _ghostty_saved_ps1=$PS1 + _ghostty_saved_ps2=$PS2 + + # Add our marks. Since we always start from a clean PS1 + # (either restored above or freshly set by a theme), we can + # unconditionally add mark1 and markB. builtin local mark2=$'%{\e]133;A;k=s\a%}' builtin local markB=$'%{\e]133;B\a%}' - # Add marks conditionally to avoid a situation where we have - # several marks in place. These conditions can have false - # positives and false negatives though. - # - # - False positive (with prompt_percent): PS1="%(?.$mark1.)" - # - False negative (with prompt_subst): PS1='$mark1' - [[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1} - [[ $PS1 == *$markB* ]] || PS1=${PS1}${markB} + PS1=${mark1}${PS1}${markB} + # Handle multiline prompts by marking newline-separated # continuation lines with k=s (mark2). We skip the newline # immediately after mark1 to avoid introducing a double # newline due to OSC 133;A's fresh-line behavior. - if [[ $PS1 == ${mark1}$'\n'* ]]; then - builtin local rest=${PS1#${mark1}$'\n'} - if [[ $rest == *$'\n'* ]]; then - PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}} + # + # We skip this when PS1 changed because injecting marks into + # newlines can break pattern matching in themes that + # strip/rebuild the prompt dynamically (e.g., Pure). + if (( ! ps1_changed )) && [[ $PS1 == *$'\n'* ]]; then + if [[ $PS1 == ${mark1}$'\n'* ]]; then + builtin local rest=${PS1#${mark1}$'\n'} + if [[ $rest == *$'\n'* ]]; then + PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}} + fi + else + PS1=${PS1//$'\n'/$'\n'${mark2}} fi - elif [[ $PS1 == *$'\n'* ]]; then - PS1=${PS1//$'\n'/$'\n'${mark2}} fi # PS2 mark is needed when clearing the prompt on resize - [[ $PS2 == *$mark2* ]] || PS2=${mark2}${PS2} - [[ $PS2 == *$markB* ]] || PS2=${PS2}${markB} + PS2=${mark2}${PS2}${markB} + + # Save the marked PS1 so we can detect modifications + # by other hooks in the next cycle. + _ghostty_marked_ps1=$PS1 (( _ghostty_state = 2 )) else # If our precmd hook is not the last, we cannot rely on prompt @@ -188,17 +215,14 @@ _ghostty_deferred_init() { _ghostty_preexec() { builtin emulate -L zsh -o no_warn_create_global -o no_aliases - # This can potentially break user prompt. Oh well. The robustness of - # this code can be improved in the case prompt_subst is set because - # it'll allow us distinguish (not perfectly but close enough) between - # our own prompt, user prompt, and our own prompt with user additions on - # top. We cannot force prompt_subst on the user though, so we would - # still need this code for the no_prompt_subst case. - PS1=${PS1//$'%{\e]133;A;cl=line\a%}'} - PS1=${PS1//$'%{\e]133;A;k=s\a%}'} - PS1=${PS1//$'%{\e]133;B\a%}'} - PS2=${PS2//$'%{\e]133;A;k=s\a%}'} - PS2=${PS2//$'%{\e]133;B\a%}'} + # Restore the original PS1/PS2 if nothing else has modified them + # since our precmd added marks. This ensures other preexec hooks + # see a clean PS1 without our marks. If PS1 was modified (e.g., + # by an async theme update), we leave it alone. + if [[ -n ${_ghostty_saved_ps1+x} && $PS1 == $_ghostty_marked_ps1 ]]; then + PS1=$_ghostty_saved_ps1 + PS2=$_ghostty_saved_ps2 + fi # This will work incorrectly in the presence of a preexec hook that # prints. For example, if MichaelAquilina/zsh-you-should-use installs @@ -419,7 +443,7 @@ _ghostty_deferred_init() { builtin typeset -ag precmd_functions if (( $+functions[_ghostty_precmd] )); then - precmd_functions=(${precmd_functions:/_ghostty_deferred_init/_ghostty_precmd}) + precmd_functions=(${precmd_functions:#_ghostty_deferred_init} _ghostty_precmd) _ghostty_precmd else precmd_functions=(${precmd_functions:#_ghostty_deferred_init})