feat: unskip command to reset repeating alarms to next natural occurrence
This commit is contained in:
parent
4524ce5cfd
commit
6cf2b7d967
2 changed files with 476 additions and 0 deletions
295
nag
295
nag
|
|
@ -1181,6 +1181,87 @@ _first_occurrence() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Usage:
|
||||
# _unskip_timestamp <timestamp> <rules>
|
||||
#
|
||||
# Description:
|
||||
# Compute the next natural occurrence of a repeating alarm from now.
|
||||
# Handles each rule type appropriately: day-of-week rules use today's
|
||||
# date at alarm's time, month preserves day-of-month, year preserves
|
||||
# month and day. For comma-separated rules, returns the earliest.
|
||||
_unskip_timestamp() {
|
||||
local _timestamp="${1}" _rules="${2}"
|
||||
local _now
|
||||
_now="$(date +%s)"
|
||||
|
||||
local _time_of_day _alarm_day _alarm_month
|
||||
_time_of_day="$(date -d "@${_timestamp}" +%H:%M:%S)"
|
||||
_alarm_day="$(date -d "@${_timestamp}" +%-d)"
|
||||
_alarm_month="$(date -d "@${_timestamp}" +%-m)"
|
||||
|
||||
local _earliest=""
|
||||
local IFS=","
|
||||
local _rule
|
||||
for _rule in ${_rules}
|
||||
do
|
||||
local _candidate=""
|
||||
case "${_rule}" in
|
||||
hour)
|
||||
_candidate="$((_now - (_now % 3600) + $(date -d "@${_timestamp}" +%s) % 3600))"
|
||||
if (( _candidate <= _now )); then
|
||||
_candidate=$((_candidate + 3600))
|
||||
fi
|
||||
;;
|
||||
day)
|
||||
_candidate="$(date -d "$(date +%Y-%m-%d) ${_time_of_day}" +%s)"
|
||||
if (( _candidate <= _now )); then
|
||||
_candidate="$(date -d "$(date +%Y-%m-%d) + 1 day ${_time_of_day}" +%s)"
|
||||
fi
|
||||
;;
|
||||
month)
|
||||
local _year_now _month_now
|
||||
_year_now="$(date +%Y)"
|
||||
_month_now="$(date +%-m)"
|
||||
_candidate="$(date -d "$(printf "%04d-%02d-%02d %s" "${_year_now}" "${_month_now}" "${_alarm_day}" "${_time_of_day}")" +%s 2>/dev/null)" || _candidate=0
|
||||
if (( _candidate <= _now )); then
|
||||
if (( _candidate == 0 )); then
|
||||
_candidate="$(date -d "$(date +%Y-%m-%d) ${_time_of_day}" +%s)"
|
||||
fi
|
||||
_candidate="$(_next_month "${_candidate}" "${_time_of_day}")"
|
||||
fi
|
||||
;;
|
||||
year)
|
||||
local _year_now
|
||||
_year_now="$(date +%Y)"
|
||||
_candidate="$(date -d "$(printf "%04d-%02d-%02d %s" "${_year_now}" "${_alarm_month}" "${_alarm_day}" "${_time_of_day}")" +%s 2>/dev/null)" || _candidate=0
|
||||
if (( _candidate <= _now )); then
|
||||
if (( _candidate == 0 )); then
|
||||
_candidate="$(date -d "$(date +%Y-%m-%d) ${_time_of_day}" +%s)"
|
||||
fi
|
||||
_candidate="$(_next_year "${_candidate}" "${_time_of_day}")"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Day-of-week rules (weekday, weekend, mon-sun, week).
|
||||
local _today_at_alarm_time
|
||||
_today_at_alarm_time="$(date -d "$(date +%Y-%m-%d) ${_time_of_day}" +%s)"
|
||||
_candidate="$(_first_occurrence "${_rule}" "${_today_at_alarm_time}")"
|
||||
if (( _candidate <= _now )); then
|
||||
_candidate="$(_next_occurrence "${_rule}" "${_candidate}")"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "${_candidate}" ]] && (( _candidate > 0 )); then
|
||||
if [[ -z "${_earliest}" ]] || (( _candidate < _earliest )); then
|
||||
_earliest="${_candidate}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
printf "%s" "${_earliest}"
|
||||
}
|
||||
|
||||
# Usage:
|
||||
# _timestamp_matches_rule <rules> <timestamp>
|
||||
#
|
||||
|
|
@ -1385,6 +1466,7 @@ Usage:
|
|||
${_ME} every <rules> <time> <message...> repeating alarm
|
||||
${_ME} stop <all|id|tag> delete alarm(s)
|
||||
${_ME} skip <all|id|tag> skip next occurrence(s)
|
||||
${_ME} unskip <all|id|tag> reset to next occurrence
|
||||
${_ME} tag <id> <tags...> add tags to an alarm
|
||||
${_ME} tag <tag> list alarms with a tag
|
||||
${_ME} untag <id> <tags...> remove tags from an alarm
|
||||
|
|
@ -1848,6 +1930,219 @@ _skip_all() {
|
|||
_release_lock
|
||||
}
|
||||
|
||||
# unskip ######################################################################
|
||||
|
||||
describe "unskip" <<HEREDOC
|
||||
Usage:
|
||||
${_ME} unskip <all|id|tag>
|
||||
|
||||
Description:
|
||||
Reset a repeating alarm to its next natural occurrence from today.
|
||||
Reverses skips that pushed the alarm further into the future.
|
||||
Only applies to repeating alarms. With a tag, applies to all
|
||||
matching repeating alarms. With "all", resets every repeating alarm.
|
||||
HEREDOC
|
||||
unskip() {
|
||||
local _target="${1:-}"
|
||||
[[ -n "${_target}" ]] || _exit_1 printf "Usage: %s unskip <all|id|tag>\\n" "${_ME}"
|
||||
|
||||
if [[ "${_target}" == "all" ]]
|
||||
then
|
||||
_unskip_all
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! "${_target}" =~ ^[0-9]+$ ]]
|
||||
then
|
||||
_unskip_by_tag "${_target}"
|
||||
return
|
||||
fi
|
||||
|
||||
_acquire_lock
|
||||
_read_alarms
|
||||
|
||||
local -a _new_alarms=()
|
||||
local _found=0 _line
|
||||
|
||||
for _line in "${_ALARMS[@]:-}"
|
||||
do
|
||||
[[ -n "${_line}" ]] || continue
|
||||
local _id _tags _timestamp _rule _message
|
||||
_id="$(_get_alarm_field "${_line}" 1)"
|
||||
_tags="$(_get_alarm_field "${_line}" 2)"
|
||||
_timestamp="$(_get_alarm_field "${_line}" 3)"
|
||||
_rule="$(_get_alarm_field "${_line}" 4)"
|
||||
_message="$(_get_alarm_field "${_line}" 5)"
|
||||
|
||||
if [[ "${_id}" == "${_target}" ]]
|
||||
then
|
||||
_found=1
|
||||
if [[ -z "${_rule}" ]]
|
||||
then
|
||||
_release_lock
|
||||
_exit_1 printf "Alarm %s is a one-shot (no rule to unskip).\\n" "${_target}"
|
||||
fi
|
||||
|
||||
local _new_ts
|
||||
_new_ts="$(_unskip_timestamp "${_timestamp}" "${_rule}")"
|
||||
|
||||
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_new_ts}" "${_rule}" "${_message}")")
|
||||
|
||||
local _human_time
|
||||
_format_time "${_new_ts}"
|
||||
_human_time="${REPLY}"
|
||||
printf "Unskipped [%s] %s — next: %s\\n" "${_id}" "${_message}" "${_human_time}"
|
||||
else
|
||||
_new_alarms+=("${_line}")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ! _found ))
|
||||
then
|
||||
_release_lock
|
||||
_exit_1 printf "No alarm with ID %s.\\n" "${_target}"
|
||||
fi
|
||||
|
||||
_ALARMS=("${_new_alarms[@]}")
|
||||
_write_alarms
|
||||
_release_lock
|
||||
}
|
||||
|
||||
_unskip_by_tag() {
|
||||
local _tag="${1}"
|
||||
|
||||
_acquire_lock
|
||||
_read_alarms
|
||||
|
||||
local _match_count=0 _line
|
||||
for _line in "${_ALARMS[@]:-}"
|
||||
do
|
||||
[[ -n "${_line}" ]] || continue
|
||||
local _tags _rule
|
||||
_tags="$(_get_alarm_field "${_line}" 2)"
|
||||
_rule="$(_get_alarm_field "${_line}" 4)"
|
||||
if _alarm_has_tag "${_tags}" "${_tag}" && [[ -n "${_rule}" ]]
|
||||
then
|
||||
_match_count=$((_match_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( _match_count == 0 ))
|
||||
then
|
||||
_release_lock
|
||||
_exit_1 printf "No repeating alarms tagged [%s].\\n" "${_tag}"
|
||||
fi
|
||||
|
||||
if ! _confirm_action "Unskip" "${_match_count}" "${_tag}"
|
||||
then
|
||||
_release_lock
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a _new_alarms=()
|
||||
|
||||
for _line in "${_ALARMS[@]:-}"
|
||||
do
|
||||
[[ -n "${_line}" ]] || continue
|
||||
local _id _tags _timestamp _rule _message
|
||||
_id="$(_get_alarm_field "${_line}" 1)"
|
||||
_tags="$(_get_alarm_field "${_line}" 2)"
|
||||
_timestamp="$(_get_alarm_field "${_line}" 3)"
|
||||
_rule="$(_get_alarm_field "${_line}" 4)"
|
||||
_message="$(_get_alarm_field "${_line}" 5)"
|
||||
|
||||
if _alarm_has_tag "${_tags}" "${_tag}" && [[ -n "${_rule}" ]]
|
||||
then
|
||||
local _new_ts
|
||||
_new_ts="$(_unskip_timestamp "${_timestamp}" "${_rule}")"
|
||||
|
||||
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_new_ts}" "${_rule}" "${_message}")")
|
||||
local _human_time
|
||||
_format_time "${_new_ts}"
|
||||
_human_time="${REPLY}"
|
||||
printf "Unskipped [%s] %s — next: %s\\n" "${_id}" "${_message}" "${_human_time}"
|
||||
else
|
||||
_new_alarms+=("${_line}")
|
||||
fi
|
||||
done
|
||||
|
||||
_ALARMS=("${_new_alarms[@]}")
|
||||
_write_alarms
|
||||
_release_lock
|
||||
}
|
||||
|
||||
_unskip_all() {
|
||||
_acquire_lock
|
||||
_read_alarms
|
||||
|
||||
local _match_count=0 _line
|
||||
for _line in "${_ALARMS[@]:-}"
|
||||
do
|
||||
[[ -n "${_line}" ]] || continue
|
||||
local _rule
|
||||
_rule="$(_get_alarm_field "${_line}" 4)"
|
||||
[[ -n "${_rule}" ]] && _match_count=$((_match_count + 1))
|
||||
done
|
||||
|
||||
if (( _match_count == 0 ))
|
||||
then
|
||||
_release_lock
|
||||
_exit_1 printf "No repeating alarms to unskip.\\n"
|
||||
fi
|
||||
|
||||
if (( ! _YES ))
|
||||
then
|
||||
if _interactive_input
|
||||
then
|
||||
printf "Unskip all %s repeating alarm(s)? [y/N] " "${_match_count}"
|
||||
local _reply
|
||||
read -r _reply
|
||||
case "${_reply}" in
|
||||
[yY]*) ;;
|
||||
*)
|
||||
_release_lock
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
else
|
||||
_release_lock
|
||||
printf "Unskip all %s repeating alarm(s)? Pass -f to confirm.\\n" "${_match_count}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a _new_alarms=()
|
||||
|
||||
for _line in "${_ALARMS[@]:-}"
|
||||
do
|
||||
[[ -n "${_line}" ]] || continue
|
||||
local _id _tags _timestamp _rule _message
|
||||
_id="$(_get_alarm_field "${_line}" 1)"
|
||||
_tags="$(_get_alarm_field "${_line}" 2)"
|
||||
_timestamp="$(_get_alarm_field "${_line}" 3)"
|
||||
_rule="$(_get_alarm_field "${_line}" 4)"
|
||||
_message="$(_get_alarm_field "${_line}" 5)"
|
||||
|
||||
if [[ -n "${_rule}" ]]
|
||||
then
|
||||
local _new_ts
|
||||
_new_ts="$(_unskip_timestamp "${_timestamp}" "${_rule}")"
|
||||
|
||||
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_new_ts}" "${_rule}" "${_message}")")
|
||||
local _human_time
|
||||
_format_time "${_new_ts}"
|
||||
_human_time="${REPLY}"
|
||||
printf "Unskipped [%s] %s — next: %s\\n" "${_id}" "${_message}" "${_human_time}"
|
||||
else
|
||||
_new_alarms+=("${_line}")
|
||||
fi
|
||||
done
|
||||
|
||||
_ALARMS=("${_new_alarms[@]}")
|
||||
_write_alarms
|
||||
_release_lock
|
||||
}
|
||||
|
||||
# check #######################################################################
|
||||
|
||||
describe "check" <<HEREDOC
|
||||
|
|
|
|||
181
test/unskip.bats
Normal file
181
test/unskip.bats
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
@test "unskip resets repeating alarm to next natural occurrence" {
|
||||
run_nag every day "tomorrow 3pm" "daily task"
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
local _skipped_ts
|
||||
_skipped_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
|
||||
run_nag unskip 1
|
||||
[ "${status}" -eq 0 ]
|
||||
[[ "${output}" =~ "Unskipped [1]" ]]
|
||||
[[ "${output}" =~ "daily task" ]]
|
||||
|
||||
local _new_ts
|
||||
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
(( _new_ts < _skipped_ts ))
|
||||
(( _new_ts > $(date +%s) ))
|
||||
}
|
||||
|
||||
@test "unskip one-shot alarm fails" {
|
||||
run_nag at "tomorrow 3pm" "one-shot"
|
||||
run_nag unskip 1
|
||||
[ "${status}" -eq 1 ]
|
||||
[[ "${output}" =~ "one-shot" ]]
|
||||
}
|
||||
|
||||
@test "unskip with nonexistent ID fails" {
|
||||
run_nag unskip 99
|
||||
[ "${status}" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "unskip without args fails" {
|
||||
run_nag unskip
|
||||
[ "${status}" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "unskip by tag requires -f" {
|
||||
run_nag every day "tomorrow 3pm" "daily work"
|
||||
run_nag tag 1 work
|
||||
run_nag skip 1
|
||||
run "${_NAG}" unskip work < /dev/null
|
||||
[ "${status}" -eq 0 ]
|
||||
[[ "${output}" =~ "Unskip" ]]
|
||||
[[ "${output}" =~ "-f" ]]
|
||||
}
|
||||
|
||||
@test "unskip by tag with -f resets matching alarms" {
|
||||
run_nag every day "tomorrow 3pm" "daily work"
|
||||
run_nag tag 1 work
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
local _skipped_ts
|
||||
_skipped_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
|
||||
run "${_NAG}" -f unskip work
|
||||
[ "${status}" -eq 0 ]
|
||||
[[ "${output}" =~ "Unskipped" ]]
|
||||
|
||||
local _new_ts
|
||||
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
(( _new_ts < _skipped_ts ))
|
||||
}
|
||||
|
||||
@test "unskip by tag with no repeating alarms fails" {
|
||||
run_nag at "tomorrow 3pm" "one-shot"
|
||||
run_nag tag 1 work
|
||||
run "${_NAG}" -f unskip work
|
||||
[ "${status}" -eq 1 ]
|
||||
[[ "${output}" =~ "No repeating" ]]
|
||||
}
|
||||
|
||||
@test "unskip by tag with no matching tag fails" {
|
||||
run_nag every day "tomorrow 3pm" "daily task"
|
||||
run "${_NAG}" -f unskip nonexistent
|
||||
[ "${status}" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "unskip all resets all repeating alarms" {
|
||||
run_nag every day "tomorrow 3pm" "daily one"
|
||||
run_nag every day "tomorrow 4pm" "daily two"
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
run_nag skip 2
|
||||
run_nag skip 2
|
||||
|
||||
run "${_NAG}" -f unskip all
|
||||
[ "${status}" -eq 0 ]
|
||||
[[ "${output}" =~ "Unskipped [1]" ]]
|
||||
[[ "${output}" =~ "Unskipped [2]" ]]
|
||||
}
|
||||
|
||||
@test "unskip yearly alarm preserves month and day" {
|
||||
# Create a yearly alarm for a future date in the current year.
|
||||
local _target_month=$(( $(date +%-m) + 1 ))
|
||||
local _target_year=$(date +%Y)
|
||||
if (( _target_month > 12 )); then
|
||||
_target_month=1
|
||||
_target_year=$((_target_year + 1))
|
||||
fi
|
||||
local _target_date="$(printf "%04d-%02d-15 14:00:00" "${_target_year}" "${_target_month}")"
|
||||
local _target_ts="$(date -d "${_target_date}" +%s)"
|
||||
|
||||
# Write alarm directly: yearly rule, timestamp in the future.
|
||||
write_alarm "$(printf "1\t\t%s\tyear\tyearly event" "${_target_ts}")"
|
||||
|
||||
# Skip it twice to push it 2 years out.
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
local _skipped_ts
|
||||
_skipped_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
|
||||
run_nag unskip 1
|
||||
[ "${status}" -eq 0 ]
|
||||
|
||||
# Should come back to the original month/day, not today's date.
|
||||
local _new_ts _new_month _new_day
|
||||
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
_new_month="$(date -d "@${_new_ts}" +%-m)"
|
||||
_new_day="$(date -d "@${_new_ts}" +%-d)"
|
||||
[ "${_new_month}" -eq "${_target_month}" ]
|
||||
[ "${_new_day}" -eq 15 ]
|
||||
(( _new_ts > $(date +%s) ))
|
||||
(( _new_ts < _skipped_ts ))
|
||||
}
|
||||
|
||||
@test "unskip monthly alarm preserves day of month" {
|
||||
# Create a monthly alarm for the 20th at 2pm.
|
||||
local _now_day=$(date +%-d)
|
||||
local _target_day=20
|
||||
local _target_ts
|
||||
|
||||
# If today is past the 20th, set it for the 20th next month.
|
||||
if (( _now_day >= _target_day )); then
|
||||
_target_ts="$(date -d "$(date +%Y-%m-${_target_day}) + 1 month 14:00:00" +%s)"
|
||||
else
|
||||
_target_ts="$(date -d "$(date +%Y-%m-${_target_day}) 14:00:00" +%s)"
|
||||
fi
|
||||
|
||||
write_alarm "$(printf "1\t\t%s\tmonth\tmonthly event" "${_target_ts}")"
|
||||
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
local _skipped_ts
|
||||
_skipped_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
|
||||
run_nag unskip 1
|
||||
[ "${status}" -eq 0 ]
|
||||
|
||||
local _new_ts _new_day
|
||||
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
_new_day="$(date -d "@${_new_ts}" +%-d)"
|
||||
[ "${_new_day}" -eq "${_target_day}" ]
|
||||
(( _new_ts > $(date +%s) ))
|
||||
(( _new_ts < _skipped_ts ))
|
||||
}
|
||||
|
||||
@test "unskip weekday alarm lands on a weekday" {
|
||||
run_nag every weekday "tomorrow 9am" "standup"
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
run_nag skip 1
|
||||
|
||||
run_nag unskip 1
|
||||
[ "${status}" -eq 0 ]
|
||||
|
||||
local _new_ts _dow
|
||||
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
|
||||
_dow="$(date -d "@${_new_ts}" +%u)"
|
||||
# 1-5 = weekday
|
||||
(( _dow >= 1 && _dow <= 5 ))
|
||||
(( _new_ts > $(date +%s) ))
|
||||
}
|
||||
|
||||
@test "unskip all with no repeating alarms fails" {
|
||||
run_nag at "tomorrow 3pm" "one-shot"
|
||||
run "${_NAG}" -f unskip all
|
||||
[ "${status}" -eq 1 ]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue