From a0b5dfe0e6394cd7c92110752391cf9079379aee Mon Sep 17 00:00:00 2001 From: ge Date: Thu, 7 Jul 2022 23:31:13 +0300 Subject: [PATCH] init --- .gitignore | 1 + Makefile | 37 +++++ README.md | 33 ++++ completion | 69 +++++++++ nexclamation | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 570 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100755 completion create mode 100755 nexclamation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df78ade --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.installation_prefix diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f43a9a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +BIN := "nexclamation" +SYMLINK := "n!" +COMPL := "completion" +TMP := "./.installation_prefix" + +all: + @echo Nothing to do. Available targets: install, uninstall, set-prefix + @echo + @echo Deafult PREFIX for root user: /usr/local + @echo Deafult PREFIX for non-root user: ~/.local + @echo Set up custom installation PREFIX by: + @echo make PREFIX=/your/path install + +install: set-prefix + $(eval PREFIX := $(shell cat $(TMP))) + @echo Installation PREFIX $(PREFIX) + COMPDIR="$(PREFIX)/share/bash-completion/completions"; \ + mkdir -p "$(PREFIX)" && \ + mkdir -p "$$COMPDIR" && \ + cp "$(BIN)" "$(PREFIX)/bin/$(BIN)" && \ + ln -s "$(PREFIX)/bin/$(BIN)" "$(PREFIX)/bin/$(SYMLINK)" && \ + cp "$(COMPL)" "$$COMPDIR/$(BIN)" + @echo Successfully installed + +uninstall: + $(eval PREFIX := $(shell cat $(TMP))) + @echo Installation PREFIX $(PREFIX) + COMPDIR="$(PREFIX)/share/bash-completion/completions"; \ + rm -f "$(PREFIX)/bin/$(BIN)" && \ + rm -f "$(PREFIX)/bin/$(SYMLINK)" && \ + rm -f "$$COMPDIR/$(BIN)" + @echo Successfully uninstalled + +set-prefix: + if [ "$$UID" == "0" ]; then \ + echo $${PREFIX:-/usr/local} > $(TMP); else \ + echo $${PREFIX:-$$HOME/.local} > $(TMP); fi; diff --git a/README.md b/README.md new file mode 100644 index 0000000..015138f --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# n! + +**n!** is shell powered note taking tool. Notes are stored locally in `$HOME/.local/nexclamation/notes` by default as plain text. + +**n!** depends on Bash, GNU coreutils, find, sed, awk. Usually these programs are already installed on most Linux distros. + +# Installation + +``` +make install +``` + +Uninstall by: + +``` +make uninstall +``` + +# Usage + +To create new note just run `n!` or: + +``` +n! note.md +``` + +See more help at: + +``` +n! --help +``` + +If you're having trouble using an exclamation mark in a command name, use command `nexclamation` instead of `n!`. diff --git a/completion b/completion new file mode 100755 index 0000000..c47d8f6 --- /dev/null +++ b/completion @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# n! (nexclamation) v0.0.11 completion script. +# Homepage: + +NPATH="${NPATH:-$HOME/.local/share/nexclamation/notes}" + +_n_list_dirs() { + find "$NPATH" -type d -exec echo {}/ \; | sed -E "s%$NPATH/?%%g;/^$/d"; +} + +_n_list_files() { + find "$NPATH" -type f | sed -E "s%$NPATH/?%%g;/^$/d" +} + +_n_get_opts() { + local all_opts="$1" + + # Find matched opts. + local used_opts="$(echo "${COMP_WORDS[@]} $all_opts" \ + | tr ' ' '\n' | sort | uniq -d \ + )" + + if [ "$used_opts" ]; then + # Delete 'help' option. + all_opts="$(sed 's%help%%' <<< "$all_opts")" + # Delete opts if match. + for opt in $used_opts; do + all_opts="$(sed "s%$opt%%" <<< "$all_opts")" + done + fi + echo "$all_opts" +} + +_nexclamation() { + local cur prev + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + case ${COMP_CWORD} in + 1) # Commands and options + COMPREPLY=($(compgen -W \ + "-v --version -h --help + q quick s search l last mkdir ls lsd rm i info + $(_n_list_dirs) $(_n_list_files)" -- ${cur})) + ;; + 2) # Subcommand completion + case ${prev} in + ls) COMPREPLY=($(compgen -W "$(_n_list_dirs)" -- ${cur})) + ;; + rm) COMPREPLY=($(compgen -W "-f --force + $(_n_list_dirs) $(_n_list_files)" -- ${cur})) + ;; + i|info) COMPREPLY=($(compgen -W "$(_n_list_files)" -- ${cur})) + ;; + *) COMPREPLY=() + ;; + esac;; + *) # Complete file and directory names + case ${COMP_WORDS[2]} in + *) + COMPREPLY=($(compgen -W \ + "$(_n_get_opts "$(_n_list_dirs) $(_n_list_files)")" -- ${cur})) + ;; + esac;; + esac +} + +complete -F _nexclamation nexclamation +complete -F _nexclamation n! diff --git a/nexclamation b/nexclamation new file mode 100755 index 0000000..7a69fb9 --- /dev/null +++ b/nexclamation @@ -0,0 +1,430 @@ +#!/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[@]}"