feature: internal storage utilities, acquiring a lock, etc.

This commit is contained in:
Lewis Wynne 2026-04-01 20:28:00 +01:00
parent d4319e1ef6
commit 003379b298

185
nag
View file

@ -36,11 +36,17 @@ _ME="$(basename "${0}")"
_VERSION="2026.14"
NAG_PATH="${NAG_PATH:-${HOME}/.local/share/nag}"
NAG_CMD="${NAG_CMD:-notify-send}"
_LOCKFILE="${NAG_PATH}.lock"
# The command nag runs to execute its notifications.
NAG_CMD="${NAG_CMD:-notify-send}"
# The default subcommand if no args are passed.
DEFAULT_SUBCOMMAND="${DEFAULT_SUBCOMMAND:-list}"
# The fallback subcommand if an arg is passed that is not defined.
_FALLBACK_COMMAND_IF_NO_MATCH="at"
# Usage:
# _debug <command> <options>...
#
@ -199,17 +205,6 @@ _piped_input() {
! _interactive_input
}
# _create_alarm()
#
# Usage:
# _create_alarm <arguments>...
#
# Description:
# Create a new alarm. Stub for future implementation.
_create_alarm() {
_exit_1 printf "Not yet implemented.\\n"
}
# Usage:
# describe <name> <description>
# describe --get <name>
@ -360,11 +355,153 @@ _main() {
then
${_SUBCOMMAND} "${_SUBCOMMAND_ARGUMENTS[@]:-}"
else
_create_alarm "${_SUBCOMMAND}" "${_SUBCOMMAND_ARGUMENTS[@]:-}"
"${_FALLBACK_COMMAND_IF_NO_MATCH}" "${_SUBCOMMAND}" "${_SUBCOMMAND_ARGUMENTS[@]:-}"
fi
}
###############################################################################
# Storage
###############################################################################
# Global array holding raw TSV alarm lines (one element per alarm).
_ALARMS=()
# Usage:
# _ensure_nag_dir
#
# Description:
# Create the parent directory for NAG_PATH if it doesn't already exist.
_ensure_nag_dir() {
local _dir
_dir="$(dirname "${NAG_PATH}")"
[[ -d "${_dir}" ]] || mkdir -p "${_dir}"
}
# Usage:
# _acquire_lock
#
# Description:
# Acquire an exclusive lock on ${_LOCKFILE} using flock. Exits with an
# error if the lock cannot be obtained (another instance is running).
_acquire_lock() {
_ensure_nag_dir
exec {_LOCK_FD}>"${_LOCKFILE}"
if ! flock -n "${_LOCK_FD}"
then
_exit_1 printf "Could not acquire lock: %s\\n" "${_LOCKFILE}"
fi
}
# Usage:
# _release_lock
#
# Description:
# Release the exclusive lock previously acquired by _acquire_lock.
_release_lock() {
if [[ -n "${_LOCK_FD:-}" ]]
then
exec {_LOCK_FD}>&-
fi
}
# Usage:
# _read_alarms
#
# Description:
# Read alarms from NAG_PATH into the global _ALARMS array. Each element
# is one raw TSV line. If the file is missing or empty, _ALARMS is set to
# an empty array.
_read_alarms() {
_ALARMS=()
if [[ -f "${NAG_PATH}" ]] && [[ -s "${NAG_PATH}" ]]
then
local _line
while IFS= read -r _line || [[ -n "${_line}" ]]
do
_ALARMS+=("${_line}")
done < "${NAG_PATH}"
fi
}
# Usage:
# _write_alarms
#
# Description:
# Write the _ALARMS array atomically to NAG_PATH. Writes to a temporary
# file first, then moves it over the original. If _ALARMS is empty, an
# empty file is written.
_write_alarms() {
_ensure_nag_dir
local _tmp
_tmp="$(mktemp "${NAG_PATH}.XXXXXX")"
if (( ${#_ALARMS[@]} > 0 ))
then
printf "%s\n" "${_ALARMS[@]}" > "${_tmp}"
else
: > "${_tmp}"
fi
mv -f "${_tmp}" "${NAG_PATH}"
}
# Usage:
# _next_id
#
# Description:
# Find the next available alarm ID (the first gap in the existing ID
# sequence starting from 1). Prints the ID to stdout.
_next_id() {
_read_alarms
if (( ${#_ALARMS[@]} == 0 ))
then
printf "%s\n" "1"
return
fi
local _ids=()
local _line
for _line in "${_ALARMS[@]}"
do
_ids+=("$(printf "%s" "${_line}" | cut -f1)")
done
# Sort numerically
local _sorted
_sorted="$(printf "%s\n" "${_ids[@]}" | sort -n)"
local _expected=1
local _id
while IFS= read -r _id
do
if (( _id != _expected ))
then
printf "%s\n" "${_expected}"
return
fi
_expected=$((_expected + 1))
done <<< "${_sorted}"
printf "%s\n" "${_expected}"
}
# Usage:
# _get_alarm_field <line> <field-number>
#
# Description:
# Extract a field from a TSV alarm line.
# Fields: 1=id, 2=timestamp, 3=rule, 4=message.
# For field 4 (message), returns everything after the 3rd tab.
_get_alarm_field() {
local _line="${1}"
local _field="${2}"
if (( _field == 4 ))
then
printf "%s" "${_line}" | cut -f4-
else
printf "%s" "${_line}" | cut -f"${_field}"
fi
}
###############################################################################
# Subcommands
###############################################################################
@ -426,7 +563,8 @@ Usage:
${_ME} list
Description:
List all alarms. This is the default when no subcommand is given.
List all alarms. This is the default ${_ME} command when no arguments
are given. This can be overriden with NAG_DEFAUL
HEREDOC
list() {
if [[ ! -f "${NAG_PATH}" ]] || [[ ! -s "${NAG_PATH}" ]]
@ -436,5 +574,24 @@ list() {
fi
}
# at ##########################################################################
describe "at" <<HEREDOC
Usage:
${_ME} [at] <time> <message...>
Description:
Create a one-shot alarm. The "at" is optional. If the first argument
doesn't match any other subcommand of ${_ME}, it'll fallback to "at".
Examples:
${_ME} 3pm take a break
${_ME} at 3pm take a break
${_ME} "tomorrow 9am" dentist appointment
HEREDOC
at() {
_exit_1 printf "Not yet implemented.\\n"
}
# _main must be called after everything has been defined.
_main