feat: tag support for skip/stop, requiring a force, similar to rm -f

This commit is contained in:
Lewis Wynne 2026-04-02 16:45:04 +01:00
parent f68a92323c
commit 8cf79c854b
5 changed files with 239 additions and 36 deletions

View file

@ -5,8 +5,8 @@ Usage:
nag list all alarms nag list all alarms
nag <time> <message...> one-shot alarm nag <time> <message...> one-shot alarm
nag every <rules> <time> <message...> repeating alarm nag every <rules> <time> <message...> repeating alarm
nag stop <id> delete alarm nag stop <id|tag> delete alarm(s)
nag skip <id> skip next occurrence nag skip <id|tag> skip next occurrence(s)
nag tag <id> <tags...> add tags to an alarm nag tag <id> <tags...> add tags to an alarm
nag tag <tag> list alarms with a tag nag tag <tag> list alarms with a tag
nag untag <id> <tags...> remove tags from an alarm nag untag <id> <tags...> remove tags from an alarm
@ -130,10 +130,10 @@ Examples:
```text ```text
Usage: Usage:
nag stop <id> nag stop <id|tag>
Description: Description:
Stop an alarm by ID. Stop an alarm by ID, or stop all alarms with a tag (requires -f).
``` ```
#### `nag skip` #### `nag skip`
@ -146,11 +146,12 @@ Description:
```text ```text
Usage: Usage:
nag skip <id> nag skip <id|tag>
Description: Description:
Skip the next occurrence of a repeating alarm (reschedule without firing). Skip the next occurrence of a repeating alarm (reschedule without firing).
For one-shot alarms, this deletes them. For one-shot alarms, this deletes them. With a tag, applies to all
matching alarms (requires -f).
``` ```
#### `nag tag` #### `nag tag`
@ -216,10 +217,11 @@ Description:
```text ```text
Usage: Usage:
nag mute nag mute [<tag>]
Description: Description:
Mute alarm sounds. Notifications still fire, but no sound is played. Mute alarm sounds. With no argument, mutes all alarms globally.
With a tag, mutes only alarms with that tag.
``` ```
#### `nag unmute` #### `nag unmute`
@ -232,10 +234,11 @@ Description:
```text ```text
Usage: Usage:
nag unmute nag unmute [<tag>]
Description: Description:
Unmute alarm sounds. Unmute alarm sounds. With no argument, unmutes everything.
With a tag, unmutes only that tag.
``` ```
#### `nag edit` #### `nag edit`

186
nag
View file

