README
¶
notify
Never miss a finished build again. Whether you're at your desk or grabbing
coffee — notify knows and reaches you the right way: a chime when you're
present, a Discord ping when you're not.
A single binary, zero-dependency notification engine for the command line. Chain sounds, speech, toast popups, and Discord messages into pipelines — all configured in one JSON file.
What is this for?
Long-running terminal commands finish silently. notify gives you instant
feedback:
notify run -- make build
Or chain it manually for more control:
make build && notify ready || notify error
kubectl rollout status deploy/api; notify done
Installation
Pre-built binaries
Download the latest binary for your platform from
GitHub Releases.
Place the binary somewhere on your PATH and copy
notify-config.example.json as notify-config.json next to it.
From source
go install github.com/Mavwarf/notify/cmd/notify@latest
Design
- Written in Go for easy cross-compilation and single-binary distribution.
- Config-driven — define notification pipelines as JSON. Each action combines sound, speech, toast, and Discord steps.
- Built-in sounds — 7 generated tones (success, error, warning, etc.) created programmatically as sine-wave patterns.
- Text-to-speech — uses OS-native TTS engines
(Windows SAPI, macOS
say, Linuxespeak). - Toast notifications — native desktop notifications on all platforms
(Windows Toast API, macOS
osascript, Linuxnotify-send). - Discord webhooks — post messages to a Discord channel via webhook,
no external dependencies (just
net/http). - AFK detection — conditionally run steps based on whether the user is at their desk or away. Play a sound when present, send a Discord message when AFK.
- Quiet hours — time-based
"hours:X-Y"condition suppresses loud steps at night and routes to silent channels instead. - Cross-platform — uses oto for native audio output on Windows (WASAPI), macOS (Core Audio), and Linux (ALSA).
Architecture
cmd/
notify/
main.go CLI entry point, flag parsing, AFK wiring
notify-config.example.json Example config file
internal/
audio/
sounds.go Generated sound definitions and PCM synthesis
player.go Playback engine (generated tones)
config/
config.go Config loading and profile/action resolution
discord/
discord.go Discord webhook integration (POST to channel)
idle/
idle_windows.go User idle time via GetLastInputInfo (Win32)
idle_darwin.go User idle time via ioreg HIDIdleTime
idle_linux.go User idle time via xprintidle
runner/
runner.go Step executor (dispatches to audio/speech/toast/discord)
eventlog/
eventlog.go Append-only invocation log (~/.notify.log)
tmpl/
tmpl.go Template variable expansion ({profile}, {command}, etc.)
shell/
escape_windows.go PowerShell string escaping
speech/
say_windows.go TTS via PowerShell System.Speech
say_darwin.go TTS via macOS say command
say_linux.go TTS via espeak-ng / espeak
toast/
toast_windows.go Windows Toast Notification API
toast_darwin.go macOS osascript notifications
toast_linux.go Linux notify-send
Usage
notify [options] [profile] <action>
notify run [options] [profile] -- <command...>
notify list # List all profiles and actions
notify version # Show version and build date
notify help # Show help
Options
| Flag | Description |
|---|---|
--volume, -v |
Override volume, 0-100 (default: config or 100) |
--config, -c |
Path to notify-config.json |
--log, -L |
Write invocation to ~/.notify.log |
Config file
notify looks for notify-config.json in this order:
--config <path>(explicit)notify-config.jsonnext to the binary~/.config/notify/notify-config.json(Linux/macOS) or%APPDATA%\notify\notify-config.json(Windows)
Config format
{
"config": {
"afk_threshold_seconds": 300,
"default_volume": 100,
"log": false,
"credentials": {
"discord_webhook": "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"
}
},
"profiles": {
"default": {
"ready": {
"steps": [
{ "type": "sound", "sound": "success", "when": "hours:8-22" },
{ "type": "say", "text": "{command} finished in {Duration}", "when": "run" },
{ "type": "say", "text": "Ready!", "when": "direct" },
{ "type": "toast", "message": "Ready!", "when": "afk" },
{ "type": "toast", "message": "Ready!", "when": "hours:22-8" },
{ "type": "discord", "text": "Ready!", "when": "afk" }
]
}
},
"boss": {
"ready": {
"steps": [
{ "type": "sound", "sound": "notification", "volume": 90 },
{ "type": "say", "text": "Boss is ready" },
{ "type": "toast", "title": "Boss", "message": "Ready to go" }
]
}
}
}
}
- Two top-level keys:
"config"for global options,"profiles"for notification pipelines. - Each profile maps action names to
{ "steps": [...] }."default"is the fallback profile. - Step types:
sound(play a built-in sound),say(text-to-speech),toast(desktop notification),discord(post to Discord channel via webhook). - Volume priority: per-step
volume> CLI--volume> config"default_volume"> 100. - Toast
titledefaults to the profile name if omitted. - Template variables: use
{profile}insaytext,toasttitle/message, ordiscordtext to inject the runtime profile name, or{Profile}for title case (e.g.boss→Boss). When usingnotify run,{command},{duration}(compact:2m15s), and{Duration}(spoken:2 minutes and 15 seconds) are also available. Use{Duration}insaysteps for natural speech output. This is especially useful with the default fallback — a single action definition can produce different messages depending on which profile name was passed on the CLI. - Event logging: set
"log": trueto append every invocation to~/.notify.log(or use--logon the CLI). Off by default. soundandsaysteps run sequentially (shared audio pipeline). All other steps (toast,discord, etc.) fire in parallel immediately.
Available sounds
| Name | Description |
|---|---|
warning |
Two-tone alternating warning signal |
success |
Ascending major chord chime |
error |
Low descending buzz indicating failure |
info |
Single clean informational beep |
alert |
Rapid high-pitched attention signal |
notification |
Gentle two-note doorbell chime |
blip |
Ultra-short confirmation blip |
Credentials
Remote notification steps (like discord) need credentials stored in the
"credentials" object inside "config":
{
"config": {
"credentials": {
"discord_webhook": "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"
}
},
"profiles": { ... }
}
To get a Discord webhook URL: Server Settings → Integrations → Webhooks → New Webhook → Copy Webhook URL.
Discord notifications
The discord step type posts a message to a Discord channel via webhook.
Especially useful with "when": "afk" to reach you when you're away:
{ "type": "discord", "text": "{Profile} build is ready", "when": "afk" }
The text field supports template variables ({profile}, {Profile},
and {command}/{duration} in run mode).
Discord steps run in parallel (they don't block the audio pipeline).
AFK detection
Steps can be conditionally filtered with a "when" condition.
AFK conditions use idle time (no keyboard/mouse input); invocation
conditions distinguish notify run from direct calls:
when value |
Step runs when... |
|---|---|
| (omitted) | Always (default, backwards compatible) |
"present" |
User is active (idle time below threshold) |
"afk" |
User is away (idle time at or above threshold) |
"run" |
Invoked via notify run (command wrapper) |
"direct" |
Invoked directly (not via notify run) |
"hours:X-Y" |
Current hour is within range (24h local time) |
Set the threshold (in seconds) in "config". Default is 300 (5 minutes):
{
"config": { "afk_threshold_seconds": 300 },
"profiles": {
"default": {
"ready": {
"steps": [
{ "type": "sound", "sound": "success" },
{ "type": "say", "text": "Ready!", "when": "present" },
{ "type": "toast", "title": "AFK", "message": "Ready!", "when": "afk" }
]
}
}
}
}
Idle detection is platform-native:
- Windows:
GetLastInputInfoWin32 API - macOS:
ioregHIDIdleTime - Linux:
xprintidle(must be installed)
If idle time cannot be determined (e.g. xprintidle not installed), notify
fails open and treats the user as present.
Quiet hours
Use "hours:X-Y" to restrict steps to certain hours of the day (24-hour
local time). Useful for suppressing loud notifications at night:
{
"steps": [
{ "type": "sound", "sound": "success", "when": "hours:8-22" },
{ "type": "toast", "message": "Build done!", "when": "hours:22-8" }
]
}
hours:8-22— runs when the hour is >= 8 and < 22hours:22-8— cross-midnight: runs when hour >= 22 or < 8- Invalid specs are skipped (fail-closed) with a stderr warning
Lookup logic
- Try
profiles[profile][action] - If not found, fall back to
profiles["default"][action] - If neither exists, error
Command wrapper (notify run)
Wrap any command to get automatic notifications on completion:
notify run -- make build # default profile, "ready" or "error"
notify run boss -- cargo test # boss profile
notify run -v 50 -- npm run build # with volume override
notify run executes the command, measures its duration, then triggers
ready on exit code 0 or error on non-zero. The -- separator is
required to distinguish notify options from the wrapped command.
Additional template variables are available in run mode:
| Variable | Description | Example |
|---|---|---|
{command} |
The wrapped command string | make build |
{duration} |
Compact elapsed time | 2m15s |
{Duration} |
Spoken elapsed time (for TTS) | 2 minutes and 15 seconds |
Use {Duration} in say steps for natural speech, {duration} in
toast/discord for compact display.
Steps can be limited to run mode with "when": "run", or excluded
from it with "when": "direct":
{ "type": "say", "text": "{command} finished in {Duration}", "when": "run" },
{ "type": "say", "text": "Ready!", "when": "direct" }
Examples
notify ready # Run "ready" from the default profile
notify default ready # Same as above (explicit default)
notify boss ready # Sound + speech + toast notification
notify -v 50 ready # Run at 50% volume
notify -c myconfig.json dev done # Use a specific config file
notify --log ready # Log this invocation to ~/.notify.log
notify run -- make build # Wrap a command, auto ready/error
notify run boss -- cargo test # Wrap with a specific profile
Event log
Event logging is opt-in. Enable it with --log (or -L) on the command
line, or set "log": true in the config "config" block. When enabled,
each invocation is appended to ~/.notify.log for history and debugging.
Only steps that actually ran are logged (steps filtered out by AFK
detection are omitted). A blank line separates each invocation:
2026-02-20T14:30:05+01:00 profile=boss action=ready steps=sound,say,toast afk=false
2026-02-20T14:30:05+01:00 step[1] sound sound=notification
2026-02-20T14:30:05+01:00 step[2] say text="Boss is ready"
2026-02-20T14:30:05+01:00 step[3] toast title="Boss" message="Ready to go"
2026-02-20T14:35:12+01:00 profile=default action=ready steps=sound,toast afk=true
2026-02-20T14:35:12+01:00 step[1] sound sound=success
2026-02-20T14:35:12+01:00 step[2] toast title="AFK" message="Ready!"
Template variables ({profile}, {Profile}, {command}, {duration}, etc.)
are expanded in the log so you see the actual text that was spoken or
displayed. Logging is best-effort — errors are printed to stderr but never
fail the command.
Building
Prerequisites
With Go directly
go build -o output/notify ./cmd/notify
With CMake
cmake -B build
cmake --build build
The binary is placed in the output/ directory.
Cross-compilation (via CMake)
| Target | Platform |
|---|---|
build-notify-linux-amd64 |
Linux (x86_64) |
build-notify-linux-arm64 |
Linux (ARM64) |
build-notify-windows-amd64 |
Windows (x86_64) |
build-notify-darwin-amd64 |
macOS (Intel) |
build-notify-darwin-arm64 |
macOS (Apple Silicon) |
cmake -B build
cmake --build build --target build-notify-darwin-arm64
Build all platforms:
cmake --build build --target build-notify-all
Install
cmake -B build
cmake --install build --prefix /usr/local
Platform notes
| Feature | Windows | macOS | Linux |
|---|---|---|---|
| Audio playback | WASAPI (built-in) | Core Audio (CGO) | ALSA (libasound2-dev) |
| Text-to-speech | System.Speech (built-in) | say (built-in) |
espeak-ng or espeak |
| Toast notifications | Toast API (Win 10+) | osascript (built-in) |
notify-send (libnotify) |
| Discord webhook | net/http (built-in) |
net/http (built-in) |
net/http (built-in) |
Contributing
See CONTRIBUTING.md for setup instructions and guidelines.
License
MIT