From 12ba1d3326373cc3fba0f546835f96daee4e0e02 Mon Sep 17 00:00:00 2001 From: Jonas Kattendick Date: Tue, 4 Jun 2024 12:06:12 +0200 Subject: [PATCH] feat!: add builtin templates WIP: document, check for breaking changes, decide on version, etc. --- CHANGELOG.md | 7 ++++ Makefile | 12 ++++-- agenda | 93 +++++++++++++++++++++++++++++++---------------- agenda.1 | 54 ++++++++++++++++++++++++--- templates/default | 6 +++ templates/md | 42 +++++++++++++++++++++ templates/xit | 56 ++++++++++++++++++++++++++++ 7 files changed, 228 insertions(+), 42 deletions(-) create mode 100755 templates/default create mode 100755 templates/md create mode 100755 templates/xit diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dba1e2..6ad04f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,4 +12,11 @@ Initial release. - Edit, list todo notes by any date format understood by `date(1)` - Write custom template programs to generate content for newly created notes +## v0.2.0 + +### Added + +- Builtin templates for the markdown and xit formats (`.md` and `.xit` + extensions); the templates carry over open tasks from previous agenda notes + [changelog.md]: https://changelog.md/ diff --git a/Makefile b/Makefile index 5da5863..3774041 100644 --- a/Makefile +++ b/Makefile @@ -6,15 +6,19 @@ PREFIX := /usr/local .PHONY: install install: ./agenda.1.gz install -m 755 -D ./agenda $(PREFIX)/bin/agenda + mkdir -p $(PREFIX)/share/agenda/templates + install -m 755 ./templates/* $(PREFIX)/share/agenda/templates install -m 644 -D ./agenda.1.gz $(PREFIX)/share/man/man1/agenda.1.gz install -m 644 -D ./completion.bash $(PREFIX)/share/bash-completion/completions/agenda .PHONY: uninstall uninstall: - rm -f $(PREFIX)/bin/agenda - rm -f $(PREFIX)/share/man/man1/agenda.1.gz - rm -f $(PREFIX)/share/bash-completion/completions/agenda + $(RM) $(PREFIX)/bin/agenda + $(RM) $(PREFIX)/share/agenda/templates/* + $(RM) $(PREFIX)/share/man/man1/agenda.1.gz + $(RM) $(PREFIX)/share/bash-completion/completions/agenda + rmdir $(PREFIX)/share/agenda/templates $(PREFIX)/share/agenda .PHONY: clean clean: - rm -f ./agenda.1.gz + $(RM) ./agenda.1.gz diff --git a/agenda b/agenda index df998d5..179ba81 100644 --- a/agenda +++ b/agenda @@ -17,6 +17,9 @@ AGENDA_EDITOR=${AGENDA_EDITOR:-${VISUAL:-${EDITOR:-vi}}} # AGENDA_DIR=${AGENDA_DIR:-} +# Location of additional files of an agenda installation. +AGENDA_DATA_DIRS=${AGENDA_DATA_DIRS:-} + # The default `-e` option. AGENDA_EXTENSION=${AGENDA_EXTENSION:-"md"} @@ -24,7 +27,10 @@ AGENDA_EXTENSION=${AGENDA_EXTENSION:-"md"} AGENDA_DEFAULT=${AGENDA_DEFAULT:-"agenda"} # Command that provides the initial contents of new agenda files. -AGENDA_TEMPLATE=${AGENDA_TEMPLATE:-"__agenda_default_template"} +# +# If this is null AGENDA_PATH is searched for an executable in the template +# subdirectory with the same name as AGENDA_EXTENSION. +AGENDA_TEMPLATE=${AGENDA_TEMPLATE:-} __usage() { fold -s <<-USAGE @@ -56,7 +62,15 @@ __main() { "--version") __version && return ;; esac + if [ -z "$AGENDA_DATA_DIRS" ]; then + # FIXME: As per spec only absolute paths should be accepted! + AGENDA_DATA_DIRS="${XDG_CONFIG_HOME:-"$HOME/.config"}/agenda" + AGENDA_DATA_DIRS="$AGENDA_DATA_DIRS:/usr/local/share/agenda" + AGENDA_DATA_DIRS="$AGENDA_DATA_DIRS:/usr/share/agenda" + fi + if [ -z "$AGENDA_DIR" ]; then + # FIXME: As per spec only absolute paths should be accepted! AGENDA_DIR="${XDG_DATA_HOME:-"$HOME/.local/share"}/agenda" fi @@ -291,6 +305,7 @@ __list() { fi done < <( # Sort the paths by basename. + # TODO: Inline the prefix functions and explain this. find "$dir" -type f \ | __agenda_sort_prefix \ | sort \ @@ -312,27 +327,41 @@ __agenda_create() { tmp_err=$(mktemp) if [ "$backlog" = "no" ]; then - # Capture exit code of the template command. - local code - set +e - "$AGENDA_TEMPLATE" > "$tmp" 2> "$tmp_err" - code=$? - set -e - - if [ $code -eq 127 ]; then - echo "${0##*/}: cannot create '$file': template '$AGENDA_TEMPLATE' does not exist" >&2 - return 127 + # Search for a template program if the AGENDA_TEMPLATE variable is empty. + if [ -z "$AGENDA_TEMPLATE" ]; then + AGENDA_TEMPLATE=$( + __agenda_get_template_path "$AGENDA_EXTENSION" || + __agenda_get_template_path "default" || + true + ) fi - if [ -s "$tmp_err" ]; then - while read -r REPLY; do - printf "agenda: $AGENDA_TEMPLATE: %s\n" "${REPLY#"${0}: line"*": "}" >&2 - done < "$tmp_err" - fi + # If a template program is set, attempt to use it to create the new agenda + # note file. + if [ -n "$AGENDA_TEMPLATE" ]; then + local code + set +e + "$AGENDA_TEMPLATE" > "$tmp" 2> "$tmp_err" + code=$? + set -e - if [ $code -gt 0 ]; then - echo "${0##*/}: cannot create '$file': template error" >&2 - return $code + if [ $code -eq 127 ]; then + echo "${0##*/}: cannot create '$file': template '$AGENDA_TEMPLATE' does not exist" >&2 + return 127 + fi + + if [ -s "$tmp_err" ]; then + while read -r REPLY; do + printf "agenda: $AGENDA_TEMPLATE: %s\n" "${REPLY#"${0}: line"*": "}" >&2 + done < "$tmp_err" + fi + + if [ $code -gt 0 ]; then + echo "${0##*/}: cannot create '$file': template error" >&2 + return $code + fi + else + touch "$tmp" fi fi @@ -340,19 +369,6 @@ __agenda_create() { mv "$tmp" "$file" } -__agenda_default_template() { - case $AGENDA_EXTENSION in - "md") - printf "# %s %s\n\n- [ ] " "$AGENDA_NAME" "$AGENDA_DATE" - ;; - "txt"|"xit"|"") - printf "%s %s\n[ ] " "$AGENDA_NAME" "$AGENDA_DATE" - ;; - *) - ;; - esac -} - __agenda_last() { local file=$1 name=$2 @@ -375,6 +391,19 @@ __agenda_last() { echo "${last:-}" } +__agenda_get_template_path() { + local template=$1 + + local IFS=":" path + for path in $AGENDA_DATA_DIRS; do + if [ -x "$path/templates/$template" ]; then + AGENDA_TEMPLATE="$path/templates/$template" + return + fi + done + return 1 +} + # https://stackoverflow.com/a/49035906 __slugify() { echo "$1" \ diff --git a/agenda.1 b/agenda.1 index c10c0d5..64d59ba 100644 --- a/agenda.1 +++ b/agenda.1 @@ -1,4 +1,4 @@ -.TH AGENDA 1 "2024-06-07" "agenda 0.1.0" +.TH AGENDA 1 "2024-06-07" "agenda 0.2.0" \#============================================================================= .SH NAME agenda \- Manage daily tasks and notes in any plain text format. @@ -107,7 +107,26 @@ The editor process has access to certain environment variables. \fBAGENDA_DIR\fP .RS 4 The home of all agenda note files read and written by agenda. If this is empty -`${XDG_DATA_HOME:-"$HOME/.local/share"}/agenda` will be used. +\fBXDG_DATA_HOME\fP\fI/agenda\fP will be used. +.PP +.RE +\# +\fBAGENDA_DATA_DIRS\fP +.RS 4 +Colon delimited Paths to additional files that come with an agenda installation +and custom overrides. Currently only used for template programs (located in the +\fItemplate/\fP sub-directory). If a file is searched in these paths the first file +that is found is used. + +.nr PI 2n +If this variable is not set or empty it is set to include (in order) +.IP \[bu] +\fBXDG_CONFIG_HOME\fP\fI/agenda\fP +.IP \[bu] +\fI/usr/local/share/agenda\fP +.IP \[bu] +\fI/usr/share/agenda\fP + .PP .RE \# @@ -127,10 +146,14 @@ The default agenda name if the \fI-t\fP option is omitted. This falls back to \# \fBAGENDA_TEMPLATE\fP .RS 4 -If this is not empty the value is interpreted as a command and executed when -creating a new agenda note with the \fI-c\fP flag. Any std output of the -command is written to the new file. The template process has access to certain -environment variables. +If this variable is not empty the value is interpreted as a command and +executed when creating a new agenda note with the \fI-c\fP flag. Any std output +of the command is written to the new file. The template process has access to +certain environment variables. +.PP +If this variable is unset or empty \fBAGENDA_DATA_DIRS\fP is searched for an +executable in the \fItemplates\fP sub-directory that is named either like the +corresponding extension or "default". .sp 1 The following example will copy the last agenda note to the new agenda note file. @@ -153,6 +176,10 @@ Note however that this example is specific to \fBbash(1)\fP, since only bash can export functions this way. In general you should write your template as a script, save it, and set it as executable. .PP +To set/override a template for a certain extension (e.g. markdown), save the +program to one of the \fBAGENDA_DATA_DIRS\fP, e.g. +\fI\fBXDG_CONFIG_HOME\fP/agenda/templates/md\fP. +.PP \#----------------------------------------------------------------------------- .SS "Templates and editor" The template command and editor processes have access to certain environment @@ -188,6 +215,21 @@ The file path of the agenda note. The last agenda file. Can be empty if no previous agenda note exists. .PP .RE +.SS "Other" +\# +\fBXDG_CONFIG_HOME\fP +.RS 4 +The default user configuration path. If this is not set the default of +\fBHOME\fP\fI/.config\fP is assumed. +.PP +.RE +\# +\fBXDG_DATA_HOME\fP +.RS 4 +The default user data path. If this is not set the default of +\fBHOME\fP\fI/.local/share\fP is assumed. +.PP +.RE \#============================================================================= .SH "SEE ALSO" \fBdate(1)\fP diff --git a/templates/default b/templates/default new file mode 100755 index 0000000..65fbe07 --- /dev/null +++ b/templates/default @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu +type agenda >/dev/null + +# Capitalize the title. +echo "$AGENDA_NAME $AGENDA_DATE" | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}' diff --git a/templates/md b/templates/md new file mode 100755 index 0000000..5911fdd --- /dev/null +++ b/templates/md @@ -0,0 +1,42 @@ +#!/bin/sh +set -eu +type agenda >/dev/null + +if [ "${AGENDA_EXTENSION:-}" != "md" ]; then + exit +fi + +# Capitalize the title. +echo "$AGENDA_NAME $AGENDA_DATE" | awk '{print "# "toupper(substr($0,0,1))tolower(substr($0,2))}' +echo + +if [ -n "${AGENDA_IS_BACKLOG:-}" ]; then + exit +fi + +# Carry over open tasks from the last agenda markdown file. +if [ -e "$AGENDA_LAST" ] && [ "${AGENDA_LAST#*.}" = "md" ]; then + while IFS= read -r REPLY; do + case ${state:-} in + "open_task") + case $REPLY in + "- [ ] "*|" "*) + # Append indented lines to open tasks. + echo "$REPLY" + ;; + *) + unset state + ;; + esac + ;; + *) + case $REPLY in + "[ ] "*|"[@] "*|"[?] "*) + state="open_task" + echo "$REPLY" + ;; + esac + ;; + esac + done < "$AGENDA_LAST" +fi diff --git a/templates/xit b/templates/xit new file mode 100755 index 0000000..4953a84 --- /dev/null +++ b/templates/xit @@ -0,0 +1,56 @@ +#!/bin/sh +set -eu +type agenda >/dev/null + +if [ "${AGENDA_EXTENSION:-}" != "xit" ]; then + exit +fi + +# Capitalize the title. +echo "$AGENDA_NAME $AGENDA_DATE" | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}' + +if [ -n "${AGENDA_IS_BACKLOG:-}" ]; then + exit +fi + +# Carry over open tasks from the last agenda xit file. +if [ -e "$AGENDA_LAST" ] && [ "${AGENDA_LAST#*.}" = "xit" ]; then + line=0 + while IFS= read -r REPLY; do + line=$(( line + 1 )) + case ${state:-} in + "open_task") + case $REPLY in + "[ ] "*|"[@] "*|"[?] "*|" "*) + # Append indented lines to open tasks. + echo "$REPLY" + ;; + *) + unset state + ;; + esac + ;; + *) + case $REPLY in + "[ ] "*|"[@] "*|"[?] "*) + # Carry over group heading with open tasks. + if [ -n "${header:-}" ]; then + echo + echo "$header" + unset header + fi + state="open_task" + echo "$REPLY" + ;; + [a-z]*|[A-Z]*) + # Skip the first heading (assumed to be the generated + # title of the agenda note). + if [ $line -gt 1 ]; then + header=$REPLY + fi + ;; + esac + ;; + esac + done < "$AGENDA_LAST" +fi