Terminal History Auto Suggestions As You Type With Oh My Zsh

.

Oh-My-Zsh is a framework for Zsh, the Z shell. It is an efficiency boom for anyone that works in the terminal. There are a few auto suggest libraries that when combined can give you a fantastic suggestion to what you want to type to save you time and increase your efficiency tenfold. In our instagram post we had some questions about how to get autocomplete like we show in the screencast:

insta-questions

Well…let’s find out!

#Goals

This is what we’re aiming for. As seen below the suggestions show up in a light blue color and those suggestions are based on commands previously typed in that directory. If there are no matches to what has been previously typed in that directory, it then suggests commands that have been previously typed on this computer at any point in any directory.

exxample gif

As a bonus, we also have the commmands “show_local_history” with a number to show the last number of X commands used in this directory. “search_local_history” will then use all the commands typed in this present working directory (PWD) to search based on the string we’re looking for.

What is this based on?

This leverages a few excellent libraries which are very useful just by themselves. zsh-autosuggestions provide us with “Fish like autosuggestions for zsh” based on the command history. It accepts a suggestion strategy that you can specify to guide it how to exactly suggest what to autocomplete with. What we’re doing is overriding the strategy and providing our own custom strategy.

Extending the suggestions

To be able to just use the present working directories history we have to track that in a different way. To do that, we leverage zsh-histdb which provides us with a SQLite database to track our commands and store them in the database. If we were to look at the schema zsh-histdb provides we would see that it has three tables:

With some SQL querying to we can use all of that data to accomplish directory specific suggestions with a fallback to all directory suggestions. Ok, let’s roll up our sleeves and get some things installed.

Installing Zsh & Oh My Zsh

This is a good guide to installing Zsh and Oh My Zsh as is the official oh-my-zsh directions.

First, we need to install Zsh since oh my zsh is a framework that sits on top of Zsh. We’re going to use homebrew which you can install by running this in your terminal

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Once that finishes, we’ll install zsh using homebrew

brew install zsh

And finally, let’s install oh my zsh

sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

If you get an error that says:

invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

You need to install xcode’s devleoper tools before you can install oh-my-zsh. Run this in your terminal:

xcode-select --install

Let’s make sure zsh is our default shell:

chsh -s $(which zsh)

Installing Autosuggestions & histdb

Next, we need to install zsh-autosuggestions. We’re going to follow their official guide

git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

Next, we need to list that as a plugin in our .zshrc. So we’ll need to edit our .zshrc

nano ~/.zshrc

Let’s add it as a plugin by adding or editing this line. If you have more than one plugin they need to be separated by a space. Be sure to check your .zshrc file as there might already be a plugins line there. If so, just add zsh-autosuggestions to the plugins with a space separating any other ones there already.

plugins=(zsh-autosuggestions)

- OR if you have multiple -

plugins=(git zsh-syntax-highlighting zsh-autosuggestions)

One thing I like to do since I use a lot of plugins is to have all the plugins listed in a file and just include that file, like so

plugins=($(<~/zshes/plugins.txt))

The contents of zshes/plugins.txt is like this:

aws
bower
brew
docker
git
git-extras
history
jira
jsontools
last-working-dir
npm
osx
terminalapp
vi-mode
zsh-256color
zsh-autosuggestions

Next we need to install sqlite3 since zsh-histdb has that as a dependency. Homebrew to the rescue

brew install sqlite3

We’re now going to follow this guide for the install.

mkdir -p $HOME/.oh-my-zsh/custom/plugins/
git clone https://github.com/larkery/zsh-histdb $HOME/.oh-my-zsh/custom/plugins/zsh-histdb

We then need to edit our .zshrc again and add this to it:

source $HOME/.oh-my-zsh/custom/plugins/zsh-histdb/sqlite-history.zsh
autoload -Uz add-zsh-hook
add-zsh-hook precmd histdb-update-outcome

Restart your terminal and you should now see auto suggestions working!

Customizing Histdb

We now have auto suggestions and zsh-histdb working, but we still want to customize it so that we get suggestions specific to our directory. To do that we need to tell zsh-autosuggestions to use our history sqlite database instead of the regular zsh history. To do that we can take advantage of the ZSH_AUTOSUGGEST_STRATEGY hook and provide a query and list of suggestions to pass along to zsh-autosuggestions.

Somewhere in your .zshrc file you can copy the below to edit how zsh-autosuggestions sends back suggestions. zsh-histdb has some queries you can use.

This will find the most frequently issued command issued exactly in this directory, or if there are no matches it will find the most frequently issued command in any directory.

_zsh_autosuggest_strategy_histdb_top() {
    local query="select commands.argv from
history left join commands on history.command_id = commands.rowid
left join places on history.place_id = places.rowid
where commands.argv LIKE '$(sql_escape $1)%'
group by commands.argv
order by places.dir != '$(sql_escape $PWD)', count(*) desc limit 1"
    suggestion=$(_histdb_query "$query")
}

ZSH_AUTOSUGGEST_STRATEGY=histdb_top

This query is fine, however, the issue I have is that it doesn’t order the results by the most recently used. So I have added another to use query so that it orders by what was also most recently used in that directory:

# Query to pull in the most recent command if anything was found similar
# in that directory. Otherwise pull in the most recent command used anywhere
# Give back the command that was used most recently
_zsh_autosuggest_strategy_histdb_top_fallback() {
    local query="
    select commands.argv from
    history left join commands on history.command_id = commands.rowid
    left join places on history.place_id = places.rowid
    where places.dir LIKE 
        case when exists(select commands.argv from history 
        left join commands on history.command_id = commands.rowid 
        left join places on history.place_id = places.rowid 
        where places.dir LIKE '$(sql_escape $PWD)%' 
        AND commands.argv LIKE '$(sql_escape $1)%') 
            then '$(sql_escape $PWD)%' 
            else '%'
            end
    and commands.argv LIKE '$(sql_escape $1)%'
    group by commands.argv
    order by places.dir LIKE '$(sql_escape $PWD)%' desc,
        history.start_time desc
    limit 1"
    suggestion=$(_histdb_query "$query")
}

ZSH_AUTOSUGGEST_STRATEGY=histdb_top_fallback

Experiment with both and see what fits your needs best! I’m open to any suggestions to improve the above query as well.

Bonus Commands

Let’s add in a command to show the local history only. So once again we’ll edit our .zshrc:

show_local_history() {
    limit="${1:-10}"
    local query="
        select history.start_time, commands.argv 
        from history left join commands on history.command_id = commands.rowid
        left join places on history.place_id = places.rowid
        where places.dir LIKE '$(sql_escape $PWD)%'
        order by history.start_time desc
        limit $limit
    "
    results=$(_histdb_query "$query")
    echo "$results"
}

This command defaults to showing 10 commands, but we could also pass in a number to get a desired number of results:

# will show 10 results
show_local_history 

# will show 50 results
show_local_history 50

We also want to be able to search that history. I personally use ack but you can also use grep for this. We’ll build on our search local history command pass a limit of 100 and come out with this:

# Grep
search_local_history() {
    show_local_history 100 | grep "$1"
}

# if you use ack
search_local_history() {
    show_local_history 100 | ack "$1"
}

Phew! That should be it! Feel free to tweet at us or DM us on instagram with questions!