This commit is contained in:
ge 2022-07-07 23:31:13 +03:00
commit a0b5dfe0e6
5 changed files with 570 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.installation_prefix

37
Makefile Normal file
View File

@ -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;

33
README.md Normal file
View File

@ -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!`.

69
completion Executable file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# n! (nexclamation) v0.0.11 completion script.
# Homepage: <http://nixhacks.net/nexclamation>
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!

430
nexclamation Executable file
View File

@ -0,0 +1,430 @@
#!/usr/bin/env bash
set -o errexit
#
# n! (nexclamation or nfactorial) -- command line note taking.
# Homepage: <http://nixhacks.net/nexclamation>
#
# COPYING
#
# MIT License
#
# Copyright (c) 2021-2022 ge <http://nixhacks.net/nexclamation>
#
# 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 <http://nixhacks.net/nexclamation>
License MIT: <https://opensource.org/licenses/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] [<command>] [<args>...]
Commands and options:
q, quick [<name>] take a quick note in current directory.
s, search <query> search in notes via grep.
l, last open last opened file in editor.
mkdir <dir> add new directory. Creates new dir in NPATH.
ls [<dir>] list all notes, or notes from <dir> in NPATH.
lsd list dirs in NPATH (empty dirs too).
rm [-f|--force] <file> remove notes or directories.
i, info [<file>] 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! [<note>]
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 <dir>' 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[@]}"