Skip to content

feat(prompt): ${var@P} renders through the PS1 renderer; prompt preview uses it - #159

Merged
LESdylan merged 1 commit into
developfrom
feat/prompt-expand-P
Sep 26, 2026
Merged

LESdylan merged 1 commit into
developfrom
feat/prompt-expand-P

Conversation

@LESdylan

Copy link
Copy Markdown
Member

What & why

Section 1 of #134: prompt preview showed prompts that never appear.

$ V='100%done'; PS1='[${V}] '
prompt:           [100%done]
prompt preview:   [100/home/me/projone]      # %d read as an escape

The preview rendered a PS1 theme with print -rP "${PS1//%/%%}". print -P is zsh's, so it substitutes a variable first and then reads the value for % escapes. A PS1 does not do that, because hellish follows bash's order there. A segment holding 100%done, 50%off or \w therefore previewed as something else. Doubling every % also hid the bilingual PS1's own %~ from the preview.

print -P is right to behave like zsh. What was missing is a way to render a PS1 exactly as the prompt does. bash has one, ${var@P}, but hellish parsed it and returned the value unchanged (a documented v1 scope-out).

Change. prompt_expand_p (new file, src/infrastructure/prompt_expand.c) implements @P with the PS1 rule itself:

  • it calls zsh_to_ps1 in the mode the dialect bit selects, then ps1_render, the same pair prompt_normal uses for PS1;
  • under set -o zsh, where PS1 is PROMPT, a % in a value is an escape again, as in the live prompt;
  • it uses ps1_render, not ps1_animated, so an @P from a hook or inside a PS1 does not rebuild the live prompt's animation cells;
  • \[ \] render as nothing outside a line editor, as in bash. HELLISH_DBG_PROMPT_MARKS keeps them, the same window print -P already gives the width tests.

prompt preview now draws a PS1 theme with "${PS1@P}". PROMPT themes stay on print -rP, which is the live PROMPT's renderer.

Refs #134

How I verified it

  • Reproduced first.
    • tests/scripts/61_prompt_expand_P.sh is graded against bash --posix 5.3.9. It covers:

      • the escapes both shells have, and \[ \];
      • \nnn and \D{};
      • $VAR, ${…} and $((…)), with values holding \w, %d and $HOME left as they are;
      • unset and empty values, positional parameters and a function's arguments;
      • quoting and assignment contexts.

      develop prints the values raw. This branch matches bash.

    • tests/prompt_expand_p_test.py runs at a real prompt with both the in-process and the forked readers. It checks that:

      • the live PS1 and ${PS1@P} are the same bytes, and stay the same after the variable changes;
      • the same holds under set -o zsh, where the value is read again;
      • prompt preview prints the live prompt for a PS1 theme.

      develop fails 8 of its 14 checks. Its preview line is the issue's bug verbatim: [100/tmp/…one.

    • tests/prompt_themes_test.py renders PS1 themes the way the preview now does. Every check passes.

  • Local gates on this commit (ASan debug build, on develop with docs(prompt): say that $( ) is not run in a prompt, and what to do instead #154 and fix(prompt): PS2 is the prompt of every continuation line #155):
    • golden tests/tester: 5363/5363;
    • tests/run_scripts.sh against bash --posix: 126/126;
    • verify_alloc.sh: identical output on both heaps;
    • alloc_stress.sh: all clean;
    • tests/pty_suite.sh: 112 ok, 6 skipped, 4 failed. None of the four is this change:
      • prompt_compat_matrix, prompt_drift_matrix and prompt_jobs_badge expect the non-root %/$ prompt and get #, because the container runs as root. They fail the same way on develop, and CI runs them as a normal user.
      • hxp_framework_test hit the 420 s per-file limit on this 4-core container. Run alone, it passes every check in 426 s, and develop's binary takes 440 s on the same machine. CI's runners finish it inside the limit (the pty job is green on feat(env): $EPOCHREALTIME and $SRANDOM #157).
  • norminette is OK on every touched file.
  • wiki/interactive.md documents ${PS1@P}.

Notes / trade-offs

  • @P on a whole list, "${@@P}" or "${a[@]@P}", is still a bad substitution. The same is true of every transform (@Q, @U, …): hellish applies the operators to one value, never element by element. That gap is older than this PR and not part of Prompt renderer, extglob and shopt: 8 findings from building a 30-theme config on 2.10.2 #134, so it is left for its own change. ${a[1]@P} works.
  • A value is rendered once, not rescanned, exactly as bash does. Under set -o zsh it is rescanned, because that is what the live zsh prompt does.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG


Generated by Claude Code

…ew uses it

    $ V='100%done'; PS1='[${V}] '
    prompt:           [100%done]
    prompt preview:   [100/home/me/projone]      # %d read as an escape

Section 1 of issue #134: `prompt preview` drew prompts that never appear.

It rendered a PS1 theme with `print -rP "${PS1//%/%%}"`. print -P is
zsh's, and like zsh's it substitutes a variable first and then reads
the value for `%` escapes. A PS1 does not; hellish follows bash's order
there. So a segment holding `100%done`, `50%off` or `\w` previewed as
something else. The doubling also hid the bilingual PS1's own `%~` from
the preview.

print -P is right to be zsh's. What was missing is a way to render a
PS1 exactly as the prompt does, and bash has one: ${var@P}. hellish
parsed it and returned the value unchanged (a documented v1 scope-out).

prompt_expand_p (prompt_expand.c) implements it as the PS1 rule itself:
  - zsh_to_ps1 in the mode the dialect bit selects, then ps1_render,
    the same pair prompt_normal uses for PS1;
  - under `set -o zsh`, where PS1 is PROMPT, a value's `%` is an escape
    again, as in the live prompt;
  - ps1_render, not ps1_animated: an @p from a hook or inside a PS1
    must not rebuild the live prompt's animation cells;
  - \[ \] become nothing outside a line editor, as in bash;
    HELLISH_DBG_PROMPT_MARKS keeps them, the window print -P already
    gives the width tests.

prompt_preview draws a PS1 theme with "${PS1@P}". PROMPT themes stay on
print -rP, which is the live PROMPT's renderer.

Tests:
  - tests/scripts/61_prompt_expand_P.sh, graded against bash --posix,
    covers:
    - the escapes both shells have, and \[ \];
    - \nnn and \D{};
    - $VAR, ${...} and $((...)), with values holding \w, %d and $HOME
      left as they are;
    - unset and empty values, positionals, a function's arguments;
    - quoting and assignment contexts.
    develop prints the values raw; this branch matches bash.
  - tests/prompt_expand_p_test.py, at a real prompt, in-process and
    forked readers, checks that:
    - the live PS1 and ${PS1@P} are the same bytes, and stay so after
      the variable changes;
    - the same holds under set -o zsh, where the value IS read again;
    - `prompt preview` prints the live prompt for a PS1 theme.
    develop fails 8 of its 14 checks. Its preview line is the issue's
    bug verbatim: [100/tmp/...one.
  - tests/prompt_themes_test.py renders PS1 themes the way the preview
    now does. All checks pass.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
@LESdylan
LESdylan merged commit f22f212 into develop Sep 26, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants