feat(crunch): voice-notified reminders and AI crunch jobs via systemd timers
crunch schedules transient systemd --user timers that fire talk (TTS) or AI agents (pi/opencode) → talk at specified times. Commands: crunch at <time> <message> One-shot crunch in <duration> <message> One-shot after delay crunch daily <time> <message> Recurring daily crunch weekly <day> <time> <message> Recurring weekly crunch ai <engine> <type> <time> <prompt> AI crunch crunch list List jobs crunch cancel <name> Cancel job crunch purge Clean fired jobs Supports German and English day names (Montag→Mon, etc).
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
writeShellScriptBin,
|
||||
coreutils,
|
||||
bash,
|
||||
}: let
|
||||
script = writeShellScriptBin "crunch" ''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# crunch — Voice-notified reminders & AI crunch jobs
|
||||
# via systemd transient timers.
|
||||
#
|
||||
# Requires: talk (ElevenLabs TTS) in PATH, ELEVENLABS_API_KEY env.
|
||||
# Optional: pi, opencode for AI crunch mode.
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
TALK="''${TALK:-talk}"
|
||||
SELF="$(readlink -f "''${BASH_SOURCE[0]:-$0}")"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
crunch — voice-notified reminders and AI jobs via systemd timers
|
||||
|
||||
Usage:
|
||||
crunch at <time> <message> One-shot (14:30 or "2026-07-03 14:30")
|
||||
crunch in <duration> <message> One-shot after delay (30m, 2h, 1h30m)
|
||||
crunch daily <time> <message> Recurring daily at time
|
||||
crunch weekly <day> <time> <message> Recurring weekly (Mon or Montag)
|
||||
crunch ai <engine> <type> <time> <prompt> AI crunch (pi|opencode)
|
||||
crunch list List active jobs
|
||||
crunch cancel <name> Cancel a job
|
||||
crunch purge Remove fired one-shot jobs
|
||||
|
||||
Examples:
|
||||
crunch at "14:30" "Müll rausbringen"
|
||||
crunch at "2026-07-03 10:00" "Zahnarzttermin"
|
||||
crunch in "30m" "Build checken"
|
||||
crunch daily "09:00" "Daily standup"
|
||||
crunch weekly "Mon 09:00" "Weekly review"
|
||||
crunch weekly "Montag 09:00" "Weekly review"
|
||||
crunch ai pi in "1h" "Fasse neueste Commits zusammen"
|
||||
crunch ai opencode daily "08:00" "Review offene Issues"
|
||||
crunch list
|
||||
crunch cancel mll-rausbringen-12345
|
||||
|
||||
Environment:
|
||||
VOICE_NOTIFY=0 Silence voice output (passed to talk)
|
||||
ELEVENLABS_API_KEY Required by talk
|
||||
TALK Override path to talk binary
|
||||
EOF
|
||||
}
|
||||
|
||||
# German → systemd day abbreviations
|
||||
norm_day() {
|
||||
local d="''${1,,}"
|
||||
d="''${d%%.*}" # strip trailing dot (German abbreviation)
|
||||
case "$d" in
|
||||
mo|mon|monday|montag) echo "Mon" ;;
|
||||
di|tue|tues|tuesday|dienstag) echo "Tue" ;;
|
||||
mi|wed|weds|wednesday|mittwoch) echo "Wed" ;;
|
||||
do|thu|thur|thurs|thursday|donnerstag) echo "Thu" ;;
|
||||
fr|fri|friday|freitag) echo "Fri" ;;
|
||||
sa|sat|saturday|samstag) echo "Sat" ;;
|
||||
so|su|sun|sunday|sonntag) echo "Sun" ;;
|
||||
*) echo "''${1^}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
make_name() {
|
||||
local msg="''${1:?No message}"
|
||||
local slug
|
||||
slug="''${msg,,}"
|
||||
slug="''${slug//[^a-z0-9 ]/}"
|
||||
slug="''${slug// /-}"
|
||||
slug="''${slug:0:25}"
|
||||
slug="''${slug%-}" # trim trailing dash
|
||||
echo "crunch-''${slug:-job}-$RANDOM"
|
||||
}
|
||||
|
||||
# schedule <type> <time_spec> <exec_cmd> <name>
|
||||
schedule() {
|
||||
local type="$1" time_spec="$2" exec_cmd="$3" name="$4"
|
||||
local timer_flag=""
|
||||
|
||||
case "$type" in
|
||||
at)
|
||||
if [[ "$time_spec" == *' '* ]]; then
|
||||
# "2026-07-03 14:30" → specific date
|
||||
timer_flag="--on-calendar=''${time_spec}:00"
|
||||
else
|
||||
# "14:30" → next matching time
|
||||
timer_flag="--on-calendar=*-*-* ''${time_spec}:00"
|
||||
fi
|
||||
;;
|
||||
in)
|
||||
timer_flag="--on-active=''${time_spec}"
|
||||
;;
|
||||
daily)
|
||||
timer_flag="--on-calendar=*-*-* ''${time_spec}:00"
|
||||
;;
|
||||
weekly)
|
||||
local day time_part
|
||||
day="$(norm_day "''${time_spec%% *}")"
|
||||
time_part="''${time_spec##* }"
|
||||
timer_flag="--on-calendar=''${day} *-*-* ''${time_part}:00"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown schedule type: $type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
systemd-run --user \
|
||||
--unit="''${name}" \
|
||||
--description="crunch job" \
|
||||
"''${timer_flag}" \
|
||||
bash -lc "''${exec_cmd}" \
|
||||
&& echo "✓ Scheduled: $name"
|
||||
}
|
||||
|
||||
# ── Internal execution (called by systemd) ────────────────────
|
||||
|
||||
case "''${1:-}" in
|
||||
_say)
|
||||
shift
|
||||
exec "$TALK" "$*"
|
||||
;;
|
||||
_ai)
|
||||
engine="$2"; shift 2
|
||||
case "$engine" in
|
||||
pi|pi-agent)
|
||||
pi -p "$*" | "$TALK"
|
||||
;;
|
||||
opencode|oc)
|
||||
opencode run "$*" -q | "$TALK"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown engine: $engine (use: pi, opencode)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── CLI ───────────────────────────────────────────────────────
|
||||
|
||||
[ $# -lt 1 ] && { usage; exit 0; }
|
||||
|
||||
cmd="$1"; shift
|
||||
|
||||
case "$cmd" in
|
||||
# ── Simple voice reminders ──
|
||||
at)
|
||||
[ $# -lt 2 ] && { echo "Usage: crunch at <time> <message>" >&2; exit 1; }
|
||||
time_spec="$1"; shift
|
||||
msg="$*"
|
||||
name="$(make_name "$msg")"
|
||||
schedule at "$time_spec" "$SELF _say $(printf '%q' "$msg")" "$name"
|
||||
;;
|
||||
|
||||
in)
|
||||
[ $# -lt 2 ] && { echo "Usage: crunch in <duration> <message>" >&2; exit 1; }
|
||||
duration="$1"; shift
|
||||
msg="$*"
|
||||
name="$(make_name "$msg")"
|
||||
schedule in "$duration" "$SELF _say $(printf '%q' "$msg")" "$name"
|
||||
;;
|
||||
|
||||
daily)
|
||||
[ $# -lt 2 ] && { echo "Usage: crunch daily <time> <message>" >&2; exit 1; }
|
||||
time_spec="$1"; shift
|
||||
msg="$*"
|
||||
name="$(make_name "$msg")"
|
||||
schedule daily "$time_spec" "$SELF _say $(printf '%q' "$msg")" "$name"
|
||||
;;
|
||||
|
||||
weekly)
|
||||
[ $# -lt 2 ] && { echo "Usage: crunch weekly <day> <time> <message>" >&2; exit 1; }
|
||||
day_time="$1"; shift
|
||||
msg="$*"
|
||||
name="$(make_name "$msg")"
|
||||
schedule weekly "$day_time" "$SELF _say $(printf '%q' "$msg")" "$name"
|
||||
;;
|
||||
|
||||
# ── AI crunch jobs ──
|
||||
ai)
|
||||
[ $# -lt 4 ] && { echo "Usage: crunch ai <pi|opencode> <at|in|daily|weekly> <time> <prompt>" >&2; exit 1; }
|
||||
engine="$1"; sub_type="$2"; time_spec="$3"; shift 3
|
||||
prompt="$*"
|
||||
name="$(make_name "$prompt")"
|
||||
schedule "$sub_type" "$time_spec" \
|
||||
"$SELF _ai $engine $(printf '%q' "$prompt")" "$name"
|
||||
;;
|
||||
|
||||
# ── Management ──
|
||||
list)
|
||||
echo "Active crunch jobs:"
|
||||
echo "───────────────────────────────────"
|
||||
systemctl --user list-timers 'crunch-*' --no-pager 2>/dev/null \
|
||||
| grep -E 'crunch-|^NEXT|^NEXT|^$' || echo " (none)"
|
||||
;;
|
||||
|
||||
cancel)
|
||||
[ $# -lt 1 ] && { echo "Usage: crunch cancel <name>" >&2; exit 1; }
|
||||
name="$1"
|
||||
# Accept with or without crunch- prefix
|
||||
[[ "$name" != crunch-* ]] && name="crunch-$name"
|
||||
systemctl --user stop "''${name}.timer" "''${name}.service" 2>/dev/null \
|
||||
&& echo "✓ Cancelled: $name" \
|
||||
|| echo "✗ Not found: $name"
|
||||
;;
|
||||
|
||||
purge)
|
||||
echo "Purging fired one-shot jobs..."
|
||||
local purged=0
|
||||
for unit in $(systemctl --user list-units 'crunch-*' --all --plain --no-legend --no-pager 2>/dev/null | awk '{print $1}'); do
|
||||
if systemctl --user is-failed "$unit" &>/dev/null; then
|
||||
systemctl --user stop "$unit" 2>/dev/null || true
|
||||
systemctl --user reset-failed "$unit" 2>/dev/null || true
|
||||
echo " Purged: $unit"
|
||||
purged=$((purged + 1))
|
||||
fi
|
||||
done
|
||||
echo "✓ Purged $purged unit(s)"
|
||||
;;
|
||||
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command: $cmd" >&2
|
||||
echo ""
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "crunch";
|
||||
version = "0.1.0";
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p "$out/bin"
|
||||
ln -s ${script}/bin/crunch "$out/bin/crunch"
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Voice-notified reminders and AI crunch jobs via systemd transient timers";
|
||||
homepage = "https://code.m3ta.dev/m3tam3re/nixpkgs";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "crunch";
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,7 @@ in {
|
||||
zellij-ps = pkgs.callPackage ./zellij-ps {};
|
||||
vibetyper = pkgs.callPackage ./vibetyper {};
|
||||
talk = pkgs.callPackage ./talk {};
|
||||
crunch = pkgs.callPackage ./crunch {};
|
||||
|
||||
# ── Pass-through packages ──────────────────────────────────────────
|
||||
# Imported directly from flake inputs. No local modifications.
|
||||
|
||||
Reference in New Issue
Block a user