23

I'd like a multi-line zsh prompt with a right alined part, that will look something like this:

2.nate@host:/current/dir                                               16:00
->

I know about RPROMPT in zsh, but that has a right-aligned prompt opposite your normal prompt, which is on the same line of text as your typing.

Is there a way to have a right-aligned portion to the first line of a multi-line command prompt? I'm looking for either a directive in the PS1 variable that says 'right align now' or a variable that is to PS1 what RPROMPT is to PROMPT.

Thanks!

4 Answers 4

13

You will find a detailed answer and an example here. The idea is to write the line before PS1 using the precmd callback, use $COLUMNS, and a bit of math to calculate the position of the text on the right side of the screen. Knowledge of escape sequences will also help you with cursor positioning and colouring.

Another solution can be to use a theme from Oh My ZSH.

13

I've been looking for this too. For me, the fact that precmd() drawn lines don't redraw on resize or when ^L is used to clear the screen was something that kept itching at me. What I'm doing now is using ANSI escape sequences to move the cursor around a bit. Though I suspect there is a more elegant way to issue them, this is working for me:

_newline=$'\n'
_lineup=$'\e[1A'
_linedown=$'\e[1B'

PROMPT=...whatever...${_newline}...whatever...
RPROMPT=%{${_lineup}%}...whatever...%{${_linedown}%}

Keep in mind that the zsh manual states that %{...%} is for literal escape sequences that don't move the cursor. Even so, I'm using them because they allow to ignore the length of it's content (couldn't figure out how to issue the escape that moves the cursor using them though)

1
  • After playing around with this for some time, it messes up occasionally and puts the cursor or date on the wrong line. Not such a big deal though; can just press enter to correct it.
    – mpen
    Apr 2, 2015 at 18:17
4

Here is how I've configured this thing just now. This approach doesn't require any escape-sequence manipulations, but will make you have two different variables for primary prompt: PS1 with coloring and NPS1 without.

# Here NPS1 stands for "naked PS1" and isn't a built-in shell variable. I've
# defined it myself for PS1-PS2 alignment to operate properly.
PS1='%S%F{red}[%l]%f%s %F{green}%n@%m%f %B%#%b '
NPS1='[%l] %n@%m # '
RPS1='%B%F{green}(%~)%f%b'

# Hook function which gets executed right before shell prints prompt.
function precmd() {
    local expandedPrompt="$(print -P "$NPS1")"
    local promptLength="${#expandedPrompt}"
    PS2="> "
    PS2="$(printf "%${promptLength}s" "$PS2")"
}

Note the usage of print -P for prompt expansion, ${#variable} for getting the length of string stored in variable, and printf "%Nd" for left-hand padding with N spaces. Both print and printf are built-in commands, so there should be no performance hit.

2

Let's define prompt with this layout:

top_left              top_right
bottom_left        bottom_right

To do this, we'll need a function that tells us how many characters a given string takes when printed.

# Usage: prompt-length TEXT [COLUMNS]
#
# If you run `print -P TEXT`, how many characters will be printed
# on the last line?
#
# Or, equivalently, if you set PROMPT=TEXT with prompt_subst
# option unset, on which column will the cursor be?
#
# The second argument specifies terminal width. Defaults to the
# real terminal width.
#
# Assumes that `%{%}` and `%G` don't lie.
#
# Examples:
#
#   prompt-length ''            => 0
#   prompt-length 'abc'         => 3
#   prompt-length $'abc\nxy'    => 2
#   prompt-length '❎'          => 2
#   prompt-length $'\t'         => 8
#   prompt-length $'\u274E'     => 2
#   prompt-length '%F{red}abc'  => 3
#   prompt-length $'%{a\b%Gb%}' => 1
#   prompt-length '%D'          => 8
#   prompt-length '%1(l..ab)'   => 2
#   prompt-length '%(!.a.)'     => 1 if root, 0 if not
function prompt-length() {
  emulate -L zsh
  local COLUMNS=${2:-$COLUMNS}
  local -i x y=$#1 m
  if (( y )); then
    while (( ${${(%):-$1%$y(l.1.0)}[-1]} )); do
      x=y
      (( y *= 2 ));
    done
    local xy
    while (( y > x + 1 )); do
      m=$(( x + (y - x) / 2 ))
      typeset ${${(%):-$1%$m(l.x.y)}[-1]}=$m
    done
  fi
  echo $x
}

We'll need another function that takes two arguments and prints a full fine with these arguments on the opposing sides of the screen.

# Usage: fill-line LEFT RIGHT
#
# Prints LEFT<spaces>RIGHT with enough spaces in the middle
# to fill a terminal line.
function fill-line() {
  emulate -L zsh
  local left_len=$(prompt-length $1)
  local right_len=$(prompt-length $2 9999)
  local pad_len=$((COLUMNS - left_len - right_len - ${ZLE_RPROMPT_INDENT:-1}))
  if (( pad_len < 1 )); then
    # Not enough space for the right part. Drop it.
    echo -E - ${1}
  else
    local pad=${(pl.$pad_len.. .)}  # pad_len spaces
    echo -E - ${1}${pad}${2}
  fi
}

Finally we can define a function that sets PROMPT and RPROMPT, instruct ZSH to call it before every prompt, and set appropriate prompt expansion options:

# Sets PROMPT and RPROMPT.
#
# Requires: prompt_percent and no_prompt_subst.
function set-prompt() {
  emulate -L zsh
  local git_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
  git_branch=${${git_branch//\%/%%}/\\/\\\\\\}  # escape '%' and '\'

  local top_left='%F{blue}%~%f'
  local top_right="%F{green}${git_branch}%f"
  local bottom_left='%B%F{%(?.green.red)}%#%f%b '
  local bottom_right='%F{yellow}%T%f'

  PROMPT="$(fill-line "$top_left" "$top_right")"$'\n'$bottom_left
  RPROMPT=$bottom_right
}

autoload -Uz add-zsh-hook
add-zsh-hook precmd set-prompt
setopt noprompt{bang,subst} prompt{cr,percent,sp}

This produces the following prompt:

~/foo/bar                     master
% █                            10:51
  • Top left: Blue current directory.
  • Top right: Green Git branch.
  • Bottom left: # if root, % if not; green on success, red on error.
  • Bottom right: Yellow current time.

You can find extra details in Multi-line prompt: The missing ingredient and complete code in this gist.

3
  • 1
    Welcome to Super User! Whilst this may theoretically answer the question, it would be preferable to include the essential parts of the answer here, and provide the link for reference.
    – CaldeiraG
    Jul 23, 2019 at 6:53
  • 1
    @CaldeiraG I rewrote my answer following your suggestion. FWIW, the shape of my original answer was informed by the highest-voted and accepted answer on this question. Jul 24, 2019 at 7:42
  • Looks way better! :p Enjoy your stay here
    – CaldeiraG
    Jul 24, 2019 at 7:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.