#!/usr/bin/env bash set -o errexit # # n! (nexclamation or nfactorial) -- command line note taking. # Homepage: # # COPYING # # MIT License # # Copyright (c) 2021-2022 ge # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. n_version=0.0.11 # Current n! version. # Defaults. # All of them can be overrided in $n_config. # Main config. n_last_opened=$HOME/.local/share/nexclamation/last_opened # Path ot save notes. NPATH="${NPATH:-$HOME/.local/share/nexclamation/notes}" # Editor. # User's default editor. Set this value for override. NEDITOR="${NEDITOR:-$EDITOR}" # Quick note filename. NQNOTENAME="${NQNOTENAME:-NOTE}" # Color scheme. R="\e[31m" # red B="\e[34m" # blue N="\e[0m" # no color b="\e[1m" # bold font n_version() { cat << EOF n! (nexclamation or 'n factorial') $n_version Copyright (C) 2021-2022 ge License MIT: . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. EOF exit 0 } n_help() { cat << EOF Command line note taking. Usage: n! [-v|--version] [-h|--help] [] [...] Commands and options: q, quick [] take a quick note in current directory. s, search search in notes via grep. l, last open last opened file in editor. mkdir add new directory. Creates new dir in NPATH. ls [] list all notes, or notes from in NPATH. lsd list dirs in NPATH (empty dirs too). rm [-f|--force] remove notes or directories. i, info [] print info about notes and configuration. -h, --help print this help message and exit. -v, --version print version and exit. Examples: Take a new note or open existing note: n! [] Take new note in 'work' directory: n! work/my_note Environment: n! uses user's default editor. You can specify editor in EDITOR environment variable in your .bashrc or .bash_profile, or select default editor by 'select-editor' command. Also you can set specific editor for nexclamation in ~/.nexclamation file. For example: NEDITOR=/usr/bin/vim.tiny Other configuration options: NPATH path to save notes. Default: $HOME/.local/share/nexclamation/notes NQNOTENAME default file name for quick notes. Default: NOTE EOF exit 0 } n_err() { echo -e "$1" >&2 echo -e "See 'n! --help' or 'nexclamation --help' for info." >&2 exit 1 } # ------------------------------------------------------------------------- # # App init # # ------------------------------------------------------------------------- # n_initialise() { # Make $NPATH if not exists. if [ ! -d "$NPATH" ]; then mkdir -p "$NPATH" 2>/dev/null fi # Set editor. n_set_editor } n_set_editor() { get_selected_editor() { # shellcheck source=/dev/null source "$HOME/.selected_editor" echo "$SELECTED_EDITOR" } # Detect default editor. # Do nothing if editor is set in $n_config if [ -z "$EDITOR" ]; then NEDITOR="$(get_selected_editor)" elif [ -f /usr/bin/select-editor ]; then select-editor NEDITOR="$(get_selected_editor)" else NEDITOR=/usr/bin/vi fi } # ------------------------------------------------------------------------- # # Functions # # ------------------------------------------------------------------------- # n_take_note() { # Take a note. # # Arguments: # $@ -- file names # Examples of $1: # my_note.txt # some_dir/my_note.txt if [[ -n "$*" ]]; then for file in "$@"; do if [[ "$file" =~ .+/.+ ]]; then # if filename contains path if [ -d "$NPATH/${file%/*}" ]; then files+=("$NPATH/$file") else echo -n "${0##*/}: $NPATH/${file%/*}" >&2 echo ": destination does not exist" >&2 echo "Run 'n! mkdir ' to create new directory." >&2 exit 1 fi else files+=("$NPATH/$file") fi done "$NEDITOR" "${files[@]}" else cd "$NPATH" || { echo "${0##*/}: Cannot cd into $NPATH"; exit 1; } "$NEDITOR" cd - >/dev/null || { echo "${0##*/}: Cannot cd into $OLDPWD"; exit 1; } fi } n_quick_note() { # Take note in current working directory. # # Default $NQNOTENAME can be set in ~/.nexclamation file. if [[ -n "$*" ]]; then local temp="$*" NQNOTENAME="${temp%% *}" fi echo "${PWD}/${NQNOTENAME}" > "$n_last_opened" "$NEDITOR" "$NQNOTENAME" exit 0 # Prevent new note taking } n_last() { # Open last opened file. # # Filename is saved in service file $n_last_opened if [ -f "$n_last_opened" ]; then local file file="$(cat "$n_last_opened")" else echo "${0##*/}: No opened files yet" >&2; exit 1 fi if [ -f "$file" ]; then "$NEDITOR" "$file"; exit 0 else echo "${0##*/}: $file: No such file" >&2; exit 1 fi } n_search() { # Search in notes. # # $1 is search query. while (( "$#" )); do case "$1" in -h|--help) n_help;; -v|--version) n_version;; -*) n_err "${0##*/}: $1: Unknown option";; *) local q="$1"; shift;; esac done cd "$NPATH" || { echo "${0##*/}: Cannot cd into $NPATH"; exit 1; } grep --recursive --ignore-case --line-number --color=auto "$q" cd - >/dev/null || { echo "${0##*/}: Cannot cd into $OLDPWD"; exit 1; } } n_mkdir() { # Create new directory in $NPATH # # $1 -- dirname while (( "$#" )); do case "$1" in -h|--help) n_help;; -v|--version) n_version;; -*) n_err "${0##*/}: $1: Unknown option";; *) local dir="$1"; shift;; esac done mkdir -p "${NPATH}/${dir}" 2>/dev/null echo -e "${b}Created:${N} ${NPATH}/${B}${b}${dir}/${N}" exit 0 } n_list() { # List files from dir or dirs. while (( "$#" )); do case "$1" in -d|--dirs) list_dirs=1; shift;; -h|--help) n_help;; -v|--version) n_version;; -*) n_err "${0##*/}: $1: Unknown option";; *) local dir="$1"; shift;; esac done if [ ${#dir[@]} -gt 1 ]; then echo -e "${0##*/}: too many arguments" >&2; exit 1 fi if [ ! -d "${NPATH}/${dir}" ]; then echo -e \ "${0##*/}: ${NPATH}/${dir}: No such directory" >&2 exit 1 fi if [ -n "$list_dirs" ]; then # list only dirs (append / for coloring) list="$(find "$NPATH" -type d -exec echo {}/ \;)" elif [ "$dir" ]; then # list files from specific directory list="$(find "${NPATH}/${dir}" -type f)" else # list all of files and dirs list="$(find "$NPATH" -type f)" fi # Print output. for path in $(echo "$list" | sed -E "s%${NPATH}/?%%g;/^$/d"); do # Add color for dirs (blue) with brainfucking parameter expansion local temp temp="${path//\//\\/}" # Escape slashes # shellcheck disable=SC2059 echo "${path}" | sed "/${temp%\\*}\//s//$(printf "${B}${b}${temp%/*}\/${N}")/" done | sort exit 0 } n_remove() { # Remove files or directories. while (( "$#" )); do case "$1" in -f|--force) forced=1; shift;; -h|--help) n_help;; -v|--version) n_version;; -*) n_err "${0##*/}: $1: Unknown option";; *) local files+=("$1"); shift;; esac done if [ ${#files[@]} -eq 0 ]; then echo -e "${0##*/}: Nothing to remove" >&2; exit 1 fi if [ ! "$forced" ]; then echo -e "These files will be removed: ${R}${files[*]}${N}" | fmt -t while [ -z "$answer" ]; do echo -en "Remove files permanently? (y/n) " read -r reply case "${reply,,}" in y|yes) answer=1;; n|no) echo "Abort"; exit 1;; *) echo 'Please, answer y or n';; esac done fi for file in "${files[@]}"; do file="${NPATH}/${file}" if [ -d "$file" ]; then rm -rf "$file" echo -e "${b}Removed:${N} $file" elif [ -f "$file" ]; then rm -f "$file" echo -e "${b}Removed:${N} $file" else echo "${0##*/}: $file: No such file or directory" >&2; exit 1 fi done exit 0 } n_info() { # Show information about notes and configuration. while (( "$#" )); do case "$1" in -h|--help) n_help;; -v|--version) n_version;; -*) n_err "${0##*/}: $1: Unknown option";; *) local file="$1"; shift;; esac done if [ -n "$file" ]; then file="$NPATH/$file" if [ ! -f "$file" ]; then echo "${0##*/}: $file: No such file" >&2; exit 1 fi { echo -e "${b}Lines:${N}|$(wc -l "$file" | tail -n 1 | awk '{print $1}')" echo -e "${b}Words:${N}|$(wc -w "$file" | tail -n 1 | awk '{print $1}')" echo -e "${b}Size:${N}|$(du -hs "$file" | cut -f 1)" } | column -t -s '|' exit 0 fi local all_files local all_dirs all_files="$(find "$NPATH" -type f)" all_dirs="$(find "$NPATH" -type d)" { echo -e "${b}Editor:${N}|${NEDITOR}" echo -e "${b}Quick notes:${N}|${NQNOTENAME}" echo -e "${b}Notes save path:${N}|${NPATH}" echo -e "${b}Dirs:${N}|$(<<< "$all_dirs" wc -l)" echo -e "${b}Files:${N}|$(<<< "$all_files" wc -l)" # shellcheck disable=SC2001 echo -e "${b}Total lines:${N}|$(<<< "$all_files" sed 's/.*/"&"/' | xargs wc -l | tail -n 1 | awk '{print $1}')" # shellcheck disable=SC2001 echo -e "${b}Total words:${N}|$(<<< "$all_files" sed 's/.*/"&"/' | xargs wc -w | tail -n 1 | awk '{print $1}')" echo -e "${b}Total size:${N}|$(du -hs "$NPATH" | cut -f 1)" echo -e "${b}Last opened:${N}|$([ -f "$n_last_opened" ] && \ cat "$n_last_opened" || echo 'no opened files yet')" } | column -t -s '|' exit 0 } # ------------------------------------------------------------------------- # # n! # # ------------------------------------------------------------------------- # n_checkopt() { if [ "$2" ]; then return 0 else n_err "${0##*/}: Missing argument for $1" fi } n_initialise while (( "$#" )); do case "$1" in q|quick) shift; n_quick_note "$@"; shift "$#";; l|last) n_last;; s|search) n_checkopt "$1" "$2"; n_search "$2"; exit 0;; mkdir) n_checkopt "$1" "$2"; n_mkdir "$2"; exit 0;; ls) shift; n_list "$@"; shift "$#";; lsd) shift; n_list -d; shift "$#";; rm) shift; n_remove "$@"; shift "$#";; i|info) shift; n_info "$@"; shift "$#";; -v|--version) n_version;; -h|--help) n_help;; -*) n_err "${0##*/}: $1: Unknown option";; *) pos_args+=("$1"); shift;; esac done if [ "${#pos_args[@]}" -gt 0 ]; then echo "${NPATH}/${pos_args[-1]}" > "$n_last_opened" fi n_take_note "${pos_args[@]}"