April 2026
Socat - unix socket notification forwarding
Needed a way to get Claude Code notifications out of Docker containers to the host desktop. Used socat with unix sockets. Here's how socat works, building up piece by piece:
How socat syntax works
The man page says socat [options] <address> <address>. The confusing bit is that fork, reuseaddr etc are NOT command-line options (those go before the addresses, like -d). They are address options — appended to an address type with commas:
The man page lists each address type (e.g. UNIX-LISTEN) with its "Useful options" — that's where you find which address options apply. fork and reuseaddr are listed under the LISTEN/CHILD option groups.
1. Basic socat — connect two things
socat connects two "addresses" bidirectionally. Simplest example — act like cat:
2. Unix sockets — listener side
Create a unix socket and listen on it. - is shorthand for STDIN/STDOUT:
After one message the listener exits. That's because UNIX-LISTEN accepts one connection then closes.
3. fork — keep listening
fork spawns a child process per connection so the listener stays alive:
4. reuseaddr — restart without "address already in use"
When socat creates a UNIX-LISTEN socket, it creates a file on disk (e.g. /tmp/test.sock). When it exits cleanly it removes the file. But if it crashes or gets killed, the stale .sock file stays behind. Next time you start the listener, socat tries to bind() to that path and gets Address already in use because the file already exists.
Try it yourself:
This is why the systemd service also has ExecStartPre = rm -f /tmp/notify-forward.sock — belt and braces.
5. SYSTEM: — run a command per connection
Instead of printing to STDOUT, run a shell command. The incoming data is available on stdin of that command:
Send a test: echo "hello" | socat - UNIX-CONNECT:/tmp/test.sock — the listener shell runs read line and can do whatever with it.
6. The actual notification listener
Putting it all together — receive title|body format, split on |, send desktop notification:
7. -d flag — debug output
-d is a command-line option (goes before addresses). Adds connection logging. Stack for more verbosity:
Lazy window
lol i NEVER new this - C-w then w is "switch window" so if focus is lost to :Lazy window just do that to go back to it 🤣 - see here
Notification history
I also forget I have :Telescope fidget which has a nice viewer with telescope
Git history search — diffview, fugitive & terminal
-S"string"is an alternative to-G— finds commits where the count of that string changed (added/removed) rather than matching the diff line
Telescope live grep with directory/filetype filtering
Uses live_grep_args extension. The key shortcut <C-f> auto-quotes the search term and appends --iglob so you can type a path/glob filter.
Search in a directory: <leader>g → type vertical → <C-f> → type configs/nvim/**
- result:
"vertical" --iglob configs/nvim/**— searches for "vertical" only in files underconfigs/nvim/
Search by filetype: <leader>g → type zc → <C-f> → type **/*.lua
- result:
"zc" --iglob **/*.lua— searches for "zc" only in.luafiles across the whole repo
--iglobis case-insensitive glob. You can chain multiple:"term" --iglob configs/** --iglob !*.json(exclude json files)
note:
<C-i>does NOT work for this because terminals send<C-i>as<Tab>— that's why we use<C-f>instead
Keybrowser — browsable keybinding pickers
Built a custom telescope-based keybinding browser in lua/custom/keybrowser.lua. All under <space>k:
<space>kk— nvim keymaps (your configured mappings, via telescope builtin)<space>kv— vim built-in keys (parsed from$VIMRUNTIME/doc/index.txt, enter opens:help)<space>kz— zsh keybindings (live-queried frombindkey -Macross all keymaps: vicmd, viins, etc)<space>kt— telescope keybindings (queried fromrequire("telescope.config").values.mappingsat runtime)
All pickers support a "mode filter" search: type two words and the first word filters the first column exactly, the second is a lua pattern on the rest. e.g. n p in the vim picker shows all normal-mode keys containing p, vicmd kill in zsh shows vicmd bindings matching "kill".
The zsh picker also translates raw terminal codes to friendly names — ^[OA → Up (SS3), ^M → Enter (CR), ^[x → Alt-x, etc.
Discovery that prompted this: C-t in telescope opens the selected entry in a new tab. I kept forgetting this existed. Now <space>kt → search "tab" and there it is.
Intuitive Zellij Stacking
Didnt realise this was a feature... ToggleGroupMarking (for me Alt Shift p) opens group mode where we can select multiple panes and then s to stack!
And then can use the alt [ to change layout (which will unstack) .. and alt t [ to move a pane to another tab!
Vim variable scopes cheat sheet
Options (settings like number, expandtab) use vim.opt / vim.o / vim.bo / vim.wo:
Examples:
vim.optvsvim.o:vim.optreturns a special object with:get(),:append(),:prepend(),:remove().vim.ois simpler direct access. For setting simple values they're interchangeable.
Terminal escape sequences cheat sheet
Escape sequences control terminal behavior. Two main types: CSI (Control Sequence Introducer) for cursor/display and OSC (Operating System Command) for terminal features.
Key:
\eor\033= ESC (0x1B)\aor\007= BEL (terminates OSC)- CSI ends with a letter (
J,H,m,q,l,h) - OSC ends with BEL (
\a) or ST (\e\\)
SGR (Select Graphic Rendition) codes for \e[...m:
Example combining: \e[1;31m = bold + red, \e[38;2;255;100;0mOrange\e[0m = truecolor orange then reset
OSC 52 clipboard breakdown:
Why base64? The clipboard content might contain special characters that would break the escape sequence. Base64 ensures it's safe ASCII.
Works over SSH because the escape sequence travels through the terminal stream back to your local terminal (kitty/wezterm/etc) which handles the actual clipboard.
Ref: https://terminalguide.namepad.de/seq/osc-52/
April 2026
Yazi as XDG file picker — research summary
Tool: xdg-desktop-portal-termfilechooser (in nixpkgs as v1.2.1)
Repo: https://github.com/hunkyburrito/xdg-desktop-portal-termfilechooser
How it works: implements org.freedesktop.impl.portal.FileChooser, so it intercepts the same D-Bus calls currently routed to xdg-desktop-portal-gtk. Spawns kitty running yazi --chooser-file=/tmp/...; portal reads the temp file when yazi exits.
To enable (3 small files):
modules/nixos-core-desktop.nix:~/.config/xdg-desktop-portal/portals.conf:~/.config/xdg-desktop-portal-termfilechooser/config:
Test: GDK_DEBUG=portals zenity --file-selection
Gotchas:
smart-enter.yaziplugin (currently in keymap) breaks directory selection — Enter navigates instead of selects. Workaround: useYAZI_CONFIG_HOME=...in wrapper for an alternate config without smart-enter.- Qt apps may bypass the portal entirely (open issue #46).
- Two closed NixOS issues (#56, #67) document systemd integration friction — reboot recommended after install.
- Save dialogs work but UX is unusual: yazi creates a help-file at the destination path with instructions.
Recommendation: try it AFTER the GTK portal click-offset fix (GDK_SCALE=1 + removing wlr.enable) lands. If that resolves the GTK issue you may not need this. But config is ~10 lines and trivially reversible.
nvim-treesitter: archived, 0.12 broken, pinned for now
On April 3, 2026 the entire nvim-treesitter/nvim-treesitter repo went Public archive on GitHub. Both master and main branches are frozen — no new commits will land anywhere in the repo. No official successor was named.
Why 0.12 breaks the current pin
Neovim 0.12 changed the treesitter query match API to support quantifiers (neovim#31293):
- 0.11:
match[capture_id]→ singleTSNode | nil - 0.12:
match[capture_id]→TSNode[](list, always)
The pinned master commit (42fc28ba) registers 7 custom directives in query_predicates.lua that do match[capture_id]:range(). On 0.12 that crashes with attempt to call method 'range' (a nil value) because the value is now a list, not a node. Markdown's injection query (set-lang-from-info-string!) is the most obvious trigger but it affects anything with injections.
The master branch will never be fixed (archived). So: either stay on 0.11, or migrate off master entirely.
Two frozen branches, two behaviours
Both are now snapshots — no future fixes, no new parsers. main happens to be the 0.12-compatible snapshot.
The main branch was a full rewrite with a different API shape:
- setup:
require('nvim-treesitter').setup()(no.configs) - install: explicit
.install({...})call (noensure_installed) - highlight/indent: not auto-enabled — you write a
FileTypeautocmd callingvim.treesitter.start() - helpers (
ts_utils,configs, etc.): gone — usevim.treesitter.*built-ins (available since 0.10)
Current decision: stay pinned
- Host nvim is 0.11.7 (NixOS
nixos-25.11), master-branch pin works fine. - Dev container (PR #6) pins to nvim 0.11.7 via
ARG NVIM_VERSION(Arch ships 0.12 by default — that's what caused the original container break). - Experiment branch that migrated config to
mainbranch was tested working on 0.12.1 (PR #7, now closed) but not worth merging yet — would commit to a frozen snapshot.
When to revisit
When NixOS bumps nvim past 0.11 (likely nixos-26.x), the pin stops helping. Options at that point, best to worst:
- Neovim built-in treesitter + tiny parser installer. 0.11+ has
vim.treesitter.start,get_node, parser loading fromruntime/parser/*.sobuilt-in. Highlighting/indent/folding don't need the plugin. You just need parsers installed somewhere onruntimepath. A small helper like tree-sitter-manager.nvim (actively maintained, lightweight, 0.12+) is probably the minimum replacement. - rocks-treesitter.nvim — rocks.nvim module, pre-built parsers hosted on rocks-binaries (no local compile). Relevant if/when you adopt rocks.nvim.
- Resurrect the main-branch migration from the closed PR #7 — works but targets an archived repo, so only a stopgap.

