Implement the !!
(last command), !$
(last argument of last command), !*
(all arguments) and !<n>
(nth argument of last command) shortcuts in Elvish.
This file is written in literate programming style, to make it easy to explain. See alias.elv for the generated file.
Install the elvish-modules
package using epm:
use epm
epm:install github.com/zzamboni/elvish-modules
In your rc.elv
, load this module.
use github.com/zzamboni/elvish-modules/bang-bang
When you press !
, you will see a menu showing you the different keys your can press, for example, if you try to execute a command that requires root
privileges:
[~]─> ls /var/spool/mqueue/ "/var/spool/mqueue/": Permission denied (os error 13)
You can type sudo
and then press !
, which will show you the menu:
[~]─> sudo ! bang-bang ! ls /var/spool/mqueue/ 0 ls 1 $ /var/spool/mqueue/ * /var/spool/mqueue/ Alt-! !
If you press !
a second time, the full command will be inserted at the point:
[~]─> sudo ls /var/spool/mqueue/
If you wanted to see the permissions on that directory next, you could use the !$
shortcut instead:
[~]─> ls -ld ! bang-bang ! sudo ls /var/spool/mqueue/ 0 sudo 1 ls 2 $ /var/spool/mqueue/ * ls /var/spool/mqueue/ Alt-! !
Pressing $
(or 2
) at this point will insert the last argument:
[~]─> ls -ld /var/spool/mqueue/
If you wanted to then see the i-node number of the directory, you could type the new partial command and then use the !*
key to insert all the arguments of the previous command:
[~]─> ls -i ! bang-bang ! ls -ld /var/spool/mqueue/ 0 ls 1 -ld 2 $ /var/spool/mqueue/ * -ld /var/spool/mqueue/ Alt-! !
Pressing *
at this point will insert all the previous command’s arguments:
[~]─> ls -i -ld /var/spool/mqueue/
By default, bang-bang:init
(which gets called automatically when the module loads) also binds the default “lastcmd” key (Alt-1
), and when repeated, it will insert the full command. This means it fully emulates the default “last command” behavior. If you want to bind bang-bang to other keys, you can pass them in a list in the &extra-triggers
option to bang-bang:init
. For example, to bind bang-bang to Alt-`
in addition to !
:
bang-bang:init &extra-triggers=["Alt-`"]
By default, Alt-!
(Alt-Shift-1
) can be used to insert an exclamation mark when you really need one. This works both from insert mode or from “bang-mode” after you have typed the first exclamation mark. If you want to bind this to a different key, specify it with the &plain-bang
option to bang-bang:init
, like this:
bang-bang:init &plain-bang="Alt-3"
We load some necessary libraries.
use ./util
use re
use str
If you want hooks to be run either before or after entering bang-bang mode, you can add them as lambdas to these variables.
var before-lastcmd = []
var after-lastcmd = []
$-plain-bang-insert
contains the key that is used to insert a plain !
, also after entering lastcmd. Do not set directly, instead pass the &plain-bang
option to init
.
var -plain-bang-insert = ""
$-extra-trigger-keys
is an array containing the additional keys that will trigger bang-bang mode. These keys will also be bound, when pressed twice, to insert the full last command. Do not set directly, instead pass the &-extra-triggers
option to init
.
var -extra-trigger-keys = []
This function gets bound to the keys specified in -plain-bang-insert
.
fn insert-plain-bang { edit:close-mode; edit:insert-at-dot "!" }
The bang-bang:lastcmd
function is the central function of this module.
fn lastcmd {
<<lastcmd code below>>
}
First, we run the “before” hooks, if any.
for hook $before-lastcmd { $hook }
We get the last command and split it in words for later use.
var last = [(edit:command-history)][-1]
var parts = [(edit:wordify $last[cmd])]
We also get how wide the first column of the display should be, so that we can draw the selector keys right-aligned.
var nitems = (count $parts)
var indicator-width = (util:max (+ 2 (count (to-string $nitems))) (count $-plain-bang-insert))
var filler = (repeat $indicator-width ' ' | str:join '')
The -display-text
function returns the string to display in the menu, with the indicator right-aligned to $indicator-width
spaces.
fn -display-text {|ind text|
var indcol = $filler$ind
put $indcol[(- $indicator-width)..]" "$text
}
We create the three “fixed” items of the bang-bang menu: the full command and the plain exclamation mark. Additionally, if the command has arguments, we create the “all arguments” item. Each menu item is a map with three keys: to-accept
is the text to insert when the option is selected, to-show
is the text to show in the menu, and to-filter
is the text which can be used by the user to filter/select options.
var cmd = [
&to-accept= $last[cmd]
&to-show= (-display-text "!" $last[cmd])
&to-filter= "! "$last[cmd]
]
var bang = [
&to-accept= "!"
&to-show= (-display-text $-plain-bang-insert "!")
&to-filter= $-plain-bang-insert" !"
]
var all-args = []
var arg-text = ""
if (> $nitems 1) {
set arg-text = (str:join " " $parts[1..])
set all-args = [
&to-accept= $arg-text
&to-show= (-display-text "*" $arg-text)
&to-filter= "* "$arg-text
]
}
We now populate the menu items for each word of the command. For the last one, we also indicate that it can be selected with $
.
var items = [
(range $nitems |
each {|i|
var text = $parts[$i]
var ind = (to-string $i)
if (> $i 9) {
set ind = ""
}
if (eq $i (- $nitems 1)) {
set ind = $ind" $"
}
put [
&to-accept= $text
&to-show= (-display-text $ind $text)
&to-filter= $ind" "$text
]
}
)
]
Finally, we put the whole list together.
var candidates = [$cmd $@items $all-args $bang]
Now we create a list with the keybindings for the different elements of the menu. One-key bindings are only assigned for the first 9 elements and for the last one.
fn insert-full-cmd { edit:close-mode; edit:insert-at-dot $last[cmd] }
fn insert-part {|n| edit:close-mode; edit:insert-at-dot $parts[$n] }
fn insert-args { edit:close-mode; edit:insert-at-dot $arg-text }
var bindings = [
&"!"= $insert-full-cmd~
&"$"= { insert-part -1 }
&$-plain-bang-insert= $insert-plain-bang~
&"*"= $insert-args~
]
for k $-extra-trigger-keys {
set bindings[$k] = $insert-full-cmd~
}
range (util:min (count $parts) 10) | each {|i|
set bindings[(to-string $i)] = { insert-part $i }
}
set bindings = (edit:binding-table $bindings)
Finally, we invoke custom-listing mode with all the information we have put together, to display the menu and act on the corresponding choice.
edit:listing:start-custom $candidates &caption="bang-bang " &binding=$bindings &accept={|arg|
edit:insert-at-dot $arg
for hook $after-lastcmd { $hook }
}
The init
function gets called to set up the keybindings. This function can receive two options:
&plain-bang
(string) to specify the key to insert a plain exclamation mark when needed. Defaults to"Alt-!"
.&extra-triggers
(array of strings) to specify additional keys (other than!
) to trigger bang-bang mode. All of these keys will also be bound, when pressed twice, to insert the full last command (just like!!
). Defaults to["Alt-1"]
, which emulates the default last-command keybinding in Elvish.
fn init {|&plain-bang="Alt-!" &extra-triggers=["Alt-1"]|
set -plain-bang-insert = $plain-bang
set -extra-trigger-keys = $extra-triggers
set edit:insert:binding[!] = $lastcmd~
for k $extra-triggers {
set edit:insert:binding[$k] = $lastcmd~
}
set edit:insert:binding[$-plain-bang-insert] = $insert-plain-bang~
}
We call init
automatically on module load, although you can call it manually if you want to change the defaults for plain-bang
or extra-triggers
.
init