Cross-platform shell + git config and a full terminal IDE (Neovim + Zellij + Yazi, with Ghostty) that works interchangeably on macOS and Linux, under both bash and zsh.
git clone https://github.com/KyleM73/dotfiles.git
cd dotfiles
./make_symlinks.shThat single command is the whole setup. make_symlinks.sh:
-
Backs up any existing real dotfiles to
~/dotfiles.bkp/(re-running is safe; it won't clobber the symlinks it already made). -
Symlinks the repo files into
$HOME, and theconfig/subdirs into~/.config($XDG_CONFIG_HOME):Repo path Symlink bashrc~/.bashrcand~/.zshrcaliases~/.aliasesvimrc~/.vimrcscreenrc~/.screenrcgitconfig~/.gitconfigconfig/nvim~/.config/nvimconfig/yazi~/.config/yaziconfig/zellij~/.config/zellijconfig/ghostty~/.config/ghostty -
Prompts for your git name/email (pre-filled from your existing
~/.gitconfig) and writes them to~/.gitconfig.local— so identity is set automatically and never committed. Skipped if~/.gitconfig.localalready exists. -
Offers to install the CLI tools the editor setup uses (Neovim, Zellij, Yazi, fzf, ripgrep, ruff, ty, …) via
install_deps.sh. Skip withSKIP_DEPS=1 ./make_symlinks.sh.
bashrc is the single cross-shell init file, sourced by both bash and zsh.
Nothing machine-specific or secret is committed. bashrc sources
~/.bashrc.local last if it exists — put per-host secrets, env vars, and
private aliases there:
# ~/.bashrc.local (chmod 600; never tracked)
export SOME_API_KEY=...
alias work='...'Git identity lives in ~/.gitconfig.local (written for you by the setup
script, and included by gitconfig):
# ~/.gitconfig.local
[user]
name = Your Name
email = you@example.comIdentity switches automatically based on a repo's remote host — no per-repo
setup. The setup script can wire this up: it writes the work [user] block to
~/.gitconfig-work and appends conditional-include rules to ~/.gitconfig.local
(both untracked, so no host or work details are committed). The rules look like:
# ~/.gitconfig.local (untracked) — replace example.com with your work host
[includeIf "hasconfig:remote.*.url:git@example.com:*/**"]
path = ~/.gitconfig-work
[includeIf "hasconfig:remote.*.url:https://*example.com/**"]
path = ~/.gitconfig-workTwo patterns are needed because git's URL glob treats / as a boundary, so a
single pattern can't match both git@… and https://… forms. Verify on any
repo with git config user.email. Needs git ≥ 2.36 (hasconfig includes;
the setup script warns if yours is older) — and push.autoSetupRemote in the
tracked config needs ≥ 2.37. Older git ignores both silently.
SSH keys: when personal and work live on different hosts, route a separate
key per host in ~/.ssh/config (works for clone, fetch, and push):
Host example.com
IdentityFile ~/.ssh/id_work
IdentitiesOnly yes
Generate keys with ssh-keygen -t ed25519 -f ~/.ssh/id_work and add the public
key to that host. (For two accounts on the same host, use a Host alias and
rewrite the remote to match it.)
Three tools combine into a lightweight, fully-in-terminal IDE that works the same locally and over SSH (designed for Ghostty, but any modern terminal works):
- Zellij — the "window / workbench". One terminal window split into panes (editor + shell + file manager), and — crucially over SSH — it survives disconnects: detach and reattach later with your session intact.
- Neovim + neo-tree — the editor and its Explorer sidebar.
- Yazi — a standalone file manager in its own pane, for filesystem-heavy
work (previews, bulk rename/move/delete) and
cd-ing your shell around.
neo-tree and Yazi don't compete: neo-tree lives inside nvim for opening files into buffers while you edit; Yazi is a separate full-screen file manager you pop over to in another pane.
make_symlinks.sh runs install_deps.sh for you (skip with SKIP_DEPS=1), but
you can also run it standalone any time:
./install_deps.sh # install everything that's missing
DRY_RUN=1 ./install_deps.sh # show what it WOULD do, change nothingIt's idempotent (skips anything already on your PATH) and best-effort
(warns and continues on failure). Homebrew is not required — it detects the
package manager and does the right thing per platform:
- macOS → Homebrew.
- Linux → the native package manager (
apt/dnf/pacman/zypper/apk, withsudowhen needed) forripgrep, a C compiler (for treesitter), and the optional preview tools. - Neovim, fzf, Zellij, Yazi, lazygit, and glow on Linux are installed from
their official prebuilt releases into
~/.local/bin(no root, no compiler) — apt's Neovim (< 0.11, too old for the Python LSP) and fzf (< 0.48, too old for the shell keybindings) lag, and the rest aren't packaged on Debian/Ubuntu — where it also symlinksfdfind→fdandbatcat→batso Yazi's previews find them. - zoxide, ripgrep, bat, and fd get the same treatment when the distro's
version is years old (e.g. Ubuntu 22.04's zoxide 0.4): a current release
goes into
~/.local/bin, whichPATHprefers. ruff+tyalways go throughuv(cross-platform, no root);uvitself is installed if missing.
~/.local/bin is added to your PATH by the shell config, so a freshly
installed Neovim wins over an older system one.
Some of the tools also hook into the shell (wired up by bashrc, only when the
tool is installed):
- fzf —
Ctrl-Rfuzzy history search,Ctrl-Tfuzzy file insert,Alt-Cfuzzy cd. - zoxide — learns directories as you
cd; jump with a fragment (z dotf), orzito pick interactively. Plaincdis untouched. - glow — read markdown in the terminal:
glow README.md(-pto page).
It also installs Hack Nerd Font (so neovim/yazi icons render; the Ghostty config uses it). Note: over SSH the font only matters on the machine running Ghostty (your Mac), not the remote box — there the Mac's Ghostty draws the glyphs.
One thing the script can't do for you:
- On macOS without a C compiler, run
xcode-select --install(for treesitter).
A hand-written, modular config under config/nvim/, managed by
lazy.nvim (auto-installs itself and all
plugins on first launch). Leader is Space.
Plugins are grouped one file per concern under lua/plugins/, and the full
VS Code-like set below ships enabled. To turn any feature off, open its file
and set enabled = false on the spec — it's skipped entirely (not installed,
no keymaps). Flip it back and restart nvim to re-install.
| Plugin | File | Role (VS Code analogue) |
|---|---|---|
| neo-tree | plugins/filetree.lua |
Explorer sidebar |
| yazi.nvim | plugins/filetree.lua |
Floating yazi → open file in editor |
| gitsigns | plugins/git.lua |
Source Control gutter (hunks/blame) |
| diffview | plugins/git.lua |
Side-by-side diff + file history |
| lazygit | plugins/git.lua |
Full git UI (needs lazygit binary) |
| treesitter | plugins/treesitter.lua |
Syntax highlighting |
| fzf-lua | plugins/finder.lua |
Cmd-P + project search |
| LSP (ruff + ty) | plugins/lsp.lua |
IntelliSense for Python |
| blink.cmp | plugins/lsp.lua |
Autocomplete popup |
| vscode.nvim | plugins/ui.lua |
Theme (VS Code Dark Modern look) |
| lualine | plugins/ui.lua |
Status bar |
| indent-blankline | plugins/ui.lua |
Indent guides |
| bufferline | plugins/ui.lua |
Editor tabs |
| which-key | plugins/editor.lua |
Keybinding hints popup |
| autopairs | plugins/editor.lua |
Auto-close brackets |
| vim-sleuth | plugins/editor.lua |
Auto-detect indentation |
| todo-comments | plugins/editor.lua |
Highlight TODO/FIXME |
| trouble | plugins/editor.lua |
Problems / diagnostics panel |
| flash | plugins/editor.lua |
Jump-to-anywhere motion (s) |
| persistence | plugins/editor.lua |
Session restore (per project) |
Disabled by default (flip enabled = true to use): tokyonight (alternative
theme, ui.lua) and mason (LSP-server installer, lsp.lua).
Key mappings (leader = Space):
| Mapping | Action |
|---|---|
<Space>? |
Cheat-sheet overlay (a tab per tool) |
<Space>e |
Toggle file explorer (neo-tree) |
<Space><Space> |
Find files (fuzzy, like Cmd-P) |
<Space>fg |
Grep across the project |
<Space>fw |
Grep word under cursor |
<Space>fb |
Switch buffers |
gd / gr / K |
Go to definition / references / hover |
<Space>rn / <Space>ca |
Rename symbol / code action |
<Space>cf |
Format buffer |
]d / [d |
Next / previous diagnostic |
]c / [c |
Next / previous git hunk |
<Space>hs / hr / hp |
Stage / reset / preview git hunk |
<Space>hb / <Space>tb |
Blame line / toggle inline blame |
<S-l> / <S-h> |
Next / previous buffer (bufferline tab) |
<C-h/j/k/l> |
Move between splits |
s / S |
Flash jump / treesitter jump |
<Space>gg |
LazyGit (full git UI) |
<Space>gd / gh / gq |
Diff view / file history / close |
<Space>xx |
Problems panel — all diagnostics (Trouble) |
]t / [t |
Next / previous TODO comment |
<Space>qs / <Space>ql |
Restore session (this dir / last) |
Python LSP uses the Astral stack (ruff for lint/format, ty for types) —
all Rust, no Node/pyright. install_deps.sh installs both (manually:
uv tool install ruff and uv tool install ty); they auto-detect the active
venv / pyproject.toml. The config uses Neovim's native vim.lsp API (0.11+);
on older nvim the LSP file warns and skips itself.
Clipboard over SSH: yanks route through OSC52 when connected over SSH, so
yy on a remote box lands in your local clipboard. Locally the native
clipboard is used.
Reproducible versions: after the first launch, commit the generated
config/nvim/lazy-lock.json to pin exact plugin versions across machines
(:Lazy restore re-pins to it).
Terminal multiplexer / workspace, configured in config/zellij/. It wraps the
editor in VS Code-like chrome: an integrated terminal pane, project tabs, a
status bar, and session persistence (survives SSH disconnects — detach with
Ctrl o then d, reattach with zellij attach).
-
Theme:
theme "terminal"— zellij's chrome follows Ghostty's ANSI palette (and tracks it if you re-theme Ghostty); nvim colors its own pane. Set e.g.theme "tokyo-night"inconfig.kdlfor a fixed palette instead. -
Layouts (
config/zellij/layouts/), each launchable viazellij --layout NAME:Layout Arrangement Alias default full-screen nvim + a small floating terminal, bottom-right ( Alt+fshows/hides it; pin it withCtrl pito keep it visible while you edit). New tabs (Ctrl+tn) open as a plain shell.zjwide nvim editor left, terminal right zjwshell plain shell, no auto-nvim (quick one-offs) zjsOpen one in a new tab from a running session:
zellij action new-tab --layout wide. -
File manager: browse with yazi inside nvim (
<Space>y) — pick a file and it opens in the editor; close the float to hide it. (A yazi zellij pane can't hand a file to a running nvim without fragile RPC, so it's wired in-editor.) -
Keys: zellij defaults are kept (status bar shows the
Ctrl-prefixes):Ctrl ppanes,Ctrl ttabs,Ctrl sscrollback (eedits it in nvim),Ctrl osession,Ctrl qquit. One exception:Ctrl his unbound so nvim's split navigation (<C-h/j/k/l>) works inside zellij. -
Copy uses OSC52 (works over SSH); on a local Mac set
copy_command "pbcopy".
Terminal file manager, configured in config/yazi/yazi.toml. Launch it with
y (not plain yazi) — the shell wrapper makes your shell cd to wherever
you ended up when you quit. Files open in $EDITOR (nvim). Toggle hidden files
with .. Richer previews depend on the optional tools listed under
Installing the tools.
Terminal config in config/ghostty/config. Sets the font to Hack Nerd Font
Mono (installed by install_deps.sh) so neovim/yazi icons render; Ghostty
also falls back to bundled Nerd Font symbols automatically, so icons work even
before the font is installed. Check what fonts Ghostty sees with
ghostty +list-fonts | grep -i nerd. It also sets macos-option-as-alt so the
Alt keybinds (zellij, fzf) work on any keyboard layout. The file has commented
extras (theme, opacity, padding) to tweak. Read on both macOS and Linux from
~/.config/ghostty/config.
Conda is lazy-loaded: it is not initialized at startup and base is never
auto-activated. The first conda call initializes conda and runs your command,
so conda activate <env> just works on demand. Detection covers
anaconda/miniconda/miniforge/mambaforge, Homebrew Caskroom, and /opt/conda.
CONDA_HOME=/path— point at a non-standard conda prefix.NO_CONDA=1— disable conda integration entirely for that shell.- Recommended once per machine:
conda config --set auto_activate_base false.
Plugins are managed by vim-plug, which
auto-installs itself and the plugin set on first launch. Leader key is Space:
| Mapping | Action |
|---|---|
<Space>n |
Toggle NERDTree |
<Space>m |
Mirror NERDTree |
<Space>o |
Open session |
<Space>t |
New tab |
<Space>e |
Enable mouse |
<Space>d |
Disable mouse |
The old Vim config is kept as-is; Neovim (above) is the primary editor. $EDITOR
prefers nvim and falls back to vim on machines without it — git follows suit
(no hardcoded core.editor).