March 2026

Quick reminder of a comment to set vim file type (but this catch 22 - vim needs to know the file type to know to parse a comment to know to apply the file type comment)

Apparently vim 'modelines' helps with the chicken and egg by scanning first few lines for vim style comments

# vim: set ft=yaml sw=2 ts=2 et :

Command-line window (q:)

q: opens an editable buffer of command history — not just for searching, you can actually edit commands in it like a normal buffer (yank, paste, modify), then hit Enter on a line to execute. Also accessible via <C-f> from the : prompt.

Completion broken again (autopairs referencing nvim-cmp)

Completion stopped working again. Previous investigation (see "Nvim glitching after zellij layout change" above) found lspsaga was the cause - disabled it, fixed it.

This time: autopairs.lua was never updated when migrating from nvim-cmp to blink.cmp (Oct 2025). It still had dependencies = { 'hrsh7th/nvim-cmp' } and called require 'cmp' on InsertEnter. Since nvim-cmp is no longer installed, this errors every time you enter insert mode, potentially breaking blink.cmp completion.

Fix: removed the nvim-cmp dependency and require 'cmp' integration from autopairs.lua. Autopairs still works standalone, just no longer auto-adds () after selecting a function from completion (blink.cmp handles this differently anyway).

Zellij edge detection + hyprland flash (TODO)

Requirement: when moving focus hits an edge (no pane in that direction), flash the hyprland border red (like move-focus.sh does for hyprland windows).

Pain points:

  • zellij bind only supports a single action — can't chain "get pane id → move focus → check pane id → flash"
  • vim-zellij-nav wasm plugin handles nvim detection internally, can't wrap it with a script
  • smart-splits.nvim at_edge = 'stop' is broken — the zellij mux hardcodes Direction.left regardless of direction arg
  • zellij plugin Key events only fire when the plugin's own pane is focused, not globally

Proposed solution: custom zellij wasm plugin move-focus-jol:

  1. Receives MessagePlugin with direction (like vim-zellij-nav)
  2. Gets pane id → calls move_focus → checks pane id again
  3. If unchanged → run_command("hyprctl", ...) to flash border
  4. Fork vim-zellij-nav to call move-focus-jol instead of move_focus directly
  5. Update nvim zellij-nav.nvim to message move-focus-jol instead of zellij action move-focus
  6. Everything funnels through one plugin that owns edge detection

Bash parameter expansion (suffix removal)

The single % is the shortest match from end of string operator, whereas %% is large suffix removal operator.

➜ joelyboy dev-setup (main) ✗ file="photo.backup.tar.gz"
➜ joelyboy dev-setup (main) ✗ echo ${file%%.*}
photo
➜ joelyboy dev-setup (main) ✗ echo ${file%%g}
photo.backup.tar.gz
➜ joelyboy dev-setup (main) ✗ echo ${file%%g*}
photo.backup.tar.
➜ joelyboy dev-setup (main) ✗ echo ${file%%a*}
photo.b
➜ joelyboy dev-setup (main) ✗ echo ${file%a*}
photo.backup.t
➜ joelyboy dev-setup (main) ✗

In this mode . is a literal dot, not a regexp dot for any char

  1. Notice how nothing matched on the file%%g: must match to end of string
  2. file%%g* is greedy so goes up to the gz ad the end
  3. file%a* so goes for the first a in backup

Vim Dadbod UI

Ok wow i found something today i cannot believe it took me this long to find?

<leader>S

whilst in vim dadbod mode will only execute selected text as a query - kind of like filip does in DBeaver - which is awesome!

Opening a blank buffer

  • :enew — opens a new blank buffer in the current window (mnemonic: edit new)
  • :new — opens a new blank buffer in a horizontal split
  • :vnew — opens a new blank buffer in a vertical split

Vim: Redirect command output to register

:new | put =execute('SomeCommand') — this opens a new scratch buffer and pastes the output of any ex command into it. Breakdown:

  • :new — opens a new empty split
  • put = — puts the result of an expression into the buffer
  • execute('...') — runs an ex command and captures its output as a string

So execute('Fidget history') runs :Fidget history and returns the output text, then put = pastes it into the buffer where you can search with /.

Works with any command, e.g. :new | put =execute('messages'), :new | put =execute('map')

Ok so (ME not claude as of now...)

| is not actually what i thought like a pipe in bash, it is actually just like ; in bash (see :h :bar in vim)

So in this case we are just saying :new to open a new buffer and THEN :put to dump the output of the register

Gotcha: | doesn't work with all commands. Some commands treat | as part of their own arguments instead of as a command separator. User-defined commands (from plugins) do this by default unless the plugin author explicitly opted in to | separation with -bar (see :h command-bar). So:

:OutlineFocus | OutlineClose   " BROKEN — OutlineClose is passed as an arg to OutlineFocus

Fix: use <cmd>...<cr> chaining in a keymap, or just use separate commands. In ex mode, there's no clean workaround — run them separately:

:OutlineFocus
:OutlineClose

Or in a keymap:

vim.keymap.set("n", "<leader>X", function()
  vim.cmd("OutlineFocus")
  vim.cmd("OutlineClose")
end)

e.g.

let @b='pls'
:put b

dumps 'pls' into current buffer

i did try this

:put = execute('echo "hi there"')

BUT echo doesn't return to a buffer so it wouldn't work. WAIT IGNORE. just needed to escape the " with

:put = execute('echo \"hi there\"')

....or...

:put = 'hi there'

Vim registers and the " prefix

Registers in vim are accessed with " (double quote) in normal mode.

Special registers:

RegisterDescriptionExample
"+System clipboard"+y yank to clipboard, let @+=expand('%:p') copy file path to clipboard
"=Expression register — evaluates vimscriptput =execute('Fidget history') dumps command output into buffer
"0Last yank"0p paste what you last yanked (not deleted)
""Default (unnamed)p pastes from here by default

to see help can use vims special 'quote' like :h quote= is equivalent to :h "=

Accessing registers — " vs @:

SyntaxContextExample
"Normal mode — select register before an operator"ay yank into a, "+p paste from clipboard
@Ex commands / vimscript — read/write registerslet @+ = expand('%:p'), @a execute macro

Why quote= in vim help? Vim help spells " as quote in its help tags, so :h quote= means "help for the "= register", :h quotea for "a, etc.

GH issues

I should really learn this.... so search for issues

have to specify --repo then just gh search issues...

➜ joelyboy dev-setup (main) ✗ gh search issues "fuzzy search" --repo 'folke/which-key.nvim'

Showing 1 of 1 issues

REPO                  ID    TITLE                                  LABELS  UPDATED
folke/which-key.nvim  #648  bug: Which Key seems to affect keymap  bug     about 1 year ago
➜ joelyboy dev-setup (main) ✗

Ok so it turned out the relevant issue i was looking for is a discussion (here) and gh api doesnt support discussions....

Disk space cheat sheet

CommandWhat it showsNotes
df -HAll mounted filesystems with size/used/availOverview of disk usage per mount
lsblk -fTree of disks, partitions, fstype, label, mountpointGood quick overview of block devices
du -sh <folder>Total size of a specific foldere.g. du -sh /home or du -sh /nix/store
sudo parted -lAll disks and partitionsDoes NOT show free space
sudo parted /dev/xxx print freePartitions + free gaps on a specific diskOnly way to see unallocated space in parted
sudo cfdisk /dev/xxxTUI partition viewer with visual blocks + free spaceNice for a quick visual, quit without saving to just view
gdu / ncduInteractive TUI drill-down into disk usageLike a fancy du
dufPretty colorized df replacement with bars

To list all block devices (the /dev/xxx names for cfdisk/parted):

lsblk

Why nix-tree shows 30GB but /nix/store is 122GB: nix-tree only shows the closure of the current system profile — the set of packages reachable from this generation. /nix/store also contains old generations, cached build deps, devenv shells, and anything not yet garbage collected. Run sudo nix-collect-garbage -d to prune old generations (careful: removes rollback ability).

Does the math add up? (March 2026)

/home          =  94 GB
/nix/store     = 122 GB
swap file      =  32 GB  (configured in nixos-base.nix)
/var + other   = ~49 GB  (postgres data, logs, docker leftovers, tmp, etc.)
               --------
total          ≈ 297 GB  ✓ (matches df -H)

Neovim display corruption on terminal resize

Ghost lines, black spots, misaligned rendering when resizing (fullscreen toggle, zellij pane resize).

Only happens inside zellij — confirmed no issue in bare kitty terminal.

Cause: Neovim's CSI 2026 (termsync / synchronized output) misimplementation conflicts with zellij's terminal emulation layer. Zellij acts as terminal-within-a-terminal and must relay escape sequences — mismatch causes artifacts. Zellij creator confirmed: "99% a misimplementation of CSI 2026 by nvim." (neovim#29427, zellij#426, zellij#3415)

Fix — disable termsync in nvim config:

vim.o.termsync = false

Other things that help:

  • Set TERM=xterm-256color inside zellij (avoid xterm-kitty etc)
  • Ctrl-L to manually force redraw
  • If using noice.nvim, set notify stages to "static" (zellij#3208)

Yazi image/video previews black in zellij

Yazi mediainfo plugin shows black screen for both image and video previews.

Only happens inside zellij — confirmed previews work fine in bare kitty terminal.

Cause: Likely zellij doesn't support passthrough for the kitty graphics protocol, which yazi uses to render inline images. Zellij's terminal emulation layer can't relay the graphics escape sequences properly. (zellij#1796)

Helix — vim equivalent workflows

Helix is selection-first (select then act) vs vim's verb-first (act then motion). Here's the mapping for common vim workflows:

VimHelixNotes
V (visual line)x to select line, repeat x to extendNo true visual line mode. X extends selection to line bounds
G (go to EOF)ge (goto mode → end)Remapped to G in config
0 (start of line)gh (goto line start)Remapped to 0 in config
yy / 3yy (yank lines)xy (select line + yank), xxx...y for multiSelect first, then yank. Or 3x doesn't work — just repeat x
Ctrl-o (jump back)Ctrl-oSame — jumplist back
Ctrl-i / Tab (jump forward)Ctrl-i / TabSame — jumplist forward
Tabs / buffersSpace+b (picker), gn/gp (next/prev):buffer-close (:bc) to close, :buffer-close-others for all but current
gx (open URL)gfOpens URLs via xdg-open, files in helix (24.7+)
:bd (close buffer):buffer-close or :bc:buffer-close! without saving, :buffer-close-all for all
:mksession (sessions)N/ANot implemented yet — #401, #899
:e (reload file):reload or :rl:e in helix opens a file instead, use :reload to refresh current buffer
{ / } (paragraph jump)[p / ]pSelects to the paragraph boundary (helix selection-first)
Ctrl-e / Ctrl-y (scroll)Ctrl-e / Ctrl-yRequires keybind in config — not bound by default
:help / key lookupSpace+?Search all keybindings interactively
Markdown previewgh markdown-preview FILEgh-markdown-preview — browser preview with live reload
Quickfix list / search resultsSpace+/ (global search)Results in picker, n/N to jump between matches in-buffer
% (matching bracket)mm (match mode → match)Jumps to matching bracket/paren/tag
f/F (find char)f/FSame but works across multiple lines (vim is single-line only)
t/T (till char)t/TSame but works across multiple lines (vim is single-line only)
} (jump to next paragraph)]p;]p selects to next paragraph, ; collapses selection to cursor (collapse_selection)
V}}}}y (visual line, 4 paragraphs, yank)xv]p]p]p]pyx select line, v enter extend mode (motions extend instead of replace), ]p ×4, y yank

Key mental shift: in helix you select first, then act. So "delete 3 lines" is xxd not 3dd. Takes getting used to but means you always see what you're about to affect before doing it.

lazydev.nvim — what it actually does

Without lazydev, lua_ls only knows Lua syntax — it has no idea vim exists as a global, so you get Undefined global 'vim' warnings and zero completions for vim.*.

What lazydev does: tells lua_ls to index Neovim's bundled type definitions ($VIMRUNTIME/lua/vim/) so it learns about vim.api.*, vim.keymap.*, vim.lsp.*, etc. Also loads types for installed plugins.

The luv/luvit thing: Neovim's event loop is built on libuv (same C library that powers Node.js — async I/O, timers, filesystem). Neovim exposes it to Lua as vim.uv. The { path = '${3rd}/luv/library', words = { 'vim%.uv' } } in the config just conditionally loads the type defs for vim.uv.* only when you actually use it.

March 2026

Zellij + Image Preview (Yazi) — Why It's Broken

The problem: yazi image preview works in zellij but large or wide images corrupt the entire zellij render — the screen becomes garbled and unusable.

Root cause: Zellij acts as a middleman for terminal graphics. When an image's pixel dimensions exceed the pane boundaries, the sixel/kitty image data bleeds past what zellij expects, corrupting its internal screen buffer. Zellij redraws fail to recover because the image escape sequences have already overwritten cell state. This is a known zellij limitation — multiplexers have to parse and clip image output, and zellij's clipping is incomplete for oversized images.

ComponentWhat it doesStatus / Notes
FootTerminal emulator, supports sixel natively. Does NOT support Kitty graphics protocol.Works. Chosen over Kitty because Kitty causes random nvim rendering glitches (likely Kitty's own escape sequence handling conflicting with nvim's TUI redraw)
Kitty (terminal)Terminal emulator with its own Kitty graphics protocol for images.Dropped — causes random nvim rendering artifacts. Kitty graphics protocol also doesn't help inside zellij since zellij must intercept and re-emit it
ZellijTerminal multiplexer. v0.40+ added passthrough support for both sixel and Kitty graphics protocol.Passthrough works for normal-sized images. Breaks on oversized images — the image data exceeds pane bounds and corrupts zellij's screen state
SixelImage protocol — encodes images as escape sequences inline in terminal output. Foot supports it, zellij 0.40+ passes it through.The protocol yazi should use with Foot. Auto-detection inside zellij can pick wrong protocol — force with YAZI_IMAGE_PROTOCOL=sixel if needed
Kitty graphics protocolAlternative image protocol. Requires Kitty terminal (or compatible).Irrelevant with Foot — Foot doesn't support it. Setting TERM=xterm-kitty in Foot is wrong and causes breakage
ÜberzugppExternal process that renders images by drawing over the terminal window using X11/Wayland compositing (not terminal escape sequences).A workaround for terminals/multiplexers that don't support inline image protocols. Unnecessary with Foot+Zellij since both support sixel natively. Can cause its own positioning issues in multiplexers
YaziFile manager with image preview. Auto-detects image protocol but can get confused inside zellij.Works mostly. The oversized image → zellij corruption is the remaining issue. Not a yazi bug per se — yazi sends valid image data, zellij fails to clip it
TERM=xterm-kittyEnv var hack to trick apps into using Kitty protocol.Don't do this. Foot doesn't speak Kitty protocol. Let Foot set its own TERM=foot

Current status: images preview fine until a large image is selected, then zellij's rendering breaks. This is a zellij-side bug with oversized image clipping. No clean workaround yet — can mitigate by setting max preview dimensions in yazi or waiting for a zellij fix.

Possible mitigation — cap yazi preview size in yazi.toml:

[preview]
max_width = 600
max_height = 400

Related GitHub issues:

IssueRepoSummary
2048zellijSixel images distorted, excessive bottom padding, scrolling breaks image and overwrites screen buffer, can crash zellij
3981zellijSixel artifacts remain on status bar and other areas after clearing, not reliably cleaned up
4124zellijMany sixel images cause zellij to slow down, hang, and become unresponsive
4336zellijYazi image preview support — notes zellij's sixel is "quite buggy with serious performance issues", lagginess, tearing
334yaziTracking issue for previews in zellij — sixel corruption when running yazi through zellij
1936yaziImage preview overflow on large images — image bleeds outside preview area
3308yaziImages exceeding 10,000px in any dimension won't display even with config allowing it

