zsh right-justify in ps1

10,207

Solution 1

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.

Solution 2

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)

Solution 3

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.

Solution 4

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.

Share:
10,207

Related videos on Youtube

So8res
Author by

So8res

Updated on September 18, 2022

Comments

  • So8res
    So8res over 1 year

    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!

  • calin
    calin about 9 years
    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.
  • CaldeiraG
    CaldeiraG almost 5 years
    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.
  • Roman Perepelitsa
    Roman Perepelitsa almost 5 years
    @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.
  • CaldeiraG
    CaldeiraG almost 5 years
    Looks way better! :p Enjoy your stay here