Skip to content

Instantly share code, notes, and snippets.

@tyalie
Last active May 7, 2024 18:14
Show Gist options
  • Save tyalie/7e13cfe2ec62d99fa341a07ed12ef7c0 to your computer and use it in GitHub Desktop.
Save tyalie/7e13cfe2ec62d99fa341a07ed12ef7c0 to your computer and use it in GitHub Desktop.
Atuin ZSH up/down arrow integration

Simple script for zsh which gives us a more native up/down arrow behavior for the atuin magial shell history plugin with behavior similar to e.g. zsh-history-substring-search. This is an improved reimplemtation of @Nezteb's gist for the same issue.

Behavior

Note

This assumes default keybindings

First and foremost: The script is aware of multiline buffers. So when going up or down, the script will first try to step through the lines of a multiline buffer, before going to the next history entry.

When pressing up the shell will iteratively go through the previous atuin history and have each result directly in the command buffer / command line. Any text in the initial buffer will be used as a search query.

The down works as expected when going through the results by showing the previous result. If pressed from the initial position, atuins search widget will open.

Configuration

One can configure the search scope by changing the $ATUIN_HISTORY_SEARCH_FILTER_MODE variable. By default it limits the atuin search to the current session, but all standard options for --filter-mode work too.

The script also exposes the atuin-history-up and atuin-history-down widgets, which can be used in keybindings. E.g.

bindkey -M vicmd 'k' atuin-history-up
bindkey -M vicmd 'j' atuin-history-down

Otherwise feel free to change the script. E.g. one can remove the popup of the default atuin interactive window, by removing the zle _atuin_search_widget line in atuin-history-down.

