├── LICENSE ├── README-src.org ├── README.org ├── builtins.elv ├── builtins.org ├── cd.elv ├── cd.org ├── comp.elv ├── comp.org ├── dd.elv ├── dd.org ├── evemu.elv ├── evemu.org ├── git.elv ├── git.org ├── git_test.elv ├── metadata.json ├── ssh.elv ├── ssh.org ├── vcsh.elv └── vcsh.org /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Diego Zamboni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-src.org: -------------------------------------------------------------------------------- 1 | #+macro: module-summary (eval (org-export-string-as (concat "- [[file:" $1 ".org][" $1 "]] :: \n #+include: " $1 ".org::module-summary\n") 'org t)) 2 | #+EXPORT_FILE_NAME: README.org 3 | 4 | #+title: Elvish completions 5 | 6 | This Elvish package contains various completions I and others have written for the [[https://elv.sh/][Elvish shell]]. 7 | 8 | * Compatibility 9 | 10 | These modules are only guaranteed to be fully compatible with [[https://elv.sh/get/][Elvish HEAD]], which is what I use. This means that occasionally, they will not work even with the latest official release, when breaking changes are introduced. Since Elvish is in active development, I highly recommend you use the latest commit too. 11 | 12 | * Installation and use 13 | 14 | To install, use [[https://elvish.io/ref/epm.html][epm]]: 15 | 16 | #+begin_src elvish 17 | use epm 18 | epm:install github.com/zzamboni/elvish-completions 19 | #+end_src 20 | 21 | For each module you want to use, you need to add the following to your =rc.elv= file: 22 | 23 | #+begin_src elvish 24 | use github.com/zzamboni/elvish-completions/ 25 | #+end_src 26 | 27 | See each module's page for detailed usage instructions. 28 | 29 | * Modules included 30 | 31 | The following modules are included: 32 | 33 | #+begin_src elvish :exports results :results drawer :eval no-export 34 | echo "" # blank lines prevents github rendering error in which the first item is now shown 35 | ls *.org | egrep -v 'README|_template' | each [m]{ echo "{{{module-summary("(basename $m .org)")}}}" } 36 | #+end_src 37 | 38 | #+RESULTS: 39 | :results: 40 | 41 | {{{module-summary(builtins)}}} 42 | {{{module-summary(cd)}}} 43 | {{{module-summary(comp)}}} 44 | {{{module-summary(dd)}}} 45 | {{{module-summary(evemu)}}} 46 | {{{module-summary(git)}}} 47 | {{{module-summary(ssh)}}} 48 | {{{module-summary(vcsh)}}} 49 | :end: 50 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # Created 2021-03-29 Mon 10:26 2 | #+TITLE: Elvish completions 3 | #+AUTHOR: Diego Zamboni 4 | #+macro: module-summary (eval (org-export-string-as (concat "- [[file:" $1 ".org][" $1 "]] :: \n #+include: " $1 ".org::module-summary\n") 'org t)) 5 | #+export_file_name: README.org 6 | 7 | This Elvish package contains various completions I and others have written for the [[https://elv.sh/][Elvish shell]]. 8 | 9 | * Compatibility 10 | 11 | These modules are only guaranteed to be fully compatible with [[https://elv.sh/get/][Elvish HEAD]], which is what I use. This means that occasionally, they will not work even with the latest official release, when breaking changes are introduced. Since Elvish is in active development, I highly recommend you use the latest commit too. 12 | 13 | * Installation and use 14 | 15 | To install, use [[https://elv.sh/ref/epm.html][epm]]: 16 | 17 | #+begin_src elvish 18 | use epm 19 | epm:install github.com/zzamboni/elvish-completions 20 | #+end_src 21 | 22 | For each module you want to use, you need to add the following to your =rc.elv= file: 23 | 24 | #+begin_src elvish 25 | use github.com/zzamboni/elvish-completions/ 26 | #+end_src 27 | 28 | See each module's page for detailed usage instructions. 29 | 30 | * Modules included 31 | 32 | The following modules are included: 33 | 34 | #+results: 35 | :results: 36 | 37 | 38 | - [[file:builtins.org][builtins]] :: 39 | #+name: module-summary 40 | Completions for some of Elvish's built-in commands, including =use=, the =epm= module and =elvish= itself. 41 | 42 | - [[file:cd.org][cd]] :: 43 | #+name: module-summary 44 | Completes directory names for the =cd= command. 45 | 46 | - [[file:comp.org][comp]] :: 47 | #+name: module-summary 48 | A framework to easily define [[https://elvish.io/ref/edit.html#completion-api][argument completers]] in Elvish. Used to implement most other modules in this repository. For a getting-started tutorial, see http://zzamboni.org/post/using-and-writing-completions-in-elvish/. 49 | 50 | - [[file:dd.org][dd]] :: 51 | #+name: module-summary 52 | Completions for =dd=, including operands, conversions, and flags. 53 | 54 | - [[file:evemu.org][evemu]] :: 55 | #+name: module-summary 56 | Completions for [[https://gitlab.freedesktop.org/libevdev/evtest][=evtest=]] and the [[https://www.freedesktop.org/wiki/Evemu/][=evemu=]] set of tools, which assist in debugging and emulating the [[https://www.kernel.org/doc/html/latest/input/input_uapi.html][Linux input subsystem]]. 57 | 58 | - [[file:git.org][git]] :: 59 | #+name: module-summary 60 | Completions for =git=, including automatically generated completions for both subcommands and command-line options. 61 | 62 | - [[file:ssh.org][ssh]] :: 63 | #+name: module-summary 64 | Completions for =ssh=, =scp= and =sftp=. 65 | 66 | - [[file:vcsh.org][vcsh]] :: 67 | #+name: module-summary 68 | Completions for [[https://github.com/RichiH/vcsh][vcsh]]. 69 | :END: 70 | -------------------------------------------------------------------------------- /builtins.elv: -------------------------------------------------------------------------------- 1 | use ./comp 2 | use re 3 | use path 4 | use str 5 | 6 | set edit:completion:arg-completer[use] = ( 7 | comp:sequence [ 8 | {|stem| 9 | if (not (str:has-prefix $stem '.')) { 10 | put './' '../' 11 | put ~/.elvish/lib/**[nomatch-ok].elv | each {|m| 12 | if (not (path:is-dir $m)) { 13 | re:replace ~/.elvish/lib/'(.*).elv' '$1' $m 14 | } 15 | } 16 | } else { 17 | if (eq $stem ".") { set stem = "./" } 18 | if (eq $stem "..") { set stem = "../" } 19 | comp:files $stem ®ex='.*\.elv' &transform={|s| re:replace '\.elv$' '' $s } 20 | } 21 | } 22 | ] 23 | ) 24 | 25 | use epm 26 | 27 | var epm-completer-one = (comp:sequence [ $epm:list~ ]) 28 | var epm-completer-many = (comp:sequence [ $epm:list~ ...]) 29 | set edit:completion:arg-completer[epm:query] = $epm-completer-one 30 | set edit:completion:arg-completer[epm:metadata] = $epm-completer-one 31 | set edit:completion:arg-completer[epm:dest] = $epm-completer-one 32 | set edit:completion:arg-completer[epm:uninstall] = $epm-completer-many 33 | set edit:completion:arg-completer[epm:upgrade] = $epm-completer-many 34 | 35 | set edit:completion:arg-completer[elvish] = (comp:sequence ^ 36 | &opts= { elvish -help | comp:extract-opts &fold } ^ 37 | [ {|arg| comp:files $arg ®ex='\.elv$' } ] ^ 38 | ) 39 | -------------------------------------------------------------------------------- /builtins.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Elvish completions for various built-in commands 2 | #+AUTHOR: Diego Zamboni 3 | #+EMAIL: diego@zzamboni.org 4 | 5 | #+name: module-summary 6 | Completions for some of Elvish's built-in commands, including =use=, the =epm= module and =elvish= itself. 7 | 8 | This file is written in [[https://leanpub.com/lit-config][literate programming style]], to make it easy to explain. See [[file:$name.elv][$name.elv]] for the generated file. 9 | 10 | * Table of Contents :TOC:noexport: 11 | - [[#usage][Usage]] 12 | - [[#implementation][Implementation]] 13 | - [[#use][use]] 14 | - [[#epm][epm]] 15 | - [[#elvish][elvish]] 16 | 17 | * Usage 18 | 19 | Install the =elvish-modules= package using [[https://elvish.io/ref/epm.html][epm]]: 20 | 21 | #+begin_src elvish 22 | use epm 23 | epm:install github.com/zzamboni/elvish-completions 24 | #+end_src 25 | 26 | In your =rc.elv=, load this module: 27 | 28 | #+begin_src elvish 29 | use github.com/zzamboni/elvish-completions/builtins 30 | #+end_src 31 | 32 | Included completions: 33 | 34 | - =elvish= (completes command-line options and =.elv= files). 35 | - =use= (completes all modules in =~/.elvish/lib=). Example (the "lack module name" message is normal and part of Elvish's as-you-type compilation, it goes away when you add a module name): 36 | #+begin_example 37 | [~]─> use 38 | COMPLETING argument _ 39 | compilation error: 3-3 in [tty]: lack module name 40 | elvish-dev/epm/epm github.com/zzamboni/elvish-modules/alias 41 | github.com/iwoloschin/elvish-packages/powernerd github.com/zzamboni/elvish-modules/atlas 42 | github.com/iwoloschin/elvish-packages/python github.com/zzamboni/elvish-modules/bang-bang 43 | github.com/iwoloschin/elvish-packages/update github.com/zzamboni/elvish-modules/dir 44 | github.com/muesli/elvish-libs/git github.com/zzamboni/elvish-modules/git-vcsh 45 | github.com/muesli/elvish-libs/theme/muesli github.com/zzamboni/elvish-modules/long-running-notifications 46 | github.com/muesli/elvish-libs/theme/powerline github.com/zzamboni/elvish-modules/nix 47 | github.com/xiaq/edit.elv/compl/go github.com/zzamboni/elvish-modules/opsgenie 48 | github.com/xiaq/edit.elv/smart-matcher github.com/zzamboni/elvish-modules/prompt-hooks 49 | github.com/zzamboni/elvish-completions/builtins github.com/zzamboni/elvish-modules/proxy 50 | github.com/zzamboni/elvish-completions/cd github.com/zzamboni/elvish-modules/semver 51 | github.com/zzamboni/elvish-completions/comp github.com/zzamboni/elvish-modules/terminal-title 52 | github.com/zzamboni/elvish-completions/git github.com/zzamboni/elvish-modules/util 53 | github.com/zzamboni/elvish-completions/ssh github.com/zzamboni/elvish-themes/chain 54 | github.com/zzamboni/elvish-completions/vcsh private 55 | #+end_example 56 | - =epm= commands: completes installed packages for the appropriate commands. 57 | 58 | * Implementation 59 | :PROPERTIES: 60 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 61 | :header-args: :mkdirp yes :comments no 62 | :END: 63 | 64 | Load the completion framework and other libraries. 65 | 66 | #+begin_src elvish 67 | use ./comp 68 | use re 69 | use path 70 | use str 71 | #+end_src 72 | 73 | ** use 74 | 75 | Completer for the =use= command, which includes all modules in =~/.elvish/lib/=, but also allows completing relative paths starting with =./= or =../=. 76 | 77 | #+begin_src elvish 78 | set edit:completion:arg-completer[use] = ( 79 | comp:sequence [ 80 | {|stem| 81 | if (not (str:has-prefix $stem '.')) { 82 | put './' '../' 83 | put ~/.elvish/lib/**[nomatch-ok].elv | each {|m| 84 | if (not (path:is-dir $m)) { 85 | re:replace ~/.elvish/lib/'(.*).elv' '$1' $m 86 | } 87 | } 88 | } else { 89 | if (eq $stem ".") { set stem = "./" } 90 | if (eq $stem "..") { set stem = "../" } 91 | comp:files $stem ®ex='.*\.elv' &transform={|s| re:replace '\.elv$' '' $s } 92 | } 93 | } 94 | ] 95 | ) 96 | #+end_src 97 | 98 | ** epm 99 | 100 | Completers for the =epm= commands. 101 | 102 | #+begin_src elvish 103 | use epm 104 | 105 | var epm-completer-one = (comp:sequence [ $epm:list~ ]) 106 | var epm-completer-many = (comp:sequence [ $epm:list~ ...]) 107 | set edit:completion:arg-completer[epm:query] = $epm-completer-one 108 | set edit:completion:arg-completer[epm:metadata] = $epm-completer-one 109 | set edit:completion:arg-completer[epm:dest] = $epm-completer-one 110 | set edit:completion:arg-completer[epm:uninstall] = $epm-completer-many 111 | set edit:completion:arg-completer[epm:upgrade] = $epm-completer-many 112 | #+end_src 113 | 114 | ** elvish 115 | 116 | Completer for the =elvish= command. 117 | 118 | #+begin_src elvish 119 | set edit:completion:arg-completer[elvish] = (comp:sequence ^ 120 | &opts= { elvish -help | comp:extract-opts &fold } ^ 121 | [ {|arg| comp:files $arg ®ex='\.elv$' } ] ^ 122 | ) 123 | #+end_src 124 | -------------------------------------------------------------------------------- /cd.elv: -------------------------------------------------------------------------------- 1 | use ./comp 2 | 3 | set edit:completion:arg-completer[cd] = (comp:sequence [ {|stem| 4 | comp:files $stem &dirs-only 5 | }]) 6 | -------------------------------------------------------------------------------- /cd.org: -------------------------------------------------------------------------------- 1 | #+title: Elvish completions for cd 2 | #+author: Diego Zamboni 3 | 4 | #+name: module-summary 5 | Completes directory names for the =cd= command. 6 | 7 | * Implementation 8 | :PROPERTIES: 9 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 10 | :header-args: :mkdirp yes :comments no 11 | :END: 12 | 13 | #+begin_src elvish 14 | use ./comp 15 | 16 | set edit:completion:arg-completer[cd] = (comp:sequence [ {|stem| 17 | comp:files $stem &dirs-only 18 | }]) 19 | #+end_src 20 | -------------------------------------------------------------------------------- /comp.elv: -------------------------------------------------------------------------------- 1 | use re 2 | use str 3 | use path 4 | 5 | var debug = $false 6 | 7 | fn -debugmsg {|@args &color=blue| 8 | if $debug { 9 | echo (styled (echo ">>> " $@args) $color) >/dev/tty 10 | } 11 | } 12 | 13 | fn decorate {|@input &code-suffix='' &display-suffix='' &suffix='' &style=$nil | 14 | if (== (count $input) 0) { 15 | set input = [(all)] 16 | } 17 | if (not-eq $suffix '') { 18 | set display-suffix = $suffix 19 | set code-suffix = $suffix 20 | } 21 | each {|k| 22 | var k-display = $k 23 | if $style { 24 | set k-display = (styled $k $style) 25 | } 26 | edit:complex-candidate &code-suffix=$code-suffix &display=$k-display$display-suffix $k 27 | } $input 28 | } 29 | 30 | fn empty { nop } 31 | 32 | fn files {|arg ®ex='' &dirs-only=$false &transform=$nil| 33 | edit:complete-filename $arg | each {|c| 34 | var x = $c[stem] 35 | if (or (path:is-dir $x) (and (not $dirs-only) (or (eq $regex '') (re:match $regex $x)))) { 36 | if $transform { 37 | edit:complex-candidate ($transform $x) 38 | } else { 39 | put $c 40 | } 41 | } 42 | } 43 | } 44 | 45 | fn dirs {|arg ®ex='' &transform=$nil| 46 | files $arg ®ex=$regex &dirs-only=$true &transform=$transform 47 | } 48 | 49 | fn extract-opts {|@cmd 50 | ®ex='^\s*(?:-(\w),?\s*)?(?:--?([\w-]+))?(?:\[=(\S+)\]|[ =](\S+))?\s*?\s\s(\w.*)$' 51 | ®ex-map=[&short=1 &long=2 &arg-optional=3 &arg-required=4 &desc=5] 52 | &fold=$false 53 | &first-sentence=$false 54 | &opt-completers=[&] 55 | | 56 | var -line = '' 57 | var capture = $all~ 58 | if $fold { 59 | set capture = { each {|l| 60 | if (re:match '^\s{8,}\w' $l) { 61 | var folded = $-line$l 62 | # -debugmsg "Folded line: "$folded 63 | put $folded 64 | set -line = '' 65 | } else { 66 | # -debugmsg "Non-folded line: "$-line 67 | put $-line 68 | set -line = $l 69 | } 70 | } 71 | } 72 | } 73 | $capture | each {|l| 74 | -debugmsg "Got line: "$l 75 | re:find $regex $l 76 | } | each {|m| 77 | -debugmsg "Matches: "(to-string $m) &color=red 78 | var g = $m[groups] 79 | var opt = [&] 80 | keys $regex-map | each {|k| 81 | if (has-key $g $regex-map[$k]) { 82 | var field = (str:trim-space $g[$regex-map[$k]][text]) 83 | if (not-eq $field '') { 84 | if (has-value [arg-optional arg-required] $k) { 85 | set opt[$k] = $true 86 | set opt[arg-desc] = $field 87 | if (has-key $opt-completers $field) { 88 | set opt[arg-completer] = $opt-completers[$field] 89 | } else { 90 | set opt[arg-completer] = $edit:complete-filename~ 91 | } 92 | } else { 93 | set opt[$k] = $field 94 | } 95 | } 96 | } 97 | } 98 | if (or (has-key $opt short) (has-key $opt long)) { 99 | if (has-key $opt desc) { 100 | if $first-sentence { 101 | set opt[desc] = (re:replace '\. .*$|\.\s*$|\s*\(.*$' '' $opt[desc]) 102 | } 103 | set opt[desc] = (re:replace '\s+' ' ' $opt[desc]) 104 | } 105 | put $opt 106 | } 107 | } 108 | } 109 | 110 | fn -handler-arity {|func| 111 | var fnargs = [ (to-string (count $func[arg-names])) (== $func[rest-arg] -1)] 112 | if (eq $fnargs [ 0 $true ]) { put no-args 113 | } elif (eq $fnargs [ 1 $true ]) { put one-arg 114 | } elif (eq $fnargs [ 1 $false ]) { put rest-arg 115 | } else { put other-args 116 | } 117 | } 118 | 119 | fn -expand-item {|def @cmd| 120 | var arg = $cmd[-1] 121 | var what = (kind-of $def) 122 | if (eq $what 'fn') { 123 | [ &no-args= { $def } 124 | &one-arg= { $def $arg } 125 | &rest-arg= { $def $@cmd } 126 | &other-args= { put '' } 127 | ][(-handler-arity $def)] 128 | } elif (eq $what 'list') { 129 | all $def 130 | } else { 131 | echo (styled "comp:-expand-item: invalid item of type "$what": "(to-string $def) red) >/dev/tty 132 | } 133 | } 134 | 135 | fn -expand-sequence {|seq @cmd &opts=[]| 136 | 137 | var final-opts = [( 138 | -expand-item $opts $@cmd | each {|opt| 139 | -debugmsg "In final-opts: opt before="(to-string $opt) &color=yellow 140 | if (eq (kind-of $opt) map) { 141 | if (has-key $opt arg-completer) { 142 | -debugmsg &color=yellow "Assigning opt[completer] = [_]{ -expand-item "(to-string $opt[arg-completer]) $@cmd "}" 143 | set opt[completer] = {|_| -expand-item $opt[arg-completer] $@cmd } 144 | } 145 | -debugmsg "In final-opts: opt after="(to-string $opt) &color=yellow 146 | put $opt 147 | } else { 148 | put [&long= $opt] 149 | } 150 | } 151 | )] 152 | 153 | var final-handlers = [( 154 | all $seq | each {|f| 155 | if (eq (kind-of $f) 'fn') { 156 | put [ 157 | &no-args= {|_| $f } 158 | &one-arg= $f 159 | &rest-arg= {|_| $f $@cmd } 160 | &other-args= {|_| put '' } 161 | ][(-handler-arity $f)] 162 | } elif (eq (kind-of $f) 'list') { 163 | put {|_| all $f } 164 | } elif (and (eq (kind-of $f) 'string') (eq $f '...')) { 165 | put $f 166 | } 167 | } 168 | )] 169 | 170 | -debugmsg Calling: edit:complete-getopt (to-string $cmd[1..]) (to-string $final-opts) (to-string $final-handlers) 171 | edit:complete-getopt $cmd[1..] $final-opts $final-handlers 172 | } 173 | 174 | fn -expand-subcommands {|def @cmd &opts=[]| 175 | 176 | var subcommands = [(keys $def)] 177 | var n = (count $cmd) 178 | var kw = [(range 1 $n | each {|i| 179 | if (has-value $subcommands $cmd[$i]) { put $cmd[$i] $i } 180 | })] 181 | 182 | if (and (not-eq $kw []) (not-eq $kw[1] (- $n 1))) { 183 | var sc sc-pos = $kw[0 1] 184 | if (eq (kind-of $def[$sc]) 'string') { 185 | set cmd[$sc-pos] = $def[$sc] 186 | -expand-subcommands &opts=$opts $def $@cmd 187 | } else { 188 | $def[$sc] (all $cmd[{$sc-pos}..]) 189 | } 190 | 191 | } else { 192 | var top-def = [ { put $@subcommands } ] 193 | -expand-sequence &opts=$opts $top-def $@cmd 194 | } 195 | } 196 | 197 | fn item {|item &pre-hook=$nop~ &post-hook=$nop~| 198 | put {|@cmd| 199 | $pre-hook $@cmd 200 | var result = [(-expand-item $item $@cmd)] 201 | $post-hook $result $@cmd 202 | put $@result 203 | } 204 | } 205 | 206 | fn sequence {|sequence &opts=[] &pre-hook=$nop~ &post-hook=$nop~| 207 | put {|@cmd &inspect=$false| 208 | if $inspect { 209 | echo "comp:sequence definition: "(to-string $sequence) 210 | echo "opts: "(to-string $opts) 211 | } else { 212 | $pre-hook $@cmd 213 | var result = [(-expand-sequence &opts=$opts $sequence $@cmd)] 214 | $post-hook $result $@cmd 215 | put $@result 216 | } 217 | } 218 | } 219 | 220 | fn subcommands {|def &opts=[] &pre-hook=$nop~ &post-hook=$nop~| 221 | put {|@cmd &inspect=$false| 222 | if $inspect { 223 | echo "Completer definition: "(to-string $def) 224 | echo "opts: "(to-string $opts) 225 | } else { 226 | $pre-hook $@cmd 227 | if (and (eq $opts []) (has-key $def -options)) { 228 | set opts = $def[-options] 229 | } 230 | del def[-options] 231 | var result = [(-expand-subcommands &opts=$opts $def $@cmd)] 232 | $post-hook $result $@cmd 233 | put $@result 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /comp.org: -------------------------------------------------------------------------------- 1 | #+title: Completion framework for Elvish 2 | #+author: Diego Zamboni 3 | #+email: diego@zzamboni.org 4 | 5 | #+name: module-summary 6 | A framework to easily define [[https://elvish.io/ref/edit.html#completion-api][argument completers]] in Elvish. Used to implement most other modules in this repository. For a getting-started tutorial, see http://zzamboni.org/post/using-and-writing-completions-in-elvish/. 7 | 8 | This file is written in [[https://leanpub.com/lit-config][literate programming style]], to make it easy to explain. See [[file:comp.elv][comp.elv]] for the generated file. 9 | 10 | * Table of Contents :TOC_3:noexport: 11 | - [[#usage][Usage]] 12 | - [[#completion-definitions][Completion definitions]] 13 | - [[#items][Items]] 14 | - [[#sequences-and-command-line-options][Sequences and command-line options]] 15 | - [[#subcommands][Subcommands]] 16 | - [[#utility-functions][Utility functions]] 17 | - [[#implementation][Implementation]] 18 | - [[#utility-functions-1][Utility functions]] 19 | - [[#comp-debugmsg][comp:-debugmsg]] 20 | - [[#compdecorate][comp:decorate]] 21 | - [[#compempty][comp:empty]] 22 | - [[#compfiles-and-compdirs][comp:files and comp:dirs]] 23 | - [[#compextract-opts][comp:extract-opts]] 24 | - [[#comp-handler-arity][comp:-handler-arity]] 25 | - [[#completion-functions][Completion functions]] 26 | - [[#comp-expand-item][comp:-expand-item]] 27 | - [[#comp-expand-sequence][comp:-expand-sequence]] 28 | - [[#comp-expand-subcommands][comp:-expand-subcommands]] 29 | - [[#completion-wrapper-functions-main-entry-points][Completion wrapper functions (main entry points)]] 30 | - [[#compitem][comp:item]] 31 | - [[#compsequence][comp:sequence]] 32 | - [[#compsubcommands][comp:subcommands]] 33 | 34 | * Usage 35 | 36 | The =comp= module provides a few functions that make it easier to define completions in Elvish. Please note that this module is not intended for direct use in an Elvish session, but to write your own [[https://elvish.io/ref/edit.html#completion-api][argument completers]]. 37 | 38 | *NOTE: This module is in development, so the API, data structures, etc. may change at any moment.* 39 | 40 | As a first step, install the =elvish-completions= package using [[https://elvish.io/ref/epm.html][epm]]: 41 | 42 | #+begin_src elvish 43 | use epm 44 | epm:install github.com/zzamboni/elvish-completions 45 | #+end_src 46 | 47 | From the file where you will define your completions, load this module: 48 | 49 | #+begin_src elvish 50 | use github.com/zzamboni/elvish-completions/comp 51 | #+end_src 52 | 53 | The main entry points for this module are =comp:item=, =comp:sequence= and =comp:subcommands=. Each one receives a single argument containing a "completion definition", which indicates how the completions will be produced. Each one receives a different kind of completion structure, and returns a corresponding completion function, which receives the current contents of the command line (as passed to the [[https://elvish.io/ref/edit.html#argument-completer][argument completer functions]]) and returns the appropriate completions. The function returned by the =comp:*= functions can be assigned directly to an element of =$edit:completion:arg-completer=. A simple example: 54 | 55 | #+begin_src elvish 56 | edit:completion:arg-completer[foo] = (comp:item [ bar baz ]) 57 | #+end_src 58 | 59 | If you type this in your terminal, and then type =foo= and press ~Tab~, you will see the appropriate completions: 60 | 61 | #+begin_example 62 | > foo 63 | COMPLETING argument _ 64 | bar baz 65 | #+end_example 66 | 67 | To create completions for new commands, your main task is to define the corresponding completion definition. The different types of definitions and functions are explained below, with examples of the different available structures and features. 68 | 69 | All three functions can also receive options =&pre-hook= and =&post-hook=. If specified, they must be lambdas which get executed before and after the completion is processed, respectively. Hooks cannot modify the result, and they should usually not be necessary, but you can use them for any maintenance or update tasks. 70 | - =&pre-hook= must receive a single rest argument, and receives the current command line: =[@cmd]{ code }= 71 | - =&post-hook= must receive an array and a rest argument, and receives the generated completions and the current command line: =[result @cmd]{ code }= 72 | 73 | *Note:* the main entry points return a ready-to-use argument handler function. If you ever need to expand a completion definition directly (maybe for some advanced usage), you can call =comp:-expand-item=, =comp:-expand-sequence= and =comp:-expand-subcommands=, respectively. These functions all take the definition structure and the current command line, and return the appropriate completions at that point. 74 | 75 | ** Completion definitions 76 | *** Items 77 | 78 | The base building block is the "item", can be one of the following: 79 | 80 | - An array containing all the potential completions (it can be empty, in which case no completions are provided). This is useful for providing a static list of completions. 81 | - A function which returns the potential completions (it can return nothing, in which case no completions are provided). The function should have one of the following arities, which affect which arguments will be passed to it (other arities are not valid, and in that case the item will not be executed): 82 | - If it takes no arguments, no arguments are passed to it. 83 | - If it takes a single argument, it gets the current (last) component of the command line =@cmd= 84 | - If it takes a rest argument, it gets the full current command line (the contents of =@cmd=) 85 | 86 | *Example #1:* a simple completer for =cd= 87 | 88 | In this case, we define a function which receives the current "stem" (the part of the filename the user has typed so far) and offers all the relevant files, then filters those which are directories, and returns them as completion possibilities. We pass the function directly as a completion item to =comp:-expand=. 89 | 90 | #+begin_src elvish 91 | fn complete-dirs [arg]{ put {$arg}* | each [x]{ if (path:is-dir $x) { put $x } } } 92 | edit:completion:arg-completer[cd] = (comp:item $complete-dirs~) 93 | #+end_src 94 | 95 | I defined the =complete-dirs= function separately only for clarity - you can also embed the lambda directly as an argument to =comp:item=. 96 | 97 | For file and directory completion, you can use the utility function =comp:files= instead of defining your own function (see [[#comp-files-and-comp-dirs][comp:files and comp:dirs]]): 98 | 99 | #+begin_src elvish 100 | edit:completion:arg-completer[cd] = (comp:item [arg]{ comp:files $arg &dirs-only }) 101 | #+end_src 102 | 103 | *** Sequences and command-line options 104 | 105 | Completion items can be aggregated in a /sequence of items/ and used with the =comp:sequence= function when you need to provide different completions for different positional arguments of a command. Sequences include support for command-line options at the beginning of the command. The definition structure in this case has to be an array of items, which will be applied depending on their position within the command parameter sequence. If the last element of the list is the string =...= (three periods), the next-to-last element of the list is repeated for all later arguments. If no completions should be provided past the last argument, simply omit the periods. If a sequence should produce no completions at all, you can use an empty list =[]=. If any specific elements of the sequence should have no completions, you can specify ={ comp:empty }= or =[]= as its value. 106 | 107 | If the =&opts= option is passed to the =comp:sequence= function, it must contain a single definition item which produces a list of command-line options that are allowed at the beginning of the command, when no other arguments have been provided. Options can be specified in either of the following formats: 108 | - As a string which gets converted to a long-style option; e.g. =all= to specify the =--all= option. The string must not contain the dashes at the beginning. 109 | - As a map which may contain the following keys: 110 | - =short= for the short one-letter option; 111 | - =long= for the long-option string; 112 | - =desc= for a descriptive string which gets shown in the completion menu; 113 | - =arg-required= or =arg-optional=: either one but not both can be set to =$true= to indicate whether the option takes a mandatory or optional argument; 114 | - =arg-completer= can be specified and contain a completion item as described in [[*Items][Items]], and which will be expanded to provide completions for that argument's values. 115 | 116 | Simple example of a completion data structure for option =-t= (long form =--type=), which has a mandatory argument which can be =elv=, =org= or =txt=: 117 | 118 | #+begin_example 119 | [ &short=t 120 | &long=type 121 | &desc="Type of file to show" 122 | &arg-required=$true 123 | &arg-completer= [ elv org txt ] 124 | ] 125 | #+end_example 126 | 127 | *Note:* options are only offered as completions when the use has typed a dash as the first character. Otherwise the argument completers are used. 128 | 129 | *Example #2:* we can improve on the previous completer for =cd= by preventing more than one argument from being completed (only the first argument will be completed using =complete-dirs=, since the list does not end with =...=): 130 | 131 | #+begin_src elvish 132 | edit:completion:arg-completer[cd] = (comp:sequence [ [arg]{ comp:files $arg &dirs-only }]) 133 | #+end_src 134 | 135 | *Example #3:* a simple completer for =ls= with a subset of its options. Note that =-l= and =-R= are only provided as completions when you have not typed any filenames yet. Also note that we are using [[*Utility functions][comp:decorate]] to display the files in a different color, and the =...= at the end of the sequence to use the same completer for all further elements. 136 | 137 | #+begin_src elvish 138 | ls-opts = [ 139 | [ &short=l &desc='use a long listing format' ] 140 | [ &short=R &long=recursive &desc='list subdirectories recursively' ] 141 | ] 142 | edit:completion:arg-completer[ls] = (comp:sequence &opts=$ls-opts \ 143 | [ [arg]{ put $arg* | comp:decorate &style=blue } ... ] 144 | ) 145 | #+end_src 146 | 147 | *Example #4:* See the [[https://github.com/zzamboni/elvish-completions/blob/master/ssh.org][ssh completer]] for a real-world example of using sequences. 148 | 149 | *** Subcommands 150 | 151 | Finally, completion sequences can be aggregated into /subcommand structures/ together with the =comp:subcommands= function, to provide completion for commands such as =git=, which accept multiple subcommands, each with their own options and completions. In this case, the definition is a map indexed by subcommand names. The value of each element can be a =comp:item=, a =comp:sequence= or another =comp:subcommands= (to provide completion for sub-sub-commands, see the example below for =vagrant=). The =comp:subcommands= function can also receive option =&opts= containing a single item definition to generate any available top-level options (to appear before the subcommand). Option definitions can also be specified within the definition map, in an element with index =-options=. This element is only used if the =&opts= option is not specified. 152 | 153 | *Example #5:* a simple completer for the =brew= package manager, with support for the =install=, =uninstall= and =cat= commands. =install= and =cat= gets as completions all available packages (the output of the =brew search= command), while =uninstall= only completes installed packages (the output of =brew list=). Note that for =install= and =uninstall= we automatically extract command-line options from their help messages using the =comp:extract-opts= function, and pass them as the =&opts= option in the corresponding sequence functions. Also note that all =&opts= elements get initialized at definition time (they are arrays), whereas the sequence completions get evaluated at runtime (they are lambdas), to automatically update according to the current packages. The =cat= command sequence allows only one option. The load-time initialization of the options incurs a small delay, and you could replace these with lambdas as well so that the options are computed at runtime. 154 | 155 | #+begin_src elvish 156 | brew-completions = [ 157 | &install= (comp:sequence \ 158 | &opts= [ (brew install -h | take 1 | comp:extract-opts ®ex='()--(\w[\w-]*)()') ] \ 159 | [ { brew search } ... ] 160 | ) 161 | &uninstall= (comp:sequence \ 162 | &opts= [ (brew uninstall -h | take 1 | comp:extract-opts ®ex='()--(\w[\w-]*)()') ] \ 163 | [ { brew list } ... ] 164 | ) 165 | &cat= (comp:sequence [{ brew search }]) 166 | ] 167 | 168 | edit:completion:arg-completer[brew] = (comp:subcommands &opts= [ version ] $brew-completions) 169 | #+end_src 170 | 171 | *Example #6:* a simple completer for a subset of =vagrant=, which receives commands which may have subcommands and options of their own. Note that the value of =&up= is a =comp:sequence=, but the value of =&box= is another =comp:subcommands= which includes the completions for =box add= and =box remove=. Also note the use of the =comp:extract-opts= function to extract the command-line arguments automatically from the help messages. 172 | 173 | *Tip:* note that the values of =&opts= are functions (e.g. ={ vagrant-up -h | comp:extract-opts }=) instead of arrays (e.g. =( vagrant up -h | comp:extract-opts )=). As mentioned in Example #5, both would be valid, but in the latter case they are all initialized at load time (when the data structure is defined), which might introduce a delay (particularly with more command definitions). By using functions the options are only extracted at runtime when the completion is requested. For further optimization, =vagrant-opts= could be made to memoize the values so that the delay only occurs the first time. 174 | 175 | #+begin_src elvish 176 | use re 177 | 178 | vagrant-completions = [ 179 | &up= (comp:sequence [] \ 180 | &opts= { vagrant up -h | comp:extract-opts } 181 | ) 182 | &box= (comp:subcommands [ 183 | &add= (comp:sequence [] \ 184 | &opts= { vagrant box add -h | comp:extract-opts } 185 | ) 186 | &remove= (comp:sequence [ { vagrant box list | re:awk [_ @f]{ put $f[0] } } ... ] \ 187 | &opts= { vagrant box remove -h | comp:extract-opts } 188 | ) 189 | ])] 190 | 191 | edit:completion:arg-completer[vagrant] = (comp:subcommands &opts= [ version help ] $vagrant-completions) 192 | #+end_src 193 | 194 | *Example #7:* See the [[https://github.com/zzamboni/elvish-completions/blob/master/git.org][git completer]] for a real-world subcommand completion example, which also shows how extensively auto-population of subcommands and options can be done by extracting information from help messages. 195 | 196 | ** Utility functions 197 | 198 | =comp:decorate= maps its input through =edit:complex-candidate= with the given options. Can be passed the same options as [[https://elvish.io/ref/edit.html#argument-completer][edit:complex-candidate]] (except for =&display=, which does not make sense when multiple inputs are provided), including deprecated options like =&display-suffix=, which is mapped to the new syntax supported in Elvish. In addition, if =&suffix= is specified, it is used to set both =&display-suffix= and =&code-suffix=. Input can be given either as arguments or through the pipeline: 199 | 200 | (*Note:* the =&style= option is ignored at the moment because Elvish no longer supports it, see [[https://github.com/elves/elvish/issues/1011][#1011]]) 201 | 202 | #+begin_src elvish 203 | > comp:decorate &suffix=":" foo bar 204 | ▶ (edit:complex-candidate foo &code-suffix=: &display=foo:) 205 | ▶ (edit:complex-candidate bar &code-suffix=: &display=bar:) 206 | > put foo bar | comp:decorate &style="red" 207 | ▶ (edit:complex-candidate foo &code-suffix='' &display=foo) 208 | ▶ (edit:complex-candidate bar &code-suffix='' &display=bar) 209 | #+end_src 210 | 211 | =comp:extract-opts= takes input from the pipeline and extracts command-line option data structures from its output. By default it understand the following common formats: 212 | 213 | #+begin_example 214 | -o, --option Option description 215 | -p, --print[=WHAT] Option with an optional argument 216 | --select TYPE Option with a mandatory argument 217 | #+end_example 218 | 219 | Typical use would be to populate an =&opts= element with something like this: 220 | 221 | #+begin_src elvish 222 | comp:sequence &opts= { vagrant -h | comp:extract-opts } [ ... ] 223 | #+end_src 224 | 225 | The regular expression used to extract the options can be specified with the =®ex= option. Its default value is: 226 | 227 | #+begin_src elvish :noweb-ref opt-capture-regex 228 | ®ex='^\s*(?:-(\w),?\s*)?(?:--?([\w-]+))?(?:\[=(\S+)\]|[ =](\S+))?\s*?\s\s(\w.*)$' 229 | #+end_src 230 | 231 | The mapping of capture groups from the regex to option components is defined by the =®ex-map= option. Its default value (which also shows the available fields) is: 232 | 233 | #+begin_src elvish :noweb-ref opt-capture-map 234 | ®ex-map=[&short=1 &long=2 &arg-optional=3 &arg-required=4 &desc=5] 235 | #+end_src 236 | 237 | At least one of =short= or =long= must be present in =regex-map=. The =arg-optional= and =arg-required= groups, if present, are handled specially: if any of them is not empty, then its contents is stored as =arg-desc= in the output, and the corresponding =arg-required= / =arg-optional= is set to =$true=. Also =completer-= is set to =comp:files= by default. 238 | 239 | If =&fold= is =$true=, then the input is preprocessed to join option descriptions which span more than one line (the heuristic is not perfect and may not work in all cases, also for now it only joins one line after the option). 240 | 241 | If the =&opt-completers= option is given, it must be a map from argument option descriptions as they appear in the help output (e.g. =WHAT= and =TYPE=) to functions which will be used to produce their completions. By default the =comp:files= completer is used. For example: 242 | 243 | #+begin_src elvish 244 | cmd --help | comp:extract-opts &opt-completers=[&WHAT= { put what1 what2 } &TYPE= {put type1 type2} ] 245 | #+end_src 246 | 247 | *Example #8:* the =brew= completer shown before can be made to show package names in different styles (green when installing, red when uninstalling). Here we also show the use of =comp:extract-opts= with custom regex for capturing the options from the =brew= help messages: 248 | 249 | #+begin_src elvish 250 | brew-completions = [ 251 | &install= (comp:sequence \ 252 | &opts= [(brew install -h | take 1 | 253 | comp:extract-opts ®ex='--(\w[\w-]*)(?:=(.*?)\])?' ®ex-map=[&long=1 &arg-required=2] 254 | )] \ 255 | [ { brew search | comp:decorate &style=green } ... ] 256 | ) 257 | &uninstall= (comp:sequence \ 258 | &opts= [(brew uninstall -h | take 1 | 259 | comp:extract-opts ®ex='--(\w[\w-]*)' ®ex-map=[&long=1] 260 | )] \ 261 | [ { brew list | comp:decorate &style=red } ... ] 262 | ) 263 | &cat= (comp:sequence [{ brew search }]) 264 | ] 265 | 266 | edit:completion:arg-completer[brew] = (comp:subcommands &opts= [ version ] $brew-completions) 267 | #+end_src 268 | 269 | =comp:files= completes filenames, using any prefix as the stem. If the =®ex= option is specified, only files matching that pattern are completed. If =&dirs-only= is =$true=, only directories are returned. If =&transform= is given, it must be a one-argument lambda that is used to transform completions. It receives a string for each one of the available completions, and it must produce as output the transformed completion. 270 | 271 | =comp:dirs= is simply a convenience wrapper around =comp:files= which sets =&dirs-only= automatically. 272 | 273 | *Example #9*: a completer for the Elvish =use= command, which completes libraries and directories within the =~/.elvish/lib/= directory, removing the leading directory name and the =.elv= extension from the files, since they are not needed in the arguments: 274 | 275 | #+begin_src elvish 276 | edit:completion:arg-completer[use] = (comp:sequence [ 277 | [stem]{ 278 | comp:files ~/.elvish/lib/$stem ®ex='.*\.elv' ^ 279 | &transform=[m]{ re:replace ~/.elvish/lib/'(.*)(.elv)?' '$1' $m } 280 | } 281 | ]) 282 | #+end_src 283 | 284 | * Implementation 285 | :PROPERTIES: 286 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 287 | :header-args: :mkdirp yes :comments no 288 | :END: 289 | 290 | We start by loading some basic modules we need. 291 | 292 | #+begin_src elvish 293 | use re 294 | use str 295 | use path 296 | #+end_src 297 | 298 | The =$comp:debug= variable triggers printing debug messages to the terminal. 299 | 300 | #+begin_src elvish 301 | var debug = $false 302 | #+end_src 303 | 304 | ** Utility functions 305 | 306 | *** comp:-debugmsg 307 | 308 | Internal function to print debug messages if the =$comp:debug= variable is set. 309 | 310 | #+begin_src elvish 311 | fn -debugmsg {|@args &color=blue| 312 | if $debug { 313 | echo (styled (echo ">>> " $@args) $color) >/dev/tty 314 | } 315 | } 316 | #+end_src 317 | 318 | *** comp:decorate 319 | 320 | =comp:decorate= maps its input through =edit:complex-candidate= with the given options. Can be passed the same options as [[https://elvish.io/ref/edit.html#argument-completer][edit:complex-candidate]] except for =&display=, which does not make sense when multiple inputs are provided. In addition, if =&suffix= is specified, it is used to set both =&display-suffix= and =&code-suffix=. 321 | 322 | #+begin_src elvish 323 | fn decorate {|@input &code-suffix='' &display-suffix='' &suffix='' &style=$nil | 324 | if (== (count $input) 0) { 325 | set input = [(all)] 326 | } 327 | if (not-eq $suffix '') { 328 | set display-suffix = $suffix 329 | set code-suffix = $suffix 330 | } 331 | each {|k| 332 | var k-display = $k 333 | if $style { 334 | set k-display = (styled $k $style) 335 | } 336 | edit:complex-candidate &code-suffix=$code-suffix &display=$k-display$display-suffix $k 337 | } $input 338 | } 339 | #+end_src 340 | 341 | *** comp:empty 342 | 343 | =comp:empty= produces no completions. It can be used to mark an item in a sequence that should not produce any completions. 344 | 345 | #+begin_src elvish 346 | fn empty { nop } 347 | #+end_src 348 | 349 | *** comp:files and comp:dirs 350 | :PROPERTIES: 351 | :CUSTOM_ID: comp-files-and-comp-dirs 352 | :END: 353 | 354 | =comp:files= completes filenames, using any typed prefix as the stem. If the =®ex= option is specified, only files matching that pattern are completed. If =&dirs-only= is =$true=, only directories are returned. If =&transform= is given, it must be a one-argument lambda that is used to transform completions. It receives a string for each one of the available completions, and it must produce as output the transformed completion. 355 | 356 | #+begin_src elvish 357 | fn files {|arg ®ex='' &dirs-only=$false &transform=$nil| 358 | edit:complete-filename $arg | each {|c| 359 | var x = $c[stem] 360 | if (or (path:is-dir $x) (and (not $dirs-only) (or (eq $regex '') (re:match $regex $x)))) { 361 | if $transform { 362 | edit:complex-candidate ($transform $x) 363 | } else { 364 | put $c 365 | } 366 | } 367 | } 368 | } 369 | #+end_src 370 | 371 | =comp:dirs= is simply a convenience wrapper around =comp:files= which sets =&dirs-only= automatically. 372 | 373 | #+begin_src elvish 374 | fn dirs {|arg ®ex='' &transform=$nil| 375 | files $arg ®ex=$regex &dirs-only=$true &transform=$transform 376 | } 377 | #+end_src 378 | 379 | *** comp:extract-opts 380 | 381 | =comp:extract-opts= takes input from the pipeline and parses it using a regular expression. The default regex contains 5 groups to parse the =short=, =long=, =arg-required=, =arg-optional= and =desc=, but both the regex and the mapping can be configured using the =®ex= and =®ex-map= options. At last one of short/long is mandatory, everything else is optional. Returns an option map with all existing keys, depending on the available groups and the keys in =$regex-map=. Only produces an output if at least =short= or =long= has a value. The =arg-optional= and =arg-required= groups, if present, are handled specially: if any of them is not empty, then its contents is stored as =arg-desc= in the output, and the corresponding =arg-required= / =arg-optional= is set to =$true=. 382 | 383 | If =&fold= is =$true=, then the input is preprocessed to join option descriptions which span more than one line (the heuristic is not perfect and may not work in all cases, also for now it only joins one line after the option). 384 | 385 | If the =&opt-completers= option is given, it must be a map from argument option descriptions as they appear in the help output (e.g. ENV, PATH, CHANNEL) to functions which will be used to produce their completions. By default the =comp:files= completer is used. 386 | 387 | #+begin_src elvish :noweb yes 388 | fn extract-opts {|@cmd 389 | <> 390 | <> 391 | &fold=$false 392 | &first-sentence=$false 393 | &opt-completers=[&] 394 | | 395 | var -line = '' 396 | var capture = $all~ 397 | if $fold { 398 | set capture = { each {|l| 399 | if (re:match '^\s{8,}\w' $l) { 400 | var folded = $-line$l 401 | # -debugmsg "Folded line: "$folded 402 | put $folded 403 | set -line = '' 404 | } else { 405 | # -debugmsg "Non-folded line: "$-line 406 | put $-line 407 | set -line = $l 408 | } 409 | } 410 | } 411 | } 412 | $capture | each {|l| 413 | -debugmsg "Got line: "$l 414 | re:find $regex $l 415 | } | each {|m| 416 | -debugmsg "Matches: "(to-string $m) &color=red 417 | var g = $m[groups] 418 | var opt = [&] 419 | keys $regex-map | each {|k| 420 | if (has-key $g $regex-map[$k]) { 421 | var field = (str:trim-space $g[$regex-map[$k]][text]) 422 | if (not-eq $field '') { 423 | if (has-value [arg-optional arg-required] $k) { 424 | set opt[$k] = $true 425 | set opt[arg-desc] = $field 426 | if (has-key $opt-completers $field) { 427 | set opt[arg-completer] = $opt-completers[$field] 428 | } else { 429 | set opt[arg-completer] = $edit:complete-filename~ 430 | } 431 | } else { 432 | set opt[$k] = $field 433 | } 434 | } 435 | } 436 | } 437 | if (or (has-key $opt short) (has-key $opt long)) { 438 | if (has-key $opt desc) { 439 | if $first-sentence { 440 | set opt[desc] = (re:replace '\. .*$|\.\s*$|\s*\(.*$' '' $opt[desc]) 441 | } 442 | set opt[desc] = (re:replace '\s+' ' ' $opt[desc]) 443 | } 444 | put $opt 445 | } 446 | } 447 | } 448 | #+end_src 449 | 450 | *** comp:-handler-arity 451 | 452 | Determine the arity of a function and return a string representation, for internal use. 453 | 454 | #+begin_src elvish 455 | fn -handler-arity {|func| 456 | var fnargs = [ (to-string (count $func[arg-names])) (== $func[rest-arg] -1)] 457 | if (eq $fnargs [ 0 $true ]) { put no-args 458 | } elif (eq $fnargs [ 1 $true ]) { put one-arg 459 | } elif (eq $fnargs [ 1 $false ]) { put rest-arg 460 | } else { put other-args 461 | } 462 | } 463 | #+end_src 464 | 465 | ** Completion functions 466 | 467 | The backend completion functions =comp:-expand-item=, =comp:-expand-sequence= and =comp:-expand-subcommands= are the ones that actually process the completion definitions and, according to them and the current command line, provide the available completions. 468 | 469 | *** comp:-expand-item 470 | 471 | =comp:-expand-item= expands a "completion item" into its completion values. If it's a function, it gets executed with arguments corresponding to its arity; if it's a list, it's exploded to its elements. 472 | 473 | #+begin_src elvish 474 | fn -expand-item {|def @cmd| 475 | var arg = $cmd[-1] 476 | var what = (kind-of $def) 477 | if (eq $what 'fn') { 478 | [ &no-args= { $def } 479 | &one-arg= { $def $arg } 480 | &rest-arg= { $def $@cmd } 481 | &other-args= { put '' } 482 | ][(-handler-arity $def)] 483 | } elif (eq $what 'list') { 484 | all $def 485 | } else { 486 | echo (styled "comp:-expand-item: invalid item of type "$what": "(to-string $def) red) >/dev/tty 487 | } 488 | } 489 | #+end_src 490 | 491 | *** comp:-expand-sequence 492 | 493 | =comp:-expand-sequence= receives an array of definition items and the current contents of the command line, and uses =edit:complete-getopt= to actually generate the completions. For this, we need to make sure the options and argument handler data structures are in accordance to what =edit:complete-getopt= expects. 494 | 495 | #+begin_src elvish 496 | fn -expand-sequence {|seq @cmd &opts=[]| 497 | #+end_src 498 | 499 | We first preprocess the options. If =&opts= is provided, it has to be a completion item which expands to a list with one element per option. Elements that are maps are assumed to be in getopt format (with keys =short=, =long=, =desc=, =arg-required=, =arg-optional= and =arg-desc=) and used as-is (their structure is not checked). Elements which are strings are considered as long option names and converted to the appropriate data structure. 500 | 501 | Because =edit:complete-getopt= supports option argument completion with key =completer=. So if option structure has an =arg-completer= key, then it is expanded as an completion item and offers as a completer. 502 | 503 | #+begin_src elvish 504 | var final-opts = [( 505 | -expand-item $opts $@cmd | each {|opt| 506 | -debugmsg "In final-opts: opt before="(to-string $opt) &color=yellow 507 | if (eq (kind-of $opt) map) { 508 | if (has-key $opt arg-completer) { 509 | -debugmsg &color=yellow "Assigning opt[completer] = [_]{ -expand-item "(to-string $opt[arg-completer]) $@cmd "}" 510 | set opt[completer] = {|_| -expand-item $opt[arg-completer] $@cmd } 511 | } 512 | -debugmsg "In final-opts: opt after="(to-string $opt) &color=yellow 513 | put $opt 514 | } else { 515 | put [&long= $opt] 516 | } 517 | } 518 | )] 519 | #+end_src 520 | 521 | We also preprocess the handlers. =edit:complete-getopt= expects each handler to receive only one argument (the current word in the command line), but =comp= allows handlers to receive no arguments, one argument (the current element of the command line) or multiple arguments (the whole command line), so we need to normalize them. Happily, Elvish's functional nature makes this easy by checking the arity of each handler and, if necessary, wrapping them in one-argument functions, but passing them the information they expect. We also wrap items which are arrays into corresponding functions. As a special case, the string ='...'= is also passed, as it is allowed by =edit:complete-getopt= to indicate that the last element needs to be repeated for future elements. Any other handlers are ignored. 522 | 523 | #+begin_src elvish 524 | var final-handlers = [( 525 | all $seq | each {|f| 526 | if (eq (kind-of $f) 'fn') { 527 | put [ 528 | &no-args= {|_| $f } 529 | &one-arg= $f 530 | &rest-arg= {|_| $f $@cmd } 531 | &other-args= {|_| put '' } 532 | ][(-handler-arity $f)] 533 | } elif (eq (kind-of $f) 'list') { 534 | put {|_| all $f } 535 | } elif (and (eq (kind-of $f) 'string') (eq $f '...')) { 536 | put $f 537 | } 538 | } 539 | )] 540 | #+end_src 541 | 542 | Finally, we call =edit:complete-getopt= with the corresponding data structures. It expects the current line /without/ the initial command, so we remove that as well. 543 | 544 | #+begin_src elvish 545 | -debugmsg Calling: edit:complete-getopt (to-string $cmd[1..]) (to-string $final-opts) (to-string $final-handlers) 546 | edit:complete-getopt $cmd[1..] $final-opts $final-handlers 547 | } 548 | #+end_src 549 | 550 | *** comp:-expand-subcommands 551 | 552 | =comp:-expand-subcommands= receives a definition map and the current contents of the command line. 553 | 554 | #+begin_src elvish 555 | fn -expand-subcommands {|def @cmd &opts=[]| 556 | #+end_src 557 | 558 | The algorithm for =comp:-expand-subcommands= is a bit counterintuitive, this is how it works: 559 | 560 | 1. Scan the current command to see if a valid subcommand is found (i.e. an element which matches an existing key in =$def=). 561 | #+begin_src elvish 562 | var subcommands = [(keys $def)] 563 | var n = (count $cmd) 564 | var kw = [(range 1 $n | each {|i| 565 | if (has-value $subcommands $cmd[$i]) { put $cmd[$i] $i } 566 | })] 567 | #+end_src 568 | 569 | 2. If a subcommand is found, call its expansion function directly, and with the command line at that position. We check if the definition is a string, in which case it's expected to be the name of some other command whose definition we need to use (to implement command aliases) - we substitute the alias for its target command and call =-expand-subcommands= with the new values. 570 | #+begin_src elvish 571 | if (and (not-eq $kw []) (not-eq $kw[1] (- $n 1))) { 572 | var sc sc-pos = $kw[0 1] 573 | if (eq (kind-of $def[$sc]) 'string') { 574 | set cmd[$sc-pos] = $def[$sc] 575 | -expand-subcommands &opts=$opts $def $@cmd 576 | } else { 577 | $def[$sc] (all $cmd[{$sc-pos}..]) 578 | } 579 | #+end_src 580 | 581 | 3. If no subcommand is found, generate a sequence definition which returns the subcommand names for the first position (including any provided options). 582 | #+begin_src elvish 583 | } else { 584 | var top-def = [ { put $@subcommands } ] 585 | -expand-sequence &opts=$opts $top-def $@cmd 586 | } 587 | } 588 | #+end_src 589 | 590 | This seems backwards from what one (or at least I) initially expected - I attempted at first multiple variations to expand the subcommands/top-options first, and then only expand the subcommand options and definition from the "tail" handlers, but this doesn't work because of the way =edit:complete-getops= works, the top-level options would get expanded for subcommands as well. This way, we catch the more specific case first (subcommand definition) and only if there's no subcommand in the command line yet, we do the top-level expansion. All with simple and clear code (you wouldn't believe some of the variations I tried while trying to get this to work!). 591 | 592 | ** Completion wrapper functions (main entry points) 593 | 594 | The wrapper functions =comp:item=, =comp:sequence= and =comp:subcommands= are the main entry points - they receive the completion definitions and call the corresponding =-expand-*= function. They also take care of running the pre- and post-hooks, if specified. 595 | 596 | *** comp:item 597 | 598 | #+begin_src elvish 599 | fn item {|item &pre-hook=$nop~ &post-hook=$nop~| 600 | put {|@cmd| 601 | $pre-hook $@cmd 602 | var result = [(-expand-item $item $@cmd)] 603 | $post-hook $result $@cmd 604 | put $@result 605 | } 606 | } 607 | #+end_src 608 | 609 | *** comp:sequence 610 | 611 | #+begin_src elvish 612 | fn sequence {|sequence &opts=[] &pre-hook=$nop~ &post-hook=$nop~| 613 | put {|@cmd &inspect=$false| 614 | if $inspect { 615 | echo "comp:sequence definition: "(to-string $sequence) 616 | echo "opts: "(to-string $opts) 617 | } else { 618 | $pre-hook $@cmd 619 | var result = [(-expand-sequence &opts=$opts $sequence $@cmd)] 620 | $post-hook $result $@cmd 621 | put $@result 622 | } 623 | } 624 | } 625 | #+end_src 626 | 627 | *** comp:subcommands 628 | 629 | #+begin_src elvish 630 | fn subcommands {|def &opts=[] &pre-hook=$nop~ &post-hook=$nop~| 631 | put {|@cmd &inspect=$false| 632 | if $inspect { 633 | echo "Completer definition: "(to-string $def) 634 | echo "opts: "(to-string $opts) 635 | } else { 636 | $pre-hook $@cmd 637 | if (and (eq $opts []) (has-key $def -options)) { 638 | set opts = $def[-options] 639 | } 640 | del def[-options] 641 | var result = [(-expand-subcommands &opts=$opts $def $@cmd)] 642 | $post-hook $result $@cmd 643 | put $@result 644 | } 645 | } 646 | } 647 | #+end_src 648 | -------------------------------------------------------------------------------- /dd.elv: -------------------------------------------------------------------------------- 1 | use str 2 | 3 | fn -comma-sep-list {|options arg| 4 | var prefix = $arg[..(+ 1 (str:last-index $arg ','))] 5 | for option [(keys $options)] { 6 | edit:complex-candidate &display=$options[$option] $prefix$option 7 | } 8 | } 9 | 10 | var -convs = [ 11 | &ascii= "ascii (from EBCDIC to ASCII)" 12 | &ebcdic= "ebcdic (from ASCII to EBCDIC)" 13 | &ibm= "ibm (from ASCII to alternate EBCDIC)" 14 | &block= "block (pad newline-terminated records with spaces to cbs-size)" 15 | &unblock= "unblock (replace trailing spaces in cbs-size records with newline)" 16 | &lcase= "lcase (change upper case to lower case)" 17 | &ucase= "ucase (change lower case to upper case)" 18 | &sparse= "sparse (try to seek rather than write all-NUL output blocks)" 19 | &swab= "swab (swap every pair of input bytes)" 20 | &sync= "sync (pad every input block with NULs to ibs-size; when used with block or unblock, pad with spaces rather than NULs)" 21 | &excl= "excl (fail if the output file already exists)" 22 | &nocreat= "nocreat (do not create the output file)" 23 | ¬runc= "notrunc (do not truncate the output file)" 24 | &noerror= "noerror (continue after read errors)" 25 | &fdatasync="fdatasync (physically write output file data before finishing)" 26 | &fsync= "fsync (likewise, but also write metadata)" 27 | ] 28 | 29 | var -flags = [ 30 | &append= "append (append mode (makes sense only for output; conv=notrunc suggested))" 31 | &direct= "direct (use direct I/O for data)" 32 | &directory= "directory (fail unless a directory)" 33 | &dsync= "dsync (use synchronized I/O for data)" 34 | &sync= "sync (likewise, but also for metadata)" 35 | &fullblock= "fullblock (accumulate full blocks of input (iflag only))" 36 | &nonblock= "nonblock (use non-blocking I/O)" 37 | &noatime= "noatime (do not update access time)" 38 | &nocache= "nocache (Request to drop cache. See also oflag=sync)" 39 | &noctty= "noctty (do not assign controlling terminal from file)" 40 | &nofollow= "nofollow (do not follow symlinks)" 41 | &count_bytes="count_bytes (treat 'count=N' as a byte count (iflag only))" 42 | &skip_bytes= "skip_bytes (treat 'skip=N' as a byte count (iflag only))" 43 | &seek_bytes= "seek_bytes (treat 'seek=N' as a byte count (oflag only))" 44 | ] 45 | 46 | var -operands = [ 47 | &bs= [&desc="read and write up to BYTES bytes at a time (default: 512); overrides ibs and obs"] 48 | &cbs= [&desc="convert BYTES bytes at a time"] 49 | &conv= [&desc="convert the file as per the comma separated symbol list" 50 | &comp={|arg| -comma-sep-list $-convs $arg }] 51 | &count= [&desc="copy only N input blocks"] 52 | &ibs= [&desc="read up to BYTES bytes at a time (default: 512)"] 53 | &if= [&desc="read from FILE instead of stdin" 54 | &comp=$edit:complete-filename~] 55 | &iflag= [&desc="read as per the comma separated symbol list" 56 | &comp={|arg| -comma-sep-list $-flags $arg }] 57 | &obs= [&desc="write BYTES bytes at a time (default: 512)"] 58 | &of= [&desc="write to FILE instead of stdout" 59 | &comp=$edit:complete-filename~] 60 | &oflag= [&desc="write as per the comma separated symbol list" 61 | &comp={|arg| -comma-sep-list $-flags $arg }] 62 | &seek= [&desc="skip N obs-sized blocks at start of output"] 63 | &skip= [&desc="skip N ibs-sized blocks at start of input"] 64 | &status=[&desc="The LEVEL of information to print to stderr" 65 | &comp={|arg| all [none noxfer progress] }] 66 | ] 67 | 68 | fn -completer {|@cmd| 69 | var last-arg = $cmd[-1] 70 | var op-length = (str:index $last-arg '=') 71 | 72 | if (== -1 $op-length) { 73 | for op [(keys $-operands)] { 74 | edit:complex-candidate &display=$op'= ('$-operands[$op][desc]')' &code-suffix='=' $op 75 | } 76 | 77 | edit:complex-candidate &display="--help (display help and exit)" '--help' 78 | edit:complex-candidate &display="--version (output version information and exit)" '--version' 79 | 80 | } else { 81 | var op = $last-arg[..$op-length] 82 | var arg = $last-arg[(+ 1 $op-length)..] 83 | 84 | if (has-key $-operands[$op] comp) { 85 | $-operands[$op][comp] $arg | each {|candidate| 86 | 87 | if (eq (kind-of $candidate) map) { 88 | put (edit:complex-candidate $op'='$candidate[stem] &display=$candidate[display]) 89 | } else { 90 | put $op'='$candidate 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | set edit:completion:arg-completer[dd] = $-completer~ 98 | -------------------------------------------------------------------------------- /dd.org: -------------------------------------------------------------------------------- 1 | #+property: header-args:elvish :tangle dd.elv 2 | #+property: header-args :mkdirp yes :comments no 3 | 4 | #+title: Elvish completions for dd 5 | #+author: Harry Cutts 6 | 7 | #+name: module-summary 8 | Completions for =dd=, including operands, conversions, and flags. 9 | 10 | * Implementation 11 | 12 | Conversions and flags are both specified as comma-separated lists, so let's have a single function for completing those, given a map of the options and their descriptions, and the argument typed so far (e.g. =foo,bar,b=). We simply take the already-typed options (before the last comma) and outputting each possible option with them tacked on to the start. 13 | 14 | #+begin_src elvish 15 | use str 16 | 17 | fn -comma-sep-list {|options arg| 18 | var prefix = $arg[..(+ 1 (str:last-index $arg ','))] 19 | for option [(keys $options)] { 20 | edit:complex-candidate &display=$options[$option] $prefix$option 21 | } 22 | } 23 | #+end_src 24 | 25 | Now we define all the possible conversions and flags, as maps to pass to =-comma-sep-list=. 26 | 27 | #+begin_src elvish 28 | var -convs = [ 29 | &ascii= "ascii (from EBCDIC to ASCII)" 30 | &ebcdic= "ebcdic (from ASCII to EBCDIC)" 31 | &ibm= "ibm (from ASCII to alternate EBCDIC)" 32 | &block= "block (pad newline-terminated records with spaces to cbs-size)" 33 | &unblock= "unblock (replace trailing spaces in cbs-size records with newline)" 34 | &lcase= "lcase (change upper case to lower case)" 35 | &ucase= "ucase (change lower case to upper case)" 36 | &sparse= "sparse (try to seek rather than write all-NUL output blocks)" 37 | &swab= "swab (swap every pair of input bytes)" 38 | &sync= "sync (pad every input block with NULs to ibs-size; when used with block or unblock, pad with spaces rather than NULs)" 39 | &excl= "excl (fail if the output file already exists)" 40 | &nocreat= "nocreat (do not create the output file)" 41 | ¬runc= "notrunc (do not truncate the output file)" 42 | &noerror= "noerror (continue after read errors)" 43 | &fdatasync="fdatasync (physically write output file data before finishing)" 44 | &fsync= "fsync (likewise, but also write metadata)" 45 | ] 46 | 47 | var -flags = [ 48 | &append= "append (append mode (makes sense only for output; conv=notrunc suggested))" 49 | &direct= "direct (use direct I/O for data)" 50 | &directory= "directory (fail unless a directory)" 51 | &dsync= "dsync (use synchronized I/O for data)" 52 | &sync= "sync (likewise, but also for metadata)" 53 | &fullblock= "fullblock (accumulate full blocks of input (iflag only))" 54 | &nonblock= "nonblock (use non-blocking I/O)" 55 | &noatime= "noatime (do not update access time)" 56 | &nocache= "nocache (Request to drop cache. See also oflag=sync)" 57 | &noctty= "noctty (do not assign controlling terminal from file)" 58 | &nofollow= "nofollow (do not follow symlinks)" 59 | &count_bytes="count_bytes (treat 'count=N' as a byte count (iflag only))" 60 | &skip_bytes= "skip_bytes (treat 'skip=N' as a byte count (iflag only))" 61 | &seek_bytes= "seek_bytes (treat 'seek=N' as a byte count (oflag only))" 62 | ] 63 | #+end_src 64 | 65 | Now we can define all the operands, with their descriptions and completion functions. Completion functions will be passed only the part of the argument after the ~=~, allowing us to reuse things like =edit:complete-filename~=. 66 | 67 | #+begin_src elvish 68 | var -operands = [ 69 | &bs= [&desc="read and write up to BYTES bytes at a time (default: 512); overrides ibs and obs"] 70 | &cbs= [&desc="convert BYTES bytes at a time"] 71 | &conv= [&desc="convert the file as per the comma separated symbol list" 72 | &comp={|arg| -comma-sep-list $-convs $arg }] 73 | &count= [&desc="copy only N input blocks"] 74 | &ibs= [&desc="read up to BYTES bytes at a time (default: 512)"] 75 | &if= [&desc="read from FILE instead of stdin" 76 | &comp=$edit:complete-filename~] 77 | &iflag= [&desc="read as per the comma separated symbol list" 78 | &comp={|arg| -comma-sep-list $-flags $arg }] 79 | &obs= [&desc="write BYTES bytes at a time (default: 512)"] 80 | &of= [&desc="write to FILE instead of stdout" 81 | &comp=$edit:complete-filename~] 82 | &oflag= [&desc="write as per the comma separated symbol list" 83 | &comp={|arg| -comma-sep-list $-flags $arg }] 84 | &seek= [&desc="skip N obs-sized blocks at start of output"] 85 | &skip= [&desc="skip N ibs-sized blocks at start of input"] 86 | &status=[&desc="The LEVEL of information to print to stderr" 87 | &comp={|arg| all [none noxfer progress] }] 88 | ] 89 | #+end_src 90 | 91 | The main completion function is only concerned with the last argument. First we need to know which part of the argument is the operand (before the ~=~). 92 | 93 | #+begin_src elvish 94 | fn -completer {|@cmd| 95 | var last-arg = $cmd[-1] 96 | var op-length = (str:index $last-arg '=') 97 | #+end_src 98 | 99 | If there isn't an equals, we offer the operands (and =--help= and =--version=, the only "normal-looking" options) as completions. 100 | 101 | #+begin_src elvish 102 | if (== -1 $op-length) { 103 | for op [(keys $-operands)] { 104 | edit:complex-candidate &display=$op'= ('$-operands[$op][desc]')' &code-suffix='=' $op 105 | } 106 | 107 | edit:complex-candidate &display="--help (display help and exit)" '--help' 108 | edit:complex-candidate &display="--version (output version information and exit)" '--version' 109 | #+end_src 110 | 111 | Otherwise, we lop off the operand and its ~=~, so that we can use nice simple completion functions for the values. 112 | 113 | #+begin_src elvish 114 | } else { 115 | var op = $last-arg[..$op-length] 116 | var arg = $last-arg[(+ 1 $op-length)..] 117 | 118 | if (has-key $-operands[$op] comp) { 119 | $-operands[$op][comp] $arg | each {|candidate| 120 | #+end_src 121 | 122 | Since some of the completion functions use =edit:complex-candidate=, we have to remake them with the operand added back on. 123 | 124 | #+begin_src elvish 125 | if (eq (kind-of $candidate) map) { 126 | put (edit:complex-candidate $op'='$candidate[stem] &display=$candidate[display]) 127 | } else { 128 | put $op'='$candidate 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | set edit:completion:arg-completer[dd] = $-completer~ 136 | #+end_src 137 | -------------------------------------------------------------------------------- /evemu.elv: -------------------------------------------------------------------------------- 1 | use ./comp 2 | use re 3 | use str 4 | 5 | var -complete-dev = { 6 | var evdev-dir = '/dev/input/' 7 | ls $evdev-dir | each {|item| 8 | if (str:has-prefix $item 'event') { 9 | var path = $evdev-dir$item 10 | var name = (cat /sys/class/input/$item/device/name) 11 | edit:complex-candidate &display=$path" ("$name")" $path 12 | } 13 | } 14 | } 15 | 16 | var ev-code-header = /usr/include/linux/input-event-codes.h 17 | 18 | fn -defs-with-prefix {|prefix| 19 | grep "#define "$prefix"_" $ev-code-header | 20 | re:awk {|line @fields| put $fields[1] } 21 | } 22 | 23 | fn -ev-codes-for-type {|type| 24 | if (eq $type 'EV_KEY') { 25 | -defs-with-prefix 'KEY' 26 | -defs-with-prefix 'BTN' 27 | } else { 28 | -defs-with-prefix (str:trim-prefix $type 'EV_') 29 | } 30 | } 31 | 32 | var -evtest-opts = [ 33 | [&long="grab" &desc="grab the device for exclusive access"] 34 | [&long="query" &desc="query a specific single-bit event code"] 35 | ] 36 | 37 | var -complete-evtest-type = {|@cmd| 38 | if (eq $cmd[1] '--query') { 39 | all [EV_KEY EV_SW EV_SND EV_LED] 40 | } 41 | } 42 | 43 | var -complete-evtest-code = {|@cmd| 44 | if (eq $cmd[1] '--query') { 45 | -ev-codes-for-type $cmd[3] 46 | } 47 | } 48 | 49 | set edit:completion:arg-completer[evtest] = ( 50 | comp:sequence &opts=$-evtest-opts [$-complete-dev $-complete-evtest-type $-complete-evtest-code]) 51 | 52 | var -autorestart-opt = [ 53 | &long="autorestart" 54 | &desc="Terminate the current recording after seconds of inactivity and restart a new recording" 55 | &arg-required=$true 56 | ] 57 | 58 | var -record-and-describe-comp = (comp:sequence &opts=[$-autorestart-opt] [$-complete-dev $comp:files~]) 59 | set edit:completion:arg-completer[evemu-describe] = $-record-and-describe-comp 60 | set edit:completion:arg-completer[evemu-record] = $-record-and-describe-comp 61 | 62 | set edit:completion:arg-completer[evemu-device] = $edit:complete-filename~ 63 | 64 | set edit:completion:arg-completer[evemu-play] = (comp:sequence [{|arg| 65 | $-complete-dev 66 | comp:files $arg 67 | }]) 68 | 69 | var -all-ev-codes = { 70 | var prefix-pattern = (-defs-with-prefix 'EV' | each {|s| str:trim-prefix $s 'EV_' } | str:join '\|') 71 | -defs-with-prefix '\('$prefix-pattern'\|BTN\)' 72 | } 73 | 74 | var -event-opts = [ 75 | [&long="sync" &desc="generate an EV_SYN event after the event"] 76 | [&long="type" 77 | &desc="the type of event to generate" 78 | &arg-required=$true 79 | &arg-completer={ -defs-with-prefix 'EV' }] 80 | [&long="code" 81 | &desc="the event code" 82 | &arg-required=$true 83 | &arg-completer=$-all-ev-codes] 84 | [&long="value" 85 | &desc="the event value" 86 | &arg-required=$true] 87 | ] 88 | 89 | set edit:completion:arg-completer[evemu-event] = (comp:sequence &opts=$-event-opts [$-complete-dev]) 90 | -------------------------------------------------------------------------------- /evemu.org: -------------------------------------------------------------------------------- 1 | #+property: header-args:elvish :tangle evemu.elv 2 | #+property: header-args :mkdirp yes :comments no 3 | 4 | #+title: Elvish completions for evtest and evemu 5 | #+author: Harry Cutts 6 | 7 | #+name: module-summary 8 | Completions for [[https://gitlab.freedesktop.org/libevdev/evtest][=evtest=]] and the [[https://www.freedesktop.org/wiki/Evemu/][=evemu=]] set of tools, which assist in debugging and emulating the [[https://www.kernel.org/doc/html/latest/input/input_uapi.html][Linux input subsystem]]. 9 | 10 | * Implementation 11 | 12 | ** Completions for all commands 13 | 14 | All of these commands operate on evdev nodes, contained in =/dev/input/=. Each represents an input device (or part of one), with a name. We can retrieve that name from sysfs and show it next to the completion. 15 | 16 | #+begin_src elvish 17 | use ./comp 18 | use re 19 | use str 20 | 21 | var -complete-dev = { 22 | var evdev-dir = '/dev/input/' 23 | ls $evdev-dir | each {|item| 24 | if (str:has-prefix $item 'event') { 25 | var path = $evdev-dir$item 26 | var name = (cat /sys/class/input/$item/device/name) 27 | edit:complex-candidate &display=$path" ("$name")" $path 28 | } 29 | } 30 | } 31 | #+end_src 32 | 33 | Some commands take axis or key constants defined in the Kernel's =input-event-codes.h=, each prefixed with a type (e.g. =KEY_A=, =BTN_LEFT=, or =REL_X=). We can list them with some simple matching against the header file. 34 | 35 | #+begin_src elvish 36 | var ev-code-header = /usr/include/linux/input-event-codes.h 37 | 38 | fn -defs-with-prefix {|prefix| 39 | grep "#define "$prefix"_" $ev-code-header | 40 | re:awk {|line @fields| put $fields[1] } 41 | } 42 | #+end_src 43 | 44 | Next we'll need a method to list all the constants corresponding to a particular event type, the name of which is prefixed by =EV_=. (The =KEY= type also includes the =BTN= constants.) 45 | 46 | #+begin_src elvish 47 | fn -ev-codes-for-type {|type| 48 | if (eq $type 'EV_KEY') { 49 | -defs-with-prefix 'KEY' 50 | -defs-with-prefix 'BTN' 51 | } else { 52 | -defs-with-prefix (str:trim-prefix $type 'EV_') 53 | } 54 | } 55 | #+end_src 56 | 57 | ** =evtest= 58 | 59 | =evtest= only has two options. 60 | 61 | #+begin_src elvish 62 | var -evtest-opts = [ 63 | [&long="grab" &desc="grab the device for exclusive access"] 64 | [&long="query" &desc="query a specific single-bit event code"] 65 | ] 66 | #+end_src 67 | 68 | ...but =--query= is a little tricky, as it takes two arguments: an event type (one with binary states), and a code of that type. We have to use completer functions that look at the other arguments by index. 69 | 70 | #+begin_src elvish 71 | var -complete-evtest-type = {|@cmd| 72 | if (eq $cmd[1] '--query') { 73 | all [EV_KEY EV_SW EV_SND EV_LED] 74 | } 75 | } 76 | 77 | var -complete-evtest-code = {|@cmd| 78 | if (eq $cmd[1] '--query') { 79 | -ev-codes-for-type $cmd[3] 80 | } 81 | } 82 | 83 | set edit:completion:arg-completer[evtest] = ( 84 | comp:sequence &opts=$-evtest-opts [$-complete-dev $-complete-evtest-type $-complete-evtest-code]) 85 | #+end_src 86 | 87 | ** =evemu-record= and =evemu-describe= 88 | 89 | These commands both take the same simple set of arguments. 90 | 91 | #+begin_src elvish 92 | var -autorestart-opt = [ 93 | &long="autorestart" 94 | &desc="Terminate the current recording after seconds of inactivity and restart a new recording" 95 | &arg-required=$true 96 | ] 97 | 98 | var -record-and-describe-comp = (comp:sequence &opts=[$-autorestart-opt] [$-complete-dev $comp:files~]) 99 | set edit:completion:arg-completer[evemu-describe] = $-record-and-describe-comp 100 | set edit:completion:arg-completer[evemu-record] = $-record-and-describe-comp 101 | #+end_src 102 | 103 | ** =evemu-device= 104 | 105 | #+begin_src elvish 106 | set edit:completion:arg-completer[evemu-device] = $edit:complete-filename~ 107 | #+end_src 108 | 109 | ** =evemu-play= 110 | 111 | This command takes either an evdev device or a recording file. 112 | 113 | #+begin_src elvish 114 | set edit:completion:arg-completer[evemu-play] = (comp:sequence [{|arg| 115 | $-complete-dev 116 | comp:files $arg 117 | }]) 118 | #+end_src 119 | 120 | ** =evemu-event= 121 | 122 | This command, for generating single events, has an argument that could take any event code, so we put all the possible event types (as well as =BTN_=, an additional prefix for the =EV_KEY= type) into a regex with which to retrieve the constants. 123 | 124 | #+begin_src elvish 125 | var -all-ev-codes = { 126 | var prefix-pattern = (-defs-with-prefix 'EV' | each {|s| str:trim-prefix $s 'EV_' } | str:join '\|') 127 | -defs-with-prefix '\('$prefix-pattern'\|BTN\)' 128 | } 129 | 130 | var -event-opts = [ 131 | [&long="sync" &desc="generate an EV_SYN event after the event"] 132 | [&long="type" 133 | &desc="the type of event to generate" 134 | &arg-required=$true 135 | &arg-completer={ -defs-with-prefix 'EV' }] 136 | [&long="code" 137 | &desc="the event code" 138 | &arg-required=$true 139 | &arg-completer=$-all-ev-codes] 140 | [&long="value" 141 | &desc="the event value" 142 | &arg-required=$true] 143 | ] 144 | 145 | set edit:completion:arg-completer[evemu-event] = (comp:sequence &opts=$-event-opts [$-complete-dev]) 146 | #+end_src 147 | -------------------------------------------------------------------------------- /git.elv: -------------------------------------------------------------------------------- 1 | use ./comp 2 | use re 3 | use str 4 | use github.com/muesli/elvish-libs/git 5 | use github.com/zzamboni/elvish-modules/util 6 | 7 | var completions = [&] 8 | 9 | var status = [&] 10 | 11 | var git-arg-completer = { } 12 | 13 | var git-command = git 14 | 15 | var modified-style = yellow 16 | var untracked-style = red 17 | var tracked-style = $nil 18 | var branch-style = blue 19 | var remote-style = cyan 20 | var unmerged-style = magenta 21 | 22 | fn -run-git {|@rest| 23 | var gitcmds = [$git-command] 24 | if (eq (kind-of $git-command) string) { 25 | set gitcmds = [(re:split " " $git-command)] 26 | } 27 | var cmd = $gitcmds[0] 28 | if (eq (kind-of $cmd) string) { 29 | set cmd = (external $cmd) 30 | } 31 | $cmd (all $gitcmds[1..]) $@rest 32 | } 33 | 34 | fn -git-opts {|@cmd| 35 | set _ = ?(-run-git $@cmd -h 2>&1) | drop 1 | if (eq $cmd []) { 36 | comp:extract-opts &fold=$true ®ex='--(\w[\w-]*)' ®ex-map=[&long=1] 37 | } else { 38 | comp:extract-opts &fold=$true 39 | } 40 | } 41 | 42 | fn MODIFIED { all $status[local-modified] | comp:decorate &style=$modified-style } 43 | fn UNTRACKED { all $status[untracked] | comp:decorate &style=$untracked-style } 44 | fn UNMERGED { all $status[unmerged] | comp:decorate &style=$unmerged-style } 45 | fn MOD-UNTRACKED { MODIFIED; UNTRACKED } 46 | fn TRACKED { set _ = ?(-run-git ls-files 2>&-) | comp:decorate &style=$tracked-style } 47 | fn BRANCHES {|&all=$false &branch=$true| 48 | var -allarg = [] 49 | var -branch = '' 50 | if $all { set -allarg = ['--all'] } 51 | if $branch { set -branch = ' (branch)' } 52 | set _ = ?(-run-git branch --list (all $-allarg) --format '%(refname:short)' 2>&- | 53 | comp:decorate &display-suffix=$-branch &style=$branch-style) 54 | } 55 | fn REMOTE-BRANCHES { 56 | set _ = ?(-run-git branch --list --remote --format '%(refname:short)' 2>&- | 57 | grep -v HEAD | 58 | each {|branch| re:replace 'origin/' '' $branch } | 59 | comp:decorate &display-suffix=' (remote branch)' &style=$branch-style) 60 | } 61 | fn REMOTES { set _ = ?(-run-git remote 2>&- | comp:decorate &display-suffix=' (remote)' &style=$remote-style ) } 62 | fn STASHES { set _ = ?(-run-git stash list 2>&- | each {|l| put [(re:split : $l)][0] } ) } 63 | 64 | var git-completions = [ 65 | &add= [ {|stem| MOD-UNTRACKED; UNMERGED; comp:dirs $stem } ... ] 66 | &stage= add 67 | &checkout= [ { MODIFIED; BRANCHES } ... ] 68 | &switch= [ { $BRANCHES~ &branch=$false; REMOTE-BRANCHES } ] 69 | &mv= [ {|stem| TRACKED; comp:dirs $stem } ... ] 70 | &rm= [ {|stem| TRACKED; comp:dirs $stem } ... ] 71 | &diff= [ { MODIFIED; BRANCHES } ... ] 72 | &push= [ $REMOTES~ $BRANCHES~ ] 73 | &pull= [ $REMOTES~ { BRANCHES &all } ] 74 | &merge= [ $BRANCHES~ ... ] 75 | &init= [ {|stem| put "."; comp:dirs $stem } ] 76 | &branch= [ $BRANCHES~ ... ] 77 | &rebase= [ { $BRANCHES~ &all } ... ] 78 | &cherry= [ { $BRANCHES~ &all } $BRANCHES~ $BRANCHES~ ] 79 | &cherry-pick= [ { $BRANCHES~ &all } ... ] 80 | &stash= [ 81 | &list= (comp:sequence []) 82 | &clear= (comp:sequence []) 83 | &show= (comp:sequence [ $STASHES~ ]) 84 | &drop= (comp:sequence &opts=[[&short=q &long=quiet]] [ $STASHES~ ]) 85 | &pop= (comp:sequence &opts=[[&short=q &long=quiet] [&long=index]] [ $STASHES~ ]) 86 | &apply= pop 87 | &branch= (comp:sequence [ [] $STASHES~ ]) 88 | &push= (comp:sequence [ $comp:files~ ... ] &opts=[ 89 | [&short=p &long=patch] 90 | [&short=k &long=keep-index] [&long=no-keep-index] 91 | [&short=q &long=quiet] 92 | [&short=u &long=include-untracked] 93 | [&short=a &long=all] 94 | [&short=m &long=message &arg-required] 95 | ]) 96 | &create= (comp:sequence []) 97 | &store= (comp:sequence [ $BRANCHES~ ] &opts=[ 98 | [&short=m &long=message &arg-required] 99 | [&short=q &long=quiet] 100 | ]) 101 | ] 102 | ] 103 | 104 | fn init { 105 | set completions = [&] 106 | -run-git help -a --no-verbose | re:awk {|line @f| if (re:match '^ [a-z]' $line) { put $@f } } | each {|c| 107 | var seq = [ $comp:files~ ... ] 108 | if (has-key $git-completions $c) { 109 | set seq = $git-completions[$c] 110 | } 111 | if (eq (kind-of $seq) string) { 112 | set completions[$c] = $seq 113 | } elif (eq (kind-of $seq) map) { 114 | set completions[$c] = (comp:subcommands $seq) 115 | } else { 116 | set completions[$c] = (comp:sequence $seq &opts={ -git-opts $c }) 117 | } 118 | } 119 | -run-git config --list | each {|l| re:find '^alias\.([^=]+)=(\S+)' $l } | each {|m| 120 | var alias target = $m[groups][1 2][text] 121 | if (has-key $completions $target) { 122 | set completions[$alias] = $target 123 | } else { 124 | set completions[$alias] = (comp:sequence []) 125 | } 126 | } 127 | set git-arg-completer = (comp:subcommands $completions ^ 128 | &pre-hook={|@_| set status = (git:status) } &opts={ -git-opts } 129 | ) 130 | set edit:completion:arg-completer[git] = $git-arg-completer 131 | } 132 | 133 | init 134 | -------------------------------------------------------------------------------- /git.org: -------------------------------------------------------------------------------- 1 | #+title: Elvish completions for git 2 | #+author: Diego Zamboni 3 | #+email: diego@zzamboni.org 4 | 5 | #+name: module-summary 6 | Completions for =git=, including automatically generated completions for both subcommands and command-line options. 7 | 8 | Some original inspiration from [[ https://github.com/occivink/config/blob/master/.elvish/rc.elv.][occivink's git completer]]. 9 | 10 | * Table of Contents :TOC:noexport: 11 | - [[#usage][Usage]] 12 | - [[#implementation][Implementation]] 13 | - [[#libraries-and-global-variables][Libraries and global variables]] 14 | - [[#configuration-variables][Configuration variables]] 15 | - [[#utility-functions][Utility functions]] 16 | - [[#initialization-of-completion-definitions][Initialization of completion definitions]] 17 | - [[#test-suite][Test suite]] 18 | 19 | * Usage 20 | 21 | Install the =elvish-completions= package using [[https://elvish.io/ref/epm.html][epm]]: 22 | 23 | #+begin_src elvish 24 | use epm 25 | epm:install github.com/zzamboni/elvish-completions 26 | #+end_src 27 | 28 | In your =rc.elv=, load this module: 29 | 30 | #+begin_src elvish 31 | use github.com/zzamboni/elvish-completions/git 32 | #+end_src 33 | 34 | *Note:* This package depends on [[https://github.com/muesli/elvish-libs][@muesli's git library]]. If you use =epm= as described above, this package will be installed automatically, but if you clone this repository by hand, you need to install it by hand. 35 | 36 | Now you can type =git=, press ~Tab~ and see the corresponding completions. All =git= commands are automatically completed with their options (automatically extracted from their help messages). Some commands get more specific completions, including =add=, =push=, =checkout=, =diff= and a few others. Git aliases are automatically detected as well. Aliases which point to a single =git= command are automatically completed like the original command. 37 | 38 | Several components are colorized, you can configure the styles by setting these variables (default values shown): 39 | 40 | #+begin_src elvish :noweb-ref git-completion-styles 41 | var modified-style = yellow 42 | var untracked-style = red 43 | var tracked-style = $nil 44 | var branch-style = blue 45 | var remote-style = cyan 46 | var unmerged-style = magenta 47 | #+end_src 48 | 49 | You can change which command is used instead of =git= by assigning it to =$git:git-command=. The command assigned needs to understand at least the same commands and options as =git=. One example of such command is [[https://hub.github.com/][hub]]. You can also assign functions (for example, a wrapper function around =git=). 50 | 51 | #+begin_src elvish :noweb-ref git-command 52 | var git-command = git 53 | #+end_src 54 | 55 | * Implementation 56 | :PROPERTIES: 57 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 58 | :header-args: :mkdirp yes :comments no 59 | :END: 60 | 61 | ** Libraries and global variables 62 | 63 | We first load a number of libraries, including =comp=, the Elvish completion framework. 64 | 65 | #+begin_src elvish 66 | use ./comp 67 | use re 68 | use str 69 | use github.com/muesli/elvish-libs/git 70 | use github.com/zzamboni/elvish-modules/util 71 | #+end_src 72 | 73 | This is where the big completion-definition map will get progressively built. 74 | 75 | #+begin_src elvish 76 | var completions = [&] 77 | #+end_src 78 | 79 | We store the output of =git:status= in a global variable to make it easier to access by the different completion functions. 80 | 81 | #+begin_src elvish 82 | var status = [&] 83 | #+end_src 84 | 85 | Here we will store the completer function (for easier access and for testing). 86 | 87 | #+begin_src elvish 88 | var git-arg-completer = { } 89 | #+end_src 90 | 91 | ** Configuration variables 92 | 93 | The =git-command= variable contains the command or function to run instead of =git= (can be assigned to =hub=, for example, or any wrapper function you want to use, as long as it accepts the same options and commands as =git=). 94 | 95 | #+begin_src elvish :noweb yes 96 | <> 97 | #+end_src 98 | 99 | The =$*-style= variables contains the style (as per =styled=) to use for different completion components the completion menu. Set to =''= (an empty string) to show in the normal style. 100 | 101 | #+begin_src elvish :noweb yes 102 | var modified-style = yellow 103 | var untracked-style = red 104 | var tracked-style = $nil 105 | var branch-style = blue 106 | var remote-style = cyan 107 | var unmerged-style = magenta 108 | #+end_src 109 | 110 | ** Utility functions 111 | 112 | The =-run-git= function executes a git-like command, with the given arguments. =$git-command= can be a single command, a multi-word command or a function and still be executed correctly. We cannot simply run =$gitcmd $@rest= because Elvish always interprets the first token (the head) to be the command. One example of a multi-word =$gitcmd= is ="vcsh "=, after which any git subcommand is valid. 113 | 114 | (please note: =$git-command= is only used for command executed from within this module, not from the =muesli/git= module which is used for some pieces of information, so the integration is not yet perfect. But for most commands it should work - for example if you use =hub= instead of =git=. 115 | 116 | #+begin_src elvish 117 | fn -run-git {|@rest| 118 | var gitcmds = [$git-command] 119 | if (eq (kind-of $git-command) string) { 120 | set gitcmds = [(re:split " " $git-command)] 121 | } 122 | var cmd = $gitcmds[0] 123 | if (eq (kind-of $cmd) string) { 124 | set cmd = (external $cmd) 125 | } 126 | $cmd (all $gitcmds[1..]) $@rest 127 | } 128 | #+end_src 129 | 130 | The =-git-opts= function receives an optional git command, runs =git [command] -h= and parses the output to extract the command line options. The parsing is done with =comp:extract-opts=, but we pre-process the output to join options whose descriptions appear in the next line. 131 | 132 | #+begin_src elvish 133 | fn -git-opts {|@cmd| 134 | set _ = ?(-run-git $@cmd -h 2>&1) | drop 1 | if (eq $cmd []) { 135 | comp:extract-opts &fold=$true ®ex='--(\w[\w-]*)' ®ex-map=[&long=1] 136 | } else { 137 | comp:extract-opts &fold=$true 138 | } 139 | } 140 | #+end_src 141 | 142 | We define the functions that return different possible values used in the completions. Some of these functions assume that =$status= contains already the output from =git:status=, which gets executed as the pre-hook of the git completer function below. 143 | 144 | #+begin_src elvish 145 | fn MODIFIED { all $status[local-modified] | comp:decorate &style=$modified-style } 146 | fn UNTRACKED { all $status[untracked] | comp:decorate &style=$untracked-style } 147 | fn UNMERGED { all $status[unmerged] | comp:decorate &style=$unmerged-style } 148 | fn MOD-UNTRACKED { MODIFIED; UNTRACKED } 149 | fn TRACKED { set _ = ?(-run-git ls-files 2>&-) | comp:decorate &style=$tracked-style } 150 | fn BRANCHES {|&all=$false &branch=$true| 151 | var -allarg = [] 152 | var -branch = '' 153 | if $all { set -allarg = ['--all'] } 154 | if $branch { set -branch = ' (branch)' } 155 | set _ = ?(-run-git branch --list (all $-allarg) --format '%(refname:short)' 2>&- | 156 | comp:decorate &display-suffix=$-branch &style=$branch-style) 157 | } 158 | fn REMOTE-BRANCHES { 159 | set _ = ?(-run-git branch --list --remote --format '%(refname:short)' 2>&- | 160 | grep -v HEAD | 161 | each {|branch| re:replace 'origin/' '' $branch } | 162 | comp:decorate &display-suffix=' (remote branch)' &style=$branch-style) 163 | } 164 | fn REMOTES { set _ = ?(-run-git remote 2>&- | comp:decorate &display-suffix=' (remote)' &style=$remote-style ) } 165 | fn STASHES { set _ = ?(-run-git stash list 2>&- | each {|l| put [(re:split : $l)][0] } ) } 166 | #+end_src 167 | 168 | ** Initialization of completion definitions 169 | 170 | =$git:git-completions= contains the specialized completions for some git commands. Each sequence is a list of functions which return the possible completions at that point in the command. The =...= as a last element in some of them indicates that the last completion function is repeated for all further argument positions. The completion can also be a string, in which case it means an alias for some other command. 171 | 172 | #+begin_src elvish 173 | var git-completions = [ 174 | &add= [ {|stem| MOD-UNTRACKED; UNMERGED; comp:dirs $stem } ... ] 175 | &stage= add 176 | &checkout= [ { MODIFIED; BRANCHES } ... ] 177 | &switch= [ { $BRANCHES~ &branch=$false; REMOTE-BRANCHES } ] 178 | &mv= [ {|stem| TRACKED; comp:dirs $stem } ... ] 179 | &rm= [ {|stem| TRACKED; comp:dirs $stem } ... ] 180 | &diff= [ { MODIFIED; BRANCHES } ... ] 181 | &push= [ $REMOTES~ $BRANCHES~ ] 182 | &pull= [ $REMOTES~ { BRANCHES &all } ] 183 | &merge= [ $BRANCHES~ ... ] 184 | &init= [ {|stem| put "."; comp:dirs $stem } ] 185 | &branch= [ $BRANCHES~ ... ] 186 | &rebase= [ { $BRANCHES~ &all } ... ] 187 | &cherry= [ { $BRANCHES~ &all } $BRANCHES~ $BRANCHES~ ] 188 | &cherry-pick= [ { $BRANCHES~ &all } ... ] 189 | &stash= [ 190 | &list= (comp:sequence []) 191 | &clear= (comp:sequence []) 192 | &show= (comp:sequence [ $STASHES~ ]) 193 | &drop= (comp:sequence &opts=[[&short=q &long=quiet]] [ $STASHES~ ]) 194 | &pop= (comp:sequence &opts=[[&short=q &long=quiet] [&long=index]] [ $STASHES~ ]) 195 | &apply= pop 196 | &branch= (comp:sequence [ [] $STASHES~ ]) 197 | &push= (comp:sequence [ $comp:files~ ... ] &opts=[ 198 | [&short=p &long=patch] 199 | [&short=k &long=keep-index] [&long=no-keep-index] 200 | [&short=q &long=quiet] 201 | [&short=u &long=include-untracked] 202 | [&short=a &long=all] 203 | [&short=m &long=message &arg-required] 204 | ]) 205 | &create= (comp:sequence []) 206 | &store= (comp:sequence [ $BRANCHES~ ] &opts=[ 207 | [&short=m &long=message &arg-required] 208 | [&short=q &long=quiet] 209 | ]) 210 | ] 211 | ] 212 | #+end_src 213 | 214 | In the =git:init= function we initialize the =$completions= map with the necessary data structure for =comp:subcommands= to provide the completions. We extract as much information as possible automatically from =git= itself. 215 | 216 | #+begin_src elvish :noweb yes 217 | fn init { 218 | set completions = [&] 219 | -run-git help -a --no-verbose | re:awk {|line @f| if (re:match '^ [a-z]' $line) { put $@f } } | each {|c| 220 | var seq = [ $comp:files~ ... ] 221 | if (has-key $git-completions $c) { 222 | set seq = $git-completions[$c] 223 | } 224 | if (eq (kind-of $seq) string) { 225 | set completions[$c] = $seq 226 | } elif (eq (kind-of $seq) map) { 227 | set completions[$c] = (comp:subcommands $seq) 228 | } else { 229 | set completions[$c] = (comp:sequence $seq &opts={ -git-opts $c }) 230 | } 231 | } 232 | -run-git config --list | each {|l| re:find '^alias\.([^=]+)=(.*)$' $l } | each {|m| 233 | var alias target = $m[groups][1 2][text] 234 | if (has-key $completions $target) { 235 | set completions[$alias] = $target 236 | } else { 237 | set completions[$alias] = (comp:sequence []) 238 | } 239 | } 240 | set git-arg-completer = (comp:subcommands $completions ^ 241 | &pre-hook={|@_| set status = (git:status) } &opts={ -git-opts } 242 | ) 243 | set edit:completion:arg-completer[git] = $git-arg-completer 244 | } 245 | #+end_src 246 | 247 | Next , we fetch the list of valid git commands from the output of =git help -a=, and store the corresponding completion sequences in =$completions=. All of them are configured to produce completions for their options, as extracted by the =-git-opts= function. Commands that have corresponding definitions in =$git-completions= get them, otherwise they get the generic filename completer. 248 | 249 | #+begin_src elvish :noweb-ref init-git-commands :tangle no 250 | -run-git help -a --no-verbose | re:awk [line @f]{ if (re:match '^ [a-z]' $line) { put $@f } } | each [c]{ 251 | seq = [ $comp:files~ ... ] 252 | if (has-key $git-completions $c) { 253 | seq = $git-completions[$c] 254 | } 255 | if (eq (kind-of $seq) string) { 256 | completions[$c] = $seq 257 | } elif (eq (kind-of $seq) map) { 258 | completions[$c] = (comp:subcommands $seq) 259 | } else { 260 | completions[$c] = (comp:sequence $seq &opts={ -git-opts $c }) 261 | } 262 | } 263 | #+end_src 264 | 265 | Next, we parse the defined aliases from the output of =git config --list=. We store the aliases in =completions= as well, but we check if an alias points to another valid command. In this case, we store the name of the target command as its value, which =comp:expand= interprets as "use the completions from the target command". If an alias does not expand to another existing command, we set up its completions as empty. 266 | 267 | #+begin_src elvish :noweb-ref init-git-aliases :tangle no 268 | -run-git config --list | each [l]{ re:find '^alias\.([^=]+)=(.*)$' $l } | each [m]{ 269 | alias target = $m[groups][1 2][text] 270 | if (has-key $completions $target) { 271 | completions[$alias] = $target 272 | } else { 273 | completions[$alias] = (comp:sequence []) 274 | } 275 | } 276 | #+end_src 277 | 278 | We setup the completer by assigning the function to the corresponding element of =$edit:completion:arg-completer=. 279 | 280 | #+begin_src elvish :noweb-ref setup-completer :tangle no 281 | git-arg-completer = (comp:subcommands $completions ^ 282 | &pre-hook=[@_]{ status = (git:status) } &opts={ -git-opts } 283 | ) 284 | edit:completion:arg-completer[git] = $git-arg-completer 285 | #+end_src 286 | 287 | We run =init= by default on load, although it can be re-run if you change any configuration variables (most notably =git:git-command=). 288 | 289 | #+begin_src elvish 290 | init 291 | #+end_src 292 | 293 | * Test suite 294 | :PROPERTIES: 295 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) "_test.elv") 296 | :header-args: :mkdirp yes :comments no 297 | :END: 298 | 299 | #+begin_src elvish 300 | use github.com/zzamboni/elvish-completions/git 301 | use github.com/zzamboni/elvish-modules/test 302 | 303 | var cmds = ($git:git-arg-completer git '') 304 | 305 | (test:set github.com/zzamboni/elvish-completions/git 306 | (test:set "common top-level commands" 307 | (test:check { has-value $cmds add }) 308 | (test:check { has-value $cmds checkout }) 309 | (test:check { has-value $cmds commit }) 310 | ) 311 | ) 312 | #+end_src 313 | -------------------------------------------------------------------------------- /git_test.elv: -------------------------------------------------------------------------------- 1 | use github.com/zzamboni/elvish-completions/git 2 | use github.com/zzamboni/elvish-modules/test 3 | 4 | var cmds = ($git:git-arg-completer git '') 5 | 6 | (test:set github.com/zzamboni/elvish-completions/git 7 | (test:set "common top-level commands" 8 | (test:check { has-value $cmds add }) 9 | (test:check { has-value $cmds checkout }) 10 | (test:check { has-value $cmds commit }) 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "zzamboni's Elvish completion definitions", 3 | "maintainers": ["Diego Zamboni "], 4 | "dependencies": ["github.com/zzamboni/elvish-modules", "github.com/muesli/elvish-libs"] 5 | } 6 | -------------------------------------------------------------------------------- /ssh.elv: -------------------------------------------------------------------------------- 1 | use ./comp 2 | use re 3 | use str 4 | 5 | var config-files = [ ~/.ssh/config /etc/ssh/ssh_config /etc/ssh_config ] 6 | 7 | fn -ssh-hosts { 8 | var hosts = [&] 9 | all $config-files | each {|file| 10 | set _ = ?(cat $file 2>&-) | re:awk {|_ @f| 11 | if (re:match '^(?i)host$' $f[0]) { 12 | all $f[1..] | each {|p| 13 | if (not (re:match '[*?!]' $p)) { 14 | set hosts[$p] = $true 15 | }}}}} 16 | keys $hosts 17 | } 18 | 19 | var -ssh-options = [] 20 | fn -gen-ssh-options { 21 | if (eq $-ssh-options []) { 22 | set -ssh-options = [( 23 | set _ = ?(cat (man -w ssh_config 2>&-)) | 24 | re:awk {|l @f| if (re:match '^\.It Cm' $l) { put $f[2] } } | 25 | comp:decorate &suffix='=' 26 | )] 27 | } 28 | all $-ssh-options 29 | } 30 | 31 | var ssh-opts = [ 32 | [ &short= o 33 | &arg-required= $true 34 | &arg-completer= $-gen-ssh-options~ 35 | ] 36 | [ &short= i 37 | &long= inventory 38 | &arg-required= $true 39 | &arg-completer= $comp:files~ 40 | ] 41 | ] 42 | 43 | fn -ssh-host-completions {|arg &suffix=''| 44 | var user-given = (str:join '' [(re:find '^(.*@)' $arg)[groups][1][text]]) 45 | -ssh-hosts | each {|host| put $user-given$host } | comp:decorate &suffix=$suffix 46 | } 47 | 48 | set edit:completion:arg-completer[ssh] = (comp:sequence &opts=$ssh-opts [$-ssh-host-completions~]) 49 | set edit:completion:arg-completer[sftp] = (comp:sequence &opts=$ssh-opts [$-ssh-host-completions~]) 50 | set edit:completion:arg-completer[scp] = (comp:sequence &opts=$ssh-opts [ 51 | {|arg| 52 | -ssh-host-completions &suffix=":" $arg 53 | edit:complete-filename $arg 54 | } 55 | ... 56 | ]) 57 | -------------------------------------------------------------------------------- /ssh.org: -------------------------------------------------------------------------------- 1 | #+title: Elvish completions for ssh 2 | #+author: Diego Zamboni 3 | #+email: diego@zzamboni.org 4 | 5 | #+name: module-summary 6 | Completions for =ssh=, =scp= and =sftp=. 7 | 8 | This file is written in [[https://leanpub.com/lit-config][literate programming style]], to make it easy to explain. See [[file:ssh.elv][ssh.elv]] for the generated file. 9 | 10 | * Table of Contents :TOC:noexport: 11 | - [[#usage][Usage]] 12 | - [[#implementation][Implementation]] 13 | - [[#libraries-and-global-variables][Libraries and global variables]] 14 | - [[#initialization][Initialization]] 15 | 16 | * Usage 17 | 18 | Install the =elvish-completions= package using [[https://elvish.io/ref/epm.html][epm]]: 19 | 20 | #+begin_src elvish 21 | use epm 22 | epm:install github.com/zzamboni/elvish-completions 23 | #+end_src 24 | 25 | In your =rc.elv=, load this module: 26 | 27 | #+begin_src elvish 28 | use github.com/zzamboni/elvish-completions/ssh 29 | #+end_src 30 | 31 | Hosts for the completions will be read from the files listed in the =$config-files= variable. Here is its default value: 32 | 33 | #+begin_src elvish :noweb-ref config-files 34 | config-files = [ ~/.ssh/config /etc/ssh/ssh_config /etc/ssh_config ] 35 | #+end_src 36 | 37 | All hosts listed in =Host= sections of the config files will be provided for completion. Patterns including any metacharacters (=*=, =?= and =!=) will not be shown. 38 | 39 | #+begin_example 40 | [~]─> ssh 41 | COMPLETING argument 42 | host1 host2 host3 43 | #+end_example 44 | 45 | Completions are also provided for config options. If you type =-o= and press ~Tab~, a list of valid configuration options will be provided. The valid configuration options are automatically extracted from the =ssh_config= man page, if it's available. 46 | 47 | #+begin_example 48 | [~]─> ssh -o 49 | COMPLETING argument _ 50 | AddKeysToAgent= ControlPath= HostKeyAlias= NoHostAuthenticationForLocalhost= ServerAliveCountMax= 51 | AddressFamily= ControlPersist= HostName= NumberOfPasswordPrompts= ServerAliveInterval= 52 | BatchMode= DynamicForward= HostbasedAuthentication= PKCS11Provider= StreamLocalBindMask= 53 | ... 54 | #+end_example 55 | 56 | * Implementation 57 | :PROPERTIES: 58 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 59 | :header-args: :mkdirp yes :comments no 60 | :END: 61 | 62 | ** Libraries and global variables 63 | 64 | We first load a number of libraries, including =comp=, the Elvish [[file:comp.org][completion framework]]. 65 | 66 | #+begin_src elvish 67 | use ./comp 68 | use re 69 | use str 70 | #+end_src 71 | 72 | List of config files from which to extract hostnames. 73 | 74 | #+begin_src elvish :noweb yes 75 | var config-files = [ ~/.ssh/config /etc/ssh/ssh_config /etc/ssh_config ] 76 | #+end_src 77 | 78 | ** Initialization 79 | 80 | The =-ssh-hosts= function extracts all hostnames from the files listed in =$config-files=. Nonexistent files in the list are ignored, and only hostnames which do not include glob characters (=*=, =?=, =!=) are returned. 81 | 82 | #+begin_src elvish 83 | fn -ssh-hosts { 84 | var hosts = [&] 85 | all $config-files | each {|file| 86 | set _ = ?(cat $file 2>&-) | re:awk {|_ @f| 87 | if (re:match '^(?i)host$' $f[0]) { 88 | all $f[1..] | each {|p| 89 | if (not (re:match '[*?!]' $p)) { 90 | set hosts[$p] = $true 91 | }}}}} 92 | keys $hosts 93 | } 94 | #+end_src 95 | 96 | We store in =-ssh-options= all the possible configuration options, by parsing them directly from the =ssh_config= man page (if available). These are initialized by the =-gen-ssh-options= on first use to reduce load time, and cached so that any delay is only incurred once. 97 | 98 | #+begin_src elvish 99 | var -ssh-options = [] 100 | fn -gen-ssh-options { 101 | if (eq $-ssh-options []) { 102 | set -ssh-options = [( 103 | set _ = ?(cat (man -w ssh_config 2>&-)) | 104 | re:awk {|l @f| if (re:match '^\.It Cm' $l) { put $f[2] } } | 105 | comp:decorate &suffix='=' 106 | )] 107 | } 108 | all $-ssh-options 109 | } 110 | #+end_src 111 | 112 | The =$ssh-opts= array stores the definitions of command-line options. For now we only complete: 113 | 114 | - =-o= (including completions for its argument) generated with =-gen-ssh-options= defined above 115 | - =-i/--inventory= generated with =comp:files= defined in =comp.elv= 116 | 117 | #+begin_src elvish 118 | var ssh-opts = [ 119 | [ &short= o 120 | &arg-required= $true 121 | &arg-completer= $-gen-ssh-options~ 122 | ] 123 | [ &short= i 124 | &long= inventory 125 | &arg-required= $true 126 | &arg-completer= $comp:files~ 127 | ] 128 | ] 129 | #+end_src 130 | 131 | =-ssh-host-completions= dynamically generates the completion definition for hostnames for ssh-related commands. The hostnames are extracted from the user's ssh config files by the =-ssh-hosts= function defined above. The completions for =ssh= and =scp=, for example, are the same except for the suffix that needs to be added to the hostnames in the completion, so we allow the suffix to be specified as an option. We also allow for a username to be specified at the beginning of the hostname (=user@=), and still generate the completions correctly, so you can type =ssh user@abc= and the corresponding hostnames will be completed. 132 | 133 | #+begin_src elvish 134 | fn -ssh-host-completions {|arg &suffix=''| 135 | var user-given = (str:join '' [(re:find '^(.*@)' $arg)[groups][1][text]]) 136 | -ssh-hosts | each {|host| put $user-given$host } | comp:decorate &suffix=$suffix 137 | } 138 | #+end_src 139 | 140 | We use =-ssh-host-completions= to produce the actual completion definitions for =ssh=, =sftp= and =scp=. For =scp= we also complete local filenames. 141 | 142 | #+begin_src elvish 143 | set edit:completion:arg-completer[ssh] = (comp:sequence &opts=$ssh-opts [$-ssh-host-completions~]) 144 | set edit:completion:arg-completer[sftp] = (comp:sequence &opts=$ssh-opts [$-ssh-host-completions~]) 145 | set edit:completion:arg-completer[scp] = (comp:sequence &opts=$ssh-opts [ 146 | {|arg| 147 | -ssh-host-completions &suffix=":" $arg 148 | edit:complete-filename $arg 149 | } 150 | ... 151 | ]) 152 | #+end_src 153 | -------------------------------------------------------------------------------- /vcsh.elv: -------------------------------------------------------------------------------- 1 | # Completer for vcsh - https://github.com/RichiH/vcsh 2 | # Diego Zamboni 3 | 4 | use ./git 5 | use re 6 | 7 | # Return all elements in $l1 except those who are already in $l2 8 | fn -all-except {|l1 l2| 9 | each {|x| if (not (has-value $l2 $x)) { put $x } } $l1 10 | } 11 | 12 | fn vcsh-completer {|cmd @rest| 13 | var n = (count $rest) 14 | var repos = [(vcsh list)] 15 | if (eq $n 1) { 16 | # Extract valid commands and options from the vcsh help message itself 17 | var cmds = [(vcsh 2>&1 | grep '^ [a-z-]' | grep -v ':$' | awk '{print $1}')] 18 | put $@repos $@cmds 19 | } elif (and (> $n 1) (has-value $repos $rest[0])) { 20 | put (git:git-completer $cmd" "$rest[0] (all $rest[1..])) 21 | } elif (eq $n 2) { 22 | # Subcommand- or option-specific completions 23 | if (eq $rest[0] "-c") { 24 | put (edit:complete-filename $rest[1]) 25 | } elif (re:match "delete|enter|rename|run|upgrade|write-ignore|list-tracked" $rest[0]) { 26 | put $@repos 27 | } elif (eq $rest[0] "list-untracked") { 28 | put $@repos "-a" "-r" 29 | } elif (eq $rest[0] "status") { 30 | put $@repos "--terse" 31 | } 32 | } elif (> $n 2) { 33 | # For more than two arguments, we recurse, removing any options that have been typed already 34 | # Not perfect but it allows completion to work properly after "vcsh status --terse", for example, 35 | # without too much repetition 36 | put (-all-except [(vcsh-completer $cmd (all $rest[0:(- $n 1)]))] $rest[0:(- $n 1)]) 37 | } 38 | } 39 | 40 | set edit:completion:arg-completer[vcsh] = $vcsh-completer~ 41 | -------------------------------------------------------------------------------- /vcsh.org: -------------------------------------------------------------------------------- 1 | #+title: Elvish completions for vcsh 2 | #+author: Diego Zamboni 3 | #+email: diego@zzamboni.org 4 | 5 | #+name: module-summary 6 | Completions for [[https://github.com/RichiH/vcsh][vcsh]]. 7 | 8 | * Implementation 9 | :PROPERTIES: 10 | :header-args:elvish: :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") 11 | :header-args: :mkdirp yes :comments no 12 | :END: 13 | 14 | #+begin_src elvish 15 | # Completer for vcsh - https://github.com/RichiH/vcsh 16 | # Diego Zamboni 17 | 18 | use ./git 19 | use re 20 | 21 | # Return all elements in $l1 except those who are already in $l2 22 | fn -all-except {|l1 l2| 23 | each {|x| if (not (has-value $l2 $x)) { put $x } } $l1 24 | } 25 | 26 | fn vcsh-completer {|cmd @rest| 27 | var n = (count $rest) 28 | var repos = [(vcsh list)] 29 | if (eq $n 1) { 30 | # Extract valid commands and options from the vcsh help message itself 31 | var cmds = [(vcsh 2>&1 | grep '^ [a-z-]' | grep -v ':$' | awk '{print $1}')] 32 | put $@repos $@cmds 33 | } elif (and (> $n 1) (has-value $repos $rest[0])) { 34 | put (git:git-completer $cmd" "$rest[0] (all $rest[1..])) 35 | } elif (eq $n 2) { 36 | # Subcommand- or option-specific completions 37 | if (eq $rest[0] "-c") { 38 | put (edit:complete-filename $rest[1]) 39 | } elif (re:match "delete|enter|rename|run|upgrade|write-ignore|list-tracked" $rest[0]) { 40 | put $@repos 41 | } elif (eq $rest[0] "list-untracked") { 42 | put $@repos "-a" "-r" 43 | } elif (eq $rest[0] "status") { 44 | put $@repos "--terse" 45 | } 46 | } elif (> $n 2) { 47 | # For more than two arguments, we recurse, removing any options that have been typed already 48 | # Not perfect but it allows completion to work properly after "vcsh status --terse", for example, 49 | # without too much repetition 50 | put (-all-except [(vcsh-completer $cmd (all $rest[0:(- $n 1)]))] $rest[0:(- $n 1)]) 51 | } 52 | } 53 | 54 | set edit:completion:arg-completer[vcsh] = $vcsh-completer~ 55 | #+end_src 56 | --------------------------------------------------------------------------------