A fast, light-themed dev setup for Python, Rust, Zig, C++, and C, with an LLM CLI tab.
dev layout:
files tab:
git blame:
rt (run test):
dotfiles/
├── install.sh # installs everything + symlinks configs
├── uninstall.sh # reverses install.sh (symlinks, rc edits, git config)
├── helix/
│ ├── config.toml # editor look & feel (Catppuccin Latte)
│ └── languages.toml # LSPs, formatters, debug adapters per language
├── alacritty/
│ └── alacritty.toml # terminal appearance, maximized, opens into zellij
├── zed/
│ └── settings.json # One Light, JetBrains keymap, autosave-as-you-type, LSP tweaks
├── zellij/
│ ├── config.kdl # keybindings, theme, mouse behavior
│ └── layouts/
│ ├── dev.kdl # files (yazi) + editor (hx) + console + llms + git
│ ├── rsdev.kdl # same as dev, no console tab; 4-console Rust ops tab
│ └── pydev.kdl # same as dev, no console tab; 4-console Python ops tab
├── lazygit/
│ └── config.yml # difftastic as the diff renderer
├── screenshots/ # README images
└── scripts/
├── llm.sh # picks the LLM CLI per machine, installed as `llm`
├── mdp.sh # live markdown preview (glow + entr), installed as `mdp`
└── doctor.sh # health check, installed as `doctor`
| Language | LSP | Formatter / linter | Debugger (DAP) |
|---|---|---|---|
| Python | pylsp (jedi, mypy, rope) | ruff (format + lint) | — (add debugpy if you need it) |
| Rust | rust-analyzer | rustfmt, clippy on save | lldb-dap |
| Zig | zls | zig fmt |
— (zls doesn't ship DAP support yet) |
| C++ | clangd | clang-tidy (via clangd) | lldb-dap |
| C | clangd | clang-tidy (via clangd) | lldb-dap |
Debug from Helix with :debug-start (or bind a key to it) once a binary
exists — it'll ask for the path to the compiled binary using the templates
in languages.toml.
rust-analyzer shows inlay type hints (binding modes, elided lifetimes,
closure return types, full function signatures) — tune them under
language-server.rust-analyzer.config in languages.toml.
Mostly no — the global config handles the LSPs/formatters automatically:
-
Rust: nothing extra.
cargo newgives rust-analyzer everything it needs. -
Zig: works as soon as
zlsis on yourPATH. -
Python: in Zed, basedpyright and the Debugpy debugger use the
.venvZed detects in the project automatically (toolchain picker in the status bar if there are several). In Helix, starthxfrom an activated venv (oruv run hx) so jedi resolves third-party imports, and to let mypy see them too add a project-local.helix/languages.toml:[language-server.pylsp.config.pylsp.plugins.pylsp_mypy] overrides = ["--python-executable", ".venv/bin/python", true]
-
C / C++: this is the one exception — clangd needs a
compile_commands.jsonper project to know your include paths and flags.bearis installed for exactly this: runbear -- make, or with CMake add-DCMAKE_EXPORT_COMPILE_COMMANDS=ON. With it, clangd also auto-inserts missing#includes (--header-insertion=iwyu); without it, that and accurate completion/diagnostics for anything outside the standard library are lost.
git clone <this repo> ~/dotfiles # or unzip it there
cd ~/dotfiles
chmod +x install.sh
./install.shDetects macOS (Homebrew), Debian/Ubuntu (apt), or Arch (pacman); installs
Helix/Alacritty/Zellij, Zed (GUI editor, with its zed CLI on PATH),
each language's toolchain/LSP, lldb-dap, cargo-watch, glow, entr,
bear, difftastic, mergiraf, and resvg (SVG rasterizer); symlinks
everything into ~/.config/, puts ~/.local/bin on your PATH and drops
the mdp/llm/doctor scripts there.
It also installs bash-completion (bash-completion@2 on Homebrew,
bash-completion on apt/pacman) and sources it from ~/.bashrc. Without
it bash only tab-completes filenames; with it you get make/gmake
targets, ssh hosts (from ~/.ssh/config and known_hosts), git
subcommands and branches, and so on.
Notes on packages it can't get from apt directly:
- Helix on Ubuntu — not in the default repos, so the script prefers the
official snap (current releases), falling back to the maveonair PPA
(
ppa:maveonair/helix-editor) when snapd isn't installed. On non-Ubuntu Debian it points you at the release tarballs on GitHub. - Zed — no apt package; installed with Zed's official installer
(
https://zed.dev/install.sh, lands in~/.local/zed.appwith the CLI linked to~/.local/bin/zed). On macOS it's thezedHomebrew cask; ifZed.appwas already downloaded by hand, the script just links its CLI to~/.local/bin/zed. - glow — installed from Charm's apt repo.
- eza, gh, lazygit, starship — installed from their official apt repos / release tarballs as needed.
zed . from a project root opens the whole tree as a workspace. The
settings in zed/settings.json are symlinked to ~/.config/zed/settings.json
(your previous file is kept as settings.json.bak) and set:
- One Light theme, JetBrainsMono Nerd Font, 100-column guide, no relative line numbers — the same look as the Helix/Alacritty setup.
- JetBrains keymap plus IntelliJ-style autosave: the buffer is written 0.5 s after you stop typing and on focus loss, so there is no "unsaved" state to think about. Formatting (rustfmt/ruff) only runs on an explicit Cmd-S so autosave never rewrites what you are typing.
- rust-analyzer runs clippy on check. Python is set up to feel like PyCharm: basedpyright (Zed downloads it) for go-to-definition, find usages, rename, auto-import completions, inlay type hints and type checking at PyCharm's "standard" strictness, plus ruff for lint, import sorting and formatting. Helix keeps pylsp instead.
- Run / debug like PyCharm:
zed/tasks.jsonholds run configurations (run file or module withuv run, pytest for the file, the whole suite or the test under the cursor, ruff),zed/debug.jsonthe matching Debugpy launch/attach configurations (adapter downloaded by Zed on first use), and tests /__main__blocks get gutter run icons.zed/keymap.jsonadds the macOS PyCharm keys on top of the JetBrains keymap: Ctrl-R run last, Ctrl-Alt-R pick a run config, Ctrl-D debug last, Ctrl-Alt-D pick a debug config, Cmd-Shift-B type definition, Cmd-U declaration, Cmd-F12 file structure, Cmd-O symbol, Cmd-P parameter info, F1 quick docs. Shift-F10 / Shift-F9 / Alt-T from the JetBrains keymap keep working. - New integrated terminals start with the project's
.venvactivated. - Inline git blame on the current line.
- Extensions auto-installed on first launch: zig, toml, kdl, just, dockerfile, make, sql, env, log, csv.
- Privacy: telemetry off and edit prediction disabled (it would stream buffer contents to Zed's servers).
On Ubuntu the script also installs libvulkan1 + mesa-vulkan-drivers: Zed
renders through Vulkan, and the Mesa package provides a software fallback so
it still starts in VMs without a GPU driver.
install.sh symlinks scripts/doctor.sh onto your PATH as doctor. It
verifies the setup is still intact — binaries present, config symlinks
pointing back into this repo, git diff/merge config wired, and the aliases/
functions present in your shell rc — and prints a PASS/FAIL per check,
exiting non-zero if anything is missing. Run it any time to catch drift
after a package-manager update:
doctoruninstall.sh reverses everything install.sh did: removes the config
symlinks, strips the aliases/functions it appended to ~/.bashrc/~/.zshrc,
and reverts the git config it touched. It only removes entries it recognizes,
so your own additions are left alone:
cd ~/dotfiles
./uninstall.shcd ~/some-project
devdev is an alias (zellij attach --create dev options --default-layout dev)
added to your shell rc by install.sh. It attaches to a session named dev
if one is already running, and only creates it (with the dev layout) when
it isn't — so your panes, running builds, and the llms tab pick up right
where you left off, and closing the terminal never leaks a fresh
randomly-named session. Note that the layout is applied only on creation and
the session is shared across directories: run kill_devs first if you want a
clean dev session in a different project.
kill_devs force-deletes the dev, rsdev and pydev sessions and then
sweeps any exited sessions, so zellij list-sessions stays short.
If you'd rather launch straight from your desktop instead of an existing
terminal, open Alacritty (maximized, light Catppuccin Latte colors) and
run dev inside it — same result.
For Rust projects, use rsdev instead — it's the same wrapper but opens the
rsdev layout. There's no console tab; instead the ops tab is a 2×2 grid
of four consoles — single test (rt), run (cr), full test suite
(ct/cargo test), and clippy (ccl) — all run manually.
For Python projects, use pydev — the same idea, but its ops tab is a 2×2
grid of four consoles: single test (ptk), run (pm), full test suite
(pt), and lint (pl).
In both layouts the editor tab is just Helix (no console), like dev.
These aliases and functions are added to ~/.bashrc / ~/.zshrc by
install.sh:
| Command | What it does |
|---|---|
dev |
Open a Zellij session with the dev layout (named after the current folder) |
rsdev |
Same, but with the rsdev layout (adds an ops tab) |
pydev |
Same, but with the pydev layout (adds a Python ops tab) |
y |
Launch yazi; on quit, cd to wherever you navigated to |
mdp |
Live markdown preview (glow + entr) |
Rust / cargo aliases:
| Command | What it does |
|---|---|
cb |
cargo build — compact one-line diagnostics, no progress spam |
cc |
cargo check — same, but skips codegen (fastest feedback loop) |
ccl |
cargo clippy — compact diagnostics |
cw |
cargo watch running cargo check on every save |
cbg |
bacon — live, always-on compact diagnostics panel |
ct |
Run the full test suite via cargo nextest run |
cf |
cargo fmt |
cu |
cargo update (bump Cargo.lock within semver constraints) |
cr |
cargo run (build + run the project binary), mirrors pm |
rt |
Fuzzy-pick and run a single Rust test (cargo-nextest + fzf) |
Python aliases (the cargo-equivalent workflow). They work in Poetry and
uv projects alike: prun (scripts/prun.sh, on PATH) looks for
poetry.lock / [tool.poetry] upward from the current directory and runs
poetry run, otherwise uv run; the functions below dispatch the same way.
| Command | What it does |
|---|---|
pvenv |
Create the project env (poetry env use python3 / uv venv) |
pd |
Install/sync the project's deps + env (poetry install / uv sync) |
pa |
Add a dependency (poetry add / uv add <pkg>) |
prm |
Remove a dependency (poetry remove / uv remove <pkg>) |
pu |
Upgrade locked deps (poetry update / uv lock --upgrade) |
prun |
Run any command in the project env (poetry run / uv run) |
pf |
Format code (ruff format .) |
pl |
Lint (ruff check .) |
pcx |
Lint with auto-fixes (ruff check --fix .) |
pt |
Run pytest: whole suite, or whatever args you pass (prun pytest). Drawings show by default; export PT_DIS_VIS=1 adds --dis-vis in repos whose conftest defines it |
pw |
Run tests on every save (prun ptw ., via pytest-watcher) |
ptk |
Fuzzy-pick test(s) with fzf and run them (ptk login pre-fills the query, Tab multi-selects, ptk -c lists parametrized ids) |
ptf |
Run all tests in one file by name (ptf login → tests/test_login.py; ambiguous or no name opens an fzf file picker) |
pm |
Run a Python entrypoint via .venv/bin/python (defaults to main.py) |
ptk lists test node ids (tests/test_x.py::TestA::test_b) from a static scan
of the test_*.py / *_test.py files, which takes ~0.2s where
pytest --collect-only takes 10-30s on a big suite (it imports every test
module and conftest). The scan does not expand names generated at runtime:
ptk -c switches to real pytest collection when you want to pick a single
parametrized case, and if a picked id turns out not to exist (e.g.
parameterized renames the method) ptk re-runs the picks as
pytest file -k name. Running the tests needs pytest in the project's
dev-dependencies: poetry add --group dev pytest or uv add --dev pytest
(add pytest-watcher too for pw). The Zed run configurations in
zed/tasks.json go through the same prun.
Three tools make reviewing and merging code less tedious, wired up by
install.sh:
| Tool | Role |
|---|---|
| difftastic | syntax-aware (structural) diffs — git diff and lazygit |
| mergiraf | syntax-aware merge driver — auto-resolves merge/rebase/cherry-pick conflicts |
rerere (git built-in) |
records how you resolved a conflict and replays it next time |
- difftastic is set as git's external diff (
diff.external), so plaingit diffshows structural diffs.deltastays on as the pager forgit log/show/blameandgit add -p. lazygit renders its diff pane through difftastic too. - mergiraf is registered as the
mergirafmerge driver and applied to all files via the global gitattributes (~/.config/git/attributes);diff3conflict style is enabled so it can reconstruct all three sides. When it auto-resolves a conflict it asks you to review withmergiraf review <id>. - rerere is enabled (
rerere.enabled), so conflicts you've solved once are re-applied automatically the next time they show up.
| Command | What it does |
|---|---|
git diff |
structural diff via difftastic (opt out with --no-ext-diff) |
git dlog |
git log -p with difftastic |
git dshow |
git show with difftastic |
mergiraf review <id> |
review a conflict mergiraf auto-resolved |
mergiraf solve <f> |
attempt to auto-resolve an existing conflicted file |
Inside the session (dev):
- Tab 1 —
files: launchesy(yazi) automatically — a terminal file manager for browsing, previewing, and editing files without leaving the session. - Tab 2 —
editor: Helix open on., full pane — no shell underneath. - Tab 3 —
console: a scratch shell for builds and ad-hoc commands. - Tab 4 —
llms: launchesllm(scripts/llm.sh), which picks whichever LLM CLI this machine has —claudeoropencode, in that order. Setexport LLM_CLI=<cmd>in your shell rc to force a specific one, so the shared layouts stay identical across machines. - Tab 5 —
git: lazygit, for status/diff/stage/commit without leaving the session.
In rsdev/pydev there's no console tab: the editor tab is just Helix,
and the ops tab (a 2×2 grid of four consoles — single test, run, full test
suite, clippy/lint) sits between editor and llms.
Line numbers are absolute (1-based from the top of the file, not relative to
the cursor) — see line-number in helix/config.toml. Extra bindings added
on top of the Helix defaults (in helix/config.toml):
| Key | What it does |
|---|---|
Ctrl-s |
Save (:w) |
Ctrl-q |
Quit (:q) |
space |
Picker menu (space → file picker, space b → buffer picker, w → save, q → quit) |
space B |
Git blame the current line — prints the commit/author that last touched it to the statusline |
m |
Render the current markdown file through glow |
Ctrl-e |
Open the current file in VS Code at the cursor line (code -g) |
Note: Ctrl-e runs in the foreground, so Helix suspends until you close
VS Code.



