Article posted on: 2021-01-23 16:45
Last edited on: 2022-02-04 14:51
Written by: Sylvain Gauthier
In my last post I talked about dmenu
and presented
a few of its applications that I use regularly.
Here, I will present a tool me and a friend
designed as a dmenu
alternative for the terminal. Alternative is not the right
word as its use cases don’t really overlap with dmenu
’s, but it’s heavily
inspired by it.
So a few years ago, me and fxc
were starting to implement an alternative to
traditional web browsing experience, called netb1
, that will probably deserve
its own post. Let’s just say the idea was very similar to that of weboobs
. One
of the goal was to browse youtube in the terminal. You would type something like
net search @youtube <query>
And it would print a list of URLs and corresponding titles of videos, separated by a space, one entry per line. My idea was then a system that would allow the user to select a title without cluttering the view with all the URLs, which in the case of Youtube, really don’t bring any information to the user.
In pure UNIX fashion, I would then need a program that would read stdin
, cut
each line at the first space, display the second part, and output the first part
of the line selected. So with the previous example, you would feed the output of
the net
query into that program, it would show only the title of the videos,
allow the user to select one, and would output the corresponding URL.
I hacked a few lines of bash together, using a bash utility developed by fxc to
abstract over shell’s escape codes to draw a proper interactive list, and that’s
how choice
was made.
Then, fxc took over the project, rewrote it in C for efficiency and added some cool features. You can look at the project page on our website here.
The performance increase when rewriting it in C was so dramatic that we are essentially able to filter tens of millions of entries in real time, refreshing the list at each character stroke. In fact, it’s more likely that your RAM will run out to store the entries in memory before the refresh rate gets below usability level.
For now it’s only guaranteed to work on linux and most likely *BSD, it can run on Mac with some minor patches that haven’t found their way to the mainstream branch yet. To install on linux systems, simply type:
git clone https://pedantic.software/git/choice.git
cd choice
sudo make install
Now that choice
is introduced, let’s take a look at a few of its applications,
that I use on a regular basis and substantially improve my productivity.
This is both the most trivial use of choice
and the one that I use the most.
Essentially, you type
g <query>
And it will grep query
recursively in the current directory, display each
result on a single line in the form <file> <line>: <matched string>
and let
the user filter and select the entries. It then opens vim
on the selected file
at the correct line.
Here is the small script to do this, that you can just append to your bashrc
once you’ve installed choice
:
g() {
res="$(grep -rnIH --exclude "tags" --exclude-dir ".git" "$*" | sed 's/:/ /' | choice -s ":" -d $'\e[34m%k\e[39m: %v')"
test -z "$res" && return 0
read -r file line <<< "$res"
test -f "$file" && vim "$file" +"$line"
}
This is a tiny vim plugin that I hacked together quickly, to leverage the
convenience of g
from within vim and boy did it deliver. It literally changed
my life to navigate huge codebases. Using ctags
to find definitions, and this
to find usages of functions or variables is a very nice and bloat-free way to
go.
To use it in vim, create ~/.vim/plugin/chope.vim
and put the following inside:
function! chope#grep(str)
execute 'normal! :silent !grep -rnIH "' . a:str . '" | sed ''s/:/ /'' | choice -s ":" -d $''\e[34m\%k\e[39m: \%v'' > /tmp/vimopen' . "\<CR>"
let fileCmd = system('cat /tmp/vimopen')
if empty(fileCmd)
execute('redraw!')
echohl WarningMsg | echo "No match found" | echohl None
return 0
endif
let fileName = split(fileCmd, " ")[0]
let lineNum = split(fileCmd, " ")[1]
if filereadable(fileName)
execute "edit +" . lineNum . " " . fileName
endif
execute('redraw!')
endfunction
function! chope#grepthis()
call chope#grep(expand("<cword>"))
endfunction
Then in your vimrc
, map the shortcut of your choice to :call chope#grepthis()<CR>
.
For example, I use the leader
key + g, so I have in my vimrc
:
nnoremap <leader>g :call chope#grepthis()<CR>
Same thing as above except instead of grep
it uses find
to look for
file names matching your query.
f() {
if [ "$#" == "0" ] ; then
FILE="$(find -type f | choice)"
else
FILE="$(find -name "$1" -type f | choice)"
fi
if [ "$?" == "2" ] ; then
printf "User cancelled\n"
return 0
fi
if [ -f "$FILE" ] ; then
$EDITOR "$FILE"
else
printf "No such file: $FILE\n"
fi
}
Bash users are probably aware that hitting Ctrl-R
while typing a command will
launch a reverse search with auto-completion. The following snippet, to append
to your bashrc
, will use choice
to prompt you with all of your history and a
pre-filled search query with whatever you already typed in the terminal:
bind -x '"\C-r": history|tac|sed "s/^ *[0-9]* *//"|(printf "\\x1b\\x5b\\x34\\x7e\\x15";choice -s "" -r "%k" -S "$READLINE_LINE")|perl -e "open(my \$f,\"<\",\"/dev/tty\");while(<>){ioctl(\$f,0x5412,\$_)for split \"\"}"'
Filter commits, git show
selection, then goes back to commit list and loops
until user presses Ctrl-C
. Very useful to quickly browse some commits from
somebody in particular, or finding which commit added a particular feature by
filtering quickly the commit messages.
sgit() {
while true ; do
COMMIT="$(git log --pretty=format:"%h %ai %an %x09%s" | choice -d $'\e[34m%k\e[39m: %v')"
if [ -n "$COMMIT" ] ; then
git show "$COMMIT" --color=always | less -R
else
return 0
fi
done
}
Again, choice
is so fast that you can almost instantly filter commits even in
huge projects like linux.
traf
is a small TODO manager that uses a simple file structure to store and
classify items, and of course choice
for listing / selecting entries. Check it
out here.
A simple bash function that feeds the content of the current directory to
choice
, then based on the selection:
rifle
if the selected entry is a file, then
comes back to the curent list once the program terminates.You can even navigate to the parent directory without the use for any special
case, since ..
is also part of the output of the find
command.
ccd() {
while true ; do
SEL="$(find "." -maxdepth 1 | sort | sed '1 a ..' | choice -s "")"
test "$?" == 2 && break
test "$SEL" = "." && break
if [ -d "$SEL" ] ; then
cd "$SEL"
elif [ -f "$SEL" ] ; then
$OPENER "$SEL"
fi
done
}
See main article here
A tag browser using ctags
tag files, will look for a tags
file in any parent
directory, list them in choice
and open the file containing the selected file
with whatever is in the $EDITOR
environment variable, with a pre-filled search
pattern using Vim’s syntax (+/blah/
).
Typing $ t <search>
will initialize the search pattern in choice
to
<search>
.
t() {
cur="$(pwd)"
while [ ! -f "$cur/tags" ] ; do
if [ -z "$cur" ] ; then
printf "no tag file found in any parent directory\n"
return 1
fi
cur="${cur%/*}"
done
sel="$(sed -n 's:\(^[^\t]*\)\t\([^\t]*\)\t\(/.*/;"\)\t\([^\t]*\).*:\2 \3\t\1 \4 [\2]:p' < "$cur/tags" | choice -s ' ' -S "$1")"
if [ -z "$sel" ] ; then
printf "User cancelled\n"
return 1
fi
read -r file pat <<< "$sel"
$EDITOR "$cur"/$file "+$pat"
}
By cloning this. f
and g
are in
the sh
directory, sgit
, t
and ccd
are in sh/source.sh
, etc.
That’s pretty much it for this post. As you can see, choice
is not a
replacement for dmenu
because its applications don’t really overlap. It’s more
terminal oriented, to increase productivity and smooth your workflow when coding
or doing sysadmin stuff.
Hopefully it inspires you to write some neat hacks with it as well!