Building a basic command palette for Bash

Dom Hastings
Sep 7, 2018 · 6 min read

I’ve been using tmux for a while at work, and in particular nested tmux sessions, one for each server I use regularly, handled via a main session on a jump host. There are around 20 different servers I connect to for various tasks, and navigating between them all via standard methods, was a little tricky. I thought of the idea (which, of course, wasn’t a new thought) of having a fuzzy find or command palette so to get to box4, I could type x4<Enter>.

I had a look for anything that existed already and found marker and fzf which seemed really promising. Unfortunately, the servers I’m using didn’t meet the requirements so I wondered how tricky making something like this would be.

I’m not unfamiliar with building things in bash. I make a lot of command-line tools that my colleagues use, of varying complexity, mostly to simplify repetitive tasks and handle commonly encountered errors, so I thought I’d have a go at making something.

My requirements were:

  1. Have a resultant action and list of options passed into the script
  2. Handle keyboard input (text and arrow keys)
  3. Select an item from the list and call action with the selected item as an argument on Enter
  4. Filter the available items
  5. Update the display in place

I wanted this to be compatible with bash 3.2 and to rely on as few external tools as possible, for speed and to avoid having to cater for any differences in BSD vs. Linux. I’m comfortable with ANSI escape sequences and have used these extensively for various purposes in the past, but thought it would be best for me to use tput to aid portability and compatibility.

With these points in mind, I started to cobble together the features.

Initial build items

1. Have a resultant action and list of options passed into the script

This seems easy enough:

2. Handle keyboard input (text and arrow keys) and filter the list on keypress

This seemed quite easy at first, I’ve taken input in scripts hundreds of times, read -s -n1 key will read one key into the variable $key. If I can use that in a while loop I can easily update the filter with what’s needed:

Running that, $filter shows what I’d expect after each keypress! Now to add some listeners for ↑ and ↓. Finding out how the terminal sees these keys is pretty straightforward, pressing Ctrl+V followed by the key you want to see will show you what the underlying sequence presented, ^[[A^[[B in this case, so that’s Esc, [ and A or B. So updating the while with that sequence:

we get…

Keyboard input isn’t so simple

…no Up or Downs on screen. This is because read returns each item in sequence, so it first gets Esc, then [ and finally A. To cater for this, we listen for each keypress in sequence and store a modifier:

Now we are getting Up and Down as expected, but I can’t use BkSp to delete from $filter, but that’s another straightforward mapping along with the empty string to handle Enter (as read is newline terminated). I’ll also update to add some functionality to track our current index that is changed via ↑ and ↓:

The main functionality is working, we can update a filter and navigate a list of items, lets work on the next item

3. Select an item from the list and call action with the selected item as an argument on Enter

I wanted a way to break a string containing newlines into individual array elements. This isn’t something I’ve had to do before, so I experimented with a few different approaches. To create an array in Bash you define it within ()s:

Splitting directly on newlines wasn’t straightforward though. Take the following example:

this doesn’t work because Bash, by default, breaks on any whitespace. A quick search and I found this helpful post that even supplied a Bash 3.2 solution which works exactly as expected:

So now we can call our script like this:

and if we update the code for the arrow keys to echo ${data[$index]} we get the expected data on each keypress:

Now we can’t see the full list, but when we use the arrow keys it’s possible to select an item and when you press Enter the first argument is called as a command with the selected list item as its argument!

4. Filter the available items

My approach for this isn’t particularly sophisticated. My plan was to intersperse $filter with *s so that it would work in a [[ $x = $y ]] conditional, I could work on ordering the results later on. To add the *s to the filter my approach was to iterate over the string appending each char, followed by * to $filterGlob. My initial approach utilised seq but I’ve since changed to a while loop:

With the glob in place, $filteredData can be updated whenever $filter is changed to contain only items that match $filterGlob. Since I’m calling this on most keypresses, it makes sense to break out into its own function:

With filtering in place, it’s time to render!

As we know we’ll be updating the display in many locations, again we want to create a function. _updateDisplay needs to iterate around $filteredData, echo out the items and indicate which item is currently selected. This does enough:

but it’s not perfect, it completely fills up your terminal buffer every time the screen is redrawn! Enter tput.

Using tput

tput is a way to simplify triggering capabilities within the terminal, positioning the cursor, setting colours, clearing the screen, or even using an alternate screen. An alternate screen is a way to display data without moving the cursor in the underlying terminal window. You might have noticed vim or less do this, well its just a call to tput (or sending the appropriate ANSI escape sequence) away!

tput smcup puts you into an alternate screen. Running this will move you to the top of your screen and running the counterpart tput rmcup will remove you from it. We can call these in our script before our first call to _updateDisplay to ensure we don’t fill up our scrollback with numerous copies of the list we’re displaying. It’s also worth re-using the space available so it’s possible to call tput cup <line> <col> to move to the location specified, tput cup 0 0 will put you at the top of your screen in the first column. What you might notice using this, is that if you move to the top of the screen, the text on the rest of the screen still stays there, but that can be easily cleared with tput ed. There are a lot of capabilities that can be triggered via tput, man tput is a good place to start, and this is a good resource for alternative capabilities (and presumably, maximum compatibility).

A somewhat working version

An annoying bug I encountered with this when trying to use it as a finder for calling git checkout after looking at items in git branch, was that it was trying to call git\ checkout (with the space being part of the command name) which of course didn’t work. I can’t remember the StackOverflow post that helped me here, but the answer is to use an array, so storing the action via action=($1) and calling via ${action[@]} ${filteredData[$index]}. I also wanted to add case-insensitivity to the program as that seemed reasonably key. This was possible using shopt -s nocasematch before the checks in _filterData.

Putting all this together we get a pretty usable base command-palette-like functionality:

There are still many bugs, but this is the basic version I started using. I’ve since added more functionality and features and published the code on GitHub. Feel free to fork, raise issues or give me feedback.


Originally published at dom.hastin.gs on September 7, 2018.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade