← Projects
MIT · Open source

ding-cli

A tiny macOS alarm/timer CLI — set a relative or absolute time, get a notification and a sound when it fires

Originally built to remember when Claude's usage quota resets — ding 5h "quota is back" and forget about it.


🌟 Features

  • Relative and absolute times5h, 90m, 1h30m, 30s, 14:30, 2:30pm, 9am; absolute times already past today roll to tomorrow
  • 🔔 Desktop notifications — zero-dependency setup; ships with node-notifier which vendors its own terminal-notifier, no brew install required
  • 🔊 Built-in alarm presets — six synthesised sounds (beep, digital, radar, bell, siren, chime), macOS Clock ringtones (e.g. Radial, Daybreak, Arpeggio), macOS system sounds, or any audio file via afplay
  • 🧙 Interactive wizardding with no arguments launches a tabbed TUI (When · Message · Sound · Notify · Mode · Review); press Space on the Sound step to preview before choosing
  • 🔗 Click-to-open notifications--open https://claude.ai makes the notification banner a shortcut to any URL
  • 🖥 Foreground and detached modes — live countdown with progress bar by default; --detach backgrounds the process and returns the prompt immediately

🚀 Quick Start

Install globally:

npm install -g @kud/ding-cli

Or run without installing:

npx @kud/ding-cli 5h "quota is back"

Set a timer and watch the countdown:

$ ding 90m "tea is ready"
ding → 16:37:22 · tea is ready
01:29:58 ▸ tea is ready

When it fires, the terminal rings (loops the alarm) until you press any key to dismiss, and a desktop notification pops up.


📖 CLI Reference

ding <time> [message] [options]

Time formats

FormatExampleMeaning
Relative — hours5h5 hours from now
Relative — minutes90m90 minutes from now
Relative — seconds30s30 seconds from now
Relative — compound1h30m1 hour 30 minutes from now
Bare number4545 minutes from now
24-hour clock14:30fires at 14:30 today (or tomorrow if past)
12-hour clock2:30pmfires at 14:30
Hour only9amfires at 09:00

Options

FlagAliasDescription
--detach-dBackground the process; prints fire time then returns the prompt
--sound <value>-sAlarm preset (beep digital radar bell siren chime), macOS Clock ringtone name (e.g. Daybreak; quote names with spaces like "Milky Way"), macOS system sound name (e.g. Glass), or path to an audio file. Default: bell
--no-soundDisable alarm sound entirely
--no-notifyDisable desktop notification
--title <text>Notification title (default: ding)
--subtitle <text>Notification subtitle
--icon <path>Absolute path to a custom notification icon image
--open <url>URL to open when the notification banner is clicked
--notify-sound <name>Notification banner sound (e.g. Glass, Ping) — distinct from --sound, which plays via afplay
--icons <mode>Icon set: nerd (default, requires Nerd Font), emoji, or ascii. Also set via DING_ICONS env var
-i / --interactiveForce-launch the interactive wizard
--help-hShow help
--version-VShow version

Examples

# Relative timer with a message
ding 90m "tea is ready"

# Absolute time
ding 14:30 "stand-up"

# Fires at 9 am — rolls to tomorrow if 9 am has already passed
ding 9am

# Open claude.ai when the notification is clicked
ding 5h "quota is back" --open https://claude.ai --subtitle "Anthropic"

# Background the process, return the prompt immediately
ding 5h "quota is back" --detach

# Silent notification (no alarm sound)
ding 30m "check the oven" --no-sound

# macOS Clock alarm ringtone (played locally via afplay; quote names with spaces)
ding 5h "wake" --sound Daybreak
ding 5h "wake" --sound "Milky Way"

# Notification banner's own macOS sound (separate from the afplay alarm)
ding 30m "stand-up" --notify-sound Glass

# Custom audio file
ding 20m "deploy done" --sound ~/Downloads/alert.aiff

# Use emoji icons (no Nerd Font required)
ding 5m "test" --icons emoji

# Or set permanently in your shell profile
export DING_ICONS=emoji

Recipe: ring when Claude's usage quota resets

ding was born to time Claude's 5-hour usage window — but instead of guessing 5h, read the exact reset time from ccusage, which parses Claude Code's local logs. Pipe its active-block remainingMinutes straight into ding:

mins=$(ccusage blocks --active --json --offline | node -e 'const {blocks=[]}=JSON.parse(require("node:fs").readFileSync(0));const a=blocks.find(b=>b.isActive);process.stdout.write(String(a?.projection?.remainingMinutes??""))')
[ -n "$mins" ] && ding "${mins}m" "Claude quota is back 🎉"

--offline keeps it fast (no network), and remainingMinutes is already relative, so there is no timezone maths to get wrong.

Interactive wizard

Running ding with no time argument (or with -i) launches a tabbed wizard:

ding  When · Message · Sound · Notify · Mode · Review

Navigate between tabs with / . On the Sound step, use / to browse presets, macOS Clock ringtones, and system sounds, and press Space to preview a sound before confirming with .

Icon sets

The countdown display and fired state use glyphs from the active icon set:

ModeActivateRequires
nerddefaulta Nerd Font-patched terminal font
emoji--icons emoji or DING_ICONS=emojiany modern terminal
ascii--icons ascii or DING_ICONS=asciinothing

--icons takes precedence over DING_ICONS. If your font is not Nerd Font-patched, add export DING_ICONS=emoji to your shell profile.

Foreground mode (default)

Without --detach, the terminal renders a live countdown. When the timer fires it rings continuously until you press any key:

$ ding 5m "quota is back"
ding → 16:42:00 · quota is back
04:59:58 ▸ quota is back

Detach mode

$ ding 5h "quota is back" --detach
ding → 21:00:00 (detached)
$

The process runs silently in the background and fires the notification and alarm at the target time — no terminal required.


🔧 Development

ding-cli/
├── src/
│   ├── index.ts          # CLI entry point and argument definitions
│   ├── countdown.ts      # Foreground countdown rendering loop
│   ├── detach.ts         # Background process spawning
│   ├── icons.ts          # Icon sets (nerd / emoji / ascii)
│   ├── notify.ts         # Desktop notification wrapper
│   ├── parse-time.ts     # Relative and absolute time parsing
│   ├── preview-sound.ts  # Sound preview during wizard
│   ├── sounds.ts         # Alarm presets (WAV synthesis) + system sound resolution
│   ├── ui/               # TUI primitives (tabs, footer hints)
│   └── wizard/           # Interactive wizard (Ink/React)
└── dist/                 # Compiled output (tsup → ESM)

Scripts

CommandDescription
npm run devRun from TypeScript source via tsx
npm run buildCompile to dist/ with tsup
npm run build:watchWatch mode compilation
npm run typecheckTypeScript type-check without emitting
npm run lintESLint
npm run formatPrettier

Setup

git clone https://github.com/kud/ding-cli.git
cd ding-cli
npm install
npm run dev -- 5s "test alarm"

After npm link, the ding binary resolves to the compiled dist/index.js (run npm run build first).


🏗 Tech Stack

PackageRole
inkReact renderer for the terminal
@inkjs/uiText input and select components
cittyCLI argument parsing
node-notifiermacOS desktop notifications (vendors terminal-notifier)
chalkTerminal colour output
tsupESM build
tsxTypeScript execution for development

MIT © kud — Made with ❤️