Merge pull request 'feat(crunch): voice-notified reminders and AI crunch jobs' (#20) from feat/crunch-package into master
Reviewed-on: #20
This commit was merged in pull request #20.
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 {};
|
zellij-ps = pkgs.callPackage ./zellij-ps {};
|
||||||
vibetyper = pkgs.callPackage ./vibetyper {};
|
vibetyper = pkgs.callPackage ./vibetyper {};
|
||||||
talk = pkgs.callPackage ./talk {};
|
talk = pkgs.callPackage ./talk {};
|
||||||
|
crunch = pkgs.callPackage ./crunch {};
|
||||||
|
|
||||||
# ── Pass-through packages ──────────────────────────────────────────
|
# ── Pass-through packages ──────────────────────────────────────────
|
||||||
# Imported directly from flake inputs. No local modifications.
|
# Imported directly from flake inputs. No local modifications.
|
||||||
|
|||||||
Reference in New Issue
Block a user