--- This is mostly ai. Don't even look at it. ---
A cross-platform, Lua-only Neovim configuration built for Rust, Python and TypeScript, with "Adrian" — a green theme drawn from the Project Hail Mary render in this repo, on Tokyonight's engine — matched between the editor, the terminal, the prompt and the shell greeting.
Developed on Windows 11, now running on Linux (CachyOS, Wayland, Ghostty) — the move was one script. Windows support remains, but Linux is the primary target.
Neovim 0.12+ · lazy.nvim · 52 plugins · ~180 ms startup · zero vimscript
| Requirement | How |
|---|---|
| File exploring | neo-tree sidebar (<leader>e) + oil.nvim filesystem-as-a-buffer (-) |
| Image viewing | snacks.image via the Kitty graphics protocol — inline in markdown, or floating (<leader>ii) |
| Completion + Intellisense | blink.cmp + native vim.lsp.config, with inlay hints, auto-import and signature help |
| Easy new language | one file in after/lsp/, one line in a list. See docs/ADDING-A-LANGUAGE.md |
| Keybinding screen | which-key — press <Space>, or <leader>sk to fuzzy-search all 168 mappings |
| Status bar | lualine — branch, diff counts, diagnostics, path, language, LSP client names, venv, indent style, encoding warnings |
Plus the things that are standard in this kind of setup: fuzzy picker, treesitter, git integration at three levels, format-on-save, linting, a test runner, sessions, project-wide search-and-replace, a symbol outline, a diagnostics panel, and a dashboard.
- Everything is commented.
lua/config/options.luadocuments every meaningful Vim option grouped by purpose — including the ones deliberately left off, and a final section on legacy options you should never copy from an old vimrc. It is the reference, not just the settings. - Every kind of word has its own colour; the chrome stays quiet. Keywords
are Adrian's lime; functions azure, types teal, traits lavender, enums aqua,
variants lemon, macros orchid, strings sand, numbers orange, fields coral,
parameters pink — each ≥ 7.3:1 on the background, and the kinds that sit next
to each other (
self.field,Enum::Variant,fn name,x: Type) picked to be the furthest apart (measured, seepalette.lua). Borders, separators and the flat statusline sit a step above the background, Zed-style. Rust shows inferred types inline; other languages keep inlay hints off (<leader>uh), and a diagnostic's message shows only on the cursor's line. - Types are "marked" individually, VS Code style, and never italic. Most themes
collapse every kind of type onto one
Typecolour, so a struct, an enum, a trait, a type alias and a generic parameter all look identical — the highlighting tells you "this is a type" and nothing more. This config styles the language server's semantic tokens instead, so each kind gets its own hue, declarations are bold,mutbindings are underlined andunsafegets an undercurl. The mapping was derived by dumping the 47 groups rust-analyzer actually emits, not guessed. Seelua/config/palette.lua(M.types) to retune. - No key is both a mapping and a prefix. That combination forces Neovim to wait
out
timeoutlenon every keystroke beneath it. Enforced across all 168 leader mappings — there is a checker script in the repo history. - The leader layout matches LazyVim. Not for compatibility, but because it is the most documented keymap set in the ecosystem: when you search for how to do something, the answer's keybinding is usually already correct here.
- Explicit over automatic. For example
mason-lspconfig'sautomatic_enableis off, because it silently startedstylua --lspas a language server on every Lua buffer. Mason installs; the config decides what runs.
Clone it anywhere — the installers locate the repo from their own path, and
nothing in it hard-codes where it lives. This machine keeps it at
~/Repos/dotfiles; the examples below use that.
git clone <this-repo> $env:USERPROFILE\Repos\dotfiles
cd $env:USERPROFILE\Repos\dotfiles
.\install.ps1 -InstallTools -SyncCreates a directory junction from %LOCALAPPDATA%\nvim to nvim/. A junction
rather than a symlink because symlinks on Windows need Administrator or Developer
Mode; junctions need neither.
git clone <this-repo> ~/Repos/dotfiles
cd ~/Repos/dotfiles
./install.sh --tools --syncSymlinks ~/.config/nvim to nvim/. Detects apt / dnf / pacman / zypper / apk /
brew and installs the right package names for each — which differ more than you'd
expect (see the fd-find note below).
Both scripts back up an existing config with a timestamp before replacing it, and both are safe to re-run.
Moving the repo later: move the directory, then re-run the installer from
the new place (./install.sh --force / .\install.ps1 -Force). The nvim link
is the only thing that points into the repo — Ghostty, Starship and fastfetch
are copies — and the installer recognises the old, now-dangling link and
re-points it.
Two single-file configs live in this repo and are installed by ./install.sh:
ghostty/config -> ~/.config/ghostty/config
starship/starship.toml -> ~/.config/starship.toml
Unlike nvim/, these are copied, not symlinked. Both get poked at live — a
Ghostty reload here, a prompt colour retuned there — and a symlink would turn
every such experiment into an uncommitted change in this repo. So the repo is what
you install from: re-run ./install.sh to push a new version out, and copy a
local tweak back by hand when it is worth keeping. An existing file that differs is
backed up with a timestamp first (--force skips the prompt).
This step is Linux-only. install.ps1 handles Neovim alone, and macOS is skipped
because the Ghostty config here carries Linux-side window-decoration settings.
Starship reads $STARSHIP_CONFIG if it is set, and the installer honours that.
Copying starship.toml is not enough to get the prompt. Starship also has to
be initialised by the shell, and that line lives in the shell's own rc file,
which is per-machine and is deliberately not in this repo:
starship init fish | source # ~/.config/fish/config.fish — last line(eval "$(starship init zsh)" in ~/.zshrc, eval "$(starship init bash)" in
~/.bashrc.) Miss it and everything looks installed while the stock prompt keeps
rendering — so install.sh checks your login shell's rc file for it and prints
the exact line if it is absent. It does not edit the rc file itself: a distro that
ships its own prompt framework (CachyOS sources cachyos-config.fish) has to be
initialised before starship, and only you know what else is in there.
fastfetch/ is copied to ~/.config/fastfetch/ the same way. A new Ghostty
window (or an SSH session) opens on fastfetch beside one of three animated ASCII
scenes: Adrian (the green planet), the Wheel (seven spokes, one gold, one
emerald), the Petrova line (Astrophage streaming between a far sun and
Adrian's horizon). It picks at random and never repeats the last one.
fastfetch/fetch.py the launcher fish_greeting calls; picks scene + display mode
fastfetch/art.py renders the scenes (numpy + Pillow): art.py preview wheel
fastfetch/config.jsonc the info pane
How it animates in Ghostty 1.3 (which cannot play kitty animations): the logo is
kitty Unicode-placeholder text, so it scrolls and clears like text, and a small
background process re-sends the next frame under the same image id while you are
at the prompt. It pauses while a command runs and exits with the tab. Frames are
rendered per terminal cell size into ~/.cache/fastfetch-art/ (about 40 MB per
scene per size); a new size renders in the background, with a still text frame
meanwhile. Over SSH it is always that still text frame.
Like starship, it only runs if the shell calls it — install.sh prints the
fish_greeting snippet if ~/.config/fish/config.fish does not have it. The
snippet only greets in Ghostty or over SSH, never in Neovim's :terminal, an IDE
terminal, or a nested shell.
The filename matters: Ghostty reads exactly config, with no extension. A
file called config.ghostty is ignored without a warning, which leaves the
terminal on its stock theme and font while Neovim renders its own palette — the
mismatch shows up as "the colours are wrong outside nvim".
It mirrors the hex values from nvim/lua/config/palette.lua (background,
foreground, cursor-color, selection-*, and palette = 0..15 from
M.colors.terminal). Ghostty cannot read Neovim's Lua, so the numbers are
repeated there with a pointer back. Retune one, retune both. Reload with
ctrl+shift+,.
Two settings there are worth knowing about: resize-overlay = never, because
Ghostty's live size readout otherwise draws over a Neovim layout that is itself
reflowing mid-drag, and shell-integration = fish (the login shell on this
machine — getent passwd $USER), which is what makes
window-inherit-working-directory work. Also note that plain ctrl+arrow is left
unbound so Neovim's split-resize keys reach Neovim; Ghostty's own defaults for
those actions are ctrl+shift+arrow and ctrl+alt+arrow.
Installed automatically by the scripts. Listed so you know what each is for — skipping one degrades a specific feature rather than breaking the editor.
| Tool | Needed for | Without it |
|---|---|---|
| Neovim ≥ 0.12 | everything | config will not load — needs vim.lsp.config and treesitter main |
| git | lazy.nvim clones plugins | nothing works |
| C compiler | compiling treesitter parsers | no syntax highlighting beyond the 7 bundled parsers |
| tree-sitter CLI ≥ 0.26.1 | ditto | same |
| ripgrep | grep picker, :grep |
project search unavailable |
| fd | file picker, venv detection | picker falls back to a slower walker |
| Node.js | TypeScript LSP, prettier, jest | no TS support |
| JetBrainsMono Nerd Font | all icons | hollow boxes everywhere — test with :CheckIcons. It has to be this family: ghostty/config names it in font-family, and Ghostty falls back to a default font, silently, if it is missing — having some other Nerd Font installed does not help |
| ImageMagick | non-PNG image conversion | only PNGs render |
| lazygit | <leader>gg |
use gitsigns and diffview instead |
rust-analyzer + rust-src |
Rust | no Rust support |
| fswatch (optional) | native LSP file watching | Neovim's vim._watch falls back to a recursive poll written in Lua |
rust-analyzer comes from your distro or from rustup, never from mason. Two routes, and on a distro that packages Rust itself the distro route is the only one that works:
sudo pacman -S rust-analyzer rust-src lazygit # Arch / CachyOS
rustup component add rust-analyzer rust-src clippy rustfmt # everywhere elseOn Arch, pacman's rustup conflicts with its rust package, so if you have
rust installed then rustup component add is not available to you. Either way
rust-src is what makes go-to-definition jump into the standard library, and
Arch's rust package does not ship it.
On Windows the C compiler is zig — a single executable
that nvim-treesitter uses as zig cc, avoiding a full MSVC PATH setup.
Platform gotchas the scripts handle for you:
- Debian/Ubuntu: the
fdpackage isfd-findand installs the binary asfdfind.install.shsymlinks afdinto~/.local/bin. - Debian stable: ships a Neovim far older than 0.12.
install.shfetches the official AppImage instead. - tree-sitter CLI is packaged almost nowhere and needs
libclangto build from source, so both scripts download the prebuilt release binary. - Clipboard on Linux: needs
wl-clipboard(Wayland) orxclip/xsel(X11). Over SSH it needs nothing — Neovim 0.10+ falls back to OSC 52 and Ghostty supports it, so yanking over SSH lands in your local clipboard.
dotfiles/
├── install.ps1 Windows installer (junction)
├── install.sh Linux/macOS installer (symlink)
├── .stylua.toml formats this repo's Lua
├── docs/
│ ├── ADDING-A-LANGUAGE.md
│ └── KEYBINDINGS.md
└── nvim/
├── init.lua entry point — fixes module load ORDER, nothing else
├── lazy-lock.json every plugin pinned to a commit ← commit this
├── lua/
│ ├── config/
│ │ ├── palette.lua ALL colours and icons — the one file to retheme
│ │ ├── options.lua every Vim option, documented and classified
│ │ ├── keymaps.lua global keymaps
│ │ ├── autocmds.lua event-driven behaviour
│ │ └── lazy.lua plugin-manager bootstrap
│ ├── util/
│ │ ├── incsel.lua incremental selection (upstream module was deleted)
│ │ └── folds.lua treesitter-highlighted foldtext + line count
│ └── plugins/
│ ├── colorscheme.lua snacks.lua ui.lua explorer.lua
│ ├── editor.lua treesitter.lua lsp.lua completion.lua
│ ├── format.lua lint.lua git.lua test.lua
│ └── lang/ rust.lua python.lua typescript.lua
├── after/
│ ├── lsp/ one file per language server (see its README)
│ └── ftplugin/ one file per filetype (see its README)
└── snippets/ your own snippets, VS Code JSON format
Two ideas do most of the organisational work:
after/lsp/<server>.lua — Neovim merges every lsp/<name>.lua on the
runtimepath in order, and after/ is read last. So nvim-lspconfig supplies the
boring base (cmd, filetypes, root_markers) for ~300 servers and you supply
only settings. Adding a language is one small file.
after/ftplugin/<filetype>.lua — runs after every other ftplugin, so your
indent width and local keymaps always win.
| Want to change… | Edit |
|---|---|
| the colours | nvim/lua/config/palette.lua |
| an editor option | nvim/lua/config/options.lua (find it by category) |
| a global keybinding | nvim/lua/config/keymaps.lua |
| a plugin's keybinding | that plugin's file in lua/plugins/ (keeps it lazy-loaded) |
| a language server's settings | nvim/after/lsp/<server>.lua |
| indent width for a language | nvim/after/ftplugin/<filetype>.lua |
| which formatter runs | nvim/lua/plugins/format.lua |
| per-type-kind colours (struct vs enum vs trait) | nvim/lua/config/palette.lua → M.types |
| italics on/off | nvim/lua/config/palette.lua → M.style.italic_comments |
| rainbow brackets, folds, sticky context, incremental selection | the ts options table at the top of nvim/lua/plugins/treesitter.lua |
| add/remove a plugin | the relevant lua/plugins/*.lua, then :Lazy sync |
Machine-specific overrides go in nvim/lua/config/local.lua, which is
git-ignored and sourced last so it can override anything.
| Command | Tells you |
|---|---|
:checkhealth |
everything that is missing or misconfigured |
:CheckIcons |
whether your font renders the Nerd Font glyphs |
:verbose set shiftwidth? |
the value and which file last set it |
:Lazy / :Lazy profile |
plugin status; what is slowing down startup |
:Mason |
installed servers, formatters and linters |
:checkhealth vim.lsp |
attached clients, their roots and capabilities |
:LspLog |
why a server exited |
:ConformInfo |
which formatter will run in this buffer |
<leader>ui |
highlight groups under the cursor — for theming |
<leader>uI |
live treesitter parse tree |
Most-likely first causes: a server that never attaches is usually a binary missing
from PATH (check :Mason) or no root_markers matching; missing highlighting is
usually a parser that failed to compile (:checkhealth nvim-treesitter).
-
Inline images need a terminal with full Kitty-graphics support. Ghostty and Kitty have it. WezTerm's implementation is partial (no Unicode placeholders), so inline images inside a scrolling document can leave artefacts — press
<C-l>to redraw; float mode (<leader>ii) is reliable. Nothing else in this config is terminal-specific. -
A treesitter parser can fail with
EPERM: could not rename tempon Windows. A race with antivirus scanning the freshly extracted files. Just run:TSInstall <lang>again. Thegitcommitgrammar hits this most often because it ships a.wasmblob. -
vim.lsp.get_buffers_by_client_id() is deprecatedappears once in Rust buffers. Upstream in rustaceanvim, harmless, will resolve on a plugin update. -
Invalid 'col': out of rangefromns=nvim.lsp.inlayhintwas a Neovim 0.12 bug: after a language server restarted, Neovim quietly stopped tracking the buffer, so the server kept working from stale text and inlay hints were drawn at columns that no longer exist. Worked around bynvim/lua/util/lspsync.lua; the workaround switches itself off on Neovim 0.13, where upstream fixed it. If you ever see it again, closing and reopening the buffer clears it (:ealone only helps until the next edit). -
Debug adapters (nvim-dap) are not installed by choice.
<leader>dis reserved for them and<leader>rdin Rust is already wired; see docs/ADDING-A-LANGUAGE.md for adding it.:checkhealth rustaceanvimreports one warning about this — expected. -
:checkhealth snacksreports errors for optional image converters. All are safe to ignore unless you want that specific format; images themselves work.Reported missing Only needed for Install if you want it gs(ghostscript)rendering PDFs inline winget install ArtifexSoftware.GhostScript/apt install ghostscriptpdflatex/tectonicrendering LaTeX math in markdown a TeX distribution mmdcrendering Mermaid diagrams npm i -g @mermaid-js/mermaid-clikitty graphics protocol — reported only when run headless; Ghostty supports it The LaTeX and Mermaid errors cannot be configured away: snacks' image healthcheck probes for
tectonic/pdflatexandmmdcunconditionally and never consultsmath.enabled, so setting it changes nothing here. Install a TeX distribution andmmdcif you want them gone.math.enabledin theimageblock ofnvim/lua/plugins/snacks.luais gated on that tooling, for a different reason: without it, a markdown file full of$…$would otherwise fire one failed conversion and one notification per expression. -
:checkhealth snackswarns about missing Treesitter languages —latex,norg,svelte,typst,vue. That is snacks.image listing parsers it could use for inline rendering. Left as-is deliberately: installing five parsers for languages that are never edited here is worse than one warning. Add them toparser_groupsinnvim/lua/plugins/treesitter.luaif you disagree. -
Four
:checkhealthcomplaints are headless-only artifacts.Snacks.dashboard: setup did not run,vim.ui.input is not set to Snacks.input,vim.ui.select is not set, and the kitty-graphics one all come fromUIEnternever firing without a UI. Run:checkhealthinside Neovim, not vianvim --headless, and they are absent. -
Startup looks ~100 ms slower under a bare pty than it really is. Neovim 0.12 probes
'background'with OSC 11 + DSR andvim.wait(100, ...)for the reply; a captured pty never answers, so it burns the whole budget. Ghostty answers in about a millisecond. This happens invim/_core/defaults.luabefore any user config loads and cannot be disabled fromoptions.lua— settingopt.backgrounddoes not help, because the guard there ignores values set from Lua.nvim --cleanshows the same 100 ms, which is how you confirm it is not this config.
- Picker:
snacks.pickeris used instead of telescope because telescope's fast sorter is a C extension needing make/cmake — a recurring Windows problem. Telescope is more widely documented; swap it intolua/plugins/snacks.luaif you prefer. - Completion:
blink.cmpinstead ofnvim-cmp. nvim-cmp needs one plugin per source; blink bundles them. - Python LSP:
basedpyrightis a stricter fork ofpyright. Change one string inlua/plugins/lsp.luato go back. - TypeScript LSP:
vtslsinstead ofts_ls, for working inlay hints and better monorepo handling.
MIT — see LICENSE.