Tiny stdin → transform → stdout text filter for CSS values. It plugs into
Zed through the vim filter command (!), so the same binary also works in
Vim, Helix, or any shell pipeline.
echo "margin: 6px; color: #ff0000;" | zh
# margin: 0.375rem /* 6px */; color: oklch(62.8% 0.2577 29.23) /* #ff0000 */;| Name | Aliases | What it does |
|---|---|---|
px2rem |
px, rem |
6px → 0.375rem /* 6px */ |
hex2oklch |
hex, oklch |
#ff0000 → oklch(62.8% 0.2577 29.23) /* #ff0000 */ |
Both helpers keep the original value as a trailing /* … */ comment.
hex2oklch also lowercases the hex in that comment (#FF0000 → #ff0000).
zh # apply ALL helpers
zh px # only px → rem
zh hex # only hex → oklch
zh --list # list available helpers (also -l, --help)Helpers are applied sequentially (a fold over the input); the order is the
order in the HELPERS array.
cargo install --path .This builds a release binary and places it at ~/.cargo/bin/zh. Make sure
~/.cargo/bin is in your PATH (the rustup installer normally adds it):
# ~/.zshrc or ~/.bashrc, if it's not there already
export PATH="$HOME/.cargo/bin:$PATH"cargo build --releaseThe binary ends up at target/release/zh. Copy it to any directory in your
PATH, for example:
mkdir -p ~/.local/bin
cp target/release/zh ~/.local/bin/ # ensure ~/.local/bin is in PATHImportant for Zed: the binary must be reachable through
PATHas seen by Zed itself. If Zed was launched from the Dock/Finder rather than a terminal, it may not pick upPATHchanges from your shell profile — restart Zed after installing, or launch it via thezedCLI from a terminal.
If you already have zh installed, pull the latest changes and rebuild from
the repo:
git pull
cargo install --path . --force # rebuild and overwrite ~/.cargo/bin/zhThe --force flag is what makes cargo install overwrite the existing
binary. If you installed manually (Option 2), rebuild and copy again instead:
git pull
cargo build --release
cp target/release/zh ~/.local/bin/ # or wherever you copied it beforeRestart Zed afterwards so it picks up the new binary.
echo "margin: 6px; color: #ff0000;" | zh
# margin: 0.375rem /* 6px */; color: oklch(62.8% 0.2577 29.23) /* #ff0000 */;
zh --list # prints the helper table
cargo test # reference values are checked against oklch.comRequires "vim_mode": true in settings.json.
Manually: select line(s) (V), press : — Zed pre-fills '<,'>, then
type !zh and hit Enter. The selected lines are replaced with the output.
Keybindings — in ~/.config/zed/keymap.json:
[
{
"context": "vim_mode == visual",
"bindings": {
"space h h": ["workspace::SendKeystrokes", ": ! z h enter"],
"space h p": ["workspace::SendKeystrokes", ": ! z h space p x enter"],
"space h c": ["workspace::SendKeystrokes", ": ! z h space h e x enter"]
}
},
{
"context": "vim_mode == normal",
"bindings": {
"space h h": ["workspace::SendKeystrokes", "shift-v : ! z h enter"]
}
}
]space h h— run all helpers at once (in normal mode it selects the current line first)space h p— px → rem onlyspace h c— hex → oklch only
The same approach works in Vim/Neovim (:'<,'>!zh) and Helix
(select, then |zh).
ZH_REM_BASE— root font-size used for the px→rem conversion (default:16). Set it in your shell profile, or per invocation:ZH_REM_BASE=10 zh px.
- Write a
fn my_helper(input: &str) -> Stringinsrc/main.rs - Register it in the
HELPERSarray (name, aliases, description, function) - Reinstall:
cargo install --path .
Why regex over the whole line instead of a precise selection?
The vim filter in Zed is line-based: the entire line is replaced, and you
can't pass just the 6px fragment inside a line. So zh finds px values and
hex colors in the line itself and converts them in place — selecting whole
lines turns out to be faster than making a precise selection.
Idempotency. Values inside comments (/* 6px */) are left untouched,
so running the filter twice over the same text is safe.
Exact output. zh writes back exactly what it transformed — no trailing newline is added, because a vim filter must return precisely the text that gets pasted back into the buffer.