1 Commits

Author SHA1 Message Date
12ba1d3326 feat!: add builtin templates
WIP: document, check for breaking changes, decide on version, etc.
2024-06-07 11:57:15 +02:00
12 changed files with 241 additions and 438 deletions

5
.gitignore vendored
View File

@@ -1,6 +1 @@
agenda.1.gz
# Added by cargo
/target

View File

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

237
Cargo.lock generated
View File

@@ -1,237 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "agenda"
version = "0.1.0"
dependencies = [
"clap",
]
[[package]]
name = "anstream"
version = "0.6.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -1,25 +0,0 @@
[package]
name = "agenda"
description = "Manage daily tasks and notes in any plain text format."
version.workspace = true
edition.workspace = true
authors.workspace = true
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = [
"Jonas Kattendick <kattendick@bde-software.com>",
]
[workspace]
members = [
# "agenda_completion",
# "agenda_manpage",
]
[dependencies]
clap = { workspace = true, features = ["derive", "env"] }
[workspace.dependencies]
clap = "4.5.20"

View File

@@ -1,45 +1,24 @@
SHELL = /bin/sh
# See the following link regarding GNU Makefile conventions.
# https://www.gnu.org/software/make/manual/html_node/Makefile-Conventions.html
# This seems stupid. Isn't install a coreutil anyways?
# INSTALL := install
# INSTALL_PROGRAM := $(INSTALL)
# INSTALL_DATA := ${INSTALL} -m 644
# Variables for installation directories.
# https://www.gnu.org/software/make/manual/html_node/Directory-Variables.html
DESTDIR :=
prefix := /usr/local
exec_prefix := $(prefix)
bindir := $(exec_prefix)/bin
datarootdir := $(prefix)/share
datadir := $(datarootdir)
# libdir := $(prefix)/lib
mandir := $(datarootdir)/man
man1dir := $(mandir)/man1
.PHONY: all
all: ./agenda.1.gz
.PHONY: install
install: ./agenda.1.gz
install -m 755 -D ./agenda $(DESTDIR)$(bindir)/agenda
install -m 644 -D ./agenda.1.gz $(DESTDIR)$(man1dir)/agenda.1.gz
install -m 644 -D ./completion.bash $(DESTDIR)$(datarootdir)/bash-completion/completions/agenda
.PHONY: uninstall
uninstall:
rm -f $(DESTDIR)$(bindir)/agenda
rm -f $(DESTDIR)$(man1dir)/agenda.1.gz
rm -f $(DESTDIR)$(datarootdir)/bash-completion/completions/agenda
.PHONY: clean
clean:
rm -f ./agenda.1.gz
PREFIX := /usr/local
./agenda.1.gz: ./agenda.1
gzip -fk ./agenda.1
.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) $(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) ./agenda.1.gz

59
agenda
View File

@@ -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,7 +327,18 @@ __agenda_create() {
tmp_err=$(mktemp)
if [ "$backlog" = "no" ]; then
# Capture exit code of the template command.
# 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 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"
@@ -334,25 +360,15 @@ __agenda_create() {
echo "${0##*/}: cannot create '$file': template error" >&2
return $code
fi
else
touch "$tmp"
fi
fi
mkdir -p "${file%/*}"
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" \

View File

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

View File

@@ -1,82 +0,0 @@
//! The application's cli.
//!
//! TODO: Value hints for existing agenda directories.
//! TODO: Value hints date and date-like strings.
use clap::{ArgAction, Args, Parser, Subcommand};
/// Manage daily tasks and notes in any plain text format.
#[derive(Debug, Parser)]
#[command(version, author)]
#[command(args_conflicts_with_subcommands = true)]
#[command(disable_help_flag = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub edit: EditArgs,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Edit(EditArgs),
List(ListArgs),
}
/// Edit a single agenda note file.
///
/// This is the default and can be omitted.
#[derive(Args, Debug)]
pub struct EditArgs {
/// Create the file if it does not exist.
#[arg(short)]
pub create: bool,
/// Edit a backlog file (use the date argument as backlog name).
#[arg(short)]
pub backlog: bool,
/// Instead of editing the file, print its path to STDOUT.
#[arg(short = 'E')]
pub unedit: bool,
/// Specify the file extension.
#[arg(short, env = "AGENDA_EXTENSION", default_value = "md")]
pub extension: String,
/// Specify which agenda to edit.
#[arg(short = 't', env = "AGENDA_DEFAULT", default_value = "agenda")]
pub agenda: String,
/// The date of the agenda note file.
///
/// If the -b flag is used this takes a name instead.
#[arg(default_value = "today")]
pub date: String,
/// Print help.
#[arg(short, long, action = ArgAction::HelpShort)]
pub help: Option<bool>,
}
/// List a range of agenda note files.
#[derive(Args, Debug)]
pub struct ListArgs {
/// Print the files' absolute path.
#[arg(short)]
pub absolute: bool,
/// Print the files' content.
#[arg(short)]
pub contents: bool,
/// Stop listing at the specified date.
#[arg(short)]
pub until: Option<String>,
/// Specify which agenda to list.
#[arg(short = 't', env = "AGENDA_DEFAULT", default_value = "agenda")]
pub agenda: String,
#[arg(default_value = "last week")]
pub from: String,
/// Print help.
#[arg(short, long, action = ArgAction::HelpShort)]
pub help: Option<bool>,
}
impl Cli {
pub fn command(self) -> Command {
self.command.unwrap_or(Command::Edit(self.edit))
}
}

View File

@@ -1,9 +0,0 @@
use clap::Parser;
use crate::cli::Cli;
mod cli;
fn main() {
println!("{:?}", Cli::parse().command());
}

6
templates/default Executable file
View File

@@ -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))}'

42
templates/md Executable file
View File

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

56
templates/xit Executable file
View File

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