joplin-snippy/snippy

503 lines
15 KiB
Plaintext
Raw Normal View History

2023-07-04 05:51:12 -04:00
#!/usr/bin/env bash
#
2022-03-17 03:30:54 -04:00
# augmented by barbuk: https://github.com/BarbUk/snippy
2016-11-05 03:41:50 -04:00
# . 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
2019-10-28 07:11:09 -04:00
# . ##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
#
2016-11-01 16:27:13 -04:00
# 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
2016-11-01 16:27:13 -04:00
# distro in the usual way.
#
# To use:
2023-02-09 10:00:26 -05:00
# 1. Create the directory ~/.config/snippy
2016-11-01 16:27:13 -04:00
#
# 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
2023-02-09 10:00:26 -05:00
# subdirectories under ~/.config/snippy.
2016-11-01 16:27:13 -04:00
#
# TIP: The name of the subdirectory will be passed to rofi as an icon name
#
2016-11-01 16:27:13 -04:00
# 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
2016-11-01 16:27:13 -04:00
2021-06-18 05:46:14 -04:00
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")
2021-06-18 05:59:47 -04:00
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)
2021-11-12 05:52:30 -05:00
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}"
2016-11-01 16:27:13 -04:00
2021-11-12 05:52:18 -05:00
tmpfile=$(mktemp)
readonly tmpfile
2018-12-22 07:22:22 -05:00
trap 'rm -f $tmpfile' EXIT HUP INT TRAP TERM
2016-11-01 16:27:13 -04:00
# colors
readonly normal="\e[0m"
readonly bold="\e[1m"
readonly underline="\e[4m"
script_content=false
action=gui
snippet=
2016-11-01 16:27:13 -04:00
# 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
2024-02-16 02:27:57 -05:00
line="$(eval "printf -- \"$(printf "%s" "$line" | sed 's/"/\\"/g')\"")"
2016-11-01 16:27:13 -04:00
echo -e "$line"
2018-04-22 01:50:52 -04:00
done
2016-11-01 16:27:13 -04:00
}
2019-05-17 09:46:00 -04:00
# Simplified version of bashdown, use echo and bash var search and replace
# Better handling of symbol char
bashdown_simple() {
2024-02-16 02:27:57 -05:00
while IFS= read -r line || [[ -n "$line" ]]; do
line="$(eval "echo \"${line//\"/\\\"}\"")"
echo "$line"
done
2019-05-17 09:46:00 -04:00
}
# 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
2020-02-09 01:13:14 -05:00
# Return false if the class if a term
if [[ "$class" =~ term|tilda|kitty|alacritty|foot|wezterm ]]; then
2020-02-09 01:13:14 -05:00
return 1
fi
2018-02-01 14:49:44 -05:00
return 0
}
2020-02-09 01:13:14 -05:00
# 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
2024-02-16 02:27:57 -05:00
2020-02-09 01:13:14 -05:00
# 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() {
2023-04-27 03:47:45 -04:00
x="${1%%"$2"*}"
[[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
}
2019-10-28 07:06:57 -04:00
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
2024-02-16 02:27:57 -05:00
until [ "$count" -eq 0 ]; do
keys+="${key} "
2024-02-16 02:27:57 -05:00
((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
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
2024-02-16 02:27:57 -05:00
local needed_programs=(wofi fzf wtype wl-copy wl-paste jq)
2023-04-27 03:48:08 -04:00
else
2024-02-16 02:27:57 -05:00
local needed_programs=(rofi fzf xsel xclip jq xdotool)
2023-04-27 03:48:08 -04:00
fi
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
2024-02-16 02:27:57 -05:00
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"
2024-02-16 02:27:57 -05:00
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() {
2024-02-16 02:27:57 -05:00
while (("$#")); do
case "$1" in
-h | --help)
usage
;;
--) # end argument parsing
shift
break
;;
*)
action="$1"
shift
return
;;
esac
done
}
error() {
2024-02-16 02:27:57 -05:00
local message="$1"
echo "$message" >&2
}
cli() {
2024-02-16 02:27:57 -05:00
snippet=$(list | fzf "${fzf_args[@]}")
}
list() {
local type="${1:-f}"
2024-02-16 02:27:57 -05:00
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() {
2016-11-01 16:27:13 -04:00
# Use the filenames in the snippy directory as menu entries.
# Get the menu selection from the user.
2017-06-10 17:41:31 -04:00
# shellcheck disable=SC2086
set +o errexit
2024-02-16 02:27:57 -05:00
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
2016-11-01 16:27:13 -04:00
# just return if nothing was selected
[ -z "${snippet}" ] && return 1
2016-11-01 16:27:13 -04:00
if [ -f "${snippets_directory}/${snippet}" ]; then
2019-02-14 08:01:04 -05:00
2016-11-01 16:27:13 -04:00
# Put the contents of the selected file into the paste buffer.
2019-02-14 08:01:04 -05:00
# 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
2024-02-16 02:27:57 -05:00
content="$(cat "${snippets_directory}/${snippet}")"
# don't parse file with the ##noparse header
2019-02-14 08:01:04 -05:00
elif grep -qE "^##noparse" "${snippets_directory}/${snippet}"; then
2024-02-16 02:27:57 -05:00
content="$(tail -n +2 "${snippets_directory}/${snippet}")"
2019-02-14 08:01:04 -05:00
2019-10-28 07:07:24 -04:00
# replace tmpfile for snippets with ##tmpfile header
elif grep -qE "^##tmpfile" "${snippets_directory}/${snippet}"; then
2024-02-16 02:27:57 -05:00
content="$(bashdown_simple <<<"$(tail -n +2 "${snippets_directory}/${snippet}" | sed "s%\$tmpfile%$tmpfile%g")")"
2019-10-28 07:07:24 -04:00
2019-02-14 08:01:04 -05:00
# execute bash script in scripts dir
elif [[ $(dirname "${snippet}") == 'scripts' ]] && grep -qE "^#!/bin/bash" "${snippets_directory}/${snippet}"; then
2024-02-16 02:27:57 -05:00
content="$(bash "${snippets_directory}/${snippet}")"
2019-02-14 08:01:04 -05:00
# default action
2016-12-09 04:11:52 -05:00
else
2024-02-16 02:27:57 -05:00
content="$(bashdown_simple <"${snippets_directory}/${snippet}")"
2016-12-09 04:11:52 -05:00
fi
2016-11-02 12:58:26 -04:00
if [ -n "$content" ]; then
2024-02-16 02:27:57 -05:00
printf "%s" "$content" >"$tmpfile"
fi
2024-02-16 02:27:57 -05:00
else
[[ ${snippet} =~ ^$ ]]
${snippet##*$} 2>/dev/null >"$tmpfile" # execute as bashcommand
fi
2016-11-05 03:35:50 -04:00
# if tmpfile is empty at this step, nothing to do.
if [ ! -s "$tmpfile" ]; then
return
fi
# save current clipboard
if [[ $is_wayland ]]; then
# Disabling errexit for wl-paste,
# because it return 1 when they are nothing to paste
set +o errexit
current_clipboard=$(wl-paste)
set -o errexit
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 \
2024-02-16 02:27:57 -05:00
-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
2016-11-02 12:58:26 -04:00
2016-11-05 03:35:50 -04:00
# 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)
2016-11-05 03:35:50 -04:00
# retrieve the line
cursor_line=$(grep $placeholder_cursor "$tmpfile")
2016-11-05 03:35:50 -04:00
# calculate snippet total lines
2024-02-16 02:27:57 -05:00
file_lines=$(wc -l <"$tmpfile")
# determine the number of line to go up
2024-02-16 02:27:57 -05:00
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} )
2024-02-16 02:27:57 -05:00
cursor_position=$((cursor_line_lenght - cursor_position - 8))
2016-11-05 03:35:50 -04:00
# remove the placeholder
sed -i -e "s/$placeholder_cursor//g" "$tmpfile"
fi
# Copy snippet in clipboard
2019-10-28 07:06:57 -04:00
if is_rich_snippet "${snippets_directory}/${snippet}"; then
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
2024-02-16 02:27:57 -05:00
wl-copy --type text/html -o <"$tmpfile"
2023-04-27 03:48:08 -04:00
else
2024-02-16 02:27:57 -05:00
xclip -target text/html -selection clipboard -in -loops 1 <"$tmpfile"
2023-04-27 03:48:08 -04:00
fi
2019-10-28 07:06:57 -04:00
else
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
2024-02-16 02:27:57 -05:00
wl-copy <"$tmpfile"
2023-04-27 03:48:08 -04:00
else
2024-02-16 02:27:57 -05:00
xsel --clipboard --input <"$tmpfile"
2023-04-27 03:48:08 -04:00
fi
2019-10-28 07:06:57 -04:00
fi
2024-02-16 02:27:57 -05:00
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
2021-11-12 05:52:30 -05:00
sleep "$focus_wait"
2020-07-03 06:34:06 -04:00
# Paste
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
wtype -M ctrl v -m ctrl -s "$focus_wait_ms"
else
xdotool key ctrl+v sleep "$focus_wait"
fi
2020-02-09 01:13:14 -05:00
move_cursor "Up" $cursor_line_position
else
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
wtype -M ctrl -M shift v -m ctrl -m shift -s "$focus_wait_ms"
else
xdotool key ctrl+shift+v sleep "$focus_wait"
fi
if is_vim; then
move_cursor "Up" $cursor_line_position
fi
2020-02-09 01:13:14 -05:00
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
2023-04-27 03:48:08 -04:00
if [[ $is_wayland ]]; then
echo -ne "$current_clipboard" | wl-copy
else
echo -ne "$current_clipboard" | xsel --clipboard --input
fi
2016-11-01 16:27:13 -04:00
fi
}
main() {
parse_options "$@"
cd "${snippets_directory}" || exit
case "$action" in
2024-02-16 02:27:57 -05:00
'gui')
gui
run
;;
2024-02-16 02:27:57 -05:00
'cli')
cli
run false
;;
2024-02-16 02:27:57 -05:00
'list')
list
;;
2024-02-16 02:27:57 -05:00
'cat')
list d
;;
2024-02-16 02:27:57 -05:00
'add')
shift
add "$@"
;;
2024-02-16 02:27:57 -05:00
'edit')
cli
edit
;;
2024-02-16 02:27:57 -05:00
*)
error "Action $action does not exists"
exit 1
;;
esac
2016-11-01 16:27:13 -04:00
}
init && main "$@"