#!/usr/bin/env zsh
##############################################################################
#
# Copyright (c) 2023 Sophie Tyalie
# Copyright (c) 2023 @Nezteb
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the FIZSH nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
##############################################################################
#----------------------------------
# main
#----------------------------------
# global configuration
: ${ATUIN_HISTORY_SEARCH_FILTER_MODE='session'}
# internal variables
typeset -g -i _atuin_history_match_index
typeset -g _atuin_history_search_result
typeset -g _atuin_history_search_query
typeset -g _atuin_history_refresh_display
atuin-history-up() {
_atuin-history-search-begin
# iteratively use the next mechanism to process up if the previous didn't succeed
_atuin-history-up-buffer ||
_atuin-history-up-search
_atuin-history-search-end
}
atuin-history-down() {
_atuin-history-search-begin
# iteratively use the next mechanism to process down if the previous didn't succeed
_atuin-history-down-buffer ||
_atuin-history-down-search ||
zle _atuin_search_widget
_atuin-history-search-end
}
zle -N atuin-history-up
zle -N atuin-history-down
bindkey '\eOA' atuin-history-up
bindkey '\eOB' atuin-history-down
#-----------END main---------------
#----------------------------------
# implementation details
#----------------------------------
_atuin-history-search-begin() {
# assume we will not render anything
_atuin_history_refresh_display=
# If the buffer is the same as the previously displayed history substring
# search result, then just keep stepping through the match list. Otherwise
# start a new search.
if [[ -n $BUFFER && $BUFFER == ${_atuin_history_search_result:-} ]]; then
return;
fi
# Clear the previous result.
_atuin_history_search_result=''
# setup our search query
if [[ -z $BUFFER ]]; then
_atuin_history_search_query=
else
_atuin_history_search_query="$BUFFER"
fi
# reset search index
_atuin_history_match_index=0
}
_atuin-history-search-end() {
# if our index is <= 0 just print the query we started with
if [[ $_atuin_history_match_index -le 0 ]]; then
_atuin_history_search_result="$_atuin_history_search_query"
fi
# draw buffer if requested
if [[ $_atuin_history_refresh_display -eq 1 ]]; then
BUFFER="$_atuin_history_search_result"
CURSOR="${#BUFFER}"
fi
# for debug purposes only
#zle -R "mn: "$_atuin_history_match_index" / qr: $_atuin_history_search_result"
#read -k -t 1 && zle -U $REPLY
}
_atuin-history-up-buffer() {
# attribution to zsh-history-substring-search
#
# Check if the UP arrow was pressed to move the cursor within a multi-line
# buffer. This amounts to three tests:
#
# 1. $#buflines -gt 1.
#
# 2. $CURSOR -ne $#BUFFER.
#
# 3. Check if we are on the first line of the current multi-line buffer.
# If so, pressing UP would amount to leaving the multi-line buffer.
#
# We check this by adding an extra "x" to $LBUFFER, which makes
# sure that xlbuflines is always equal to the number of lines
# until $CURSOR (including the line with the cursor on it).
#
local buflines XLBUFFER xlbuflines
buflines=(${(f)BUFFER})
XLBUFFER=$LBUFFER"x"
xlbuflines=(${(f)XLBUFFER})
if [[ $#buflines -gt 1 && $CURSOR -ne $#BUFFER && $#xlbuflines -ne 1 ]]; then
zle up-line-or-history
return 0
fi
return 1
}
_atuin-history-down-buffer() {
# attribution to zsh-history-substring-search
#
# Check if the DOWN arrow was pressed to move the cursor within a multi-line
# buffer. This amounts to three tests:
#
# 1. $#buflines -gt 1.
#
# 2. $CURSOR -ne $#BUFFER.
#
# 3. Check if we are on the last line of the current multi-line buffer.
# If so, pressing DOWN would amount to leaving the multi-line buffer.
#
# We check this by adding an extra "x" to $RBUFFER, which makes
# sure that xrbuflines is always equal to the number of lines
# from $CURSOR (including the line with the cursor on it).
#
local buflines XRBUFFER xrbuflines
buflines=(${(f)BUFFER})
XRBUFFER="x"$RBUFFER
xrbuflines=(${(f)XRBUFFER})
if [[ $#buflines -gt 1 && $CURSOR -ne $#BUFFER && $#xrbuflines -ne 1 ]]; then
zle down-line-or-history
return 0
fi
return 1
}
_atuin-history-up-search() {
_atuin_history_match_index+=1
offset=$((_atuin_history_match_index-1))
search_result=$(_atuin-history-do-search $offset "$_atuin_history_search_query")
if [[ -z $search_result ]]; then
# if search result is empty, there's no more history
# so just show the previous result
_atuin_history_match_index+=-1
return 1
fi
_atuin_history_refresh_display=1
_atuin_history_search_result="$search_result"
return 0
}
_atuin-history-down-search() {
# we can't go below 0
if [[ $_atuin_history_match_index -le 0 ]]; then
return 1
fi
_atuin_history_refresh_display=1
_atuin_history_match_index+=-1
offset=$((_atuin_history_match_index-1))
_atuin_history_search_result=$(_atuin-history-do-search $offset "$_atuin_history_search_query")
return 0
}
_atuin-history-do-search() {
if [[ $1 -ge 0 ]]; then
atuin search --filter-mode "$ATUIN_HISTORY_SEARCH_FILTER_MODE" --search-mode prefix \
--limit 1 --offset $1 --format "{command}" \
"$2"
fi
}
#------END implementation----------
: ${ATUIN_AUTOCOMPLETE_FILTER_MODE='global'}
_zsh_autosuggest_strategy_atuin() {
suggestion=( $(
atuin search --filter-mode $ATUIN_AUTOCOMPLETE_FILTER_MODE --search-mode prefix --limit 1 --format "{command}" "$1"
) )
}
# use it as e.g., but import it before zsh-autosuggestions
# ZSH_AUTOSUGGEST_STRATEGY=(atuin)
@NyaomiDEV
Copy link

Is it just me or the autosuggest bugs out when you press arrow up / down?

@ThePsyjo
Copy link

ThePsyjo commented Dec 6, 2023

I needed to add additional key-binds for it to work

bindkey '^[[A' atuin-history-up
bindkey '^[[B' atuin-history-down

@tyalie
Copy link
Author

tyalie commented Dec 9, 2023

oh interesting. I removed those from the zsh script I based this on, because I didn't knew what they were for / how to trigger them. An unwise decision in hindsight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment