$ duy_

$ 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.

neofetch, roughly
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

aliasexpands towhy
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

keysdoeswhy
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.