A small live clock in the footer mode labels, ticking on a timer instead of on activity

.config/i3
.config/nvim
Figuring out what works for me
Used ideas from people like ThePrimeagen and others.
Things needed -nerd fonts -zsh -oh-my-zsh
-zoxide -fzf -tmux -tpm -tmux session wizard (should probably roll my own)
hooks/register.ts 67 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// 🐇 follow the white rabbit
5export const label = (hhmm: string) => `\u{1F407} ${hhmm}`
6
7const time = atom({ plugin: 'clock', key: 'time' } as const, '')
8
9// The module's environment may not share the host's time zone, so the
10// offset comes from the host's own `date` once per load.
11const offsetMs = async (run: (argv: readonly string[]) => Promise<{ exitCode: number; stdout: string }>) => {
12 try {
13 const { exitCode, stdout } = await run(['date', '+%z'])
14 const m = /^([+-])(\d{2})(\d{2})/.exec(stdout.trim())
15 if (exitCode !== 0 || !m) return 0
16 const sign = m[1] === '-' ? -1 : 1
17 return sign * (Number(m[2]) * 60 + Number(m[3])) * 60_000
18 } catch {
19 return 0
20 }
21}
22
23export const format = (epochMs: number, offset: number) => {
24 const d = new Date(epochMs + offset)
25 const hh = String(d.getUTCHours()).padStart(2, '0')
26 const mm = String(d.getUTCMinutes()).padStart(2, '0')
27 return `${hh}:${mm}`
28}
29
30export const register: Register = on => {
31 on('session.start', async ($, e, next) => {
32 const started = await next(e)
33 if (!e.isInteractive) return started
34
35 // Re-read hourly so a DST change or a new time zone shows up without a reload.
36 let offset = await offsetMs(argv => $.process.run(argv))
37 let offsetReadAt = await $.clock.now()
38 let shown = ''
39 const tick = async () => {
40 const now = await $.clock.now()
41 if (now - offsetReadAt >= 3_600_000) {
42 offset = await offsetMs(argv => $.process.run(argv))
43 offsetReadAt = now
44 }
45 const text = format(now, offset)
46 if (text === shown) return
47 shown = text
48 await update($, time, () => text)
49 }
50
51 // Earlier versions pinned the clock with $.ui.status, which the engine
52 // prefixes with its own icon; clear any such entry a reload left behind.
53 $.ui.status(undefined)
54 await tick()
55 $.clock.every(5_000, () => void tick())
56
57 return started
58 })
59
60 // Drawn as a footer mode label, beside the engine's own (`focus`, ...).
61 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
62 const now = await read($, time)
63 if (now === '') return next(e)
64 return next({ ...e, props: { ...e.props, modes: [...e.props.modes, label(now)] } })
65 })
66}
67types/index.d.ts 9 lines1/** The time the footer label shows, as HH:MM in local time; '' before the first tick. */
2export type ClockTime = string
3
4declare module 'claude-code' {
5 interface PluginState {
6 clock: { time: ClockTime }
7 }
8}
9