aboutsummaryrefslogtreecommitdiff
path: root/src/td.in
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xsrc/td.in540
1 files changed, 540 insertions, 0 deletions
diff --git a/src/td.in b/src/td.in
new file mode 100755
index 0000000..6f1c516
--- /dev/null
+++ b/src/td.in
@@ -0,0 +1,540 @@
+#!/bin/sh
+set -eu
+
+MSGS=''
+add_msg() {
+ MSGS="$MSGS $1"
+}
+
+
+MSG_USAGE="$(cat <<-'EOF'
+ Usage:
+ @NAME@ [-c] [-t TYPE] [-m MESSAGE]
+ @NAME@ [-HlL]
+ @NAME@ [-hV]
+EOF
+)"
+add_msg USAGE
+
+MSG_HELP1="$(cat <<-'EOF'
+
+ Options:
+ -c commit directly only with the title, without
+ adding a description. Assumes -m
+ -m MESSAGE_TITLE the title message of the entry
+ -t TYPE the type of entry to be added (default: |TYPE|)
+ -s STATE the state of entry to be added (default: |STATE|)
+ -H pre-process issues file before generating HTML
+ -l list the supported values for $TD_USE_BUILTIN_HOOKS
+ -L lint the issues file
+ -h, --help show this help message
+ -V, --version print the version number
+
+ Examples:
+ Create a new entry with the default type with a custom title:
+ $ td -cm 'An entry title'
+
+ Create a new entry with a custom type and state:
+ $ td -cm 'Another title' -t bug -s DONE
+
+ States of an entry:
+EOF
+)"
+add_msg HELP1
+
+MSG_HELP2="$(cat <<-'EOF'
+ Types of entry:
+EOF
+)"
+add_msg HELP2
+
+MSG_HELP3="$(cat <<-'EOF'
+ See "man @NAME@.tutorial" for getting started.
+EOF
+)"
+add_msg HELP3
+
+MSG_UNEDITED_ENTRY="$(cat <<-'EOF'
+ Abandoning unedited entry.
+EOF
+)"
+add_msg UNEDITED_ENTRY
+
+MSG_UNREGISTERED_TYPE="$(cat <<-'EOF'
+ Unregistered type: %s
+EOF
+)"
+add_msg UNREGISTERED_TYPE
+
+MSG_UNREGISTERED_STATE="$(cat <<-'EOF'
+ Unregistered state: %s
+EOF
+)"
+add_msg UNREGISTERED_TYPE
+
+MSG_ADDED_LOG="$(cat <<-'EOF'
+ %s added to %s.
+EOF
+)"
+add_msg ADDED_LOG
+
+MSG_COMMIT_MSG="$(cat <<-'EOF'
+ $TD_FILE: Add $TD_IDREF
+EOF
+)"
+add_msg COMMIT_MSG
+
+MSG_TYPE_DOESNT_EXIST_CREATING="$(cat <<-'EOF'
+ Type "%s" doesn't exist yet, creating new section with it.
+EOF
+)"
+add_msg TYPE_DOESNT_EXIST_CREATING
+
+MSG_FILE_DOESNT_EXIST_CREATING="$(cat <<-'EOF'
+ File "%s" doesn't exist yet, creating a brand new one.
+EOF
+)"
+add_msg FILE_DOESNT_EXIST_CREATING
+
+#
+# End translatable strings
+#
+
+
+
+dump_translatable_strings() {
+ for msg in $MSGS; do
+ eval "echo \"\$MSG_$msg\"" > src/locale/"$msg".en.txt
+ done
+
+ cat <<-'EOF'
+ #!/bin/sh
+ # shellcheck disable=2034
+ set -eu
+
+ EOF
+ for msg in $MSGS; do
+ printf 'MSG_%s="$(cat @LOCALEDIR@/@LANG@/LC_MESSAGES/@NAME@/%s.@LANG@.txt)"\n' \
+ "$msg" "$msg"
+ done
+}
+
+if [ -n "${TD_DUMP_TRANSLATABLE_STRINGS:-}" ]; then
+ dump_translatable_strings
+ exit
+fi
+
+if [ -r '@LIBEXECDIR@/@NAME@/load-messages.sh' ]; then
+ . '@LIBEXECDIR@/@NAME@/load-messages.sh'
+fi
+
+
+
+#
+# Documentation functions
+#
+
+usage() {
+ printf '%s\n' "$MSG_USAGE"
+}
+
+help() {
+ printf '%s\n' "$MSG_HELP1" |
+ sed -e "s/|TYPE|/$DEFAULT_TYPE/" \
+ -e "s/|STATE|/$DEFAULT_STATE/"
+ echo "$TD_STATES" | sed -e 's/^/ /'
+
+ printf '\n%s\n' "$MSG_HELP2"
+ echo "$TD_TYPES" | awk -F: '{ printf " %s\n", $1 }'
+
+ printf '\n%s\n' "$MSG_HELP3"
+}
+
+version() {
+ printf '@NAME@-@VERSION@ @DATE@\n'
+}
+
+
+#
+# Utilities
+#
+
+uuid() {
+ # Taken from:
+ # https://serverfault.com/a/799198
+ od -xN20 /dev/urandom |
+ head -n 1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+mkstemp() {
+ F="${TMPDIR:-/tmp}/td-${1:-$(uuid)}"
+ touch "$F"
+ echo "$F"
+}
+
+mkdtemp() {
+ F="${TMPDIR:-/tmp}/td-${1:-$(uuid)}"
+ mkdir -p "$F"
+ echo "$F"
+}
+
+
+#
+# Core functions
+#
+
+insert_at_line() {
+ N="$1"
+ F="$2"
+ TYPE="$3"
+ TMPF="$(mkstemp)"
+
+ if [ ! -e "$F" ]; then
+ printf "$MSG_FILE_DOESNT_EXIST_CREATING\n" "$TD_FILE" >&2
+ cat - > "$TMPF"
+ elif [ "$N" = 0 ]; then
+ printf "$MSG_TYPE_DOESNT_EXIST_CREATING\n" "$TYPE" >&2
+ printf '%s\n\n\n%s\n' \
+ "$(cat -)" \
+ "$(cat "$F")" \
+ > "$TMPF"
+ else
+ printf '%s\n\n%s\n\n%s\n' \
+ "$(head "-n$N" "$F")" \
+ "$(cat -)" \
+ "$(tail "-n+$((N+1))" "$F")" \
+ > "$TMPF"
+ fi
+
+ cp "$TMPF" "$F"
+}
+
+name_for_type() {
+ TYPE="$1"
+ echo "$TD_TYPES" | awk -F: "/^$TYPE:/ { print \$2; exit }"
+}
+
+linenumber_for_type() {
+ TYPE="$1"
+ if [ ! -e "$TD_FILE" ]; then
+ echo ''
+ else
+ NAME="$(name_for_type "$TYPE")"
+ awk -F: "/^# $NAME\$/ {print NR; exit}" "$TD_FILE"
+ fi
+}
+
+
+GIT_BUILTIN_PRE_ADD_HOOK="$(cat <<-EOF
+ if ! git diff --exit-code --quiet -- "\$TD_FILE" ||
+ ! git diff --exit-code --quiet --staged -- "\$TD_FILE"
+ then
+ git stash push --quiet -- "\$TD_FILE"
+ touch "\$TD_HOOKS_DIR/has-diff"
+ fi
+EOF
+)"
+GIT_BUILTIN_POST_ADD_HOOK="$(cat <<-EOF
+ git add "\$TD_FILE"
+ git commit -m "$MSG_COMMIT_MSG"
+ if [ -e "\$TD_HOOKS_DIR/has-diff" ]; then
+ git stash pop --quiet
+ fi
+EOF
+)"
+load_hooks() {
+ case "${TD_USE_BUILTIN_HOOKS:-}" in
+ git)
+ export TD_PRE_ADD_HOOK="${TD_PRE_ADD_HOOK:-$GIT_BUILTIN_PRE_ADD_HOOK}"
+ export TD_POST_ADD_HOOK="${TD_POST_ADD_HOOK:-$GIT_BUILTIN_POST_ADD_HOOK}"
+ ;;
+ *)
+ ;;
+ esac
+}
+
+list_hooks() {
+ for t in git; do
+ TD_USE_BUILTIN_HOOKS="$t"
+ load_hooks
+ printf '%s:\n' "$t"
+ printf ' TD_PRE_ADD_HOOK:\n'
+ printf '%s\n' "$TD_PRE_ADD_HOOK" | sed 's/^/ /'
+ printf ' TD_POST_ADD_HOOK:\n'
+ printf '%s\n' "$TD_POST_ADD_HOOK" | sed 's/^/ /'
+ done
+}
+
+pre_add_hook() {
+ sh -c "${TD_PRE_ADD_HOOK:-}"
+}
+
+post_add_hook() {
+ sh -c "${TD_POST_ADD_HOOK:-}"
+}
+
+
+
+#
+# HTML pre-processing
+#
+
+html_inject_checkboxes() {
+ awk '{
+ if (match($0, /^(-|[0-9]*.) \[( |x)\] /)) {
+ printf \
+ "%s <input type=\"checkbox\" disabled %s/> %s\n",
+ $1,
+ substr($2, 2, 1) == "x" ? "checked " : "",
+ substr($0, 1 + length($1) + length(" [ ] "))
+ } else {
+ print
+ }
+ }'
+}
+
+html_process_tags() {
+ sed 's|tag:\([^ ]*\)|<span class="tag">\1</span>|g'
+}
+
+html_process_ids() {
+ STATES_REGEX="$(
+ echo "$TD_STATES" |
+ grep . |
+ paste -sd '|' |
+ sed 's/|/\\|/g' |
+ printf '\(%s\)' "$(cat -)"
+ )"
+
+ sed 's:^## '"$STATES_REGEX"' \(.*\) {#\(.*\)}\(.*\)$:## <a href="#\3"><span class="\1">\1</span> \2</a>\4\
+<pre class="header-anchor" id="\3">#\3</pre>\
+:g'
+}
+
+html_pre_process() {
+ html_inject_checkboxes |
+ html_process_tags |
+ html_process_ids
+}
+
+
+
+#
+# Linting
+#
+
+lint_assert_order() {
+ GREP_FLAGS="$(echo "$TD_TYPES" | cut -d: -f2- | sed 's/\(.*\)/-e "\1"/' | tr '\n' ' ')"
+ echo eval "grep -F $GREP_FLAGS $TD_FILE"
+}
+
+known_heading() {
+ line="$1"
+ TYPE_HEADINGS="$2"
+ while read -r heading; do
+ if [ "$heading" = "$line" ]; then
+ return 0
+ fi
+ done < "$TYPE_HEADINGS"
+ return 1
+}
+
+warning() {
+ echo "$1" "$2" "$3"
+}
+
+lint() {
+ TYPE_HEADINGS="$(mkstemp)"
+ echo "$TD_TYPES" | cut -d: -f2- | sed 's/^/# /' > "$TYPE_HEADINGS"
+ IN_TD_SECTION=false
+ IN_TD_ENTRY=false
+ LINE_NUMBER=0
+ while read -r line; do
+ LINE_NUMBER=$((LINE_NUMBER + 1))
+ if [ "$IN_TD_SECTION" = false ]; then
+ if known_heading "$line" "$TYPE_HEADINGS"; then
+ IN_TD_SECTION=true
+ fi
+ fi
+
+ if [ "$IN_TD_SECTION" = false ]; then
+ continue
+ fi
+
+ if echo "$line" | grep -q '^# .*' &&
+ ! known_heading "$line" "$TYPE_HEADINGS"; then
+ break
+ else
+ IN_TD_ENTRY=false
+ echo "$line" | grep -q '^#.*'
+ # continue
+ fi
+
+ if echo "$line" | grep -q '^## .*'; then
+ IN_TD_ENTRY=true
+ fi
+
+ if [ "$IN_TD_ENTRY" = false ] && [ "$line" != '' ]; then
+ warning "$TD_FILE" "$LINE_NUMBER" \
+ 'Content outside of td entry'
+ fi
+ done < "$TD_FILE"
+}
+
+
+
+#
+# Default values, config "run commands" file and derived values
+#
+
+ID="$(uuid)"
+export TD_IDREF="#td-$ID"
+export TD_FILE='TODOs.md'
+export TD_TYPES="$(cat <<-'EOF'
+ task:Tasks
+ bug:Bugs
+ improvement:Improvements
+ question:Questions
+ decision:Decisions
+ idea:Ideas
+ proposal:Proposals
+EOF
+)"
+export TD_STATES="$(cat <<-'EOF'
+ TODO
+ DOING
+ WAITING
+ INACTIVE
+ NEXT
+ CANCELLED
+ DONE
+ WONTFIX
+EOF
+)"
+export TD_HOOKS_DIR="$(mkdtemp)"
+
+if [ -r "$PWD/.tdrc" ]; then
+ . "$PWD/.tdrc"
+fi
+
+DEFAULT_TYPE="$(echo "$TD_TYPES" | awk -F: '{ print $1; exit }')"
+DEFAULT_STATE="$(echo "$TD_STATES" | awk -F: '{ print $1; exit }')"
+TYPE="$DEFAULT_TYPE"
+STATE="$DEFAULT_STATE"
+NAME="$(name_for_type "$TYPE")"
+
+load_hooks
+
+
+
+#
+# Main
+#
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ --version)
+ version
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+SHORT=false
+MESSAGE=FIXME
+while getopts 'ct:s:m:lLHhV' flag; do
+ case "$flag" in
+ c)
+ SHORT=true
+ ;;
+ t)
+ TYPE="$OPTARG"
+ NAME="$(name_for_type "$TYPE")"
+ if [ -z "$NAME" ]; then
+ printf "$MSG_UNREGISTERED_TYPE\n" "$TYPE" >&2
+ exit 1
+ fi
+ ;;
+ s)
+ STATE="$OPTARG"
+ if ! echo "$TD_STATES" | grep -q "$STATE"; then
+ printf "$MSG_UNREGISTERED_STATE\n" "$STATE" >&2
+ exit 1
+ fi
+ ;;
+ m)
+ MESSAGE="$OPTARG"
+ ;;
+ l)
+ list_hooks
+ exit
+ ;;
+ L)
+ lint
+ exit
+ ;;
+ H)
+ html_pre_process < "$TD_FILE"
+ exit
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ V)
+ version
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+TYPE_LINE="$(linenumber_for_type "$TYPE")"
+if [ -z "$TYPE_LINE" ]; then
+ TYPE_LINE=-1
+ PREAMBLE="# $(name_for_type "$TYPE")"'
+
+'
+fi
+INSERT_LINE=$((TYPE_LINE + 1))
+TITLE_LINE="${PREAMBLE:-}$(printf '## %s %s {%s}\n- %s in %s\n' "$STATE" "$MESSAGE" "$TD_IDREF" "$STATE" "$(date -I)")"
+
+if [ "$SHORT" = 'true' ] && [ "$MESSAGE" != 'FIXME' ]; then
+ pre_add_hook
+ echo "$TITLE_LINE" | insert_at_line "$INSERT_LINE" "$TD_FILE" "$TYPE"
+ post_add_hook
+else
+ TMPFILE_ORIG="$(mkstemp)"
+ TMPFILE_COPY="$(mkstemp)"
+ printf '%s\n\n---\n\nFIXME\n' "$TITLE_LINE" > "$TMPFILE_ORIG"
+ cp "$TMPFILE_ORIG" "$TMPFILE_COPY"
+ CMD="${VISUAL:-${EDITOR:-vi}}"
+ $CMD "$TMPFILE_ORIG"
+ if diff "$TMPFILE_ORIG" "$TMPFILE_COPY" > /dev/null; then
+ echo "$MSG_UNEDITED_ENTRY" >&2
+ exit 1
+ fi
+
+ pre_add_hook
+ insert_at_line "$INSERT_LINE" "$TD_FILE" "$TYPE" < "$TMPFILE_ORIG"
+ post_add_hook
+fi
+
+printf "$MSG_ADDED_LOG\n" "$TD_IDREF" "$TD_FILE" >&2