diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 19:35:46 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 19:35:46 +0200 |
| commit | 8543890f2e90af75a9bed7fa692ac92fc43285a4 (patch) | |
| tree | a1649e7a14b951e84f9141904b2e5ed5bf9f6094 | |
| parent | f8a687cbb855ed4e3aa54128624101a21ba5a8c3 (diff) | |
| download | ttym-8543890f2e90af75a9bed7fa692ac92fc43285a4.tar.gz ttym-8543890f2e90af75a9bed7fa692ac92fc43285a4.zip | |
hooks: replace the examples with ones that do what the binary cannot
The previous examples largely duplicated features of the patched build.
on_done sent a desktop notification and played a sound, both of which
the notify patch already does, so a user who copied it got two of each.
on_quit recorded abandoned sessions, but the logging patch calls
write_log() unconditionally at the end of every run: abandoned sessions
were already in the log behind timer -r.
The new set does two things nothing else can:
- Intent versus outcome. The log stores elapsed seconds only, so a
finished 25m and an abandoned 30m that ran 25m are indistinguishable
in it. The duration actually requested is visible to on_start and
nowhere else, so the examples write START/DONE/QUIT records to
$XDG_STATE_HOME/ttym/sessions.tsv.
- Focus mode. During a countdown, pause dunst notifications and any
playerctl/mpc playback; restore on done or quit. Countdown only, so
a stopwatch cannot silence notifications indefinitely.
A timewarrior bridge is included but commented out.
Two behaviours documented in the file comments, both read off the code
rather than assumed: notify_and_sound() runs before the on_done hook, so
with dunst paused the completion popup is queued and appears when the
hook unpauses -- delayed, not lost; and nothing restores dunst if the
process is killed with -9.
Every external command is guarded with command -v, so the examples
degrade to the session record alone on a machine without those tools.
Each hook works standalone -- the man page and README now say so
explicitly, since installing only on_quit is a normal setup. All three
pass sh -n and shellcheck, and were tested across all three paths:
completed countdown, abandoned countdown, stopped stopwatch, with
dunstctl is-paused confirmed false afterwards.
Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
| -rw-r--r-- | README.md | 11 | ||||
| -rwxr-xr-x | hooks/on_done | 43 | ||||
| -rwxr-xr-x | hooks/on_quit | 36 | ||||
| -rwxr-xr-x | hooks/on_start | 52 | ||||
| -rw-r--r-- | timer.1 | 34 |
5 files changed, 120 insertions, 56 deletions
@@ -56,10 +56,13 @@ arguments: mode (`countdown`/`stopwatch`), seconds, and the `-c` comment. Dropping in an executable file is the only thing needed to enable one. -Three commented examples ship in `hooks/` and are installed to -`$(PREFIX)/share/doc/ttym/hooks/` -- terminal title, desktop -notification plus sound on completion, and a TSV record of abandoned -sessions: +The three are independent -- installing only `on_quit` is a normal +setup. Three commented examples ship in `hooks/` and are installed to +`$(PREFIX)/share/doc/ttym/hooks/`. They record intent against outcome +(the duration you asked for is visible only to `on_start`, so the log +behind `timer -r` cannot show whether a session was cut short), and +run a focus mode that pauses dunst notifications and music during a +countdown. A commented timewarrior bridge is included: mkdir -p ~/.config/ttym/hooks cp /usr/local/share/doc/ttym/hooks/on_done ~/.config/ttym/hooks/ diff --git a/hooks/on_done b/hooks/on_done index 955cd13..cfe57f2 100755 --- a/hooks/on_done +++ b/hooks/on_done @@ -1,31 +1,36 @@ #!/bin/sh # ttym on_done -- runs when a countdown reaches zero. # -# $1 mode always "countdown" (a stopwatch never completes) +# $1 mode always "countdown" -- a stopwatch never completes # $2 seconds the full duration that elapsed # $3 comment the -c text, or an empty string # -# timer waits for this to exit before ringing the bell and entering the -# alert loop, so background anything slow. +# Nothing here duplicates the notify patch: if your build has it, the +# desktop notification and sound have already fired by the time this +# runs. Use this for what the binary does not do. +# +# timer waits for this before the completion bell and the alert loop, so +# background anything slow with &. -seconds=$2 comment=$3 -mins=$((seconds / 60)) +mode=$1 seconds=$2 comment=$3 -# Desktop notification. Gives the base build what the notify patch adds. -if command -v notify-send > /dev/null 2>&1; then - notify-send -u critical "Timer" "${mins}m done${comment:+ -- $comment}" & +# --- session record ------------------------------------------------------ +state=${XDG_STATE_HOME:-$HOME/.local/state}/ttym +if mkdir -p "$state" 2>/dev/null; then + printf '%s\tDONE\t%s\t%s\t%s\n' \ + "$(date +%Y-%m-%dT%H:%M:%S)" "$mode" "$seconds" "$comment" \ + >> "$state/sessions.tsv" fi -# A sound, first player that exists wins. Backgrounded so it never blocks. -for player in paplay aplay mpv ffplay; do - if command -v "$player" > /dev/null 2>&1; then - sound=/usr/share/sounds/freedesktop/stereo/complete.oga - [ -r "$sound" ] && "$player" "$sound" > /dev/null 2>&1 & - break - fi -done +# --- leave focus mode ---------------------------------------------------- +# Note the ordering: the notify patch emits its completion popup *before* +# this hook runs, so with dunst paused that popup is queued and appears +# the moment the next line unpauses. Delayed, not lost. +command -v dunstctl > /dev/null 2>&1 && dunstctl set-paused false +command -v playerctl > /dev/null 2>&1 && playerctl play 2>/dev/null +command -v mpc > /dev/null 2>&1 && mpc -q play 2>/dev/null -# Push to your phone (uncomment and set your topic): -# curl -fsS -d "${mins}m done${comment:+ -- $comment}" ntfy.sh/your-topic & +# --- timewarrior --------------------------------------------------------- +# command -v timew > /dev/null 2>&1 && timew stop > /dev/null 2>&1 -wait +exit 0 diff --git a/hooks/on_quit b/hooks/on_quit index 44957e8..f89f6cf 100755 --- a/hooks/on_quit +++ b/hooks/on_quit @@ -7,22 +7,30 @@ # $2 seconds time actually elapsed, excluding time spent paused # $3 comment the -c text, or an empty string # -# A countdown abandoned after two seconds reports 2, not the duration -# you asked for. That is what makes this useful for tracking follow- -# through rather than intent. +# $2 is what really ran, not what you asked for: a 25m countdown +# abandoned after four minutes reports 240. Compare against the START +# line written by on_start to see follow-through. +# +# If your build has the logging patch, `timer -r` already records this +# session -- it logs every run, completed or not. What it cannot show is +# that the run was cut short, which is what the record below adds. mode=$1 seconds=$2 comment=$3 -log=${XDG_STATE_HOME:-$HOME/.local/state}/ttym -mkdir -p "$log" 2>/dev/null || exit 0 +# --- session record ------------------------------------------------------ +state=${XDG_STATE_HOME:-$HOME/.local/state}/ttym +if mkdir -p "$state" 2>/dev/null; then + printf '%s\tQUIT\t%s\t%s\t%s\n' \ + "$(date +%Y-%m-%dT%H:%M:%S)" "$mode" "$seconds" "$comment" \ + >> "$state/sessions.tsv" +fi + +# --- leave focus mode ---------------------------------------------------- +command -v dunstctl > /dev/null 2>&1 && dunstctl set-paused false +[ "$mode" = countdown ] && command -v playerctl > /dev/null 2>&1 && playerctl play 2>/dev/null +[ "$mode" = countdown ] && command -v mpc > /dev/null 2>&1 && mpc -q play 2>/dev/null -printf '%s\t%s\t%s\t%s\n' \ - "$(date +%Y-%m-%dT%H:%M:%S)" "$mode" "$seconds" "$comment" \ - >> "$log/abandoned.tsv" +# --- timewarrior --------------------------------------------------------- +# command -v timew > /dev/null 2>&1 && timew stop > /dev/null 2>&1 -# Restore the terminal title set by on_start. -case $TERM in -xterm*|rxvt*|tmux*|screen*|alacritty|foot*) - printf '\033]0;%s\007' "${SHELL##*/}" > /dev/tty - ;; -esac +exit 0 diff --git a/hooks/on_start b/hooks/on_start index 441d6f1..fa38f7c 100755 --- a/hooks/on_start +++ b/hooks/on_start @@ -2,25 +2,45 @@ # ttym on_start -- runs when a session begins. # # $1 mode countdown | stopwatch -# $2 seconds requested duration for a countdown, 0 for a stopwatch +# $2 seconds requested duration of a countdown, 0 for a stopwatch # $3 comment the -c text, or an empty string # -# stdin, stdout and stderr are /dev/null: printing here goes nowhere and -# cannot corrupt the redrawn progress line. timer waits for this to exit, -# so keep it quick or background the slow part with &. +# Each hook works on its own: install only the ones you want. A hook that +# is missing or not executable is skipped silently. +# +# timer waits for this to exit, so keep it quick. stdout and stderr go to +# /dev/null -- printing here goes nowhere and cannot corrupt the progress +# line. To debug, run the hook by hand: +# +# ~/.config/ttym/hooks/on_start countdown 1500 "+work" mode=$1 seconds=$2 comment=$3 -# Name the terminal window after what is running. -case $TERM in -xterm*|rxvt*|tmux*|screen*|alacritty|foot*) - if [ "$mode" = countdown ]; then - printf '\033]0;timer %sm %s\007' "$((seconds / 60))" "$comment" > /dev/tty - else - printf '\033]0;stopwatch %s\007' "$comment" > /dev/tty - fi - ;; -esac +# --- session record ------------------------------------------------------ +# The log behind `timer -r` stores elapsed seconds only. It cannot tell a +# finished 25m from an abandoned 30m that ran 25m, because the duration +# you *asked for* is visible here and nowhere else. Pair this with the +# matching lines in on_done and on_quit to get intent versus outcome. +state=${XDG_STATE_HOME:-$HOME/.local/state}/ttym +if mkdir -p "$state" 2>/dev/null; then + printf '%s\tSTART\t%s\t%s\t%s\n' \ + "$(date +%Y-%m-%dT%H:%M:%S)" "$mode" "$seconds" "$comment" \ + >> "$state/sessions.tsv" +fi + +# --- focus mode ---------------------------------------------------------- +# Countdowns only: a stopwatch has no end, and silencing notifications +# indefinitely is not what you want. If the process is killed with -9 +# nothing restores these; recover with `dunstctl set-paused false`. +if [ "$mode" = countdown ]; then + command -v dunstctl > /dev/null 2>&1 && dunstctl set-paused true + command -v playerctl > /dev/null 2>&1 && playerctl pause 2>/dev/null + command -v mpc > /dev/null 2>&1 && mpc -q pause 2>/dev/null +fi + +# --- timewarrior --------------------------------------------------------- +# Turns -c comments into timew tags, so `timew summary :week +work` covers +# timer sessions alongside everything else you track. Uncomment to use. +# command -v timew > /dev/null 2>&1 && timew start ${comment:-timer} > /dev/null 2>&1 -# Pause music while a countdown runs (uncomment to use): -# [ "$mode" = countdown ] && playerctl pause 2>/dev/null +exit 0 @@ -256,6 +256,9 @@ or when a stopwatch is stopped. .PP A hook is any executable file at that name; there is nothing to enable. A file that is missing or not marked executable is skipped silently. +The three are independent: installing only +.BR on_quit , +for instance, is a complete and normal setup. .SS Arguments Each hook is run with exactly three positional arguments. .TP @@ -316,9 +319,34 @@ line like any other. Three commented, working examples ship with the source and are installed to .IR PREFIX/share/doc/ttym/hooks/ . -They set the terminal title, send a desktop notification and play a -sound on completion, and record abandoned sessions to a TSV file. Copy -the ones you want and edit: +They form one coherent set but each works alone. Together they do two +things the binary cannot: +.IP \(bu 2 +Record intent against outcome. The session log behind +.B timer \-r +stores elapsed seconds only, so a finished 25m and an abandoned 30m that +ran 25m look identical in it. The duration you asked for is visible to +.B on_start +and nowhere else, so the examples write a START/DONE/QUIT record to +.I $XDG_STATE_HOME/ttym/sessions.tsv +that shows follow\-through. +.IP \(bu 2 +Focus mode. During a countdown they pause +.BR dunst (1) +notifications and any +.BR playerctl (1) +or +.BR mpc (1) +playback, and restore both when it ends. Every external command is +guarded, so the examples are harmless on a machine without them. +.PP +A commented +.BR timew (1) +bridge is included but off by default; it turns +.B \-c +comments into timewarrior tags. +.PP +Copy the ones you want and edit: .PP .RS .nf |
