Last active
May 3, 2025 12:15
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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