<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ttym.git, branch main</title>
<subtitle>simple, modular CLI timer and stopwatch written in C</subtitle>
<id>https://git.labunix.xyz/ttym.git/atom?h=main</id>
<link rel='self' href='https://git.labunix.xyz/ttym.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/'/>
<updated>2026-08-25T18:28:45Z</updated>
<entry>
<title>timer.1: document how to run hooks selectively</title>
<updated>2026-08-25T18:28:45Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-25T18:28:45Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=9ba93a546018ef119f3b06812391d8baa57358be'/>
<id>urn:sha1:9ba93a546018ef119f3b06812391d8baa57358be</id>
<content type='text'>
Hooks are all-or-nothing per user: a file is installed or it is not, and
there is no per-run flag. That the gate belongs inside the hook, and
what it has to gate on, was only discoverable by reading the source.

A hook inherits the environment timer was run with and is handed the -c
comment as $3, so three gates are available: an environment variable for
explicit opt-in, a +tag in the comment (free if you already pass -c, and
still visible to timer --stats), or the session itself via $1 and $2 --
countdowns over ten minutes, say -- which needs nothing remembered.

Also records the asymmetry that is easy to get wrong: gate on_start
only. A hook that undoes something must run unconditionally in on_done
and on_quit, or a session ending without the gate set leaves
notifications paused or music stopped. Restore paths are harmless when
there was nothing to restore.

The shipped on_start now points at the new section, so the trick is
visible from the file someone is already editing.

Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
</content>
</entry>
<entry>
<title>hooks: replace the examples with ones that do what the binary cannot</title>
<updated>2026-08-25T17:35:46Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-25T17:35:46Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=8543890f2e90af75a9bed7fa692ac92fc43285a4'/>
<id>urn:sha1:8543890f2e90af75a9bed7fa692ac92fc43285a4</id>
<content type='text'>
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
</content>
</entry>
<entry>
<title>hooks: ship three commented examples, install them, document the contract</title>
<updated>2026-08-25T17:19:14Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-25T17:19:14Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=f8a687cbb855ed4e3aa54128624101a21ba5a8c3'/>
<id>urn:sha1:f8a687cbb855ed4e3aa54128624101a21ba5a8c3</id>
<content type='text'>
Add hooks/on_start, hooks/on_done and hooks/on_quit. They are named
after the hooks themselves so copying one into ~/.config/ttym/hooks/ is
the entire installation step, and each carries its argument contract in
a header comment.

  on_start  sets the terminal window title to what is running
  on_done   desktop notification plus a sound, both backgrounded, with
            the first available of paplay/aplay/mpv/ffplay
  on_quit   records abandoned sessions to a TSV under XDG_STATE_HOME and
            restores the terminal title

make install places them in PREFIX/share/doc/ttym/hooks; make uninstall
removes them and both directories; make dist ships hooks/ with the
executable bits intact.

The man page's HOOKS section now documents what the code actually does
rather than what could be assumed from the call sites:

  - $2 differs per hook: requested duration on on_start, full duration
    on on_done, and time actually elapsed -- excluding time spent paused
    -- on on_quit. An abandoned 25m countdown reports what really ran.
  - $3 is always passed, so it is never unset.
  - hooks block: timer forks and waitpid()s, so on_done delays the
    completion bell and the alert loop. Background anything slow.
  - stdin, stdout and stderr all go to /dev/null, which is why printing
    is pointless and cannot corrupt the redrawn progress line.
  - execvp(3) means a script needs a #! line.

README gains a Hooks section. All three examples pass sh -n and
shellcheck, and were tested end to end through the documented flow:
make install to a staging prefix, copy into a config dir, run.

Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
</content>
</entry>
<entry>
<title>patches: drop generated config.h and binary sections from the diffs</title>
<updated>2026-08-25T17:08:14Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-25T17:08:14Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=168361ef112c61391a615d8b1f0048b79a2b7237'/>
<id>urn:sha1:168361ef112c61391a615d8b1f0048b79a2b7237</id>
<content type='text'>
The 3.0 patch files each carried a spurious diff section for config.h --
a file the Makefile generates from config.def.h -- plus "Binary files
a/timer and b/timer differ" noise. Applying any of them made patch(1)
stop and prompt "File to patch:", which stalls a non-interactive apply
and prints "1 out of 1 hunk ignored" otherwise.