@ -1192,8 +1192,8 @@ Usage:
${_ME} list all alarms ${_ME} list all alarms
${_ME} <time> <message...> one-shot alarm ${_ME} <time> <message...> one-shot alarm
${_ME} every <rules> <time> <message...> repeating alarm ${_ME} every <rules> <time> <message...> repeating alarm
${_ME} stop <id> delete alarm ${_ME} stop <id|tag> delete alarm(s)
${_ME} skip <id> skip next occurrence ${_ME} skip <id|tag> skip next occurrence(s)
${_ME} tag <id> <tags...> add tags to an alarm ${_ME} tag <id> <tags...> add tags to an alarm
${_ME} tag <tag> list alarms with a tag ${_ME} tag <tag> list alarms with a tag
${_ME} untag <id> <tags...> remove tags from an alarm ${_ME} untag <id> <tags...> remove tags from an alarm
@ -1300,14 +1300,20 @@ list() {
describe "stop" <<HEREDOC describe "stop" <<HEREDOC
Usage: Usage:
${_ME} stop <id> ${_ME} stop <id|tag>
Description: Description:
Stop an alarm by ID. Stop an alarm by ID, or stop all alarms with a tag (requires -f).
HEREDOC HEREDOC
stop() { stop() {
local _target_id="${1:-}" local _target="${1:-}"
[[ -n "${_target_id}" ]] || _exit_1 printf "Usage: %s stop <id>\\n" "${_ME}" [[ -n "${_target}" ]] || _exit_1 printf "Usage: %s stop <id|tag>\\n" "${_ME}"
if [[ ! "${_target}" =~ ^[0-9]+$ ]]
then
_stop_by_tag "${_target}"
return
fi
_acquire_lock _acquire_lock
_read_alarms _read_alarms
@ -1320,8 +1326,8 @@ stop() {
do do
[[ -n "${_line}" ]] || continue [[ -n "${_line}" ]] || continue
local _id local _id
_id="$(_get_alarm_field "${_line}" 1)" _id="${_line%%$'\t'*}"
if [[ "${_id}" == "${_target_id}" ]] if [[ "${_id}" == "${_target}" ]]
then then
_found=1 _found=1
else else
@ -1329,13 +1335,13 @@ stop() {
fi fi
done done
if [[ "${_found}" -eq 0 ]] if (( ! _found ))
then then
_release_lock _release_lock
_exit_1 printf "No alarm with ID %s.\\n" "${_target_id}" _exit_1 printf "No alarm with ID %s.\\n" "${_target}"
fi fi
if [[ "${#_new_alarms[@]}" -eq 0 ]] if (( ${#_new_alarms[@]} == 0 ))
then then
_ALARMS=() _ALARMS=()
: > "${_ALARMS_FILE}" : > "${_ALARMS_FILE}"
@ -1345,22 +1351,85 @@ stop() {
fi fi
_release_lock _release_lock
printf "Stopped alarm %s.\\n" "${_target_id}" printf "Stopped alarm %s.\\n" "${_target}"
}
_stop_by_tag() {
local _tag="${1}"
_acquire_lock
_read_alarms
local -a _new_alarms=()
local -a _matched=()
local _line
for _line in "${_ALARMS[@]:-}"
do
[[ -n "${_line}" ]] || continue
local _tags
_tags="$(_get_alarm_field "${_line}" 2)"
if _alarm_has_tag "${_tags}" "${_tag}"
then
_matched+=("${_line}")
else
_new_alarms+=("${_line}")
fi
done
if (( ${#_matched[@]} == 0 ))
then
_release_lock
_exit_1 printf "No alarms tagged [%s].\\n" "${_tag}"
fi
if (( ! _YES ))
then
_release_lock
printf "Would stop %s alarm(s) tagged [%s]. Pass -f to confirm.\\n" "${#_matched[@]}" "${_tag}"
return 0
fi
if (( ${#_new_alarms[@]} == 0 ))
then
_ALARMS=()
: > "${_ALARMS_FILE}"
else
_ALARMS=("${_new_alarms[@]}")
_write_alarms
fi
_release_lock
local _m
for _m in "${_matched[@]}"
do
local _id _message
_id="${_m%%$'\t'*}"
_message="$(_get_alarm_field "${_m}" 5)"
printf "Stopped [%s] %s\\n" "${_id}" "${_message}"
done
} }
# skip ######################################################################## # skip ########################################################################
describe "skip" <<HEREDOC describe "skip" <<HEREDOC
Usage: Usage:
${_ME} skip <id> ${_ME} skip <id|tag>
Description: Description:
Skip the next occurrence of a repeating alarm (reschedule without firing). Skip the next occurrence of a repeating alarm (reschedule without firing).
For one-shot alarms, this deletes them. For one-shot alarms, this deletes them. With a tag, applies to all
matching alarms (requires -f).
HEREDOC HEREDOC
skip() { skip() {
local _target_id="${1:-}" local _target="${1:-}"
[[ -n "${_target_id}" ]] || _exit_1 printf "Usage: %s skip <id>\\n" "${_ME}" [[ -n "${_target}" ]] || _exit_1 printf "Usage: %s skip <id|tag>\\n" "${_ME}"
if [[ ! "${_target}" =~ ^[0-9]+$ ]]
then
_skip_by_tag "${_target}"
return
fi
_acquire_lock _acquire_lock
_read_alarms _read_alarms
@ -1379,7 +1448,7 @@ skip() {
_rule="$(_get_alarm_field "${_line}" 4)" _rule="$(_get_alarm_field "${_line}" 4)"
_message="$(_get_alarm_field "${_line}" 5)" _message="$(_get_alarm_field "${_line}" 5)"
if [[ "${_id}" == "${_target_id}" ]] if [[ "${_id}" == "${_target}" ]]
then then
_found=1 _found=1
if [[ -n "${_rule}" ]] if [[ -n "${_rule}" ]]
@ -1389,22 +1458,95 @@ skip() {
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_next_ts}" "${_rule}" "${_message}")") _new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_next_ts}" "${_rule}" "${_message}")")
_format_time "${_next_ts}" _format_time "${_next_ts}"
_human_time="${REPLY}" _human_time="${REPLY}"
printf "Skipped. Next: %s\\n" "${_human_time}" printf "Skipped [%s] %s — next: %s\\n" "${_id}" "${_message}" "${_human_time}"
else else
printf "Stopped alarm %s.\\n" "${_id}" printf "Stopped [%s] %s\\n" "${_id}" "${_message}"
fi fi
else else
_new_alarms+=("${_line}") _new_alarms+=("${_line}")
fi fi
done done
if [[ "${_found}" -eq 0 ]] if (( ! _found ))
then then
_release_lock _release_lock
_exit_1 printf "No alarm with ID %s.\\n" "${_target_id}" _exit_1 printf "No alarm with ID %s.\\n" "${_target}"
fi fi
if [[ "${#_new_alarms[@]}" -eq 0 ]] if (( ${#_new_alarms[@]} == 0 ))
then
_ALARMS=()
: > "${_ALARMS_FILE}"
else
_ALARMS=("${_new_alarms[@]}")
_write_alarms
fi
_release_lock
}
_skip_by_tag() {
local _tag="${1}"
_acquire_lock
_read_alarms
local -a _new_alarms=()
local _match_count=0
local _line
for _line in "${_ALARMS[@]:-}"
do
[[ -n "${_line}" ]] || continue
local _tags
_tags="$(_get_alarm_field "${_line}" 2)"
if _alarm_has_tag "${_tags}" "${_tag}"
then
_match_count=$((_match_count + 1))
fi
done
if (( _match_count == 0 ))
then
_release_lock
_exit_1 printf "No alarms tagged [%s].\\n" "${_tag}"
fi
if (( ! _YES ))
then
_release_lock
printf "Would skip %s alarm(s) tagged [%s]. Pass -f to confirm.\\n" "${_match_count}" "${_tag}"
return 0
fi
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}"
then
if [[ -n "${_rule}" ]]
then
local _next_ts _human_time
_next_ts="$(_next_occurrence "${_rule}" "${_timestamp}")"
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_next_ts}" "${_rule}" "${_message}")")
_format_time "${_next_ts}"
_human_time="${REPLY}"
printf "Skipped [%s] %s — next: %s\\n" "${_id}" "${_message}" "${_human_time}"
else
printf "Stopped [%s] %s\\n" "${_id}" "${_message}"
fi
else
_new_alarms+=("${_line}")
fi
done
if (( ${#_new_alarms[@]} == 0 ))
then then
_ALARMS=() _ALARMS=()
: > "${_ALARMS_FILE}" : > "${_ALARMS_FILE}"

View file

@ -7,8 +7,8 @@ load test_helper
[ "${status}" -eq 0 ] [ "${status}" -eq 0 ]
[[ "${output}" =~ "<time> <message...>" ]] [[ "${output}" =~ "<time> <message...>" ]]
[[ "${output}" =~ "every <rules> <time> <message...>" ]] [[ "${output}" =~ "every <rules> <time> <message...>" ]]
[[ "${output}" =~ "stop <id>" ]] [[ "${output}" =~ "stop <id|tag>" ]]
[[ "${output}" =~ "skip <id>" ]] [[ "${output}" =~ "skip <id|tag>" ]]
[[ "${output}" =~ "check" ]] [[ "${output}" =~ "check" ]]
[[ "${output}" =~ "help [<subcommand>]" ]] [[ "${output}" =~ "help [<subcommand>]" ]]
[[ "${output}" =~ "Options:" ]] [[ "${output}" =~ "Options:" ]]
@ -39,14 +39,14 @@ load test_helper
run_nag help stop run_nag help stop
[ "${status}" -eq 0 ] [ "${status}" -eq 0 ]
[[ "${output}" =~ "Usage:" ]] [[ "${output}" =~ "Usage:" ]]
[[ "${output}" =~ "stop <id>" ]] [[ "${output}" =~ "stop <id|tag>" ]]
} }
@test "help skip shows skip usage" { @test "help skip shows skip usage" {
run_nag help skip run_nag help skip
[ "${status}" -eq 0 ] [ "${status}" -eq 0 ]
[[ "${output}" =~ "Usage:" ]] [[ "${output}" =~ "Usage:" ]]
[[ "${output}" =~ "skip <id>" ]] [[ "${output}" =~ "skip <id|tag>" ]]
} }
@test "help every shows every usage" { @test "help every shows every usage" {

View file

@ -32,3 +32,32 @@ load test_helper
run_nag skip run_nag skip
[ "${status}" -eq 1 ] [ "${status}" -eq 1 ]
} }
@test "skip by tag requires -f" {
run_nag every day "tomorrow 3pm" "daily work"
run_nag tag 1 work
run "${_NAG}" skip work
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Would skip" ]]
[[ "${output}" =~ "-f" ]]
}
@test "skip by tag with -f reschedules matching alarms" {
run_nag every day "tomorrow 3pm" "daily work"
run_nag tag 1 work
local _old_ts
_old_ts="$(cut -f3 "${NAG_DIR}/alarms")"
run "${_NAG}" -f skip work
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Skipped" ]]
[[ "${output}" =~ "daily work" ]]
local _new_ts
_new_ts="$(cut -f3 "${NAG_DIR}/alarms")"
(( _new_ts > _old_ts ))
}
@test "skip by tag with no matches fails" {
run_nag at "tomorrow 3pm" "test alarm"
run "${_NAG}" -f skip work
[ "${status}" -eq 1 ]
}

View file

@ -32,3 +32,32 @@ load test_helper
run_nag stop run_nag stop
[ "${status}" -eq 1 ] [ "${status}" -eq 1 ]
} }
@test "stop by tag requires -f" {
run_nag at "tomorrow 3pm" "tagged alarm"
run_nag tag 1 work
run "${_NAG}" stop work
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Would stop" ]]
[[ "${output}" =~ "-f" ]]
[ -s "${NAG_DIR}/alarms" ]
}
@test "stop by tag with -f removes matching alarms" {
run_nag at "tomorrow 3pm" "work alarm"
run_nag tag 1 work
run_nag at "tomorrow 4pm" "personal alarm"
run "${_NAG}" -f stop work
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Stopped" ]]
[[ "${output}" =~ "work alarm" ]]
run_nag
[[ "${output}" =~ "personal alarm" ]]
[[ ! "${output}" =~ "work alarm" ]]
}
@test "stop by tag with no matches fails" {
run_nag at "tomorrow 3pm" "test alarm"
run "${_NAG}" -f stop work
[ "${status}" -eq 1 ]
}