index

Article posted on: 2021-01-23 16:45
Last edited on: 2022-02-04 14:51
Written by: Sylvain Gauthier

Minimal tips 2: ‘choice’ and its applications

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.

choice

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.

Install choice

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.

g: choice over grep

Demo gif

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

chope.vim: same but in vim

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>

f: choice over find

Demo gif

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
}

History auto-completion with Ctrl-R

Demo gif

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 \"\"}"'

sgit: choice over git log / show

Demo gif

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: minimal TODO list management using choice

Demo gif

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.

ccd: super fast and simple file browser

A simple bash function that feeds the content of the current directory to choice, then based on the selection:

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
}

sneed: minimal RSS client

Demo gif

See main article here

t: choice over ctags

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

Get all of these code snippets at once

By cloning this. f and g are in the sh directory, sgit, t and ccd are in sh/source.sh, etc.

Conclusion

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!