Zsh prompt showing last error code only once - zsh

I would like my prompt to show a cross (✘) when the previous command fails. I use the following code:
export PROMPT=$'%(?..✘\n)\n› '
This gives me the following output:
› echo Hello
Hello
› asjdfiasdf
zsh: command not found: asjdfiasdf
✘
›
✘
I would like to modify the prompt so that it does not repeat the cross when the prompt is redrawn after Enter (the third case in the example above).
Is it possible?

I think I got it. Let me know if you find a bug...
preexec() {
preexec_called=1
}
precmd() {
if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]
then echo ✘; unset preexec_called; fi
}
PROMPT=$'\n› '
Result:
› ofaoisfsaoifoisafas
zsh: command not found: ofaoisfsaoifoisafas
✘
›
› echo $? # (not overwritten)
127

I do this in my zsh, though with colors rather than unicode characters. It's the same principle.
First, I set up my colors, ensuring that they are only used when they are supported:
case $TERM in
( rxvt* | vt100* | xterm* | linux | dtterm* | screen )
function PSC() { echo -n "%{\e[${*}m%}"; } # insert color-specifying chars
ERR="%(0?,`PSC '0;32'`,`PSC '1;31'`)" # if last cmd!=err, hash=green, else red
;;
( * )
function PSC() { true; } # no color support? no problem!
ERR=
;;
esac
Next, I set up a magic enter function (thanks to this post about an empty command (ignore the question, see how I adapt it here):
function magic-enter() { # from https://superuser.com/a/625663
if [[ -n $BUFFER ]]
then unset Z_EMPTY_CMD # Enter was pressed on an empty line
else Z_EMPTY_CMD=1 # The line was NOT empty when Enter was pressed
fi
zle accept-line # still perform the standard binding for Enter
}
zle -N magic-enter # define magic-enter as a widget
bindkey "^M" magic-enter # Backup: use ^J
Now it's time to interpret capture the command and use its return code to set the prompt color:
setopt prompt_subst # allow variable substitution
function preexec() { # just after cmd has been read, right before execution
Z_LAST_CMD="$1" # since $_ is unreliable in the prompt
#Z_LAST_CMD="${1[(wr)^(*=*|sudo|-*)]}" # avoid sudo prefix & options
Z_LAST_CMD_START="$(print -Pn '%D{%s.%.}')"
Z_LAST_CMD_START="${Z_LAST_CMD_START%.}" # zsh <= 5.1.1 makes %. a literal dot
Z_LAST_CMD_START="${Z_LAST_CMD_START%[%]}" # zsh <= 4.3.11 makes %. literal
}
function precmd() { # just before the prompt is rendered
local Z_LAST_RETVAL=$? # $? only works on the first line here
Z_PROMPT_EPOCH="$(print -Pn '%D{%s.%.}')" # nanoseconds, like date +%s.%N
Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%.}" # zsh <= 5.1.1 makes %. a literal dot
Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%[%]}" # zsh <= 4.3.11 makes %. a literal %.
if [ -n "$Z_LAST_CMD_START" ]; then
Z_LAST_CMD_ELAPSED="$(( $Z_PROMPT_EPOCH - $Z_LAST_CMD_START ))"
Z_LAST_CMD_ELAPSED="$(printf %.3f "$Z_LAST_CMD_ELAPSED")s"
else
Z_LAST_CMD_ELAPSED="unknown time"
fi
# full line for error if we JUST got one (not after hitting <enter>)
if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
N=$'\n' # set $N to a literal line break
LERR="$N$(PSC '1;0')[$(PSC '1;31')%D{%Y/%m/%d %T}$(PSC '1;0')]"
LERR="$LERR$(PSC '0;0') code $(PSC '1;31')$Z_LAST_RETVAL"
LERR="$LERR$(PSC '0;0') returned by last command"
LERR="$LERR (run in \$Z_LAST_CMD_ELAPSED):$N"
LERR="$LERR$(PSC '1;31')\$Z_LAST_CMD$(PSC '0;0')$N$N"
print -PR "$LERR"
fi
}
Finally, set the prompt:
PROMPT="$(PSC '0;33')[$(PSC '0;32')%n#%m$(PSC '0;33') %~$PR]$ERR%#$(PSC '0;0') "
Here's how it looks:
A more direct answer to the question, adapted from the above:
function magic-enter() { # from https://superuser.com/a/625663
if [[ -n $BUFFER ]]
then unset Z_EMPTY_CMD # Enter was pressed on an empty line
else Z_EMPTY_CMD=1 # The line was NOT empty when Enter was pressed
fi
zle accept-line # still perform the standard binding for Enter
}
zle -N magic-enter # define magic-enter as a widget
bindkey "^M" magic-enter # Backup: use ^J
function precmd() { # just before the prompt is rendered
local Z_LAST_RETVAL=$? # $? only works on the first line here
# full line for error if we JUST got one (not after hitting <enter>)
if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
echo '✘'
fi
}
PROMPT=$'\n› '
With screen shot:

Use the prexec and precmd hooks:
The preexec hook is called before any command executes. It isn't called when no command is executed. For example, if you press enter at an empty prompt, or a prompt that is only whitespace, it won't be called. A call to this hook signals that a command has been run.
The precmd hook is called before the prompt will be displayed to collect the next command. Before printing the prompt you can print out the exit status. In here we can check if a command was just executed, and if there's a status code we want to display.
This is very similar to the solution suggested by #sneep, which is also a great solution. It's worth using the hooks though so that if you've got anything else registering for these hooks they can do so too.
# print exit code once after last command output
function track-exec-command() {
zsh_exec_command=1
}
function print-exit-code() {
local -i code=$?
(( code == 0 )) && return
(( zsh_exec_command != 1 )) && return
unset zsh_exec_command
print -rC1 -- ''${(%):-"%F{160}✘ exit status $code%f"}''
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec track-exec-command
add-zsh-hook precmd print-exit-code

Thanks to everyone for their answers. Four years later, I would like to illustrate a variation on sneep's answer for those looking for the error code and an alert without a symbol. This is a minimalist prompt but when an error occurs it displays the error code and > in red following the top level directory.
preexec() {
preexec_called=1
}
precmd() {
if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]; then
unset preexec_called
PROMPT='%B%F{blue}%1~%f%b%F{red} $? > %F{black}'
else
PROMPT='%B%F{blue}%1~%f%b%F{blue} > %F{black}'
fi
}

Related

zsh: print time next to command on execute

