joplin-snippy/snippy

444 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 ~/.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 ~/.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
# 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() {
class="$(xprop -id "$(xdotool getwindowfocus)" WM_CLASS | cut -d'"' -f2 | tr '[:upper:]' '[:lower:]')"
# Return false if the class if a term
if [[ "$class" =~ term|tilda|kitty|alacritty ]]; then
return 1
fi
return 0
}
# Detect vim
is_vim() {
name="$(xprop -id "$(xdotool getwindowfocus)" WM_NAME | cut -d'"' -f2)"
# 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
xdotool key --delay 0.1 $keys
fi
}
init() {
# Check basic dependency
local all_needed_programs_installed=true
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
current_clipboard=$(xsel --clipboard)
# clear clipboard
xsel --clipboard --clear
# 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
xclip -target text/html -selection clipboard -in -loops 1 < "$tmpfile"
else
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
xdotool key ctrl+v sleep "$focus_wait"
move_cursor "Up" $cursor_line_position
else
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
# Restore current clipboard
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 "$@"