$ cat uses.md
Uses
A /uses page is usually a list of things somebody wishes they used. This one is read off the machine I am typing on, and the interesting column is the last one — why, and what it cost.
- os
- Fedora Linux
- desktop
- Hyprland — Wayland, tiling, no desktop environment underneath it
- shell
- zsh + oh-my-zsh, powerlevel10k prompt
- multiplexer
- tmux, prefix rebound to C-s
- editor
- Neovim (NvChad on lazy.nvim), leader = space
- ide
- VS Code, only for dev containers
- mesh
- Tailscale — every machine reachable, nothing exposed
Fedora and Hyprland
This was a Linux userland hosted inside a different operating system for a long time, and most of what is on this page came through the move unchanged — zsh, tmux, Neovim and every tool in the infrastructure list below never cared what was underneath them. What changed is everything at the boundary.
On the old setup the annoying problems were never Linux problems, they were translation problems. A GUI needed a display server bolted on from outside. The clipboard needed a bridge, in each direction, and the bridge was the thing that broke. Paths needed converting whenever a program on one side handed a filename to a program on the other. Font rendering and DPI were negotiated twice by two systems that disagreed. None of that is hard, and I had written or installed a shim for all of it — but none of it is work either. It is overhead you stop noticing and keep paying.
There is now one system. The compositor is Wayland, the clipboard belongs to the compositor, and the terminal is talking to the same display server as everything else. Nothing in the middle needs to translate, so nothing in the middle can be the bug.
Hyprland specifically, because window layout should be a consequence of what is open rather
than something I arrange by hand every morning. My working shape is stable and boring — an
editor, a shell, and a browser holding documentation — and a tiling compositor puts those
in the same place every time without a single drag. It is the same argument as tmux, one
level up: I already refuse to arrange panes by hand inside a terminal, so arranging them
by hand outside one was always inconsistent. It is also why
C-h/j/k/l is worth binding twice.
The honest cost is that the desktop is now mine to maintain. A compositor with no desktop environment under it ships no defaults for the boring parts — screen sharing, portals, the lock screen, the notification daemon, what happens when a laptop lid closes. Every one of those is a decision I now own, and each is a thing that can be broken by an update on a Monday. I took that trade because the failures are at least mine and legible; the shims they replaced failed in a layer I could not read.
The shell
zsh with oh-my-zsh, which is more framework than I strictly need and which I keep anyway because autosuggestions and syntax highlighting pay for themselves within a day. The plugin list is deliberately short — every one of these is either completion for a tool I use daily or a behaviour I would otherwise write myself:
- git
- ssh-agent
- sudo
- zsh-256color
- zsh-autosuggestions
- zsh-syntax-highlighting
- docker
- docker-compose
- kubectl
- terraform
- aws
- pip
- npm
- jsontools
- web-search
ssh-agent is the one that matters operationally: keys get loaded once per
session rather than once per git push, and the agent is what forwards into the
machines I reach over Tailscale.
Aliases I would miss
| alias | expands to | why |
|---|---|---|
ls | eza -x | columns, and it colours by file type without a flag |
ll | eza -lha --sort=name --group-directories-first | the one I actually type |
ld | eza -lhD | directories only, for when a tree got out of hand |
k | kubectl | the only alias worth muscle memory |
vi | nvim | so the habit and the binary finally agree |
rm | rm -i | one confirmation is cheaper than one restore |
up | sudo dnf upgrade --refresh | Monday morning |
eixt | exit | I type it wrong often enough to have automated the typo |
One confession: grep is aliased to grep -i. It is a bad habit —
it makes my interactive shell disagree with every script and every colleague's terminal —
and I have kept it for years. It is on the list of things to fix precisely because it is
the kind of small local divergence that turns into "works on my machine" later.
tmux
Every long-running thing lives in tmux, because a dropped SSH session should cost nothing. The configuration is small and each line is load-bearing:
prefix C-s- C-b fights with readline and with vim; C-s does not, once flow control is off
mode-keys vi- copy mode with v / C-v / y, because the muscle memory is already there
C-h/j/k/l- pane navigation that passes through to vim when vim is the focused pane
history-limit 100000- the scrollback is the incident log until something better exists
base-index 1- window 1 is on key 1; window 0 on key 0 is a small daily tax
set-clipboard on- OSC 52, so a yank inside a remote tmux lands in the clipboard in front of me
Editor
Neovim, configured from NvChad on top of lazy.nvim, leader on space.
lazy-lock.json pins 48 plugins, and I want to be exact about who owns them:
roughly half are NvChad's own — the theme layer, the file tree, telescope, gitsigns,
which-key, the nvim-cmp stack and its sources, LuaSnip, mason. I did not build that and I
am not going to pretend otherwise. A distribution that starts fast and has completion and
LSP wired up correctly is worth more to me than the config being mine.
What I do own is about twenty plugin specs in lua/plugins/init.lua and the
lua/configs/ layer under them, and that layer is almost entirely about one
thing: making the editor understand the file formats I actually spend my day in. Sixteen
language servers are configured and enabled by hand —
- lua_ls
- terraformls
- ansiblels
- yamlls
- jsonls
- bashls
- dockerls
- docker_compose_language_service
- pyright
- ts_ls
- clangd
- csharp_ls
- html
- cssls
- tailwindcss
- efm
— plus rust_analyzer, which is deliberately not in that list. Treesitter
installs 37 parsers, which is the same story from the highlighting side.
The parts that are opinions, not defaults
virtual_text = false, float on CursorHold- inline diagnostics reflow the line I am reading and lie about how wide the code is. updatetime is 300ms and the message opens in an unfocused float instead, one at a time.
yamlls, wired to schemas rather than to YAML- a GitHub workflow, an Ansible playbook under playbooks/, a compose file, a kustomization and a Chart.yaml each get their own schema, and *.k8s.yaml gets the Kubernetes one. A misspelled key is a diagnostic on the line instead of a red pipeline ten minutes later. This is the single highest-value thing in the whole config.
clangd formatting disabled in on_attach- the server can format and so can clang-format through conform. Two formatters on one buffer is a fight, so the language server loses on purpose.
rust_analyzer never handed to lspconfig- nvim-lspconfig gets a setup function that returns true and does nothing, so rustaceanvim owns the Rust server outright — clippy on save, chaining and parameter inlay hints on, lifetime and reborrow hints off because they are noise.
everything at 80 columns- black, prettier, stylua and rustfmt are all pinned to 80 in the formatter args. The width is arbitrary; having one width across five languages is not.
27 built-in plugins turned off in the rtp- netrw, the zip and tar handlers, the tutor, the compiler plugin. lazy.nvim also defaults every spec to lazy = true, so the cost of a plugin is paid at first use rather than at startup.
Formatting and linting
conform.nvim maps twenty-odd filetypes onto formatters — terraform_fmt for HCL,
shfmt -i 2 -ci for shell, prettier for the web and for YAML, stylua for Lua,
black and isort for Python. nvim-lint runs the linters that matter to me on
BufEnter, BufWritePre and InsertLeave:
tflint, yamllint, shellcheck, hadolint,
markdownlint. Those are the same checks a pipeline would run, which is the
entire point — a lint failure should cost me a save, not a push and a wait.
Two things here are wrong and I would rather write them down than tidy them out of sight.
First, conform's own format_on_save block is commented out and replaced by a
hand-rolled BufWritePre autocommand that calls
conform.format({ async = true }). Formatting asynchronously in a pre-write hook
means the write can win the race and the formatted buffer lands on the next save instead of
this one. It has not bitten me yet, which is exactly the property that makes it worth
fixing. Second, there is a .stylua.toml in the repository asking for 120
columns and four-space indents, and conform passes --column-width 80 and
--indent-width 2 on the command line, where they win. The checked-in file is
dead configuration that describes a convention the editor does not follow.
Keymaps worth the muscle memory
| keys | does | why |
|---|---|---|
jk | <Esc> in insert mode | the only way out of insert I use; Escape is a reach and C-[ is a chord |
<leader>fj | hop.nvim word jump | label-driven motion for the middle distance, where a count is guesswork and a search is too much typing |
<A-j> / <A-k> | move line or selection down / up | re-indents after the move, and gv=gv keeps the selection so it can be repeated |
= / - | vertical resize ±5 | this takes = away from the indent operator. It is a real cost and I pay it, because I resize splits far more often than I re-indent by hand |
+ / _ | horizontal resize ±2 | the same trade, on the axis I use less |
<C-h/j/k/l> | move between splits and tmux panes | the other half of the tmux binding above — one set of keys whether the thing next door is a vim split or a pane |
<F5> / <F10> / <F11> / <F12> | dap continue, over, into, out | the same four keys every other debugger has bound for twenty years |
<leader>d… | breakpoints, REPL, watches, quit | db toggles, dB prompts for a condition, dw floats the scopes widget, dq terminates the session and closes the UI together |
<leader>a | RustLsp codeAction | rust buffers only, from after/ftplugin/rust.lua |
<leader>mt / <leader>mp | toggle in-buffer markdown render / preview in a browser | the first for reading, the second for anything with an image in it |
Debugging, and the leftovers
nvim-dap with lldb for C and C++, debugpy for Python and codelldb for Rust, and the
rustaceanvim adapter is wired up only if mason actually has codelldb installed — the lookup
is inside a pcall, so a missing debugger degrades to no debugger rather than to
a stack trace at startup. dap-ui opens itself when a session initialises and closes itself
when the session ends, because a debugger UI that has to be summoned is a debugger UI that
does not get used.
There is cruft, too. image.nvim is still in the plugin list with
enabled = false, disabled because the terminal I used to run could not do
graphics protocols — a reason that left with the old machine and that I have not gone back
to undo. There is a previewer config file in lua/configs/ that nothing loads
any more. A config that has been carried across machines accumulates this, and the fix is
to delete it, not to describe it as intentional.
VS Code stays installed for exactly one job — dev containers, where the remote extension is genuinely better than anything I would assemble myself.
Infrastructure tooling
- Terraform
- anything that lives in a cloud account. State in S3 once more than one person touches it.
- Ansible
- anything below the API server — kernel settings, containerd, kubeadm, node lifecycle.
- kubectl + Helmfile
- day-to-day cluster work; Helmfile for the platform tier bootstrap.
- Argo CD
- so the repository is the only way a cluster changes.
- Cosign + Kyverno
- signing is the easy half; admission refusing the unsigned image is the other one.
- AWS CLI
- with short-lived credentials only. There is no long-lived access key on this machine.
Networking
Tailscale, on every machine including the ones in the cupboard. It is the reason my bare-metal Kubernetes nodes have a stable address without a public IP, without port forwarding on a home router, and without a VPN appliance to maintain. It is also how I previewed this site from a laptop while it was being built on a different machine entirely.
The trade-off is honest and worth stating: it puts a third-party coordination service on my admin path. I accept that for a homelab and for a thesis cluster. I would think harder about it for something with real users.
This site
Astro, no UI framework, no web fonts, one JavaScript file. Terraform for the AWS side and GitHub Actions with OIDC for delivery — all of which is written out properly, with the real headers and the real bill, on the infra page.