Skip to content

Instantly share code, notes, and snippets.

@oficsu
Last active May 3, 2025 12:15
Show Gist options
  • Save oficsu/57c7aace2d69855066a783b1d3a300eb to your computer and use it in GitHub Desktop.
Save oficsu/57c7aace2d69855066a783b1d3a300eb to your computer and use it in GitHub Desktop.
An alterntive to __fzf_history__ that supports immediate execution, moving around match and showing date with arbitrary HISTTIMEFORMAT
#!/bin/bash
# Demonstration: https://youtu.be/Uj3nmYq5LnQ
# MIT License
#
# Copyright (c) 2024 Ofee Oficsu
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# yes: print warnings and hints on initialization
# no: print only errors
MY_FZF_VERBOSE="${MY_FZF_VERBOSE:-yes}"
# a positive number: count of history search results in lines
MY_FZF_WIDGET_HEIGHT="${MY_FZF_WIDGET_HEIGHT:-5}"
# disable or a fzf key, toggle-sort fzf action
MY_FZF_TOGGLE_SORT_SHORTCUT="${MY_FZF_TOGGLE_SORT_SHORTCUT:-ctrl-s}"
# disable or a fzf key, toggle-preview fzf action
MY_FZF_PREVIEW_SHORTCUT="${MY_FZF_PREVIEW_SHORTCUT:-ctrl-p}"
# disable or a fzf key, execute selected item immediately
MY_FZF_EVAL_SHORTCUT="${MY_FZF_EVAL_SHORTCUT:-disable}"
# disable or a fzf key, if an item is selected, execute it; otherwise, paste the query
MY_FZF_EVAL_SHORTCUT_OR_PASTE_QUERY="${MY_FZF_EVAL_SHORTCUT_OR_PASTE_QUERY:-enter}"
# disable or a fzf key, paste the item with the cursor at the end of the last pattern
MY_FZF_EDIT_SHORTCUT="${MY_FZF_EDIT_SHORTCUT:-disable}"
# disable or a fzf key, paste the item with the cursor at the end of the line
MY_FZF_EDIT_AT_END_SHORTCUT="${MY_FZF_EDIT_AT_END_SHORTCUT:-ctrl-e}"
# disable or a fzf key, paste the item with the cursor at the start of the line
MY_FZF_EDIT_AT_START_SHORTCUT="${MY_FZF_EDIT_AT_START_SHORTCUT:-ctrl-a}"
# disable or a fzf key, paste the query
MY_FZF_PASTE_QUERY_SHORTCUT="${MY_FZF_PASTE_QUERY_SHORTCUT:-ctrl-w}"
# disable: arrow keys used for navigation inside the history search query
# enable: an arrow key immediately substitutes the history element,
# closes history search, and moves cursor one character left/right
MY_FZF_ARROWS_SHORTCUTS="${MY_FZF_ARROWS_SHORTCUTS:-enable}"
# disable: will display commands only if they match the query
# enable: will also include commands whose date matches the query
MY_FZF_SEARCH_BY_DATE="${MY_FZF_SEARCH_BY_DATE:-disable}"
# dropdown: latest history elements at the top of dropdown
# dynamic: use dropdown until there is enough space, then invert items order
# dropdown-reserve: dropdown + reserves space at the bottom of the terminal
MY_FZF_LAYOUT="${MY_FZF_LAYOUT:-dropdown-reserve}"
# charcters used for conditional logic in ctrl+r bash binding,
# they were chosen randomly from the private-use unicode range
MY_FZF_BINING_HISTORY="${MY_FZF_BINING_HISTORY:-$'\xEE\x8C\xB5'}" # U+E335
MY_FZF_BINING_ACCEPT="${MY_FZF_BINING_ACCEPT:-$'\xEE\x8C\xB6'}" # U+E336
# is an invisible separator between a command and its date;
# probably you don't want to change it, it will be stripped from the dates
MY_FZF_CMD_DELIMITER="${MY_FZF_CMD_DELIMITER:-$'\xE2\x81\xA3'}" # U+2063
# enable: try using bat for bash syntax highlighting in preview window
# disable: don't try using bat at all
MY_FZF_HIGHLIGHT_SYNTAX=${MY_FZF_HIGHLIGHT_SYNTAX:-enable}
# preview highlighting theme, full list available with 'bat --list-themes'
MY_FZF_HIGHLIGHT_THEME=${MY_FZF_HIGHLIGHT_THEME:-Nord}
# any extra argument list to pass to underlying fzf invocation
MY_FZF_EXTRA_ARGS=()
__my_fzf_history_note__() {
[ "$MY_FZF_VERBOSE" == no ] && return
local blue='\033[0;34m'
local nc='\033[0m'
echo 1>&2 -e "[${blue} note ${nc}][ my fzf history ]" "$@"
}
__my_fzf_history_warning__() {
[ "$MY_FZF_VERBOSE" == no ] && return
local yellow='\033[0;33m'
local nc='\033[0m'
echo 1>&2 -e "[${yellow} warning ${nc}][ my fzf history ]" "$@"
}
__my_fzf_history_error__() {
local red='\033[0;31m'
local nc='\033[0m'
echo 1>&2 -e "[${red} error ${nc}][ my fzf history ]" "$@"
}
__my_fzf_history_reversed__() {
tac <(HISTFILE=/dev/stdout; history -a) "$HISTFILE"
}
__my_fzf_history_highlight_preview__() {
local width="$FZF_PREVIEW_COLUMNS"
# a unique invisible character that won't
# break syntax higlighting in bat like \n
# see github.com/sharkdp/bat/issues/3079
local magic=$'\U2062'
fold --spaces --width "$width" \
` # remove trailing spaces ` \
| sed -E 's/\s*$//' \
| sed -zE "s/\n/$magic/g" \
| bat --tabs 8 ` # as in fold util ` \
--color=always \
--paging=never \
--decorations=never \
--theme "$MY_FZF_HIGHLIGHT_THEME" \
"$@" \
| sed -zE "s/$magic/\n/g"
}
__my_fzf_history_formatted__() {
__my_fzf_history_reversed__ \
| awk -v fmt="$HISTTIMEFORMAT" \
-v delim="$MY_FZF_CMD_DELIMITER" '
function print_separated(date, command) {
# everything will be broken if a date
# occasionally contains the delimiter,
# so remove it, it is invisible anyway
gsub(delim, "", date)
print date delim command
}
{
if (/#[0-9]+\s*$/) {
if (prev) {
ts = substr($0, 2)
date = strftime(fmt, ts)
print_separated(date, prev)
}
prev = ""
} else {
if (prev) { print_separated("", prev) }
gsub("\\s*$", "", $0)
if ($0 && !a[$0]++) {
prev = $0
} else {
prev = ""
}
}
}'
}
# removes datetime prefix from a fzf output
__my_fzf_history_extract_command__() {
grep -Poz "$MY_FZF_CMD_DELIMITER\K[\s\S]*" | tr -d '\0'
}
__my_fzf_history_extract_date__() {
grep -Poz "^[^$MY_FZF_CMD_DELIMITER]*" | tr -d '\0'
}
__my_fzf_history_preview__() {
local date=$(echo "$@" | __my_fzf_history_extract_date__)
local command=$(echo "$@" | __my_fzf_history_extract_command__)
if [ "$MY_FZF_HIGHLIGHT_SYNTAX" == enable ]; then
printf %s\\n "$date" | __my_fzf_history_highlight_preview__ --language log
printf %s "$command" | __my_fzf_history_highlight_preview__ --language bash
else
echo "${date}${command}"
fi
}
# https://stackoverflow.com/questions/2575037
__my_fzf_history_prompt_pos__() (
exec < /dev/tty
local v=()
local t="$(stty -g)"
stty -echo
echo -en "\033[6n" > /dev/tty
IFS='[;' read -rd R -a v
stty "$t"
echo "${v[1]}"
)
__my_fzf_history_fzf_version_greater_than__() {
local test="$1"
local fzf=$(fzf --version | grep -oP '\d+\.\d+\.\d+')
local greater=$(echo -e "$test\n$fzf" | sort --version-sort | tail -1)
[ "$greater" == "$fzf" ]
}
__my_fzf_history_selector__() {
local __my_fzf_history_binds__=(
--print-query
)
if [ "$MY_FZF_TOGGLE_SORT_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--bind "$MY_FZF_TOGGLE_SORT_SHORTCUT:toggle-sort")
fi
if [ "$MY_FZF_PREVIEW_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--bind "$MY_FZF_PREVIEW_SHORTCUT:toggle-preview")
fi
if [ "$MY_FZF_EVAL_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_EVAL_SHORTCUT")
fi
if [ "$MY_FZF_EVAL_SHORTCUT_OR_PASTE_QUERY" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_EVAL_SHORTCUT_OR_PASTE_QUERY")
fi
if [ "$MY_FZF_EDIT_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_EDIT_SHORTCUT")
fi
if [ "$MY_FZF_EDIT_AT_START_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_EDIT_AT_START_SHORTCUT")
fi
if [ "$MY_FZF_EDIT_AT_END_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_EDIT_AT_END_SHORTCUT")
fi
if [ "$MY_FZF_PASTE_QUERY_SHORTCUT" != disable ]; then
__my_fzf_history_binds__+=(--expect "$MY_FZF_PASTE_QUERY_SHORTCUT")
fi
if [ "$MY_FZF_ARROWS_SHORTCUTS" != disable ]; then
__my_fzf_history_binds__+=(--expect left,right)
fi
local __my_fzf_history_layout__=(
--layout=reverse
--bind=ctrl-r:down # height + prompt
--height $(( $MY_FZF_WIDGET_HEIGHT + 1 ))
)
local pos=$(__my_fzf_history_prompt_pos__)
local wanted_height=$(($pos + $MY_FZF_WIDGET_HEIGHT))
if [ "$MY_FZF_LAYOUT" == dynamic ] && (($wanted_height > $LINES)); then
__my_fzf_history_layout__=(
--layout=default
--bind=ctrl-r:up
--border top # height + prompt + border
--height $(( $MY_FZF_WIDGET_HEIGHT + 2 ))
)
fi
# make compatible with fzf < 0.27.2
local bright_blue=12
local white=7
local green=2
local dark_green=#224433
local __my_fzf_history_style__=(
--color=bg+:$dark_green
--color=hl+:$bright_blue
--color=hl:$bright_blue
--color=fg+:$white
--color=pointer:$green
--color=gutter:-1
--pointer=▌
--ansi
)
# debian 11&12 provide too old fzf releases
if __my_fzf_history_fzf_version_greater_than__ 0.52.0; then
__my_fzf_history_style__+=(--highlight-line)
fi
if __my_fzf_history_fzf_version_greater_than__ 0.42.0; then
__my_fzf_history_style__+=(--info=inline-right)
else
__my_fzf_history_style__+=(--info=hidden)
fi
if __my_fzf_history_fzf_version_greater_than__ 0.27.0; then
__my_fzf_history_style__+=(--preview-window :hidden,right,wrap,border-left)
else
__my_fzf_history_style__+=(--preview-window right:wrap:hidden)
fi
if __my_fzf_history_fzf_version_greater_than__ 0.28.0; then
__my_fzf_history_style__+=(--scroll-off 2)
fi
if __my_fzf_history_fzf_version_greater_than__ 0.36.0; then
__my_fzf_history_style__+=(--no-scrollbar)
fi
if __my_fzf_history_fzf_version_greater_than__ 0.35.0; then
__my_fzf_history_style__+=(--no-separator)
fi
local __my_fzf_history_args__=(
--no-multi
--no-info
--preview "bash -c 'source $(printf %q "$BASH_SOURCE"); __my_fzf_history_preview__ \"\$@\"' bash {}"
--prompt="${PS1@P}"
"${__my_fzf_history_binds__[@]}"
"${__my_fzf_history_style__[@]}"
"${__my_fzf_history_layout__[@]}"
--delimiter "$MY_FZF_CMD_DELIMITER"
)
if [ "${MY_FZF_SEARCH_BY_DATE}" == disable ]; then
__my_fzf_history_args__+=(--nth="2..")
fi
if __my_fzf_history_fzf_version_greater_than__ 0.33.0; then
__my_fzf_history_args__+=(--scheme=history)
fi
__my_fzf_history_args__+=("${MY_FZF_EXTRA_ARGS[@]}")
(
export FZF_DEFAULT_OPTS=
export FZF_DEFAULT_OPTS_FILE=
export FZF_API_KEY=
export MY_FZF_VERBOSE=no
__my_fzf_history_formatted__ | fzf "${__my_fzf_history_args__[@]}"
)
}
__my_fzf_history_reserve_space__() (
local lines=5
# move cursor down with scrolling
for _ in `seq $lines`; do
echo -ne "\eD"
done
# move cursor up without scrolling
echo -ne "\e[${lines}A"
)
__my_fzf_history_allow_accept_line__() {
for m in emacs-standard vi-{command,insert}; do
bind -m $m '"'$MY_FZF_BINING_ACCEPT'"':accept-line
done
}
__my_fzf_history_block_accept_line__() {
for m in emacs-standard vi-{command,insert}; do
bind -m $m -x '"'$MY_FZF_BINING_ACCEPT'"':
done
}
__my_fzf_history_eval_immediately__() {
if [ -z "$1" ]; then
return
fi
READLINE_LINE="$1"
__my_fzf_history_allow_accept_line__
}
__my_fzf_history_edit_selection__() {
local cli="$1"
local pos="$2"
READLINE_LINE="$cli"
READLINE_POINT="$pos"
__my_fzf_history_block_accept_line__
}
__my_fzf_history_eval_immediately_or_paste_query__() {
local item="$1"
local quey="$2"
if [ -n "$item" ]; then
__my_fzf_history_eval_immediately__ "$item"
else
__my_fzf_history_edit_selection__ "$query" "${#query}"
fi
}
__my_fzf_history_last_index_of__() {
local word="$1"
local string="$2"
echo "$string" | grep -Fobie "$word" | cut -d: -f1 | tail -1
}
__my_fzf_history_last_match_index__() {
local query="$1"
local item="$2"
local expr="$3"
local result="${#item}"
for q in ${query[*]}; do
local index="$(__my_fzf_history_last_index_of__ "$q" "$item")"
if [ -n "$index" ]; then
result=$(( "$index" + "${#q}" ))
fi
done
echo $(("$result" "$expr"))
}
__my_fzf_history__() {
# workaround: sometimes bash bindings are
# broken right after the current function
# exits or when a command is invoked from
# the history, so use stty to fix the bug
# https://unix.stackexchange.com/a/535654
stty_save="$(stty -g)"
stty sane
trap "stty '$stty_save'; trap - RETURN SIGINT" RETURN SIGINT
local output
output=$(__my_fzf_history_selector__)
local status=$?
local query=$(echo "$output" | sed -n 1p)
local key=$(echo "$output" | sed -n 2p)
local item=$(echo "$output" | sed -n 3p | __my_fzf_history_extract_command__)
case "$status" in
0) ;;
# fzf's "no match"
1) ;;
# fzf's error
2) return;;
# interrupted by ctrl+c or esc
130) return;;
# ooops...
127) __my_fzf_history_error__ invalid shell command for become; return;;
*) __my_fzf_history_error__ unexpected exit code: $status; return;;
esac
case "$key" in
# enter by default, if a command is selected, execute it; otherwise, paste the query
$MY_FZF_EVAL_SHORTCUT_OR_PASTE_QUERY)
__my_fzf_history_eval_immediately_or_paste_query__ "$item" "$query";;
# disabled by default, if a command is selected, execute it; otherwise, do nothing
$MY_FZF_EVAL_SHORTCUT)
__my_fzf_history_eval_immediately__ "$item";;
# disabled by default, edit at the end of the last match
$MY_FZF_EDIT_SHORTCUT)
local pos="$(__my_fzf_history_last_match_index__ "$query" "$item")"
__my_fzf_history_edit_selection__ "$item" "$pos";;
# ctrl-e by default, edit at the end of the line
$MY_FZF_EDIT_AT_END_SHORTCUT)
__my_fzf_history_edit_selection__ "$item" "${#item}";;
# ctrl-a by default, edit at the start of the line
$MY_FZF_EDIT_AT_START_SHORTCUT)
__my_fzf_history_edit_selection__ "$item" 0;;
# ctrl-w by default, paste the current query
$MY_FZF_PASTE_QUERY_SHORTCUT)
__my_fzf_history_edit_selection__ "$query" "${#query}";;
# right key, edit at the end of the last match
right)
local pos="$(__my_fzf_history_last_match_index__ "$query" "$item" +1)"
__my_fzf_history_edit_selection__ "$item" "$pos";;
# left key, the same, but moves the cursor one symbol left
left)
local pos="$(__my_fzf_history_last_match_index__ "$query" "$item" -1)"
__my_fzf_history_edit_selection__ "$item" "$pos";;
*) __my_fzf_history_error__ unexpected key: "'$key'";;
esac
}
if ! command -v 1>/dev/null fzf; then
__my_fzf_history_warning__ "fzf is not installed, history widget won't be enabled"
return
fi
if [ "$MY_FZF_HIGHLIGHT_SYNTAX" == enable ]; then
if ! command -v bat &>/dev/null; then
__my_fzf_history_note__ "bat is not installed, syntax highlight will be disabled"
MY_FZF_HIGHLIGHT_SYNTAX=disable
elif ! bat --list-themes | grep -qF "$MY_FZF_HIGHLIGHT_THEME"; then
__my_fzf_history_warning__ "bat doesn't support selected theme: '$MY_FZF_HIGHLIGHT_THEME'"
__my_fzf_history_warning__ " you can set a new theme by setting MY_FZF_HIGHLIGHT_THEME environment variable"
__my_fzf_history_warning__ " see 'bat --list-themes' for theme names"
MY_FZF_HIGHLIGHT_THEME=$(bat --list-themes | head -1)
fi
fi
for m in emacs-standard vi-{command,insert}; do
{
bind -m $m -x '"'$MY_FZF_BINING_HISTORY'"':__my_fzf_history__
bind -m $m '"'$MY_FZF_BINING_ACCEPT'"':accept-line
bind -m $m '"\C-r": "'${MY_FZF_BINING_HISTORY}${MY_FZF_BINING_ACCEPT}'"'
} 2>/dev/null
done
if [ "$MY_FZF_LAYOUT" == dropdown-reserve ]; then
PROMPT_COMMAND+=$'\n__my_fzf_history_reserve_space__'
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment