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

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
}

Related

In zsh, is the tag "dynamic-dirs" special?

I’m wanting to do basically what this function does. It comes from the zsh manual page and other zsh documentation. I’m trying to “vaguely understand” without diving into the fine details what this function is doing and how it works.
In this case, I understand everything more or less except the _wanted line and in particular, is the tag "dynamic-dirs" any arbitrary tag or does it need to match what the higher level driving functions are looking for? I read briefly about tags and it seems like they would need to match up but I can’t find dynamic-dirs anywhere in any code that I’ve grep’ed. Nor have a found any type of list of tags that are used and what they means except the example of “files” and “directories” mentioned in the first few paragraphs of zshcompsys(1).
zsh_directory_name() {
emulate -L zsh
setopt extendedglob
local -a match mbegin mend
if [[ $1 = d ]]; then
# turn the directory into a name
if [[ $2 = (#b)(/home/pws/perforce/)([^/]##)* ]]; then
typeset -ga reply
reply=(p:$match[2] $(( ${#match[1]} + ${#match[2]} )) )
else
return 1
fi
elif [[ $1 = n ]]; then
# turn the name into a directory
[[ $2 != (#b)p:(?*) ]] && return 1
typeset -ga reply
reply=(/home/pws/perforce/$match[1])
elif [[ $1 = c ]]; then
# complete names
local expl
local -a dirs
dirs=(/home/pws/perforce/*(/:t))
dirs=(p:${^dirs})
_wanted dynamic-dirs expl 'dynamic directory' compadd -S\] -a dirs
return
else
return 1
fi
return 0
}
The other question is about the ’n’ section. It is going to return a reply even if the directory doesn’t exist. Am I reading the code right?
I guess this would be nice if I was going to do mkdir ~p:foodog ?

Zsh prompt showing last error code only once

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
}

ZSH Completion based on previous flag

I am trying to create a completion where one of my completions will be dynamically generated depending on the values of the other flags. For example
local state
_arguments \
'-f[fabric]:fabric:->fabrics' \
'-c[containers/application where log resides]:container:->containers' \
'-l[name of log file]:log:->logs'
case "$state" in
(fabrics)
_values 'fabrics' \
'fab1' \
'fab2'
;;
(containers)
_values 'containers' \
'container1' \
'container2'
;;
(logs)
# show A B C if "-c container1" was entered
# show D E F if "-c container2" was entered
# show G if "-c" was not provided yet
esac
I am having trouble getting the "-l" flag to be dynamically generated.
We could inspect the $words:
Completion Special Parameters
...
Inside completion widgets, and any functions called from them, some parameters have special meaning;
...
words
This array contains the words present on the command line currently being edited.
-- zshcompwid(1): Completion Special Parameters, Completion Widgets
We could do the stuff like this:
(logs)
local -i index=${words[(I)-c]}
local -i ret=0
if ((index == 0)); then
_values 'logs' F
ret=$?
elif [[ "$words[index+1]" == container1 ]]; then
_values 'logs' A B C
ret=$?
elif [[ "$words[index+1]" == container2 ]]; then
_values 'logs' D E F
ret=$?
fi
return ret
To inspect arrays, it is usefull to use the array Subscript Flags:
Subscript Flags
If the opening bracket, or the comma in a range, in any subscript expression is directly followed by an opening parenthesis, the string up to the matching closing one is considered to be a list of flags, as in name[(flags)exp].
-- zshparam(1), Subscript Flags, Array Subscripts, Array Parameters
So,$words[(I)-c] means I "flag" + -c as "exp" for $words which is "Last matching element's index for "-c" in the array $word". For example:
$ tmp=(my-test-command -f flag -c container1 -l)
$ echo $tmp[(I)-c]
4
$ echo $tmp[(I)container1]
5
$ tmp=(my-test-command -f flag -c container1 -c container2 -l)
$ echo $tmp[(I)-c]
6

Version Comparison using KSH

I'm trying to write a function to compare the versions of the products.
my versions can be XX.XX.XX or xx-xx-xx either it's separated with "." or "-"
and number of fields can be different either xx.xx or xx.xx.xx or xx.xx.xx.xx
the versions which im gonna compare will identical in delimiters and with the fields
#!/bin/ksh
set -x
compareVersions ()
{
typeset IFS='.'
typeset -a v1=( $1 )
typeset -a v2=( $2 )
typeset n diff
for (( n=0; n<4; n+=1 )); do
diff=$((v1[n]-v2[n]))
if [ $diff -ne 0 ] ; then
[ $diff -le 0 ] && echo '-1' || echo '1'
return
fi
done
echo '0'
} # ---------- end of function compareVersions ----------
#compareVersions "6100-09-03" "6100-09-02"
compareVersions "6100.09.03" "6100.09.02"
Please check and give me suggestions
I have tried with the below thing which i have got a other post.. but there is no luck.. hope there should some modification should be done. I have to use across platforms ( linux, solaris, AIX ) so i have preferred KSH, i have idea only in shell scripting though.
Create arrays from version strings, then loop through them comparing elements one by one and return values accordingly. The following example will compare two version strings and returns either 0 (versions are equal), 1 (the first version string is greater) or 2 (the second version string is greater).
#!/bin/ksh
function vertest {
set -A av1 `echo $1 | sed -e 's/\'$3'/ /g'`
set -A av2 `echo $2 | sed -e 's/\'$3'/ /g'`
for (( i=0; i < ${#av1[#]}; i++ )) ; do
[[ ${av1[$i]} -eq ${av2[$i]} ]] && continue
[[ ${av1[$i]} -gt ${av2[$i]} ]] && return 1
[[ ${av1[$i]} -lt ${av2[$i]} ]] && return 2
done
return 0
}
v1="2-7-2-1"
v2="1-8-0-1"
vertest $v1 $v2 '-'
exit $?
# end of file.
This example will exit to shell with exit code 1. Should you change $v1 to 1-7-2-1, it will exit to shell with exit code 2. And so on, and so forth.
The separator escaping is not complete, but this works with most reasonable separators like a period (.) and a dash (-). This, as well as parameter checking for the vertest() is left as an exercise for the reader.
When the format of both numbers is equal (leading zero as your example), you can use
compareVersions ()
{
val1=$(echo $1| tr -d ".-")
echo ${val1}
val2=$(echo $2| tr -d ".-")
echo ${val2}
if [ ${val1} -gt ${val2} ] ; then
echo 1
return
fi
if [ ${val1} -eq ${val2} ] ; then
echo 0
return
fi
echo '-1'
} # ---------- end of function compareVersions ----------

zsh preexec command modification

Is there a way to modify the command that is about to execute?
I would like to redirect the output to a file, as well as print it on the terminal.
I found that ls > file.txt | cat does the job, so I would like to add that > file.txt | cat to any command that is about to execute.
Is there a better way to redirect to file and print to terminal? I am trying to make a logger.
You can change the action that is performed when you execute a line to alter the command that will be executed. This can be done by defining a function which you then bind to the enter key.
Lets first define a function that can add the '> file.txt | cat' ending to any command:
function log_and_accept {
BUFFER="$BUFFER > file.txt | cat"
zle accept-line
}
The next part is to actually replace the default enter key behaviour with your new function. The default behaviour we are replacing is the accept-line function, and if we look at the zle documentation, you will see that accept-line is bound to ^J and ^M.
To bind this function to those letters you first need to turn it into a widget:
zle -N log_and_accept_widget log_and_accept
Then you can bind it, replacing the old behaviour:
bindkey '^J' log_and_accept_widget
bindkey '^M' log_and_accept_widget
Now you will be expanding that command for every single command you do. Every cd, ls, vim etc etc. As such I recommend that you define a couple more functions that actually turn this on and off:
function turn_on_logging {
bindkey '^J' log_and_accept_widget
bindkey '^M' log_and_accept_widget
}
function turn_off_logging {
bindkey '^J' accept-line
bindkey '^M' accept-line
}
zle -N turn_on_logging_widget turn_on_logging
zle -N turn_off_logging_widget turn_off_logging
bindkey '^P' turn_on_logging_widget
bindkey '^O' turn_off_logging_widget
I think you should be careful with this. After testing it a little, I quickly grew to dislike it.
There are several ways to do that, the 1 I like the most is this block I found here http://git.grml.org/?p=grml-etc-core.git;a=blob_plain;f=etc/zsh/zshrc;hb=HEAD
abk=(
'...' '../..'
'....' '../../..'
'BG' '& exit'
'C' '| wc -l'
'G' '|& grep '${grep_options:+"${grep_options[*]}"}
'H' '| head'
'Hl' ' --help |& less -r' #d (Display help in pager)
'L' '| less'
'LL' '|& less -r'
'M' '| most'
'N' '&>/dev/null' #d (No Output)
'R' '| tr A-z N-za-m' #d (ROT13)
'SL' '| sort | less'
'S' '| sort -u'
'T' '| tail'
'V' '|& vim -'
'co' './configure && make && sudo make install'
'fc' '> file.txt | cat'
)
zleiab() {
emulate -L zsh
setopt extendedglob
local MATCH
if (( NOABBREVIATION > 0 )) ; then
LBUFFER="${LBUFFER},."
return 0
fi
matched_chars='[.-|_a-zA-Z0-9]#'
LBUFFER=${LBUFFER%%(#m)[.-|_a-zA-Z0-9]#}
LBUFFER+=${abk[$MATCH]:-$MATCH}
}
zle -N zleiab && bindkey ",." zleiab
Also notice that I added 'fc' '> file.txt | cat' to the list abk
What this does in that that you type fc after the command and then you hit ,. (comma and period) in rapid succession and zsh will replace fc for > file.txt | cat

Resources