Skip to content

Latest commit

 

History

History
302 lines (235 loc) · 9.93 KB

bang-bang.org

File metadata and controls

302 lines (235 loc) · 9.93 KB

Bang-bang key bindings for Elvish

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.

Table of Contents

Usage

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"

Implementation

Libraries

We load some necessary libraries.

use ./util
use re
use str

Configuration

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 = []

Inserting a plain exclamation mark

This function gets bound to the keys specified in -plain-bang-insert.

fn insert-plain-bang { edit:close-mode; edit:insert-at-dot "!" }

bang-bang mode function

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 }
}

Initialization

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