#!/usr/bin/bash # # augmented by barbuk: https://github.com/BarbUk/snippy # . restore current clipboard # . {clipboard} placeholder to use current clipboard in snippet # . {cursor} placeholder to place the cursor # . go left to the correct position for cli and gui paste # . go up for block snippet for gui paste # . ##noparse header in snippet to not parse # . ##tmpfile header in snippet to replace $tmpfile by the temp filename used in the script # . ##richsnippet header in snippet to use paste buffer in rich mode (To copy html content in gui) # . execute command begining by $ # . execute bash script in $snippets_directory/scripts # . copy script content when selection is selected by CTRL+Return # . icons ! Icon name is set from first dir name. If you have the following snippets, the terminal icon will be displayed in rofi # terminal/ # ├── other # │   └── date # └── script # └── test # # augmented by "opennomad": https://gist.github.com/opennomad/15c4d624dce99066a82d # originally written by "mhwombat": https://bbs.archlinux.org/viewtopic.php?id=71938&p=2 # Based on "snippy" by "sessy" # (https://bbs.archlinux.org/viewtopic.php?id=71938) # # You will also need "rofi", "xsel", "xclip" and "xdotool". Get them from your linux # distro in the usual way. # # To use: # 1. Create the directory ~/.config/snippy # # 2. Create a file in that directory for each snippet that you want. # The filename will be used as a menu item, so you might want to # omit the file extension when you name the file. # # TIP: If you have a lot of snippets, you can organise them into # subdirectories under ~/.config/snippy. # # TIP: The name of the subdirectory will be passed to rofi as an icon name # # TIP: The contents of the file will be pasted asis, so if you # don't want a newline at the end when the text is pasted, don't # put one in the file. # # 3. Bind a convenient key combination to this script. # # TIP: If you're using XMonad, add something like this to xmonad.hs # ((mod4Mask, xK_s), spawn "/path/to/snippy") # set -o errexit -o pipefail -o nounset readonly snippets_directory=${XDG_CONFIG_HOME:-$HOME/.config}/snippy readonly rofi_args=(-no-lazy-grab -dmenu -i -sort -sorting-method fzf -async-pre-read 20 -theme-str 'element-icon { size: 2.35ch;}' -kb-accept-custom "" -kb-custom-1 "Ctrl+Return") readonly fzf_args=(--select-1 --reverse --inline-info --multi --preview '( bat --style auto --color always --language bash {} 2> /dev/null || highlight --force -O ansi -l {} 2> /dev/null ) | head -200' -1) readonly focus_wait=0.15 # Detect Wayland. focus_wait_ms is used, as wtype uses milliseconds instead of seconds. readonly is_wayland="${WAYLAND_DISPLAY:+1}" readonly focus_wait_ms=150 # Placeholders readonly placeholder_cursor="{cursor}" readonly placeholder_clipboard="{clipboard}" readonly placeholder_clipboard_urlencode="{clipboard_urlencode}" tmpfile=$(mktemp) readonly tmpfile trap 'rm -f $tmpfile' EXIT HUP INT TRAP TERM # colors readonly normal="\e[0m" readonly bold="\e[1m" readonly underline="\e[4m" script_content=false action=gui snippet= # smarty like template engine which executes inline bash in (bashdown) strings (replaces variables with values e.g.) # @link http://github.com/coderofsalvation/bashdown # @dependancies: sed cat # @example: echo 'hi $NAME it is $(date)' | bashdown # fetches a document and interprets bashsyntax in string (bashdown) templates # @param string - string with bash(down) syntax (usually surrounded by ' quotes instead of ") bashdown() { while IFS= read -r line; do line="$(eval "printf -- \"$( printf "%s" "$line" | sed 's/"/\\"/g' )\"")"; echo -e "$line" done } # Simplified version of bashdown, use echo and bash var search and replace # Better handling of symbol char bashdown_simple() { while IFS= read -r line || [[ -n "$line" ]]; do line="$(eval "echo \"${line//\"/\\\"}\"")" echo "$line" done } # Detect if focused app is a terminal or a gui is_gui() { if [[ $is_wayland ]]; then class="$(swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).app_id' | tr '[:upper:]' '[:lower:]')" else class="$(xprop -id "$(xdotool getwindowfocus)" WM_CLASS | cut -d'"' -f2 | tr '[:upper:]' '[:lower:]')" fi # Return false if the class if a term if [[ "$class" =~ term|tilda|kitty|alacritty|foot|wezterm ]]; then return 1 fi return 0 } # Detect vim is_vim() { if [[ $is_wayland ]]; then name="$(swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).name' | tr '[:upper:]' '[:lower:]')" else name="$(xprop -id "$(xdotool getwindowfocus)" WM_NAME | cut -d'"' -f2)" fi # vim with `set title` set the term title with: # document - VIM if [[ "${name:(-3)}" == VIM ]]; then return 0 fi return 1 } # Find the index of a string in a string strindex() { x="${1%%"$2"*}" [[ "$x" = "$1" ]] && echo -1 || echo "${#x}" } is_rich_snippet() { file="$1" grep -q "##richsnippet" "$file" } # Move the cursor up or left move_cursor() { local key=$1 local count=$2 local keys="End " if [[ $count -gt 0 ]]; then until [ "$count" -eq 0 ]; do keys+="${key} " ((count-=1)) || true done # shellcheck disable=SC2086 if [[ $is_wayland ]]; then wtype -d 100 -P $keys else xdotool key --delay 0.1 $keys fi fi } init() { # Check basic dependency local all_needed_programs_installed=true [[ $is_wayland ]] && local needed_programs=( wofi fzf wtype wl-copy wl-paste jq ) \ || local needed_programs=( rofi fzf xsel xclip jq xdotool ) for program in "${needed_programs[@]}"; do if ! command -v "$program" >/dev/null 2>&1; then all_needed_programs_installed=false echo -e "${bold}$program${normal} missing" fi done if [ "$all_needed_programs_installed" = false ] ; then echo -e "\nPlease install the previous dependancies" exit 1 fi # Check snippet directory if [[ ! -d "$snippets_directory" ]]; then mkdir -p "$snippets_directory" echo "$snippets_directory created" echo "hi it is \$(date)" > "$snippets_directory/test" fi } usage() { echo "Usage:" echo -e "\t${bold}$0${normal} [OPTION] ACTION" echo -e "\tSnippy snippets manager" echo "Options" echo -e "\t-h, --help Show help" echo "Actions" echo -ne "\t${bold}gui${normal}" echo -e "\t Browse snippet and paste it in the focused window ${underline}(default)${normal}" echo -ne "\t${bold}cli${normal}" echo -e "\t list snippet in cli mode, only copy snippet in the paste buffer" echo -ne "\t${bold}edit${normal}" echo -e "\t Browse snippet and edit it" echo -ne "\t${bold}add${normal}" echo -e "\t Add a new snippet" echo -ne "\t${bold}list${normal}" echo -e "\t list snippet" echo -ne "\t${bold}cat${normal}" echo -e "\t list category" echo -ne "\t${bold}completion${normal}" echo -e "\t bash completion" exit } parse_options() { while (( "$#" )); do case "$1" in -h|--help) usage exit ;; --) # end argument parsing shift break ;; *) action="$1" shift return ;; esac done } error() { local message="$1" echo "$message" >&2 } cli() { snippet=$( list | fzf "${fzf_args[@]}") } list() { local type="${1:-f}" find -L . -type "$type" \ | grep -vE '^\.$|\.git|\.gitconfig|\.gitkeep|\.gitignore' \ | sed -e 's!\.\/!!' \ | sort } add() { local snippet="$*" if [ -e "${snippet}" ]; then error "snippet ${snippet} already exists" return 1 fi if [ -z "$EDITOR" ]; then EDITOR=vim fi $EDITOR "$snippet" } edit() { [ -z "${snippet}" ] && return 1 if [ -z "$EDITOR" ]; then EDITOR=vim fi $EDITOR "$snippet" } gui() { # Use the filenames in the snippy directory as menu entries. # Get the menu selection from the user. # shellcheck disable=SC2086 set +o errexit snippet=$( list | sed -re 's_^([^/]*)/(.*)_&\x0icon\x1f\1_' | rofi "${rofi_args[@]}" -p '❯ ') if [ $? -eq 10 ]; then script_content=true fi set -o errexit } run() { local paste="${1:-true}" local current_clipboard cursor_line cursor_line_position cursor_line cursor_position cursor_line_lenght # just return if nothing was selected [ -z "${snippet}" ] && return 1 if [ -f "${snippets_directory}/${snippet}" ]; then # Put the contents of the selected file into the paste buffer. # If file is empty, the content is the basename of the file if [ ! -s "${snippets_directory}/${snippet}" ]; then content="$(basename "${snippet}")" # Custom selection, copy the script content without parsing elif [ "$script_content" = true ]; then content="$( cat "${snippets_directory}/${snippet}" )" # don't parse file with the ##noparse header elif grep -qE "^##noparse" "${snippets_directory}/${snippet}"; then content="$( tail -n +2 "${snippets_directory}/${snippet}" )" # replace tmpfile for snippets with ##tmpfile header elif grep -qE "^##tmpfile" "${snippets_directory}/${snippet}"; then content="$( bashdown_simple <<< "$(tail -n +2 "${snippets_directory}/${snippet}" | sed "s%\$tmpfile%$tmpfile%g" )" )" # execute bash script in scripts dir elif [[ $(dirname "${snippet}") == 'scripts' ]] && grep -qE "^#!/bin/bash" "${snippets_directory}/${snippet}"; then content="$( bash "${snippets_directory}/${snippet}" )" # default action else content="$( bashdown_simple < "${snippets_directory}/${snippet}" )" fi if [ -n "$content" ]; then printf "%s" "$content" > "$tmpfile" fi else [[ ${snippet} =~ ^$ ]] ${snippet##*$} 2>/dev/null > "$tmpfile" # execute as bashcommand fi # if tmpfile is empty at this step, nothing to do. if [ ! -s "$tmpfile" ]; then return fi # save current clipboard if [[ $is_wayland ]]; then set +e current_clipboard=$(wl-paste) set -e else current_clipboard=$(xsel --clipboard) fi # clear clipboard if [[ $is_wayland ]]; then wl-copy --clear else xsel --clipboard --clear fi # replace {clipboard} by the clipboard content # use awk to handle correctly multiline clipboard if grep -q "$placeholder_clipboard" "$tmpfile"; then awk -i inplace \ -v clipboard="$current_clipboard" \ -v placeholder="$placeholder_clipboard" \ '{ gsub(placeholder, clipboard); print }' "$tmpfile" # remove last EOL perl -pi -e 'chomp if eof' "$tmpfile" fi if grep -q "$placeholder_clipboard_urlencode" "$tmpfile"; then awk -i inplace \ -v clipboard="$( echo "$current_clipboard" | jq -sRr @uri)" \ -v placeholder="$placeholder_clipboard_urlencode" \ '{ gsub(placeholder, clipboard); print }' "$tmpfile" # remove last EOL perl -pi -e 'chomp if eof' "$tmpfile" fi # define cursor position and line at 0, we don't need to go up or left if there is no {cursor} placeholder cursor_line_position=0 cursor_position=0 # Check if there is a {cursor} placeholder if grep -qF $placeholder_cursor "$tmpfile"; then # retrieve the line number of the cursor placeholder cursor_line_position=$(grep -n "$placeholder_cursor" "$tmpfile" | cut -d: -f1) # retrieve the line cursor_line=$(grep $placeholder_cursor "$tmpfile") # calculate snippet total lines file_lines=$(wc -l < "$tmpfile") # determine the number of line to go up cursor_line_position=$(( file_lines - cursor_line_position + 1 )) # Extract cursor position cursor_position=$(strindex "$cursor_line" $placeholder_cursor) # total cursor line lenght cursor_line_lenght=${#cursor_line} # Compute the final cursor position ( 8 is the lenght of the placeholder {cursor} ) cursor_position=$(( cursor_line_lenght - cursor_position - 8 )) # remove the placeholder sed -i -e "s/$placeholder_cursor//g" "$tmpfile" fi # Copy snippet in clipboard if is_rich_snippet "${snippets_directory}/${snippet}"; then [[ $is_wayland ]] && wl-copy --type text/html -o < "$tmpfile" \ || xclip -target text/html -selection clipboard -in -loops 1 < "$tmpfile" else [[ $is_wayland ]] && wl-copy < "$tmpfile" \ || xsel --clipboard --input < "$tmpfile" fi if [ "$paste" = true ] ; then # Paste into the current application. if is_gui; then # We need a little pause to handle the time to focus on the window sleep "$focus_wait" # Paste [[ $is_wayland ]] && wtype -M ctrl v -m ctrl -s "$focus_wait_ms" \ || xdotool key ctrl+v sleep "$focus_wait" move_cursor "Up" $cursor_line_position else [[ $is_wayland ]] && wtype -M ctrl -M shift v -m ctrl -m shift -s "$focus_wait_ms" \ || xdotool key ctrl+shift+v sleep "$focus_wait" if is_vim; then move_cursor "Up" $cursor_line_position fi fi move_cursor "Left" $cursor_position # We need a little pause to handle the time to have the content of tmpfile pasted if is_gui sleep "$focus_wait" # Restore current clipboard [[ $is_wayland ]] && echo -ne "$current_clipboard" | wl-copy \ || echo -ne "$current_clipboard" | xsel --clipboard --input fi } main() { parse_options "$@" cd "${snippets_directory}" || exit case "$action" in 'gui' ) gui run ;; 'cli' ) cli run false ;; 'list' ) list ;; 'cat' ) list d ;; 'add' ) shift add "$@" ;; 'edit' ) cli edit ;; *) error "Action $action does not exists" exit 1 ;; esac } init && main "$@"