- Zig 88.3%
- Nix 11.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FzYHRR9MPePBCEKfm4WQq6 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| minisign.pub | ||
| README.md | ||
| REUSE.toml | ||
clipwatch
A small CLI that listens for clipboard pastes using the kitty clipboard protocol and renders whatever was pasted:
- Images (
image/png) are drawn inline using the kitty graphics protocol with Unicode placeholders, so they also work inside tmux (which needsallow-passthrough onset). - Text (and any other data) is printed as a classic
offset / hex / ASCIIhexdump.
On terminals without the kitty clipboard protocol, clipwatch falls back to bracketed paste (mode 2004) when available: pastes are still hexdumped, but as plain text only — no MIME types and no images.
Source code is hosted at https://codeberg.org/jcollie/clipwatch.
Requirements
- Zig 0.16
- A terminal that implements the kitty clipboard protocol (OSC 5522) and reports mode 5522 via DECRQM — kitty itself, Ghostty 1.4 or newer, or another terminal with equivalent support.
- Running inside tmux works too, provided the outer terminal qualifies and
tmux has
set -g allow-passthrough on: clipwatch talks to the outer terminal through tmux's passthrough sequence, and tmux forwards the outer terminal's replies and paste events to the active pane.
Building
zig build # binaries land in zig-out/bin/
zig build test # run unit tests
Or with Nix:
nix build # binary lands in result/bin/
The Nix build runs in a sandbox with no network access, so it cannot let
Zig fetch build.zig.zon dependencies itself. Instead they are resolved
offline from build.zig.zon.nix, a package set generated by
zon2nix. Whenever you change the
dependencies in build.zig.zon, regenerate that file (zon2nix is provided
in the dev shell):
zon2nix --nix=build.zig.zon.nix build.zig.zon
and commit the result, otherwise the Nix build will use stale
dependencies. The plain zig build path is unaffected — it fetches
dependencies directly.
Usage
Run the binary directly — it needs to own the tty, so don't run it through
zig build run-style steps:
./zig-out/bin/clipwatch
Then paste into the terminal window (e.g. ctrl+shift+v). Each paste is
reported with its available MIME types and rendered as an image or hexdump.
Press q, ctrl+c, or ctrl+d to quit.
Hexdump output is capped at 20 lines by default; larger pastes end with a
… N more bytes not shown note. Override the cap with --limit, taking a
count of lines or bytes (a bare number means lines) or unlimited:
./zig-out/bin/clipwatch --limit 50lines
./zig-out/bin/clipwatch --limit 4096bytes
./zig-out/bin/clipwatch --limit unlimited
Verifying release binaries
Release artifacts are signed with
minisign; each binary has a
detached <name>.minisig beside it. The signatures are produced during the
release build by a small helper that uses the
minizign module, run through the
build graph (never installed):
zig build sign -- <secret-key-file> <file>...
The project's minisign public key is in minisign.pub
(key ID 3B011EBF652D3FA5). Verify a download with:
minisign -Vm clipwatch-x86_64-linux-musl -p minisign.pub
How it works
-
Support probe. On startup clipwatch sends DECRQM queries for mode 5522 (
CSI ? 5522 $ p) and mode 2004 (bracketed paste, the fallback), followed by DA1 (CSI c). Every terminal answers DA1, which bounds the wait: if the DA1 reply arrives without a positive DECRPM report for mode 5522, clipwatch falls back to bracketed paste when mode 2004 was reported (pastes arrive as plain text betweenCSI 200~/CSI 201~markers and are hexdumped — no images), and exits with an error when neither mode is available.Inside tmux (detected via
$TMUXor a tmux/screen$TERM) the probe changes shape, because tmux would otherwise answer both queries itself (DECRPM "not recognized" and its own DA1): the DECRQM is wrapped in tmux's passthrough sequence so the outer terminal answers it, and the terminator becomes a wrapped DSR 5 (CSI 5 n→CSI 0 n) — tmux swallows DA1 responses arriving from its client but forwards DECRPM and DSR replies (like other escape sequences it does not recognize) to the active pane verbatim. All later protocol writes — the mode set/reset andtype=readrequests — are passthrough-wrapped the same way, while paste events and data flow back as forwarded OSC 5522 input. This needsallow-passthrough on; without it the wrapped queries never reach the outer terminal and clipwatch waits forever (it prints a hint, andqstill quits). The mode-2004 query stays unwrapped like the size queries: tmux implements bracketed paste per pane itself, so its answer is the authoritative one, and in fallback mode it re-brackets pastes coming from the outer terminal into the pane. -
Paste events, not polling. It then enables kitty's paste-events mode (
CSI ? 5522 h). In this mode the terminal does not deliver pasted text as input. Instead, each user paste produces an unsolicited OSC 5522 read-response for the special.MIME type: the list of MIME types on the clipboard, plus a one-time password (pw). -
Reading the clipboard. clipwatch picks a MIME type —
image/pngif offered, elsetext/plain, else the first listed — and sends atype=readrequest authorized with the one-time password, so no permission prompt appears. The base64DATAchunks are reassembled untilDONE. -
Rendering. PNG data is transmitted with the graphics protocol as a virtual placement (
ESC _G a=T,U=1,f=100,q=2,i=<id>,c=<cols>,r=<rows>, chunked at 4096 encoded bytes per escape), then displayed by printing a grid ofU+10EEEEplaceholder cells carrying the image id in their foreground color and their row/column in combining diacritics. The grid is sized from the image dimensions in the PNG header and the cell/window sizes reported during the handshake (CSI 16 t/CSI 18 t; inside tmux these stay unwrapped, and tmux answers them itself with pane-local values). Because the placement is plain text, this works inside tmux: when$TMUX(or a tmux/screen$TERM) is detected, the graphics escapes are wrapped in tmux's passthrough sequence (ESC Ptmux;with doubledESCs), which requiresset -g allow-passthrough onin the tmux config. Everything that isn't a PNG goes through the hexdump printer.
The tool puts the terminal into a raw mode with echo, line buffering,
and the ctrl+c signal key disabled — via termios on POSIX systems, or
SetConsoleMode (with virtual-terminal input/output enabled and UTF-8
code pages) on Windows. Quit keys are handled in-process, so the exit
path always disables paste-events mode and restores the original
terminal state. Newline output processing is left on for both platforms,
so normal \n-terminated messages render correctly in raw mode.
License
MIT, © 2026 Jeffrey C. Ollie. The machine-generated
flake.lock is dedicated to the public domain under
CC0-1.0. The project follows the
REUSE specification; per-file licensing is
declared in SPDX headers and REUSE.toml.