#!/usr/bin/env bash # rice-reminder - desktop reminders backed by systemd user timers. # # rice-reminder new ask in rofi # rice-reminder add when: 20m | 1h30m | 15:30 | "tomorrow 9:00" | "2026-09-15 09:00" # rice-reminder list upcoming reminders as a notification # rice-reminder json upcoming reminders as JSON (calendar panel) # rice-reminder cancel | clear # # In rofi, type "20m tea", "15:30 standup" or "tomorrow 9:00 | dentist". # Reminders use app name "rice", so they still show during do-not-disturb. set -uo pipefail self="$(readlink -f "$0")" run="${XDG_RUNTIME_DIR:-/tmp}/rice" mkdir -p "$run" poke() { date +%s%N > "$run/reminder.rev"; } say() { notify-send -a rice -u low -i alarm-symbolic "$@"; } # Resolve a "when" into epoch seconds, or fail. resolve() { local when="$1" now t now=$(date +%s) if [[ $when =~ ^([0-9]+[smhd])+$ ]]; then local secs=0 part for part in $(grep -oE '[0-9]+[smhd]' <<<"$when"); do case "${part: -1}" in s) secs=$((secs + ${part%?})) ;; m) secs=$((secs + ${part%?} * 60)) ;; h) secs=$((secs + ${part%?} * 3600)) ;; d) secs=$((secs + ${part%?} * 86400)) ;; esac done echo $((now + secs)); return fi if [[ $when =~ ^[0-9]{1,2}:[0-9]{2}$ ]]; then t=$(date -d "today $when" +%s 2>/dev/null) || return 1 (( t <= now )) && t=$(date -d "tomorrow $when" +%s) echo "$t"; return fi t=$(date -d "$when" +%s 2>/dev/null) || return 1 (( t > now )) || return 1 echo "$t" } add() { local when="$1" text="${2:-Reminder}" at unit at=$(resolve "$when") || { say "Reminder" "Couldn't read \"$when\" as a time"; return 1; } unit="rice-reminder-$(date +%s%N)" systemd-run --user --quiet --collect --unit="$unit" --description="$text" \ --on-calendar="$(date -d "@$at" '+%Y-%m-%d %H:%M:%S')" \ --timer-property=AccuracySec=1s \ -- "$self" fire "$text" || { say "Reminder" "Could not schedule it"; return 1; } say "Reminder set" "$text · $(date -d "@$at" '+%a %H:%M')" poke } upcoming() { local unit next systemctl --user list-timers 'rice-reminder-*' --all -o json 2>/dev/null | jq -r '.[] | [.unit, (.next / 1000000 | floor)] | @tsv' | while IFS=$'\t' read -r unit next; do (( next > 0 )) || continue printf '%s\t%s\t%s\n' "${unit%.timer}" "$next" "$(systemctl --user show -p Description --value "$unit")" done | sort -t$'\t' -k2,2n } case "${1:-}" in new) input=$(rofi -dmenu -theme "$HOME/.config/rofi/prompt.rasi" -p "Reminder" \ -mesg "20m tea · 15:30 standup · tomorrow 9:00 | dentist") || exit 0 [[ -n $input ]] || exit 0 if [[ $input == *"|"* ]]; then when="${input%%|*}"; text="${input#*|}" else when="${input%% *}"; text="${input#"$when"}" fi when="$(sed 's/^ *//; s/ *$//' <<<"$when")"; text="$(sed 's/^ *//; s/ *$//' <<<"$text")" add "$when" "${text:-Reminder}" ;; add) add "${2:?when}" "${3:-Reminder}" ;; fire) notify-send -a rice -u critical -i alarm-symbolic "Reminder" "${2:-Reminder}" command -v paplay >/dev/null && [[ -f /usr/share/sounds/freedesktop/stereo/complete.oga ]] && paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null poke ;; list) body=$(upcoming | while IFS=$'\t' read -r _ next text; do printf '%s %s\n' "$(date -d "@$next" '+%a %H:%M')" "$text"; done) say "Upcoming reminders" "${body:-Nothing scheduled}" ;; json) upcoming | jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t") | {id: .[0], at: (.[1] | tonumber), time: (.[1] | tonumber | strflocaltime("%H:%M")), date: (.[1] | tonumber | strflocaltime("%Y-%m-%d")), text: .[2]})' ;; cancel) [[ ${2:-} =~ ^rice-reminder-[0-9]+$ ]] || { echo "rice-reminder: cancel takes a reminder id" >&2; exit 2; } systemctl --user stop "$2.timer" && poke ;; clear) systemctl --user list-timers 'rice-reminder-*' --all --no-legend 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /\.timer$/) print $i }' | xargs -r systemctl --user stop say "Reminders" "All cleared"; poke ;; *) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; esac