I want to configure zsh to append the time each command started next to the line command was executed on. For example:
# before I press ENTER
$ ./script
# after I press enter
$ ./script [15:55:58]
Running script...
I came up with the following config (which also colors the timestamp yellow):
preexec () {
TIME=`date +"[%H:%M:%S] "`
echo -e "$1 %{$fg[yellow]%}\033[1A\033[1C${TIME}$reset_color"
}
But it breaks and prints { and % characters on basic commands such as cat and echo. It also breaks on password prompts (macOS terminal). For example with echo:
$ echo "hello" [15:55:58]
hello"hello" %{%}
How can I fix this config?
Thank you.
You inspired me and based on your script I wrote mine. I have tested this on zsh 5.4.smth.
preexec () {
local TIME=`date +"[%H:%M:%S] "`
local zero='%([BSUbfksu]|([FK]|){*})'
local PROMPTLEN=${#${(S%%)PROMPT//$~zero/}}
echo "\033[1A\033[$(($(echo -n $1 | wc -m)+$PROMPTLEN))C $fg[blue]${TIME}$reset_color"
}
In your ~/.zshrc file, put:
function preexec() {
timer=${timer:-$SECONDS}
}
function precmd() {
if [ $timer ]; then
timer_show=$(($SECONDS - $timer))
export RPROMPT="%F{cyan}${timer_show}s %F{$black%}"
unset timer
fi
}
And that should give you something like this:

Make zsh zle restore characters on backspace in vi overwrite-mode

I am using Zsh in Vi-mode.
When $KEYMAP == vicmd (i.e. command-mode), I want hitting backspace to move the cursor to the left by one character, without deleting anything. [working]
When $KEYMAP == viins && $ZLE_STATE == *insert* (i.e. insert-mode), I want hitting backspace to move the cursor to the left by one character, deleting the immediately preceding character on the line. [working]
When $KEYMAP == viins && $ZLE_STATE == *overwrite* (i.e. overwrite-mode / replace-mode), I want hitting backspace to move the cursor to the left by one character, restoring the immediately preceding character on the line with the one that had originally been there prior to entering into overwrite-mode. [NOT working]
Here is an example:
# [COMMAND MODE] We start with the following string on the command line:
$ Hello, world!
^
cursor position
# [REPLACE MODE] Now, I hit "R" to enter replace-mode and I type "stuff".
$ Helstufforld!
^
cursor position
# [REPLACE MODE] Finally, I hit backspace 3 times.
$ Helst, world!
^
cursor position
The above example shows what I want to happen when I hit backspace while in overwrite-mode; however, what really happens is the following:
# [COMMAND MODE] We start with the following string on the command line:
$ Hello, world!
^
cursor position
# [REPLACE MODE] Now, I hit "R" to enter replace-mode and I type "stuff".
$ Helstufforld!
^
cursor position
# [REPLACE MODE] Finally, I hit backspace 3 times.
$ Helstworld!
^
cursor position
Notice how, when hitting backspace in the second example, rather than restoring the original 3 characters that were just overwritten (i.e. ", w"), instead the last 3 characters that replaced these characters (i.e. "uff") were deleted, and the characters to the right of the cursor were shifted to the left.
How do I get the behavior that I want?
Okay, so I ended up hacking together a solution to the problem I was having, which I will post here in case anyone else encounters the same issue.
Solution
Put this in your .zshrc:
readonly ZLE_VI_MODE_CMD=0
readonly ZLE_VI_MODE_INS=1
readonly ZLE_VI_MODE_REP=2
readonly ZLE_VI_MODE_OTH=3
function zle-vi-mode {
if [[ $KEYMAP == vicmd ]]; then
echo -n $ZLE_VI_MODE_CMD
elif [[ $KEYMAP == (viins|main) ]] && [[ $ZLE_STATE == *insert* ]]; then
echo -n $ZLE_VI_MODE_INS
elif [[ $KEYMAP == (viins|main) ]] && [[ $ZLE_STATE == *overwrite* ]]; then
echo -n $ZLE_VI_MODE_REP
else
echo -n $ZLE_VI_MODE_OTH
fi
}
function zle-backward-delete-char-fix {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_REP)
if [[ $CURSOR -le $MARK ]]; then
CURSOR=$(( $(($CURSOR-1)) > 0 ? $(($CURSOR-1)) : 0 ))
MARK=$CURSOR
else
zle undo
fi
;;
*)
zle backward-delete-char
;;
esac
}
zle -N zle-backward-delete-char-fix
## Change cursor shape according to the current Vi-mode.
function zle-line-init zle-keymap-select {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_CMD) echo -ne '\e[2 q' ;; # cursor -> block
$ZLE_VI_MODE_INS) echo -ne '\e[6 q' ;; # cursor -> vertical bar
$ZLE_VI_MODE_REP)
echo -ne '\e[4 q' # cursor -> underline
MARK=$CURSOR
;;
*)
;;
esac
}
zle -N zle-line-init
zle -N zle-keymap-select
bindkey -v
bindkey '^?' zle-backward-delete-char-fix
bindkey '^h' zle-backward-delete-char-fix
The above code will, additionally, cause your cursor shape to change depending on what vi-mode you are currently in (i.e. since this is a copy/paste from my .zshrc, and that's what I like). If you don't want this, and just want the plain fix, replace the zle-init-line / zle-keymap-select function with the following:
function zle-line-init zle-keymap-select {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_REP)
MARK=$CURSOR
;;
*)
;;
esac
}

