feat: add agenda script
Import agenda script from my dotfiles.
This commit is contained in:
349
agenda
Normal file
349
agenda
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# The home of all files read and written by agenda.
|
||||||
|
#
|
||||||
|
# If this is left empty, this will default to the xdg user DOCUMENTS directory (in a
|
||||||
|
# child agenda directory), if `xdg-user-dir` is in PATH. Otherwise it will default
|
||||||
|
# to `~/.agenda`.
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# $AGENDA_DIR
|
||||||
|
# ├── {agenda-name}
|
||||||
|
# │ ├── 0000-{backlog-name}.md
|
||||||
|
# │ ├── 1970-01-01.md
|
||||||
|
# │ ├── 1970-01-02.md
|
||||||
|
# │ └── 1970-01-03.md
|
||||||
|
# └── {agenda-name}
|
||||||
|
# ├── 1970-01-01.md
|
||||||
|
# ├── 1970-01-02.md
|
||||||
|
# └── 1970-01-03.md
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
AGENDA_DIR=${AGENDA_DIR:-}
|
||||||
|
|
||||||
|
# The default `-e` option.
|
||||||
|
AGENDA_EXT=${AGENDA_EXT:-"md"}
|
||||||
|
|
||||||
|
# The default name if the `-t` option is omitted.
|
||||||
|
AGENDA_DEFAULT=${AGENDA_DEFAULT:-"agenda"}
|
||||||
|
|
||||||
|
# A template that is automatically written to any created agenda file (excluding
|
||||||
|
# backlogs). A few magic strings are replaced:
|
||||||
|
#
|
||||||
|
# - `{date}` The agenda date (`%Y-%m-%d`)
|
||||||
|
# - `{name}` The agenda name
|
||||||
|
# - `{slug}` The agenda name (slugged)
|
||||||
|
AGENDA_TMPL="${AGENDA_TMPL:-"# {name} {date}\\n\\n- [ ] "}"
|
||||||
|
|
||||||
|
__usage() {
|
||||||
|
fold -s <<-USAGE
|
||||||
|
Usage: ${0##*/} [-bcE] [-e <extension>] [-t <name>] [<date>]
|
||||||
|
${0##*/} <subcommand>
|
||||||
|
|
||||||
|
Organize and edit your agenda in any plain text format.
|
||||||
|
|
||||||
|
-b Backlog (use the date argument as backlog name)
|
||||||
|
-c Create the file if it does not exist
|
||||||
|
-E Instead of editing the file, print the path to STDOUT
|
||||||
|
-e <extension> Specify the file extension
|
||||||
|
-t <name> Specify which agenda to edit
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
edit The default, if subcommand is omitted.
|
||||||
|
list List a range of agenda files.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
__version() {
|
||||||
|
echo "${0##*/} version 0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
__main() {
|
||||||
|
case $* in
|
||||||
|
"--help") __usage && return ;;
|
||||||
|
"--version") __version && return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$AGENDA_DIR" ]; then
|
||||||
|
if type xdg-user-dir >/dev/null 2>&1; then
|
||||||
|
AGENDA_DIR="$(xdg-user-dir DOCUMENTS)/agenda"
|
||||||
|
else
|
||||||
|
AGENDA_DIR="$HOME/.agenda"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Subcommand
|
||||||
|
case ${1:-} in
|
||||||
|
"list"|"ls") __list "${@:2}" ;;
|
||||||
|
"edit") __edit "${@:2}" ;;
|
||||||
|
*) __edit "$@" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
__edit_usage() {
|
||||||
|
fold -s <<-USAGE
|
||||||
|
Usage: ${0##*/} edit [-bcE] [-e <extension>] [-t <name>] [<date>]
|
||||||
|
|
||||||
|
Edit an agenda file.
|
||||||
|
|
||||||
|
-b Backlog (use the date argument as backlog name)
|
||||||
|
-c Create the file if it does not exist
|
||||||
|
-E Instead of editing the file, print the path to STDOUT
|
||||||
|
-e <extension> Specify the file extension
|
||||||
|
-t <name> Specify which agenda to edit
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
__edit() {
|
||||||
|
case $* in
|
||||||
|
"--help") __edit_usage && return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local opt_b opt_c opt_E opt_e opt_t
|
||||||
|
opt_b="no"
|
||||||
|
opt_c="no"
|
||||||
|
opt_E="no"
|
||||||
|
opt_e="$AGENDA_EXT"
|
||||||
|
opt_t="$AGENDA_DEFAULT"
|
||||||
|
while getopts 'bcEe:t:' opt; do
|
||||||
|
case $opt in
|
||||||
|
b)
|
||||||
|
opt_b="yes"
|
||||||
|
;;
|
||||||
|
c)
|
||||||
|
opt_c="yes"
|
||||||
|
;;
|
||||||
|
E)
|
||||||
|
opt_E="yes"
|
||||||
|
;;
|
||||||
|
e)
|
||||||
|
opt_e=$OPTARG
|
||||||
|
;;
|
||||||
|
t)
|
||||||
|
opt_t=$OPTARG
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
__edit_usage && return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $(( OPTIND - 1 ))
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
if [ "$opt_b" = "yes" ]; then
|
||||||
|
set -- "backlog"
|
||||||
|
else
|
||||||
|
set -- "today"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base ext slug file
|
||||||
|
if [ "$opt_b" = "yes" ]; then
|
||||||
|
base="0000-$(___slugify "$*")"
|
||||||
|
else
|
||||||
|
base=$(date -d "$*" +%F)
|
||||||
|
fi
|
||||||
|
ext=$opt_e
|
||||||
|
slug=$(___slugify "$opt_t")
|
||||||
|
file="$AGENDA_DIR/$slug/$base.$ext"
|
||||||
|
|
||||||
|
# Existing files may overwrite the `-e` option.
|
||||||
|
local exists
|
||||||
|
for exists in "${file%."$ext"}".*; do
|
||||||
|
if [ -e "$exists" ] && [ "$file" != "$exists" ]; then
|
||||||
|
echo "${0##*/}: ignoring '$ext' extension: '$exists' already exists" >&2
|
||||||
|
ext="${exists##*.}"
|
||||||
|
file="$exists"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! [ -f "$file" ]; then
|
||||||
|
if [ "$opt_c" = "yes" ]; then
|
||||||
|
__agenda_create "$file" "$opt_b" "$opt_t" "$slug" "$base"
|
||||||
|
else
|
||||||
|
echo "${0##*/}: cannot access '$file': No such file" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$opt_E" = "no" ]; then
|
||||||
|
export AGENDA_FILE="$file"
|
||||||
|
exec ${VISUAL:-${EDITOR:-vi}} "$file"
|
||||||
|
else
|
||||||
|
echo "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
__list_usage() {
|
||||||
|
fold -s <<-USAGE
|
||||||
|
Usage: ${0##*/} list [-ac] [-u <date>] [-t <name>] [<date>]
|
||||||
|
|
||||||
|
List a range of agenda files.
|
||||||
|
|
||||||
|
-a Print the files' absolute path
|
||||||
|
-c Print the files' content
|
||||||
|
-u <date> Stop listing at <date>
|
||||||
|
-t <name> Specify which agenda to list
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
__list() {
|
||||||
|
case $* in
|
||||||
|
"--help") __list_usage && return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local opt opt_a opt_c opt_u opt_t
|
||||||
|
opt_a="no"
|
||||||
|
opt_c="no"
|
||||||
|
opt_u="next week" # FIXME: Not portable.
|
||||||
|
opt_t=""
|
||||||
|
while getopts 'abcu:t:' opt; do
|
||||||
|
case $opt in
|
||||||
|
a)
|
||||||
|
opt_a="yes"
|
||||||
|
;;
|
||||||
|
c)
|
||||||
|
opt_c="yes"
|
||||||
|
;;
|
||||||
|
u)
|
||||||
|
opt_u="$OPTARG"
|
||||||
|
;;
|
||||||
|
t)
|
||||||
|
opt_t="$OPTARG"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
list_usage
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $(( OPTIND - 1 ))
|
||||||
|
|
||||||
|
# TODO: Check if stdout is a file that is a terminal or a pipe that is
|
||||||
|
# connected to a terminal.
|
||||||
|
local padlength
|
||||||
|
padlength=80
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
# FIXME: Not portable.
|
||||||
|
set -- "last week"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local date until dir
|
||||||
|
date=$(date -d "$*" +%F)
|
||||||
|
until=$(date -d "$opt_u" +%F)
|
||||||
|
if [ -n "$opt_t" ]; then
|
||||||
|
slug=$(slugify "$opt_t")
|
||||||
|
dir="$AGENDA_DIR/$slug"
|
||||||
|
else
|
||||||
|
dir="$AGENDA_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -d "$dir" ]; then
|
||||||
|
echo "${0##*/}: cannot access '$dir': Directory does not exist" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local date_cmd until_cmd
|
||||||
|
date_cmp="${date%%-*}${date#*-}"
|
||||||
|
date_cmp="${date_cmp%%-*}${date_cmp#*-}"
|
||||||
|
until_cmp="${until%%-*}${until#*-}"
|
||||||
|
until_cmp="${until_cmp%%-*}${until_cmp#*-}"
|
||||||
|
|
||||||
|
while read -r REPLY; do
|
||||||
|
if ! [ -f "$REPLY" ]; then
|
||||||
|
echo "${0##*/}: cannot access '$REPLY': File does not exist" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file_date
|
||||||
|
file_date="${REPLY##*/}"
|
||||||
|
file_date="${file_date%.*}"
|
||||||
|
|
||||||
|
local file_date_cmp
|
||||||
|
file_date_cmp="${file_date%%-*}${file_date#*-}"
|
||||||
|
file_date_cmp="${file_date_cmp%%-*}${file_date_cmp#*-}"
|
||||||
|
|
||||||
|
# Agenda may allow special files that are not named by date in the
|
||||||
|
# future.
|
||||||
|
# https://stackoverflow.com/a/18620446
|
||||||
|
case $file_date_cmp in
|
||||||
|
*[!0-9]* | '') continue ;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# This was originally written in POSIX shell. Let's keep it that way for
|
||||||
|
# now.
|
||||||
|
# https://unix.stackexchange.com/a/84472
|
||||||
|
if [ "$file_date_cmp" -ge "$date_cmp" ] && [ "$file_date_cmp" -le "$until_cmp" ]; then
|
||||||
|
local file_name
|
||||||
|
file_name="$REPLY"
|
||||||
|
if [ "$opt_a" = "no" ]; then
|
||||||
|
file_name="${file_name#"$AGENDA_DIR/"}"
|
||||||
|
file_name="${file_name%.*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$opt_c" = "yes" ]; then
|
||||||
|
printf "%*s\n" "$padlength" "[$file_name]" | tr " " "-"
|
||||||
|
cat "$REPLY"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "$file_name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(
|
||||||
|
# Sort the paths by basename.
|
||||||
|
find "$dir" -type f \
|
||||||
|
| __agenda_sort_prefix \
|
||||||
|
| sort \
|
||||||
|
| __agenda_strip_sort_prefix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
__agenda_create() {
|
||||||
|
local file=$1 backlog=$2 name=$3 slug=$4 date=$5
|
||||||
|
|
||||||
|
if [ -e "$file" ]; then
|
||||||
|
echo "${0##*/}: cannot create '$file': File already exists" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local content
|
||||||
|
if [ "$backlog" = "no" ]; then
|
||||||
|
content=$(echo "$AGENDA_TMPL" \
|
||||||
|
| sed "s/{date}/$date/g" \
|
||||||
|
| sed "s/{name}/$name/g" \
|
||||||
|
| sed "s/{slug}/$slug/g")
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${file%/*}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2059
|
||||||
|
printf "${content:-}" > "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/49035906
|
||||||
|
___slugify() {
|
||||||
|
echo "$1" \
|
||||||
|
| iconv -c -t ascii//TRANSLIT \
|
||||||
|
| sed -E "s/[~^]+//g" \
|
||||||
|
| sed -E "s/[^a-zA-Z0-9]+/-/g" \
|
||||||
|
| sed -E "s/^-+|-+$//g" \
|
||||||
|
| tr "[:upper:]" "[:lower:]"
|
||||||
|
}
|
||||||
|
|
||||||
|
__agenda_sort_prefix() {
|
||||||
|
while read -r REPLY; do
|
||||||
|
echo "${REPLY##*/}---$REPLY"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
__agenda_strip_sort_prefix() {
|
||||||
|
while read -r REPLY; do
|
||||||
|
echo "${REPLY#*---}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
__main "$@"
|
||||||
|
|
||||||
|
# vi: noet sts=4 sw=4 tw=84
|
||||||
Reference in New Issue
Block a user