Terminal History Auto Suggestions As You Type With Oh My Zsh
14 April 2019
.
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:
Well…let’s find out!
Our favorite resources
#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.
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:
- commands: which provides us with an argv column
- places: which provides us with a host (computer host) and a dir which is the present working directory that command was used in
- history: which provides us with a session id, a command_id which links with the commands table, a place_id which links with the places table, an exit_status, a start_time, and a duration
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!