How to complete a variable number of arguments containing spaces

I've build a command line tool and I need to complete arguments with zsh. I never wrote a zsh completion function so I looked in the scripts provided with zsh but I missed something so that it could work properly.
So, mytool can take a variable number of values and two options.
Here are some call examples:
mytool ONE
mytool ONE TWO
mytool AAA BBB CCC DDD EEE --info
In order to complete the values, I hava another executable that outputs all possible lines to stdout, like this simplified script named getdata:
#!/usr/local/bin/zsh
echo ONE
echo TWO ONE
echo TWO TWO
# ... a lot of lines
echo OTHER ONE
echo ONE ANOTHER LINE
echo AAA BBB CCC DDD EEE
Each completion must match to a whole line, so in my getdata example, it will not be possible to just complete with the value TWO because this whole line does not exist, it must be TWO ONE or TWO TWO.
As this script is quite time consuming, I would like to use zsh caching feature. So, here is my zsh complete script:
compdef _complete_mytool mytool
__mytool_caching_policy() {
oldp=( "$1"(Nmh+1) ) # 1 hour
(( $#oldp ))
}
__mytool_deployments() {
local cache_policy
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
if [[ -z "$cache_policy" ]]; then
zstyle ":completion:${curcontext}:" cache-policy __mytool_caching_policy
fi
if ( [[ ${+_mytool_values} -eq 0 ]] || _cache_invalid mytool_deployments ) \
&& ! _retrieve_cache mytool_deployments;
then
local -a lines
_mytool_values=(${(f)"$(_call_program values getdata)"})
_store_cache mytool_deployments _mytool_values
fi
_describe "mytool values" _mytool_values
}
_complete_mytool() {
integer ret=1
local -a context expl line state state_descr args
typeset -A opt_args
args+=(
'*:values:->values'
'--help[show this help message and exit]'
'(-i --info)'{-i,--info}'[display info about values and exit]'
'(-v --version)'{-v,--version}'[display version about values and exit]'
)
_call_function res __mytool_deployments
return ret
}
But when I try to complete, spaces are escaped with backslash, and I don't want this behaviour.
mytool OTHER\ ONE
The options seem not to be completed too... So, any help will be greatly appreciated.
Thanks to okdana on the freenode zsh channel who helped me a lot.
So, the solution is:
compdef _complete_mytool mytool
__mytool_caching_policy() {
oldp=( "$1"(Nmh+1) ) # 1 hour
(( $#oldp ))
}
__mytool_deployments() {
local cache_policy
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
if [[ -z "$cache_policy" ]]; then
zstyle ":completion:${curcontext}:" cache-policy __mytool_caching_policy
fi
if ( [[ ${+_mytool_values} -eq 0 ]] || _cache_invalid mytool_deployments ) \
&& ! _retrieve_cache mytool_deployments;
then
local -a lines
_mytool_values=(${(f)"$(_call_program values getdata)"})
_store_cache mytool_deployments _mytool_values
fi
_describe "mytool values" _mytool_values -Q
}
_complete_mytool() {
_arguments : \
': :__mytool_deployments' \
'--help[show this help message and exit]' \
'(-i --info)'{-i,--info}'[display info about values and exit]' \
'(-v --version)'{-v,--version}'[display version about values and exit]'
}

Not able to change global variable in function used for zsh prompt

I'm trying to build a zsh function that returns an output based on a time interval. Initially the "You're thirsty" condition is true, but after changing the variable thirsty through the command line and setting it to false, the initial if statement goes through, but the variable thirsty in it doesn't change the global variable thirsty. Is there a way to modify the global variable thirsty?
thirsty=
last_time=
drink_water() {
echo -n "$thirsty"
if [[ $thirsty == false ]]; then
last_time="$[$(date +%s) + 10]"
thirsty=true
echo -n "${last_time} $(date +%s) ${thirsty}"
elif [[ $[last_time] -lt $(date +%s) ]]; then
echo -n "💧 You're thirsty"
fi
}
Since your code is actually called from:
PROMPT='$(drink_water)'
...everything it contains is run in a subprocess spawned as part of this command substitution operation ($() is a "command substitution": It creates a new subprocess, runs the code given in that subprocess, and reads the subprocess's output). When that subprocess exits, changes to variables -- even global variables -- made within the subprocess are lost.
If you put your update code directly inside a precmd function, then it would be run before each prompt is printed but without a command substitution intervening. That is:
precmd() {
local curr_time=$(date +%s) # this is slow, don't repeat it!
if [[ $thirsty = false ]]; then
last_time="$(( curr_time + 10 ))"
thirsty=true
PROMPT="$last_time $curr_time $thirsty"
elif (( last_time < curr_time )); then
PROMPT="💧 You're thirsty"
fi
}
Of course, you can set your PROMPT with a command substitution, but updates to variable state have to be done separately, outside that command substitution, if they are to persist.
You can use Zsh Hooks.
Hooks avoid the issues of command substitution here because they run in the same shell, rather than a subshell.
drink_water_prompt=
thirsty=
last_time=
drink_water_gen_prompt() {
drink_water_prompt="$thirsty"
if [[ $thirsty == false ]]; then
last_time="$[$(date +%s) + 10]"
thirsty=true
drink_water_prompt+="${last_time} $(date +%s) ${thirsty}"
elif [[ $[last_time] -lt $(date +%s) ]]; then
drink_water_prompt+="💧 You're thirsty"
fi
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd drink_water_gen_prompt
PROMPT='${drink_water_prompt}'
These also allow more than one precmd() function.

Running custom zsh function for tmux status bar not displaying output

I'm wrote a function called test_status that I am trying to incorporate in my tmux status bar. To give some background, my tests will output to a file called .guard_result with either success or failure and the test_status function reads from that file and echoes a 💚 if my tests are passing and a ❤️ if they are failing.
The good news is running test_status works just fine, I'm just having trouble getting it to work with tmux. What am I missing here?
# ~/.oh-my-zsh/custom/aliases.zsh
function test_status {
if [ ! -f "./.guard_result" ]; then
echo "?"
return 1
fi
result="$(cat ./.guard_result)"
if [[ $result == *"success"* ]]
then
echo "💚";
elif [[ $result == *"fail"* ]]
then
echo "❤️";
fi
}
This function works... Here is Tmux configuration (which doesn't show result):
# ~/.tmux.conf
set -g status-right "#(test_status) #[fg=colour245]%d %b %Y #[fg=white]:: #[fg=colour245]%l:%M %p"
I know I must be missing something simple... Thanks for your help!
tmux passes shell commands to /bin/sh not zsh. And even if tmux would use zsh, the function would not be available in that context as ~/.zshrc, which loads oh-my-zsh, is only read for interactive shells.
In order to get the the output of test_status into tmux, I would suggest to put the function into a zsh script and call that.
You can either source ~/.oh-my-zsh/custom/aliases.zsh from within the script and then call test_status:
#!/usr/bin/zsh
# ^ make sure this reflects the path to zsh (`type zsh`)
source ~/.oh-my-zsh/custom/aliases.zsh
test_status
Or you can just put the entire function into the script, so as to not clutter alias.zsh:
#!/usr/bin/zsh
function test_status {
if [ ! -f "./.guard_result" ]; then
echo "?"
return 1
fi
result="$(cat ./.guard_result)"
if [[ $result == *"success"* ]]
then
echo "💚";
elif [[ $result == *"fail"* ]]
then
echo "❤️";
fi
}
Safe the script somewhere (e.g. /path/to/test_status.zsh), make it executable (chmod a+x /path/to/test_status.zsh) and call it by path in the tmux configuration.

Resources