Files
vns/vns
2025-12-17 23:34:03 -06:00

542 lines
14 KiB
Bash

#!/bin/bash
# Vim Note System, a simple script for managing encrypted plaintext notes
readonly VNS_STORE="${VNS_STORE:-$HOME/.vns}" # Where vns expects its note store
GPG_TTY="$(tty)" # Sets up gpg-agent for use
export GPG_TTY
vns_raise (){
# raise (message) (exit code)
# Prints error (message) to stderr, then exits with (exit code)
local -r VNS_RED_COLOR='\033[0;31m'
local -r VNS_RESET_COLOR='\033[0m'
printf "$VNS_RED_COLOR!$VNS_RESET_COLOR - %s\\n" "$1" 1>&2
exit "$2"
}
vns_report () {
# report (message)
# Prints message to the console, formatted with a blue asterisk to indicate
# some successful operation has completed
local -r VNS_BLUE_COLOR='\033[0;34m'
local -r VNS_RESET_COLOR='\033[0m'
printf "${VNS_BLUE_COLOR}*${VNS_RESET_COLOR} - %s\n" "$*"
}
vns_checkDeps (){
# checkDeps
# Prints a list of missing dependencies
# Currently these dependencies are {vim, tree, git}
for DEP in 'vim' 'tree' 'git'; do
if ! command -v "$DEP" >/dev/null 2>&1; then
printf "%s " "$DEP"
fi
done
}
vns_sanityCheck () {
# sanityCheck
# Terminates script if environment is not suitable for vns
# --- Dependencies ---
# Get list of missing dependencies
MISSING="$(vns_checkDeps)"
# Inform the user of missing dependencies and exit on code 20 if any were found
if [ -n "$MISSING" ]; then
vns_raise "Missing Dependencies: $MISSING" 20
# --- Store ---
# Verify that the note store has been initialized
elif [ ! -d "$VNS_STORE" ]; then
vns_raise "The vns store needs to be initialized (hint: -I)" 21
# Verify that intended GPG recipients are stored in compliance with v2.0
elif [ ! -r "$VNS_STORE/.gpg-id" ]; then
vns_raise "Re-initialize (-I) with gpg recipients. Existing notes will be re-encrypted" 21
fi
}
vns_init () {
# init (GPG recipients...)
# Prepares $VNS_STORE for use
# Verify that recipients were specified
if [ "$#" -lt 1 ]; then
vns_raise "No GPG Recipients Specified" 3
# Re-encrypt existing notes, if present
elif [ -d "$VNS_STORE/.git" ]; then
# Reset list of GPG recipients
echo -n '' > "${VNS_STORE}/.gpg-id"
vns_reencrypt "$@"
# No existing store found; initialize a new one
else
# Create a new VNS Store
git init "$VNS_STORE" >/dev/null
vns_report "Initialized new VNS store"
fi
# Store GPG Recipients for later note creation whether re-encrypting, or
# initializing a new store
for recipient in "$@"; do
echo "$recipient" >> "${VNS_STORE}/.gpg-id"
done
vns_git add .gpg-id >/dev/null
vns_git commit -am "Updated GPG recipients" >/dev/null
}
vns_reencrypt () {
# reencrypt (GPG recipients...)
# Re-encrypt all notes with provided gpg recipients.
# Construct list of arguments from provided recipients
local -a GPG_RECIPS;
for recipient in "$@"; do
GPG_RECIPS=( "${GPG_RECIPS[@]}" "-r" "$recipient" )
done
# Find all notes and re-encrypt them
find "${VNS_STORE}" -name "*.gpg" | while read -r FILE; do\
gpg2 --batch --decrypt "$FILE" 2>/dev/null | gpg2 --batch "${GPG_RECIPS[@]}" --encrypt --output "${FILE}_new".gpg >/dev/null 2>&1
mv "${FILE}_new.gpg" "$FILE";
done
vns_report "Re-encrypted existing notes with $*"
vns_git commit -am "Re-encrypted existing notes with ${GPG_RECIPS[*]}" >/dev/null 2>&1
}
vns_gpgid () {
# gpgid
# Print a list of all recipients listed in .gpg-id
sed 's/^/-r /' "$VNS_STORE/.gpg-id"
}
vns_printHelp (){
# printHelp
# Prints help information to stdout
printf "%s" "usage: vns [-cdelpqr] <notebook/section/name>"
printf "\\n%s" " vns -I GPG_RECIPIENT (GPG_RECIPIENTS...)"
printf "\\n%s" " vns -h"
printf "\\n%s" " vns git ..."
printf "\\n"
printf "\\n%s" " -c : Create note"
printf "\\n%s" " -d : Delete note(s)"
printf "\\n%s" " -e : Open note for editing"
printf "\\n%s" " -h : Display this message"
printf "\\n%s" " -i : import file as note"
printf "\\n%s" " -I : Initialize note store"
printf "\\n%s" " -j : Open Daily Journal Entry"
printf "\\n%s" " -l : List all notes in <notebook>"
printf "\\n%s" " -m : Merge two or more notes"
printf "\\n%s" " -p : Print note to console"
printf "\\n%s" " -q : Query notes for expression"
printf "\\n%s" " -r : Rename/move a note"
printf "\\n%s" " -u : Duplicate note as given path"
printf "\\n\\n"
}
vns_list () {
# list (notebook)
# Prints a tree containing all notes in the notebook
# If no notebook is specified, all notes are shown
# Check for default behavior
if [ "$#" -lt 1 ]; then
local -r "NOTEBOOK"=""
printf "%s\\n" "vns"
else
readonly NOTEBOOK="$1"
printf "%s\\n" "$1"
fi
tree --noreport --prune "$VNS_STORE/$NOTEBOOK"\
| tail -n +2\
| sed s/\.gpg//g
}
vns_rm () {
# rm (note)
# removes (note) from the store
# Verify notes were provided
if [ "$#" -lt 1 ]; then
vns_raise "Insufficient arguments" 30
fi
while [ "$#" -gt 0 ]; do
# Verify the note exists
if [ ! -r "$VNS_STORE/$1.gpg" ]; then
vns_raise "Note does not exist" 31;
fi
# Delete the Note
vns_git rm "$VNS_STORE/$1.gpg" > /dev/null
vns_git commit -m "Deleted $1" > /dev/null
# Report success
vns_report "Deleted $1"
# Move to the next item
shift
done
}
vns_header () {
# header (title)
# Print note title and date in Markdown format
if [ "$#" -lt 1 ]; then
vns_raise "header: no note title provided" 4
else
echo "# $1"
printf "## %s" "$(date)"
fi
}
vns_create () {
# create (note)
# Create a new note and open it for editing
local -r VNS_RED_COLOR='\033[0;31m'
local -r VNS_RESET_COLOR='\033[0m'
local -a create_gpgid="($(vns_gpgid))"
if [ "$#" -lt 1 ]; then
printf "usage: %s %s\n" "$(basename "$0")" "-c [notebook/section/](name)"
return
fi
# Refuse to overwrite a note
if [ -r "$VNS_STORE/$1.gpg" ]; then vns_raise "Note already exists" 41; fi
# If the note belongs to a new notebook, create the notebook
if [ ! -d "$(dirname "$1")" ]; then mkdir -p "$VNS_STORE/$(dirname "$1")"; fi
# Create empty note
mkdir -p "$(dirname "$VNS_STORE/$1.gpg")"
if [ -n "${VNS_TITLE+isset}" ]; then
vns_header "${VNS_TITLE}" | gpg2 --batch "${create_gpgid[@]}" --encrypt -o "$VNS_STORE/$1.gpg" 2>/dev/null
else
vns_header "$(basename "$1")" | gpg2 --batch "${create_gpgid[@]}" --encrypt -o "$VNS_STORE/$1.gpg" 2>/dev/null
fi
# Edit the note
vim "$VNS_STORE/$1.gpg"
# Update vns_git
vns_git add "$VNS_STORE/$1.gpg" > /dev/null
vns_git commit -m "Added $1" > /dev/null
}
vns_edit () {
# edit (note)
# Open (note) for editing in Vim
# Verify $1 is bound
if [ "$#" -lt 1 ]; then vns_raise "Insufficient arguments" 50; fi
# Refuse to edit non-existent notes
if [ ! -r "$VNS_STORE/$1.gpg" ]; then vns_raise "Note not found: $1" 51; fi
# Edit the specified note
vim "$VNS_STORE/$1.gpg"
# Update vns_git
vns_git add "$VNS_STORE/$1.gpg" > /dev/null
vns_git commit -m "Edited $1" > /dev/null
}
vns_journal () {
# journal
# Create or edit a Daily Journal Entry under yy/mm/dd
TODAY="$(date '+%y/%m/%d')"
if [ -r "$VNS_STORE/${TODAY}.gpg" ]; then
vns_edit "${TODAY}"
else
VNS_TITLE="Daily Journal for $TODAY" vns_create "${TODAY}"
fi
}
vns_import () {
# import (src) (dest)
# If src is the path to a file, and dest is a valid note name,
# import src dest copies an encrypted version of src to the note store
if [ "$#" -lt 2 ]; then
printf "usage: %s %s\n" "$(basename "$0")" "-i (source) (destination)"
return
fi
if [ ! -f "$1" ]; then
vns_raise "Cannot read $1" 92
elif [ -f "$VNS_STORE/$2" ]; then
printf "%s" "Note already exists. Overwrite? (Y/N) "
local response=''; read -r response
case "$response" in
Y|y)
rm "$VNS_STORE/$2";;
*)
vns_raise "User declined overwrite" 0;;
esac
fi
#printf "%s:" "Specify GPG Public Key: "
#local gpg_key=''; read -r gpg_key
# Create any necessary directories
mkdir -p "$VNS_STORE/$(dirname "$2.gpg")"
if [ "${1##*.}" == "gpg" ]; then
gpg2 --batch --decrypt "$1" | gpg2 --batch "$(vns_gpgid)" -o "$VNS_STORE/$2.gpg" --encrypt
else
gpg2 --batch "$(vns_gpgid)" -o "$VNS_STORE/$2.gpg" --encrypt "$1"
fi
vns_git add "$VNS_STORE/$2.gpg"
vns_git commit -m "Imported file $1 as $2"
}
vns_print () {
# print (note)
# print the given note to stdout
# Verify $1 is bound
if [ "$#" -lt 1 ]; then
vns_raise "Insufficient arguments" 11
# Verify $1 exists
elif ! [ -r "$VNS_STORE/$1.gpg" ]; then
vns_raise "Could not read $1" 12
else
# Decrypt the specified note
gpg2 --batch -d "$VNS_STORE/$1.gpg"
fi
}
vns_mv () {
# mv (src) (dest)
# If (src) is a note in the vns store, mv src dest moves src to dest
# Ensure two arguments were provided
if [ "$#" -lt 2 ]; then vns_raise "Insufficient arguments" 70; fi
# Ensure source can be read
if [ ! -r "$VNS_STORE/$1.gpg" ]; then vns_raise "Cannot read $1" 71; fi
# Refuse to overwrite existing notes
if [ -r "$VNS_STORE/$2.gpg" ]; then vns_raise "Note $2 already exists" 72; fi
# Create any necessary directories
mkdir -p "$VNS_STORE/$(dirname "$2.gpg")"
# Rename the note
vns_git mv "$VNS_STORE/$1.gpg" "$VNS_STORE/$2.gpg"
vns_git commit -am "Renamed $1 -> $2"
}
vns_merge () {
# merge (output) (notes[])
# If notes[] is an array of note names and output is a new note name,
# merge (output) (notes[]) concatenates notes[] into output.
if [ "$#" -lt 2 ]; then
printf "usage: %s %s\n" "$(basename "$0")" "-m (output) (notes...)"
return
fi
if [ -r "$VNS_STORE/$1.gpg" ]; then vns_raise "Output file already exists." 82; fi
if [ ! -r "$VNS_STORE/$2.gpg" ]; then vns_raise "Could not read $2" 83; fi
local -r NOTE_NAME="$1"
local -r OUTPUT="${VNS_STORE}/${1}.gpg"
shift
vns_gpgcat () {
for note in "$@"; do
if [ ! -r "$VNS_STORE/$note.gpg" ]; then
rm "$VNS_STORE/$1.gpg"
vns_raise "Could not read $note" 83;
fi
printf "File: %s\n" "$note"
gpg2 --batch -d "$VNS_STORE/$note.gpg" 2>/dev/null
printf "\n"
done
}
vns_gpgcat "$@" | gpg2 --batch --yes "$(vns_gpgid)" --encrypt -o "$OUTPUT"
printf "%s" "Remove old notes? (y/N) "
local response=''; read -r response
case "$response" in
Y|y)
for note in "$@"; do
vns_git rm "$VNS_STORE/$note.gpg"
done;;
esac
vns_git add "$OUTPUT" >/dev/null 2>&1
vns_git commit -m "merged files $* into $NOTE_NAME" >/dev/null 2>&1
vns_report "Successfully merged files $* into $NOTE_NAME"
}
vns_duplicate () {
if [ "$#" -lt 2 ]; then
vns_report "usage: $(basename "$0") -u (source) (destination)"
return
fi
if [ ! -f "$VNS_STORE/$1.gpg" ]; then
vns_raise "Cannot read $1" 102
elif [ -f "$VNS_STORE/$2.gpg" ]; then
printf "%s" "Note already exists. Overwrite? (Y/N) "
response=''; read -r response
case "$response" in
Y|y)
rm "$VNS_STORE/$2.gpg";;
*)
vns_raise "User declined overwrite" 0;;
esac
fi
mkdir -p "$VNS_STORE/$(dirname $2)"
cp "$VNS_STORE/$1.gpg" "$VNS_STORE/$2.gpg"
vns_git add "$VNS_STORE/$2.gpg"
}
vns_query () {
# query EXPR
# Iterate through notes and find all instances of regular expression EXPR
if [ "$#" -lt 1 ]; then
vns_report "usage: $(basename "$0") -q EXPR"
return
fi
local -r VNS_ULNE="$(tput setaf smul)"
local -r VNS_="$(tput setaf rmul)"
# For each note, grep for QUERY
find "$VNS_STORE" -name "*.gpg" -not -name ".*" | while read -r note; do
# Remove VNS_STORE path and file extension for reporting
note_name="$(sed -e "s#$VNS_STORE/##" -e "s/.gpg//" -e "s#:#\\:#g" <<< "$note")"
# Decrypt note and perform the actual query
local query="$(gpg -d "$note" 2>/dev/null | grep -C 1 --color=always -nE "$1" )"
# Report only matches
if [ -n "${query:-}" ]; then
printf "$(tput smul)%s$(tput rmul)\n" "$note_name"
printf "%s\n" "$query"
fi
done
}
vns_git () {
# Simple passthrough for executing git commands in the VNS store
git -C "$VNS_STORE" "$@"
}
vns () {
# Bypass sanity check if told to initialize store
if [ "$1" != "-I" ]; then
vns_sanityCheck;
fi
primary_arg="$1"
shift
case "$primary_arg" in
"-c")
vns_create "$@"
;;
"-d")
vns_rm "$@"
;;
"-e")
vns_edit "$@"
;;
"-l")
vns_list "$@"
;;
"-h")
vns_printHelp "$@"
;;
"-I")
vns_init "$@"
;;
"-i")
vns_import "$@"
;;
"-j")
vns_journal "$@"
;;
"-m")
vns_merge "$@"
;;
"-p")
vns_print "$@"
;;
"-q")
vns_query "$@"
;;
"-r")
vns_mv "$@"
;;
"-u")
vns_duplicate "$@"
;;
"git")
vns_git "$@"
;;
*)
vns_list "$@"
esac
exit 0;
}
vns "$@"