Zellij — Moving Panes into Stacks

The problem: no obvious keybinding to move an existing pane into an existing stack in zellij.

Solution: use zellij action stack-panes CLI action — takes pane IDs and combines them into a stack.

zellij action stack-panes -- <id1> <id2>

Getting pane IDs: need zellij action list-panes (added in v0.44.0), which isn't available on NixOS yet. Until then, zellij action dump-layout can be parsed for pane IDs as a fallback, or use multi-select (Alt+p on each pane) to stack interactively.

LibreOffice Calc — Reloading a File

To reload the current file in LibreOffice Calc: press Alt+F to open the File menu, then press Down arrow until you reach Reload, then press Enter.

SteelSeries Arctis 7 — Mic Not Showing Up

The problem: mic input wasn't appearing as an available source.

Solution: open pavucontrol → Configuration tab → select Game Output + Game Input profile for the SteelSeries Arctis 7.

Why this works: USB audio devices like the Arctis 7 expose multiple endpoints (output-only, output+input, chat, game, etc). PipeWire/PulseAudio represents these combos as profiles on the sound card. Profiles are entirely host-side — the device just advertises available USB audio endpoints, and the audio server decides which to activate. If the selected profile doesn't include a capture endpoint, no mic source gets created. Switching to a profile that includes input activates the capture endpoint and the mic appears.

Terminal equivalent:

# list cards and available profiles
pactl list cards

# set the right profile
pactl set-card-profile <card_name> output:analog-stereo+input:analog-stereo

# or with wireplumber
wpctl status
wpctl set-profile <device_id> <profile_index>

Profile selection is persisted in ~/.local/state/wireplumber/ (PipeWire) or ~/.config/pulse/ (PulseAudio) so it survives reboots.

Linux Audio Debugging — Tools & Terminology

Audio stack (top to bottom):

LayerWhatNixOS
PipeWiremodern audio server, replaces PulseAudioservices.pipewire.enable
WirePlumbersession manager for PipeWire, handles routing/defaultsservices.pipewire.wireplumber.enable
ALSAkernel-level audio interfaceservices.pipewire.alsa.enable (compatibility layer)

Key terms:

TermMeaning
Sinkoutput device (speakers/headphones)
Sourceinput device (microphone)
Profilea config preset on a sound card that activates certain endpoints (e.g. "Game Output + Game Input")
Nodea PipeWire object representing an audio stream or device
@DEFAULT_AUDIO_SOURCE@WirePlumber alias for the current default mic
* in wpctl statusmarks the current default sink/source

Recording/playback tools tested:

ToolPackageResultWhy
rec / playsoxworkedqueries PipeWire for actual source format (rate, channels, bit depth) and adapts automatically
arecord / aplayalsa-utilsrecorded but playback was garbled/fastold ALSA tool, assumes defaults (often 44100Hz stereo) instead of negotiating with PipeWire — rate mismatch causes chipmunk audio
pw-recordpipewireerror: open failed: Operation not supported.wav container not supported in this PipeWire build, needs raw format or different container

Useful commands:

CommandWhat it does
wpctl statusshow all audio nodes, * = default
wpctl set-default <id>set default sink/source
wpctl inspect @DEFAULT_AUDIO_SOURCE@detailed info on default mic
wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0unmute mic
wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 1.0max mic volume
rec /tmp/test.wav trim 0 3record 3 seconds (sox, recommended)
play /tmp/test.wavplay back (sox)
pavucontrolGUI for managing audio devices/profiles

Agenix - rekeying secrets after adding a new host

When you add a new host public key to secrets/secrets.nix, you need to rekey so the secret gets re-encrypted for the new recipient.

The command that actually works:

cd secrets
sudo agenix -r -i /etc/ssh/ssh_host_ed25519_key

Key gotchas:

  • -i wants the private SSH key of a host already in secrets.nix, NOT the .age file
  • needs sudo because /etc/ssh/ssh_host_ed25519_key is root-owned
  • agenix -r alone fails with "no identity matched any of the recipients" — it doesn't auto-find the host key
  • the error messages are completely unhelpful