Cause: the diff-generation step cleaned its scratch trees with a glob
that matched nothing, and the shell aborts the whole command line on a
failed glob, so the cleanup never ran and the build products were left
in place.

The resulting builds were correct either way, since make regenerates
config.h from config.def.h, but the patches were not clean. Each patch
now touches only ttym.c and config.def.h.

Verified with patch --batch &lt; /dev/null so a prompt is fatal rather than
silently skipped: all five apply with no prompt, skip, ignored hunk or
fuzz, and compile.

Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
</content>
</entry>
<entry>
<title>ttym 3.0: fix terminal and config bugs, fold terminal-only patches into base</title>
<updated>2026-08-25T17:02:35Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-25T17:02:35Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=8f99f1650e4a7363af1d83190b4082fb72a9f73f'/>
<id>urn:sha1:8f99f1650e4a7363af1d83190b4082fb72a9f73f</id>
<content type='text'>
Correctness fixes:
  - restore the terminal on SIGQUIT and SIGHUP. atexit never runs on an
    uncaught fatal signal, so Ctrl+\ left the cursor hidden and autowrap
    disabled.
  - drop the unconditional newline in restore_tty. Every run emitted a
    stray blank line, including piped output.
  - treat EINTR as an interruption rather than a poll() failure, which
    delayed Ctrl+C by up to one 100ms frame.

config-file patch:
  - make comment stripping quote-aware so a value may contain '#', and
    strip one matched pair of quotes. bar_fill = "#" was silently dropped,
    despite '#' being the glyph of the hash style this build ships.
  - resolve bar_fill/bar_empty inside resolve_bar and clear them on --bar,
    so command-line flags beat the config file as documented. Previously
    config glyphs silently overrode --bar.
  - report unknown keys, unparseable booleans and malformed lines on
    stderr. flash = enabled silently meant off.
  - never write to disk. --dump-config prints a commented template on
    stdout instead of the binary creating ~/.config/ttym/config on first
    run.

Patch set:
  - fold persist-alert, flash and bar-styles into the base. The base is
    now terminal-only: it opens /dev/tty, draws and exits, with no file
    writes and no subprocesses. Everything still in patches/ changes what
    the program touches.
  - seven patches become four; config-file's prerequisites drop from six
    patches to one (notify), so it is regenerable mechanically.
  - regenerate every patch. All apply with zero fuzz and compile.

Source:
  - rewrite argv in place rather than into a malloc'd copy (C99
    5.1.2.2.1), removing die(), four free() calls and &lt;stdarg.h&gt;.
  - fold the duplicated elapsed-time expression into elapsed().
  - drop dead code: have_any, (void)cfg, and an unreachable branch in
    parse_duration.
  - replace hand-counted padding with \033[K.

Docs and build:
  - timer.1: -c is the third hook argument, not the fourth. Add EXIT
    STATUS (a stopwatch always exits 130 -- it has no natural end) and
    ENVIRONMENT. Document config precedence and quoting.
  - wire VERSION into a dist target; it was defined and never used.
  - track .gitignore, which previously ignored itself, so a fresh clone
    had no ignore rules.

Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
</content>
</entry>
<entry>
<title>modified readme.md</title>
<updated>2026-05-20T12:34:20Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-05-20T12:34:20Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=2f6b29abd0d766ef477eb29c5a7bf6c1e88e9630'/>
<id>urn:sha1:2f6b29abd0d766ef477eb29c5a7bf6c1e88e9630</id>
<content type='text'>
</content>
</entry>
<entry>
<title>initial commit</title>
<updated>2026-05-20T12:23:09Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-05-20T12:23:09Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/ttym.git/commit/?id=9e3fcd953e7136b900d6aabafe1c64c083a927be'/>
<id>urn:sha1:9e3fcd953e7136b900d6aabafe1c64c083a927be</id>
<content type='text'>
</content>
</entry>
</feed>
