0

I recently wrote a function called zuperPrompt that prints out a nice looking prompt and I've set my PROMPT variable in my .zshrc to call that function like so:

setopt PROMPT_SUBST
PROMPT='$(zuperPrompt)'

It is the same issue found here, but I think the problem there was from using a newline escape character in the awk command. I tried doing the whole prompt without any newline characters, but I got the same result :(

Here is the function:

zuperPrompt() {
# count chars in directory name
num_chars=1
if [ ! $(dirs) = '~' ]; then
   let "num_chars=$(basename "`pwd`" | wc -m) - 1"
fi

# draw line
line="\n╭──"
for i in {1..$num_chars}; do
   line+="─"
done
line+="──╯"

directory="%F{8}%K{8}%B%F{blue}%1~%k%F{8}"
arrows="%B%F{magenta}❯%B%F{yellow}❯%F{cyan}❯ "

row1="\n%B%F{green}◆ "$directory"─╮"
row2=$line
row3="\n╰─ "$arrows

print -P $row1$row2$row3

}

Here is the result of pressing tab after typing python3

enter image description here

I'm not sure what is going on here. Thank you in advance if anyone can help!

Update after looking at the syntax highlighting of this in my web browser, I see that $row3 is a different color than the others. I ran my code with just $row1$row2 and it works fine with no shifting of the cursor. Anyone know what's going on there?

Update 2 I removed the -P option from the final print statement and it is working with all three rows.

Kusalananda
  • 333,661
Sam
  • 11
  • 2
  • Thanks for the response, Thomas. No, unfortunately (unless it is related and I just am unaware). I don't have any instances of curly brackets. – Sam Feb 05 '22 at 23:36
  • I remove your "SOLVED" tagging in the title. If you have a solution to you issue, then please add that as an answer. Accepting an answer marks an issue as resolved. See e.g. https://unix.stackexchange.com/help/self-answer and https://unix.stackexchange.com/help/someone-answers – Kusalananda Feb 06 '22 at 07:18

2 Answers2

0

The primary prompt string in the zsh shell is the string assigned to the PROMPT or PS1 variable. It may contain various escape characters and codes that informs the shell about how to dynamically expand the string into the visible prompt.

You can do the expansion of the prompt string yourself using print -rP -- $PROMPT or print -rP -- $PS1.

However, if you assign a string to PROMPT that you have already expanded with print -P, the escape codes that tell the shell what parts of the prompt string is visible and what part of the prompt string is just terminal codes for e.g. changing colors, the shell can no longer keep track of the prompt's size. This gives rise to the type of issues that you describe. A common other issue is that line wrapping for long commands becomes messy.

Therefore, do not use print -P to produce the string that later gets assigned to PROMPT, but instead print -r it to output it "raw".

This is the same issue as described in zsh prompt not escaped properly, but with a slightly different cause (you're removing useful information about non-printing characters from the prompt string rather than never adding information about non-printing characters).


The slightly awkward code

num_chars=1
if [ ! $(dirs) = '~' ]; then
   let "num_chars=$(basename "`pwd`" | wc -m) - 1"
fi

could be written as the single line

num_chars=$( print -n -P '%1~' | wc -m )

(%1~ is the prompt code for the current directory name which you are using later to actually include that string in your prompt.)

And the loop

line="\n╭──"
for i in {1..$num_chars}; do
   line+="─"
done
line+="──╯"

can be written as

printf -v line '\n╭──%*s──╯' $num_chars ''
line=${line// /─}

But you could combine these into

print -v wd -n -P '%1~'
printf -v line '\n╭──%*s──╯' $#wd ''
line=${line// /─}

which gets rid of an ugly command substitution and external call to wc.

Kusalananda
  • 333,661
  • @StéphaneChazelas Thanks for the input. I'll leave the second half of the answer as is (unless you want to make edits), because it's mostly irrelevant to the actual question. Thanks for the print -r info! – Kusalananda Feb 06 '22 at 10:07
0

With the promptsubst option on, you can have parameter expansions, command substitutions and arithmetic expansion being expanded upon prompt expansion, so that you can have PS1='$(command-that-generates-PS1-dynamically)', but the result of that expansion is still meant to be a prompt string where %~ / %m are expanded.

So the last thing you want to do is for command-that-generates-PS1-dynamically to call print -P. It should rather be the other way round. If the command-that-generates-PS1-dynamically output is meant to be displayed literally, you should rather make sure the % (and ! if promptbang is on) are escaped, and all the control sequences that don't move the cursor are wrapped in %{, %}, etc.

Another, more zsh-y approach to have dynamic text in $PS1 other than the ones that all the %x escapes can provide is to not set $promptsubst but generate the dynamic text in a precmd hook and store them in the $psvar array whose member you reference in %1v, %2v... in $PS1.

So, instead of:

command-that-generates-PS1-dynamically() {
  local some_dynamic_data=$(some-cmd)
  print -r -- "%F{red}${some_dynamic_data//\%/%%}%f$ "
}
set -o promptsubst
PS1='$(command-that-generates-PS1-dynamically)'

(with the ${some_dynamic_data//\%/%%} only taking care of escaping the %s, not handling ! nor escape sequences that $some_dynamic_data might contain).

Do:

prompt-hook() {
  psvar[1]=$(some-cmd)
}
precmd_function+=(prompt-hook)
PS1='%F{red}%1v%f$ '

That also saves having to fork a subshell (which also allows your prompt-hook to maintain some data across prompt expansions), and the %1v expansion (which is meant to be for printable text) takes care or transforming unprintable characters so they become visible.

In your case, that could look like

display_width() REPLY=$(($#1 * 3 - ${#${(ml[$#1 * 2])1}}))

prompt-hook() { display_width ${(%):-%1~} psvar[1]=${(l[$REPLY][─])} }

precmd_functions+=(prompt-hook)

() { local directory='%F{8}%K{8}%F{blue}%1~%k%F{8}' local arrows='%B%F{magenta}❯%F{yellow}❯%F{cyan}❯' local line=$'─╮\n╭──%1v──╯\n╰─' PS1=$'\n'"%B%F{green}◆ ${directory}${line} ${arrows}%b%f " }

Where $PS1 is static (no need for $promptsubst; we just use temporary ephemeral variables here to improve legibility), but the dynamic custom part (the part of the line that's meant to be the same length as the display width of the expansion of %1~) is generated as part of our precmd hook.

You'll notice that no subshell is forked in that process.

See this answer to Get the display width of a string of characters for details about that display_width function to compute the display width of a string (which is not necessarily the same as the number of characters in it).