From 18a2a14e1dee8fed24c68d29a85c4cbd9080f390 Mon Sep 17 00:00:00 2001 From: Eragos Date: Mon, 11 Jul 2016 19:51:41 +0200 Subject: [PATCH] add tmux plugin copycat --- .dfminstall | 7 + .tmux.conf | 7 + .tmux/copycat/copycat.tmux | 68 ++++ .tmux/copycat/scripts/check_tmux_version.sh | 78 ++++ .../scripts/copycat_generate_results.sh | 57 +++ .tmux/copycat/scripts/copycat_git_special.sh | 58 +++ .tmux/copycat/scripts/copycat_jump.sh | 334 ++++++++++++++++++ .../copycat/scripts/copycat_mode_bindings.sh | 40 +++ .tmux/copycat/scripts/copycat_mode_quit.sh | 36 ++ .tmux/copycat/scripts/copycat_mode_start.sh | 21 ++ .tmux/copycat/scripts/copycat_search.sh | 8 + .tmux/copycat/scripts/helpers.sh | 184 ++++++++++ .../copycat/scripts/stored_search_helpers.sh | 23 ++ .tmux/copycat/scripts/variables.sh | 26 ++ 14 files changed, 947 insertions(+) create mode 100755 .tmux/copycat/copycat.tmux create mode 100644 .tmux/copycat/scripts/check_tmux_version.sh create mode 100755 .tmux/copycat/scripts/copycat_generate_results.sh create mode 100755 .tmux/copycat/scripts/copycat_git_special.sh create mode 100755 .tmux/copycat/scripts/copycat_jump.sh create mode 100755 .tmux/copycat/scripts/copycat_mode_bindings.sh create mode 100755 .tmux/copycat/scripts/copycat_mode_quit.sh create mode 100755 .tmux/copycat/scripts/copycat_mode_start.sh create mode 100755 .tmux/copycat/scripts/copycat_search.sh create mode 100644 .tmux/copycat/scripts/helpers.sh create mode 100644 .tmux/copycat/scripts/stored_search_helpers.sh create mode 100644 .tmux/copycat/scripts/variables.sh diff --git a/.dfminstall b/.dfminstall index 4057227..1088346 100644 --- a/.dfminstall +++ b/.dfminstall @@ -2,6 +2,13 @@ bin recurse .config recurse bin/dfm chmod 0755 +.tmux/copycat/scripts/copycat_generate_results.sh 0755 +.tmux/copycat/scripts/copycat_git_special.sh 0755 +.tmux/copycat/scripts/copycat_jump.sh 0755 +.tmux/copycat/scripts/copycat_mode_bindings.sh 0755 +.tmux/copycat/scripts/copycat_mode_quit.sh 0755 +.tmux/copycat/scripts/copycat_mode_start.sh 0755 +.tmux/copycat/scripts/copycat_search.sh 0755 .tmux/newpanes 0755 .tmux/prefix_highlight.tmux 0755 .tmux/scripts/check_tmux_version.sh 0755 diff --git a/.tmux.conf b/.tmux.conf index cb48e8c..ebb2d1d 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -149,3 +149,10 @@ set -g @prefix_highlight_bg 'blue' # default is 'colour04' set -g @prefix_highlight_show_copy_mode 'on' set -g @prefix_highlight_copy_mode_attr 'fg=black,bg=yellow,bold' # default is 'fg=default,bg=yellow' +# tmux-copycat (https://github.com/tmux-plugins/tmux-copycat) +run-shell ~/.tmux/copycat/copycat.tmux +set -g @copycat_search_C-d "[[:digit:]]+" +set -g @copycat_search_C-f "(^|^\.|[[:space:]]|[[:space:]]\.|[[:space:]]\.\.|^\.\.)[[:alnum:]~_-]*/[][[:alnum:]_.#$%&+=/@-]*" +set -g @copycat_search_C-u "(https?://|git@|git://|ssh://|ftp://|file:///)[[:alnum:]?=%/_.:,;~@!#$&()*+-]*" +set -g @copycat_search_M-h "\b[0-9a-f]{7,40}\b" +set -g @copycat_search_M-i "[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}" diff --git a/.tmux/copycat/copycat.tmux b/.tmux/copycat/copycat.tmux new file mode 100755 index 0000000..d77e5e9 --- /dev/null +++ b/.tmux/copycat/copycat.tmux @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/scripts/variables.sh" +source "$CURRENT_DIR/scripts/helpers.sh" +source "$CURRENT_DIR/scripts/stored_search_helpers.sh" + +# this function defines default stored searches +set_default_stored_searches() { + local file_search="$(get_tmux_option "$copycat_file_search_option" "$default_file_search_key")" + local url_search="$(get_tmux_option "$copycat_url_search_option" "$default_url_search_key")" + local digit_search="$(get_tmux_option "$copycat_digit_search_option" "$default_digit_search_key")" + local hash_search="$(get_tmux_option "$copycat_hash_search_option" "$default_hash_search_key")" + local ip_search="$(get_tmux_option "$copycat_ip_search_option" "$default_ip_search_key")" + + if stored_search_not_defined "$url_search"; then + tmux set-option -g "${COPYCAT_VAR_PREFIX}_${url_search}" "(https?://|git@|git://|ssh://|ftp://|file:///)[[:alnum:]?=%/_.:,;~@!#$&()*+-]*" + fi + if stored_search_not_defined "$file_search"; then + tmux set-option -g "${COPYCAT_VAR_PREFIX}_${file_search}" "(^|^\.|[[:space:]]|[[:space:]]\.|[[:space:]]\.\.|^\.\.)[[:alnum:]~_-]*/[][[:alnum:]_.#$%&+=/@-]*" + fi + if stored_search_not_defined "$digit_search"; then + tmux set-option -g "${COPYCAT_VAR_PREFIX}_${digit_search}" "[[:digit:]]+" + fi + if stored_search_not_defined "$hash_search"; then + tmux set-option -g "${COPYCAT_VAR_PREFIX}_${hash_search}" "\b[0-9a-f]{7,40}\b" + fi + if stored_search_not_defined "$ip_search"; then + tmux set-option -g "${COPYCAT_VAR_PREFIX}_${ip_search}" "[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}" + fi +} + +set_start_bindings() { + set_default_stored_searches + local stored_search_vars="$(stored_search_vars)" + local search_var + local key + local pattern + for search_var in $stored_search_vars; do + key="$(get_stored_search_key "$search_var")" + pattern="$(get_stored_search_pattern "$search_var")" + tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_mode_start.sh '$pattern'" + done +} + +set_copycat_search_binding() { + local key_bindings=$(get_tmux_option "$copycat_search_option" "$default_copycat_search_key") + local key + for key in $key_bindings; do + tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_search.sh" + done +} + +set_copycat_git_special_binding() { + local key_bindings=$(get_tmux_option "$copycat_git_search_option" "$default_git_search_key") + local key + for key in $key_bindings; do + tmux bind-key "$key" run-shell "$CURRENT_DIR/scripts/copycat_git_special.sh #{pane_current_path}" + done +} + +main() { + set_start_bindings + set_copycat_search_binding + set_copycat_git_special_binding +} +main diff --git a/.tmux/copycat/scripts/check_tmux_version.sh b/.tmux/copycat/scripts/check_tmux_version.sh new file mode 100644 index 0000000..b0aedec --- /dev/null +++ b/.tmux/copycat/scripts/check_tmux_version.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +VERSION="$1" +UNSUPPORTED_MSG="$2" + +get_tmux_option() { + local option=$1 + local default_value=$2 + local option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + +# this is used to get "clean" integer version number. Examples: +# `tmux 1.9` => `19` +# `1.9a` => `19` +get_digits_from_string() { + local string="$1" + local only_digits="$(echo "$string" | tr -dC '[:digit:]')" + echo "$only_digits" +} + +tmux_version_int() { + local tmux_version_string=$(tmux -V) + echo "$(get_digits_from_string "$tmux_version_string")" +} + +unsupported_version_message() { + if [ -n "$UNSUPPORTED_MSG" ]; then + echo "$UNSUPPORTED_MSG" + else + echo "Error, Tmux version unsupported! Please install Tmux version $VERSION or greater!" + fi +} + +exit_if_unsupported_version() { + local current_version="$1" + local supported_version="$2" + if [ "$current_version" -lt "$supported_version" ]; then + display_message "$(unsupported_version_message)" + exit 1 + fi +} + +main() { + local supported_version_int="$(get_digits_from_string "$VERSION")" + local current_version_int="$(tmux_version_int)" + exit_if_unsupported_version "$current_version_int" "$supported_version_int" +} +main diff --git a/.tmux/copycat/scripts/copycat_generate_results.sh b/.tmux/copycat/scripts/copycat_generate_results.sh new file mode 100755 index 0000000..e633103 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_generate_results.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +search_pattern="$1" + +capture_pane() { + local file=$1 + # copying 9M lines back will hopefully fetch the whole scrollback + tmux capture-pane -S -9000000 -p > "$file" +} + +# doing 2 things in 1 step so that we don't write to disk too much +reverse_and_create_copycat_file() { + local file=$1 + local copycat_file=$2 + local grep_pattern=$3 + (tac 2>/dev/null || tail -r) < "$file" | grep -oniE "$grep_pattern" > "$copycat_file" +} + +delete_old_files() { + local scrollback_filename="$(get_scrollback_filename)" + local copycat_filename="$(get_copycat_filename)" + rm -f "$scrollback_filename" "$copycat_filename" +} + +generate_copycat_file() { + local grep_pattern="$1" + local scrollback_filename="$(get_scrollback_filename)" + local copycat_filename="$(get_copycat_filename)" + mkdir -p "$(_get_tmp_dir)" + capture_pane "$scrollback_filename" + reverse_and_create_copycat_file "$scrollback_filename" "$copycat_filename" "$grep_pattern" +} + +if_no_results_exit_with_message() { + local copycat_filename="$(get_copycat_filename)" + # check for empty filename + if ! [ -s "$copycat_filename" ]; then + display_message "No results!" + exit 0 + fi +} + +main() { + local grep_pattern="$1" + if not_in_copycat_mode; then + delete_old_files + generate_copycat_file "$grep_pattern" + if_no_results_exit_with_message + set_copycat_mode + copycat_increase_counter + fi +} +main "$search_pattern" diff --git a/.tmux/copycat/scripts/copycat_git_special.sh b/.tmux/copycat/scripts/copycat_git_special.sh new file mode 100755 index 0000000..6e30bf6 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_git_special.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +PANE_CURRENT_PATH="$1" + +source "$CURRENT_DIR/helpers.sh" + +git_status_files() { + local git_working_dir="$PANE_CURRENT_PATH" + local git_dir="$PANE_CURRENT_PATH/.git" + echo "$(git --git-dir="$git_dir" --work-tree="$git_working_dir" status --porcelain)" +} + +formatted_git_status() { + local raw_gist_status="$(git_status_files)" + echo "$(echo "$raw_gist_status" | cut -c 4-)" +} + +exit_if_no_results() { + local results="$1" + if [ -z "$results" ]; then + display_message "No results!" + exit 0 + fi +} + +concatenate_files() { + local git_status_files="$(formatted_git_status)" + exit_if_no_results "$git_status_files" + + local result="" + # Undefined until later within a while loop. + local file_separator + while read -r line; do + result="${result}${file_separator}${line}" + file_separator="|" + done <<< "$git_status_files" + echo "$result" +} + +# Creates one, big regex out of git status files. +# Example: +# `git status` shows files `foo.txt` and `bar.txt` +# output regex will be: +# `(foo.txt|bar.txt) +git_status_files_regex() { + local concatenated_files="$(concatenate_files)" + local regex_result="(${concatenated_files})" + echo "$regex_result" +} + +main() { + local search_regex="$(git_status_files_regex)" + # starts copycat mode + $CURRENT_DIR/copycat_mode_start.sh "$search_regex" +} +main diff --git a/.tmux/copycat/scripts/copycat_jump.sh b/.tmux/copycat/scripts/copycat_jump.sh new file mode 100755 index 0000000..f1ed80f --- /dev/null +++ b/.tmux/copycat/scripts/copycat_jump.sh @@ -0,0 +1,334 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +MAXIMUM_PADDING="25" # maximum padding below the result when it can't be centered + +# jump to 'next' or 'prev' match +# global var for this file +NEXT_PREV="$1" + +# 'vi' or 'emacs', this variable used as a global file constant +TMUX_COPY_MODE="$(tmux_copy_mode)" + +_file_number_of_lines() { + local file="$1" + echo "$(wc -l $file | $AWK_CMD '{print $1}')" +} + +_get_result_line() { + local file="$1" + local number="$2" + echo "$(head -"$number" "$file" | tail -1)" +} + +_string_starts_with_digit() { + local string="$1" + echo "$string" | + \grep -q '^[[:digit:]]\+:' +} + +_get_line_number() { + local string="$1" + local copycat_file="$2" # args 2 & 3 used to handle bug in OSX grep + local position_number="$3" + if _string_starts_with_digit "$string"; then + # we have a number! + local grep_line_number="$(echo "$string" | cut -f1 -d:)" + # grep line number index starts from 1, tmux line number index starts from 0 + local tmux_line_number="$((grep_line_number - 1))" + else + # no number in the results line This is a bug in OSX grep. + # Fetching a number from a previous line. + local previous_line_num="$((position_number - 1))" + local result_line="$(_get_result_line "$copycat_file" "$previous_line_num")" + # recursively invoke this same function + tmux_line_number="$(_get_line_number "$result_line" "$copycat_file" "$previous_line_num")" + fi + echo "$tmux_line_number" +} + +_get_match() { + local string="$1" + local full_match + if _string_starts_with_digit "$string"; then + full_match="$(echo "$string" | cut -f2- -d:)" + else + # This scenario handles OS X grep bug "no number in the results line". + # When there's no number at the beginning of the line, we're taking the + # whole line as a match. This handles the result line like this: + # `http://www.example.com` (the `http` would otherwise get cut off) + full_match="$string" + fi + echo -n "$full_match" +} + +_escape_backslash() { + local string="$1" + echo "$(echo "$string" | sed 's/\\/\\\\/g')" +} + +_get_match_line_position() { + local file="$1" + local line_number="$2" + local match="$3" + local adjusted_line_num=$((line_number + 1)) + local result_line=$(tail -"$adjusted_line_num" "$file" | head -1) + + # OS X awk cannot have `=` as the first char in the variable (bug in awk). + # If exists, changing the `=` character with `.` to avoid error. + local platform="$(uname)" + if [ "$platform" == "Darwin" ]; then + result_line="$(echo "$result_line" | sed 's/^=/./')" + match="$(echo "$match" | sed 's/^=/./')" + fi + + # awk treats \r, \n, \t etc as single characters and that messes up match + # highlighting. For that reason, we're escaping backslashes so above chars + # are treated literally. + result_line="$(_escape_backslash "$result_line")" + match="$(_escape_backslash "$match")" + + local index=$($AWK_CMD -v a="$result_line" -v b="$match" 'BEGIN{print index(a,b)}') + local zero_index=$((index - 1)) + echo "$zero_index" +} + +_copycat_jump() { + local line_number="$1" + local match_line_position="$2" + local match="$3" + local scrollback_line_number="$4" + _copycat_enter_mode + _copycat_exit_select_mode + _copycat_jump_to_line "$line_number" "$scrollback_line_number" + _copycat_position_to_match_start "$match_line_position" + _copycat_select "$match" +} + +_copycat_enter_mode() { + tmux copy-mode +} + +# clears selection from a previous match +_copycat_exit_select_mode() { + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi mode + tmux send-keys Escape + else + # emacs mode + tmux send-keys C-g + fi +} + +# "manually" go up in the scrollback for a number of lines +_copycat_manually_go_up() { + local line_number="$1" + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-keys "$line_number" k 0 + else + # emacs copy mode + for (( c=1; c<="$line_number"; c++ )); do + tmux send-keys C-p + done + tmux send-keys C-a + fi +} + +_copycat_create_padding_below_result() { + local number_of_lines="$1" + local maximum_padding="$2" + local padding + + # Padding should not be greater than half pane height + # (it wouldn't be centered then). + if [ "$number_of_lines" -gt "$maximum_padding" ]; then + padding="$maximum_padding" + else + padding="$number_of_lines" + fi + + # cannot create padding, exit function + if [ "$padding" -eq "0" ]; then + return + fi + + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-keys "$padding" j "$padding" k + else + # emacs copy mode + for (( c=1; c<="$padding"; c++ )); do + tmux send-keys C-n + done + for (( c=1; c<="$padding"; c++ )); do + tmux send-keys C-p + done + fi +} + +# performs a jump to go to line +_copycat_go_to_line_with_jump() { + local line_number="$1" + # first jumps to the "bottom" in copy mode so that jumps are consistent + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-keys G 0 : + else + # emacs copy mode + tmux send-keys "M->" C-a g + fi + tmux send-keys "$line_number" C-m +} + +# maximum line number that can be reached via tmux 'jump' +_get_max_jump() { + local scrollback_line_number="$1" + local window_height="$2" + local max_jump=$((scrollback_line_number - $window_height)) + # max jump can't be lower than zero + if [ "$max_jump" -lt "0" ]; then + max_jump="0" + fi + echo "$max_jump" +} + +_copycat_jump_to_line() { + local line_number="$1" + local scrollback_line_number="$2" + local window_height="$(tmux display-message -p '#{pane_height}')" + local correct_line_number + + local max_jump=$(_get_max_jump "$scrollback_line_number" "$window_height") + local correction="0" + + if [ "$line_number" -gt "$max_jump" ]; then + # We need to 'reach' a line number that is not accessible via 'jump'. + # Introducing 'correction' + correct_line_number="$max_jump" + correction=$((line_number - $correct_line_number)) + else + # we can reach the desired line number via 'jump'. Correction not needed. + correct_line_number="$line_number" + fi + + _copycat_go_to_line_with_jump "$correct_line_number" + + if [ "$correction" -gt "0" ]; then + _copycat_manually_go_up "$correction" + fi + + # If no corrections (meaning result is not at the top of scrollback) + # we can then 'center' the result within a pane. + if [ "$correction" -eq "0" ]; then + local half_window_height="$((window_height / 2))" + # creating as much padding as possible, up to half pane height + _copycat_create_padding_below_result "$line_number" "$half_window_height" + fi +} + +_copycat_position_to_match_start() { + local match_line_position="$1" + [ "$match_line_position" -eq "0" ] && return 0 + + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-keys "$match_line_position" l + else + # emacs copy mode + # emacs doesn't have repeat, so we're manually looping :( + for (( c=1; c<="$match_line_position"; c++ )); do + tmux send-keys C-f + done + fi +} + +_copycat_select() { + local match="$1" + local length="${#match}" + if [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-keys Space "$length" l h # selection correction for 1 char + else + # emacs copy mode + tmux send-keys C-Space + # emacs doesn't have repeat, so we're manually looping :( + for (( c=1; c<="$length"; c++ )); do + tmux send-keys C-f + done + # NO selection correction for emacs mode + fi +} + +# all functions above are "private", called from `do_next_jump` function + +get_new_position_number() { + local copycat_file="$1" + local current_position="$2" + local new_position + + # doing a forward/up jump + if [ "$NEXT_PREV" == "next" ]; then + local number_of_results=$(wc -l "$copycat_file" | $AWK_CMD '{ print $1 }') + if [ "$current_position" -eq "$number_of_results" ]; then + # position can't go beyond the last result + new_position="$current_position" + else + new_position="$((current_position + 1))" + fi + + # doing a backward/down jump + elif [ "$NEXT_PREV" == "prev" ]; then + if [ "$current_position" -eq "1" ]; then + # position can't go below 1 + new_position="1" + else + new_position="$((current_position - 1))" + fi + fi + echo "$new_position" +} + +do_next_jump() { + local position_number="$1" + local copycat_file="$2" + local scrollback_file="$3" + + local scrollback_line_number=$(_file_number_of_lines "$scrollback_file") + local result_line="$(_get_result_line "$copycat_file" "$position_number")" + local line_number=$(_get_line_number "$result_line" "$copycat_file" "$position_number") + local match=$(_get_match "$result_line") + local match_line_position=$(_get_match_line_position "$scrollback_file" "$line_number" "$match") + _copycat_jump "$line_number" "$match_line_position" "$match" "$scrollback_line_number" +} + +notify_about_first_last_match() { + local current_position="$1" + local next_position="$2" + local message_duration="1500" + + # if position didn't change, we are either on a 'first' or 'last' match + if [ "$current_position" -eq "$next_position" ]; then + if [ "$NEXT_PREV" == "next" ]; then + display_message "Last match!" "$message_duration" + elif [ "$NEXT_PREV" == "prev" ]; then + display_message "First match!" "$message_duration" + fi + fi +} + +main() { + if in_copycat_mode; then + local copycat_file="$(get_copycat_filename)" + local scrollback_file="$(get_scrollback_filename)" + local current_position="$(get_copycat_position)" + local next_position="$(get_new_position_number "$copycat_file" "$current_position")" + do_next_jump "$next_position" "$copycat_file" "$scrollback_file" + notify_about_first_last_match "$current_position" "$next_position" + set_copycat_position "$next_position" + fi +} +main diff --git a/.tmux/copycat/scripts/copycat_mode_bindings.sh b/.tmux/copycat/scripts/copycat_mode_bindings.sh new file mode 100755 index 0000000..b2155b6 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_mode_bindings.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +# Extends a keyboard key. +# Benefits: tmux won't report errors and everything will work fine even if the +# script is deleted. +extend_key() { + local key="$1" + local script="$2" + + # 1. 'key' is sent to tmux. This ensures the default key action is done. + # 2. Script is executed. + # 3. `true` command ensures an exit status 0 is returned. This ensures a + # user never gets an error msg - even if the script file from step 2 is + # deleted. + tmux bind-key -n "$key" run-shell "tmux send-keys '$key'; $script; true" +} + +copycat_cancel_bindings() { + # keys that quit copy mode are enhanced to quit copycat mode as well. + local cancel_mode_bindings=$(copycat_quit_copy_mode_keys) + local key + for key in $cancel_mode_bindings; do + extend_key "$key" "$CURRENT_DIR/copycat_mode_quit.sh" + done +} + +copycat_mode_bindings() { + extend_key "$(copycat_next_key)" "$CURRENT_DIR/copycat_jump.sh 'next'" + extend_key "$(copycat_prev_key)" "$CURRENT_DIR/copycat_jump.sh 'prev'" +} + +main() { + copycat_mode_bindings + copycat_cancel_bindings +} +main diff --git a/.tmux/copycat/scripts/copycat_mode_quit.sh b/.tmux/copycat/scripts/copycat_mode_quit.sh new file mode 100755 index 0000000..38c29f6 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_mode_quit.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +unbind_cancel_bindings() { + local cancel_mode_bindings=$(copycat_quit_copy_mode_keys) + local key + for key in $cancel_mode_bindings; do + tmux unbind-key -n "$key" + done +} + +unbind_prev_next_bindings() { + tmux unbind-key -n "$(copycat_next_key)" + tmux unbind-key -n "$(copycat_prev_key)" +} + +unbind_all_bindings() { + unbind_cancel_bindings + unbind_prev_next_bindings +} + +main() { + if in_copycat_mode; then + reset_copycat_position + unset_copycat_mode + copycat_decrease_counter + # removing all bindings only if no panes are in copycat mode + if copycat_counter_zero; then + unbind_all_bindings + fi + fi +} +main diff --git a/.tmux/copycat/scripts/copycat_mode_start.sh b/.tmux/copycat/scripts/copycat_mode_start.sh new file mode 100755 index 0000000..7c0c209 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_mode_start.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +SUPPORTED_VERSION="1.9" + +PATTERN="$1" + +supported_tmux_version_ok() { + $CURRENT_DIR/check_tmux_version.sh "$SUPPORTED_VERSION" +} + +main() { + local pattern="$1" + if supported_tmux_version_ok; then + $CURRENT_DIR/copycat_generate_results.sh "$pattern" # will `exit 0` if no results + $CURRENT_DIR/copycat_mode_bindings.sh + $CURRENT_DIR/copycat_jump.sh 'next' + fi +} +main "$PATTERN" diff --git a/.tmux/copycat/scripts/copycat_search.sh b/.tmux/copycat/scripts/copycat_search.sh new file mode 100755 index 0000000..b77f1f2 --- /dev/null +++ b/.tmux/copycat/scripts/copycat_search.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +main() { + tmux command-prompt -p "copycat search:" "run-shell \"$CURRENT_DIR/copycat_mode_start.sh '%1'\"" +} +main diff --git a/.tmux/copycat/scripts/helpers.sh b/.tmux/copycat/scripts/helpers.sh new file mode 100644 index 0000000..da52f42 --- /dev/null +++ b/.tmux/copycat/scripts/helpers.sh @@ -0,0 +1,184 @@ +# config options + +default_next_key="n" +tmux_option_next="@copycat_next" + +default_prev_key="N" +tmux_option_prev="@copycat_prev" + +# keeps track of number of panes in copycat mode +tmux_option_counter="@copycat_counter" + +# === awk vs gawk === +command_exists() { + command -v "$@" > /dev/null 2>&1 +} +AWK_CMD='awk' +if command_exists gawk; then + AWK_CMD='gawk' +fi + +# === general helpers === + +get_tmux_option() { + local option=$1 + local default_value=$2 + local option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +set_tmux_option() { + local option=$1 + local value=$2 + tmux set-option -gq "$option" "$value" +} + +tmux_copy_mode() { + tmux show-option -gwv mode-keys +} + +# === copycat mode specific helpers === + +set_copycat_mode() { + set_tmux_option "$(_copycat_mode_var)" "true" +} + +unset_copycat_mode() { + set_tmux_option "$(_copycat_mode_var)" "false" +} + +in_copycat_mode() { + local copycat_mode=$(get_tmux_option "$(_copycat_mode_var)" "false") + [ "$copycat_mode" == "true" ] +} + +not_in_copycat_mode() { + if in_copycat_mode; then + return 1 + else + return 0 + fi +} + +# === copycat mode position === + +get_copycat_position() { + local copycat_position_variable=$(_copycat_position_var) + echo $(get_tmux_option "$copycat_position_variable" "0") +} + +set_copycat_position() { + local position="$1" + local copycat_position_variable=$(_copycat_position_var) + set_tmux_option "$copycat_position_variable" "$position" +} + +reset_copycat_position() { + set_copycat_position "0" +} + +# === scrollback and results position === + +get_scrollback_filename() { + echo "$(_get_tmp_dir)/scrollback-$(_pane_unique_id)" +} + +get_copycat_filename() { + echo "$(_get_tmp_dir)/results-$(_pane_unique_id)" +} + +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + +# === counter functions === + +copycat_increase_counter() { + local count=$(get_tmux_option "$tmux_option_counter" "0") + local new_count="$((count + 1))" + set_tmux_option "$tmux_option_counter" "$new_count" +} + +copycat_decrease_counter() { + local count="$(get_tmux_option "$tmux_option_counter" "0")" + if [ "$count" -gt "0" ]; then + # decreasing the counter only if it won't go below 0 + local new_count="$((count - 1))" + set_tmux_option "$tmux_option_counter" "$new_count" + fi +} + +copycat_counter_zero() { + local count="$(get_tmux_option "$tmux_option_counter" "0")" + [ "$count" -eq "0" ] +} + +# === key binding functions === + +copycat_next_key() { + echo "$(get_tmux_option "$tmux_option_next" "$default_next_key")" +} + +copycat_prev_key() { + echo "$(get_tmux_option "$tmux_option_prev" "$default_prev_key")" +} + +# function expected output: 'C-c Enter q' +copycat_quit_copy_mode_keys() { + local commands_that_quit_copy_mode="cancel\|copy-selection\|copy-pipe" + local copy_mode="$(tmux_copy_mode)-copy" + tmux list-keys -t "$copy_mode" | + \grep "$commands_that_quit_copy_mode" | + $AWK_CMD '{ print $4}' | + sort -u | + sed 's/C-j//g' | + xargs echo +} + +# === 'private' functions === + +_copycat_mode_var() { + local pane_id="$(_pane_unique_id)" + echo "@copycat_mode_$pane_id" +} + +_copycat_position_var() { + local pane_id="$(_pane_unique_id)" + echo "@copycat_position_$pane_id" +} + +_get_tmp_dir() { + echo "${TMPDIR:-/tmp}/tmux-$EUID-copycat" +} + +# returns a string unique to current pane +# sed removes `$` sign because `session_id` contains is +_pane_unique_id() { + tmux display-message -p "#{session_id}-#{window_index}-#{pane_index}" | + sed 's/\$//' +} diff --git a/.tmux/copycat/scripts/stored_search_helpers.sh b/.tmux/copycat/scripts/stored_search_helpers.sh new file mode 100644 index 0000000..6d192fe --- /dev/null +++ b/.tmux/copycat/scripts/stored_search_helpers.sh @@ -0,0 +1,23 @@ +stored_search_not_defined() { + local key="$1" + local search_value="$(tmux show-option -gqv "${COPYCAT_VAR_PREFIX}_${key}")" + [ -z $search_value ] +} + +stored_search_vars() { + tmux show-options -g | + \grep -i "^${COPYCAT_VAR_PREFIX}_" | + cut -d ' ' -f1 | # cut just variable names + xargs # splat var names in one line +} + +# get the search key from the variable name +get_stored_search_key() { + local search_var="$1" + echo "$(echo "$search_var" | sed "s/^${COPYCAT_VAR_PREFIX}_//")" +} + +get_stored_search_pattern() { + local search_var="$1" + echo "$(get_tmux_option "$search_var" "")" +} diff --git a/.tmux/copycat/scripts/variables.sh b/.tmux/copycat/scripts/variables.sh new file mode 100644 index 0000000..82f8f7a --- /dev/null +++ b/.tmux/copycat/scripts/variables.sh @@ -0,0 +1,26 @@ +# stored search variable prefix +COPYCAT_VAR_PREFIX="@copycat_search" + +# basic search +default_copycat_search_key="/" +copycat_search_option="@copycat_search" + +# git special search +default_git_search_key="C-g" +copycat_git_search_option="@copycat_git_special" + +# regular searches +default_file_search_key="C-f" +copycat_file_search_option="@copycat_file_search" + +default_url_search_key="C-u" +copycat_url_search_option="@copycat_url_search" + +default_digit_search_key="C-d" +copycat_digit_search_option="@copycat_digit_search" + +default_hash_search_key="M-h" +copycat_hash_search_option="@copycat_hash_search" + +default_ip_search_key="M-i" +copycat_ip_search_option="@copycat_ip_search"