One Fish-powered installer that bootstraps a complete development environment on macOS, Arch (Omarchy/Hyprland), and Ubuntu.
These are my personal dotfiles. They take a bare machine and turn it into a fully configured development environment: shell, prompt, terminal, editor, language toolchains, and desktop apps. Everything is driven by a single run.fish entry point that detects your OS and runs the right steps. Take and use anything you want.
- Highlights
- Supported Platforms
- Prerequisites
- Quick Start
- How It Works
- What Gets Installed
- Configuration Tour
- Symlink Map
- Desktop Environment (Omarchy/Hyprland)
- Custom Fish Functions
- Environment Variables
- Advanced Usage
- Customization
- Project Structure
- Troubleshooting
- One installer, three operating systems. A single
fish run.fishdetects macOS, Omarchy (Arch/Hyprland), or Ubuntu and runs the matching scripts. Shared steps live inos/common; platform-specific steps live underos/darwin,os/omarchy, andos/ubuntu. - Phase-based and idempotent. Setup runs in
pre,main, andpostphases. Re-running is safe: symlinks useln -sfv, and installers check before reinstalling. - One toolchain manager. Almost every CLI tool and language runtime is pinned in
mise/mise.tomland installed by mise, so the same versions land on every machine. - Catppuccin Mocha everywhere. Kitty, Starship, Neovim, Yazi, and Opencode all share the same palette.
- A custom Nerd Font.
SethensSuperCode.ttfcarries the icon glyphs used across the terminal, prompt, and editor. - Interactive or hands-off. Run the whole thing automatically, or use
--launcherto pick individual steps from a filterable menu.
| Platform | Base requirement |
|---|---|
| macOS (Darwin) | A working macOS install. Homebrew is installed for you if missing. |
| Omarchy (Hyprland) | A base Arch Linux system with Hyprland installed via Omarchy. |
| Ubuntu | Ubuntu 24.10+ with an internet connection. |
| Requirement | Installation |
|---|---|
| Fish Shell | Arch: sudo pacman -S fish macOS: brew install fish Ubuntu: sudo apt install fish |
| Git | Usually pre-installed; otherwise use your package manager. |
# 1. Clone the repository
git clone <your-repo-url> ~/Developer/dotfiles
cd ~/Developer/dotfiles
# 2. Set your git identity (required for commits)
git config user.name "Your Name"
git config user.email "your.email@example.com"
# 3. Run the setup
fish run.fish # Full automated setup
fish run.fish --launcher # Interactive menu (pick individual steps)Flags can be combined:
| Flag | Effect |
|---|---|
-l, --launcher |
Open the interactive gum menu instead of running everything. |
-u, --update |
Run an update pass: the system package manager plus mise upgrade. That means brew update && brew upgrade on macOS and apt-get update && apt-get upgrade on Ubuntu, both followed by mise upgrade; on Omarchy it is omarchy update alone, which upgrades mise tools itself. Without it, tools are only installed when missing, never bumped. |
-r, --reboot |
Reboot after setup completes. |
run.fish is the single entry point. It:
- Detects the OS from
unameand setsSYSTEM_OStodarwin,omarchy, orubuntu. - Sets global paths (
DOTFILES_DIRECTORY,HOME_CONFIG_DIRECTORY, and friends). - Loads Fish functions by adding every
os/<platform>andos/commonsubdirectory tofish_function_path, so eachinstall-*/symlink-*/ helper function becomes callable. - Runs the phases for your platform.
The installer is organized into three phases so that prerequisites are always in place before the things that depend on them:
run.fish
├── Pre Phase (os/common/pre, os/<platform>/pre)
│ ├── Switch the login shell to fish
│ ├── Create directories (~/.config, ~/Developer, ~/.config/mise, ...)
│ ├── Symlink every config file/directory into place
│ ├── Install mise (curl) and add it to PATH for the run
│ ├── mise self-update → only a self-managed mise; a packaged one moves with the OS
│ ├── mise install → installs all tools from mise/mise.toml
│ ├── mise upgrade → only with --update; bumps `latest` specs to newest
│ ├── verify-mise-tools → fails loudly if a requested tool never installed
│ ├── Install herdr agent integrations (claude, opencode)
│ └── Authenticate with GitHub (ssh key check, else `gh auth login`)
├── Main Phase (os/common/main, os/<platform>/main)
│ ├── Install OS packages (brew / pacman / apt / snap / flatpak)
│ ├── Install language servers (via bun)
│ └── Clone repositories (dotfiles, wallpapers)
│ └── Symlink wallpapers into every Omarchy theme (omarchy only)
└── Post Phase (os/common/post)
└── Final configuration
Why this shape? The phase split keeps ordering correct (mise exists before mise install, configs are symlinked before tools read them), and the common vs per-platform split means a tool only needs documenting once while platform quirks stay isolated.
mise/mise.toml is the source of truth for tool versions. mise install reads it and installs everything below.
Omarchy takes
omarchy updaterather than a bareyay. Omarchy 4 ships a pacmanPreTransactionhook,omarchy-update-pacman-guard, that aborts any transaction carrying both-Sand-u, so a direct full upgrade fails withfailed to run transaction hooksand upgrades nothing. Single-package installs are untouched, which is whyyay-install-packagestill works.omarchy updateis also a superset: cache prune, snapshot, keyring refresh, repo upgrade, migrations, thenomarchy-update-aur-pkgs.
On Omarchy the shared
mise upgradeis skipped, becauseomarchy updateends withMISE_MINIMUM_RELEASE_AGE=0 mise upagainst this samemise.tomland runs first. Worth knowing what that costs: mise's 24-hour release cooldown is effectively gone there, since the pass that bypasses it happens before the pass that respects it. That is what makes a publisher who tags a version before uploading its binaries, as HashiCorp did with terraform 1.16.0, fail the whole update.
Note that mise install is not an upgrade: a tool that is already installed satisfies a latest spec indefinitely, so re-running setup will never move it forward. Pass -u / --update (or run mise upgrade yourself) to bump versions. mise outdated shows what is behind.
mise self-update runs before anything installs through mise, but only when mise lives under $HOME. This is not cosmetic: mise's tool registry is compiled into the mise binary, so a mise older than a tool's registry entry cannot resolve that tool by name and mise install fails identically on every run. A mise from a system package manager (omarchy/mise-bin here) is built with self-update compiled out and only prints errors when asked, so those installs move with the platform upgrade instead. verify-mise-tools runs after the install pass and reports anything in mise.toml that never landed, since mise install exits 0 even when a tool is missing.
Languages & runtimes
| Tool | Description |
|---|---|
| bun | JavaScript/TypeScript runtime |
| node | Node.js (LTS) |
| python | Python |
| ruby | Ruby (uses precompiled binaries; compile = false) |
| go | Go toolchain |
| rust | Rust toolchain with cargo |
| java | Java JDK |
| dotnet | .NET SDK |
| zig | Zig compiler |
| clojure | Clojure |
| erlang | Erlang/OTP |
Build & parsing
| Tool | Description |
|---|---|
| cmake | Cross-platform build system |
| tree-sitter | Incremental parser toolkit |
Containers & infrastructure
| Tool | Description |
|---|---|
| docker-cli | Docker CLI |
| docker-compose | Docker Compose |
| kubectl | Kubernetes CLI |
| terraform | Infrastructure as code |
CLI utilities
| Tool | Description |
|---|---|
| fd | Fast file finder |
| fzf | Fuzzy finder |
| ripgrep | Fast line-oriented search |
| jq | JSON processor (required by herdr-start and create-agent-workspace) |
| gum | Pretty interactive shell scripts |
| starship | Shell prompt |
| yazi | Terminal file manager |
| neovim | Modern Vim editor |
| gh | GitHub CLI |
| herdr | Agent session manager (Omarchy ships it too; mise covers macOS and Ubuntu) |
Terminal UIs & AI
| Tool | Description |
|---|---|
| btop | TUI for system resources |
| lazygit | TUI for Git |
| lazydocker | TUI for Docker |
| lazyssh | SSH manager |
| crush | AI coding agent |
| opencode | AI coding assistant |
| claude | Anthropic's official CLI for Claude (registry alias of claude-code; declare only one of the two) |
Installed during the main phase for editor LSP support: bash-language-server, fish-lsp, typescript + typescript-language-server, vscode-langservers-extracted, and yaml-language-server.
Cross-platform apps appear in more than one table on purpose: each OS installs them through its native package manager.
macOS (Homebrew)
CLI (brew) |
GUI (brew --cask) |
|---|---|
| fortune, git, gnupg, nginx | brave-browser, kitty, font-jetbrains-mono, spotify, virtualbox |
Omarchy (pacman / yay)
The MySQL client is
mariadb-clientsrather than a mise tool. mise installs MySQL's official glibc2.28 tarball, which links againstlibncurses.so.6; Arch ships only the wide-charlibncursesw.so.6, so that binary cannot start at all. Ubuntu getsmariadb-clientfor the same reason of consistency, even though the tarball does work there.
brave, vlc, virtualbox, postgresql, mariadb-clients, nginx, ffmpeg, gparted, gpick, font-manager, grub, mdadm, openssh, ca-certificates, curl, fortune-mod, spotify-launcher, discord, kitty, ttf-jetbrains-mono
Kitty is a system package rather than a mise tool on purpose: it is a GUI application with an OpenGL renderer and a desktop entry, none of which mise's backends install. A real package also supplies the terminfo and the icon.
On Omarchy and macOS it comes from the platform package manager. On Ubuntu it comes from upstream's installer into
~/.local/kitty.appinstead of apt, because the apt build lags behind thegoto_sessionandactive_session_namesupport the session config here depends on.
Ubuntu (apt / snap / flatpak)
| Source | Packages |
|---|---|
| apt | brave-browser, vlc, virtualbox, postgresql, nginx, gparted, gpick, font-manager, fonts-jetbrains-mono, autoconf, bison, build-essential, ca-certificates, gnupg, gnome-tweaks, lsb-release, mdadm, ncurses, fortune-mod |
| snap | discord, spotify |
| flatpak | zen-browser, flatpak |
| custom | kitty (upstream installer), White Sur icon theme (git) |
The GitHub CLI (
gh) is installed through mise, not a system package manager, so it is the same version on every platform.
config.fish sets up the interactive shell:
- Exports
DEVELOPER_DIRECTORYandBUN_INSTALL, and puts~/.bun/bin,~/.local/bin, and Homebrew onPATH. - Activates mise when present (
if type -q mise). - For interactive sessions, initializes
starship(guarded bytype -q). - Greeting comes from
fortune.
Minimal, fast prompt using the Catppuccin Mocha palette. Shows user, directory, language versions (c, dotnet, golang, nodejs, python, ruby, rust), and git branch/status.
The primary terminal. Kitty draws the windows and multiplexes panes, tabs and named sessions. It has no server behind the GUI, so sessions do not outlive the kitty process; run tmux inside a tab if you need that.
Keybindings are Kitty's defaults. Everything this repo adds sits on CTRL+SHIFT+ALT+*, so no default is shadowed except shift+insert, which is deliberately the clipboard rather than the selection. CTRL+SHIFT+SPACE searches tabs by name, CTRL+SHIFT+ALT+O lists the sessions, CTRL+SHIFT+ALT+1..4 jump to one directly. Run kitty +list-keybinds for the rest.
Almost all of it is shared across every OS. Only listen_on and the window-decoration handling differ, and those live in one small per-platform file.
kitty/kitty.conf: fonts, the pinned Catppuccin Mocha palette, chrome and keys. Ends withinclude os-local.conf.kitty/os/linux.conf,kitty/os/darwin.conf: the per-platform handful.symlink-kitty-config-fileslinks one of them to~/.config/kitty/os-local.confbased on$SYSTEM_OS, because Kitty has no conditional include. macOS has noXDG_RUNTIME_DIR, which is why the control socket path is not in the shared file.kitty/tab_bar.py:tab_bar_style custom. Bar at the bottom, tabs centered, session name pinned right. The centering is done here rather than withtab_bar_align center: Kitty centers inalign_with_factor(), which runs after every tab is drawn and shifts the line withinsert_characters(), which would carry a right-pinned badge off the edge.kitty/sessions/*.kitty-session: one per project.dotfilesgetsnvim,lazydocker, anopencodeagent withlazygitsplit in beside it, and a shell;gemadds astacktab that brings the compose stack up and tails it;mainholds what belongs to no project (shell,herdr,yazi,btop). Sessions deliberately do not open their own OS window; they share one, andtab_bar_filter(see above) hides the tabs of whichever session is not active, so switching swaps the tab bar in place. Paths use$DEVELOPER_DIRECTORY, which session files expand, so they work unchanged on any machine. Each command runs through a loginfishso mise is onPATH, and usesexecso the program becomes the window's own process and names the tab; the two that must not (a fish function, and the multi-statementstackcommand) deliberately skip it. The agents are plain panes rather than herdr agents, so each is rooted in its own codebase; herdr is one global session with one shared agent list and no project in it, which is also whyherdris only inmain.kitty-restartkills kitty so the sessions rebuild.kitty/tab-search.sh:CTRL+SHIFT+SPACE. Kitty's ownselect_tabrenders a numbered list through the hints kitten rather than a filter, so this pipeskitty @ lsthrough fzf and focuses the result. Searches every session, not just the current window.xdg/xdg-terminals.list: names Kitty as the terminalxdg-terminal-execshould pick, which is what Omarchy'sSUPER+RETURNand its launcher call. Without it the choice among installedTerminalEmulatorentries is unspecified. It sits outsidekitty/because it is a system-level choice of terminal rather than Kitty configuration.
Config in yazi/:
yazi.toml: manager settings (permission line mode, show hidden, show symlinks)theme.toml: selects thecatppuccin-mochaflavor and defines a large icon table (per-extension glyphs and colors)flavors/catppuccin-mocha.yazi/: the installed flavor package:flavor.toml(UI colors) andtmtheme.xml(syntax highlighting for the preview pane)
A full Lua configuration under nvim/lua/sethen/ using lazy.nvim:
- Core (
core/): options, keymaps, LSP setup, autocommands, constants. - Plugins (
plugins/): one file per plugin area.
Highlights: catppuccin theme, lualine, nvim-tree, telescope (+ fzf-native), treesitter (pinned to master; main is an incompatible rewrite), blink-cmp completion, mason, gitsigns, oil, which-key, and todo-comments. The agents run as their own kitty tabs rather than as nvim plugins, so there is no copilot or opencode integration in here.
On first launch, Mason installs language servers including: bash-language-server, dockerfile-language-server, gopls, html/css, json-lsp, lua-lsp, pyright, ruby-lsp, rust-analyzer, sqlls, tailwindcss-language-server, typescript-language-server, and yaml-language-server.
AI coding assistant config in opencode/:
| Setting | Value |
|---|---|
| Theme | catppuccin-mocha |
| Model | opencode/big-pickle |
| Auto-update | enabled |
opencode/opencode.json and opencode/themes/ are symlinked into ~/.config/opencode/. See opencode.ai.
Agent multiplexer config in herdr/:
| Setting | Value |
|---|---|
| Theme | catppuccin-mocha |
| Shell | fish |
| Prefix | ctrl+b |
| Sidebar | agent state, workspace, tab |
| Notifications | system toast |
herdr/config.toml is symlinked into ~/.config/herdr/. Herdr runs AI coding agents (Claude Code, OpenCode, etc.) in persistent panes with state tracking. The claude and opencode integrations are installed automatically by install-herdr-integrations during setup. They are what report agent state back to herdr; without them the sidebar state columns stay empty and herdr agent wait --status idle never resolves. The claude integration writes a hook to ~/.claude/hooks/ and registers a SessionStart entry in ~/.claude/settings.json. Check with herdr integration status. See herdr.dev.
| Font | Description |
|---|---|
SethensSuperCode.ttf |
Custom Nerd Font-style font with icon glyphs (U+F000-U+F1B2) |
Installed to ~/.local/share/fonts/ (Omarchy/Ubuntu) or ~/Library/Fonts/ (macOS), and used by Kitty, Starship, and Neovim for symbols.
Config lives in this repo and is symlinked into place, so edits here are live everywhere.
| Source | Destination | Platforms |
|---|---|---|
config.fish |
~/.config/fish/config.fish |
all |
fish/functions/ |
~/.config/fish/functions/ |
all |
nvim/ |
~/.config/nvim/ |
all |
starship/starship.toml |
~/.config/starship.toml |
all |
kitty/kitty.conf |
~/.config/kitty/kitty.conf |
all |
kitty/tab_bar.py |
~/.config/kitty/tab_bar.py |
all |
kitty/tab-search.sh |
~/.config/kitty/tab-search.sh |
all |
kitty/sessions/ |
~/.config/kitty/sessions/ |
all |
kitty/os/{linux,darwin}.conf |
~/.config/kitty/os-local.conf |
all |
xdg/xdg-terminals.list |
~/.config/xdg-terminals.list |
Omarchy |
yazi/ |
~/.config/yazi/ |
all |
opencode/opencode.json |
~/.config/opencode/opencode.json |
all |
opencode/themes/ |
~/.config/opencode/themes/ |
all |
herdr/config.toml |
~/.config/herdr/config.toml |
all |
mise/mise.toml |
~/.config/mise/mise.toml |
all |
mise/.default-gems |
~/.default-gems |
all |
.gitconfig |
~/.gitconfig |
all |
.gitignore_global |
~/.gitignore_global |
all |
hypr/monitors.lua |
~/.config/hypr/monitors.lua |
Omarchy |
hypr/omarchy-launch-screensaver |
~/.local/bin/omarchy-launch-screensaver |
Omarchy |
quickshell/shell.json |
~/.config/omarchy/shell.json |
Omarchy |
quickshell/shell.toml |
~/.config/omarchy/shell.toml |
Omarchy |
quickshell/bar/ |
~/.config/omarchy/bar/ |
Omarchy |
On Omarchy, the base Wayland desktop is provided by Omarchy; these dotfiles layer config on top:
- Hyprland: monitor layout in
hypr/monitors.lua. - Omarchy shell: bar layout and idle timings in
quickshell/shell.json, theme overrides inquickshell/shell.toml, custom bar widgets inquickshell/bar/modules/. - Screensaver:
hypr/omarchy-launch-screensaver, a PATH shim covered below.
Hyprland keybindings, window rules, and animations are managed by Omarchy itself, as are the stock bar widgets. This repo only owns the monitor config, the bar layout, the shell color overrides, and the per-app theming above.
Omarchy 4 replaced the omarchy 3 desktop wholesale with a single long-running Quickshell process. Four things this repo used to own moved or disappeared:
| Omarchy 3 | Omarchy 4 |
|---|---|
waybar/config.jsonc, waybar/style.css |
quickshell/shell.json (layout), quickshell/shell.toml (colors) |
hypr/hypridle.conf |
idle.screensaver / idle.lock in quickshell/shell.json |
hypr/hyprlock.conf |
the omarchy.lock Quickshell plugin, which takes no config |
hypr/monitors.conf |
hypr/monitors.lua |
The waybar, hyprlock, and hypridle packages are no longer installed. The monitor change is the one that fails quietly: Hyprland 0.56 reads Lua (hyprctl systeminfo reports configProvider: lua), so a leftover monitors.conf is simply never loaded and the display silently falls back to its preferred mode.
Bar font size. Waybar ran 16px text. The shell derives every surface from one rem root ([font] base-size in quickshell/shell.toml), so raising it scales panels, notifications, and the menu too, and grows the bar past its stock 26px height. Set to 16 to match waybar; drop to 12 for omarchy's intended proportions.
The bar uses omarchy's own icons, untouched. Its widgets draw them as literals in QML
and expose no icon setting, so changing one means cloning the widget into
~/.config/omarchy/plugins/ and owning a copy of its source — which then stops tracking
upstream. That was tried and reverted: the maintenance is not worth it for a different
glyph, and a font carrying both icon sets shadowed the nerd font ranges neovim's devicons
use, changing every file icon in the editor.
assets/fonts/SethensSuperCode.ttf is nonicons (MIT,
© ya2s), covering f000-f1b2. kitty/kitty.conf maps that range to it with
JetBrainsMono Nerd Font Mono in the fallback chain, so it answers for that range and
nothing else — anything wider and it starts answering for glyphs the nerd font owns.
quickshell/bar/modules/cpu.qml is the one custom widget. Omarchy ships no cpu module, so
there is nothing to clone; it is ours outright and needs no patching.
dot-launcher (run via fish run.fish --launcher) uses gum to present a filterable list of every available function, so you can run individual steps instead of the full install.
Set in config.fish:
| Variable | Default | Description |
|---|---|---|
DEVELOPER_DIRECTORY |
$HOME/Developer |
Working directory for projects |
BUN_INSTALL |
$HOME/.bun |
Bun installation directory |
Set in run.fish during setup: SYSTEM_OS, DOTFILES_DIRECTORY, DOTFILES_OS_DISTRO_DIRECTORY, DOTFILES_OS_COMMON_DIRECTORY, HOME_CONFIG_DIRECTORY, HOME_FISH_DIRECTORY, plus RUN_DOTFILES_REBOOT / RUN_DOTFILES_UPDATE when the matching flags are passed.
fish -c "source run.fish; run-darwin-pre" # or run-omarchy-pre / run-ubuntu-pre
fish -c "source run.fish; run-darwin-main" # or run-omarchy-main / run-ubuntu-main
fish -c "source run.fish; run-common-post"fish run.fish --launcher # pick from the menu
fish -c "source run.fish; install-kitty" # or call directlyfish run.fish --update # update pass
fish run.fish --reboot # reboot when doneEdit mise/mise.toml:
[tools]
your-tool = "latest" # or a specific versionThen run mise install.
Add a file (or entry) under nvim/lua/sethen/plugins/:
return {
"owner/repo",
event = "VeryLazy",
config = function()
-- your config
end,
}- Omarchy: add an
install-*function and call it inos/omarchy/main/run-omarchy-main.fish. - Ubuntu: add it to
os/ubuntu/main/run-ubuntu-main.fish(apt, snap, or flatpak). - macOS: add it to
os/darwin/main/run-darwin-main.fish.
dotfiles/
├── run.fish # Main entry point
├── config.fish # Fish shell configuration
├── .gitconfig # Git configuration
├── .gitignore_global # Global gitignore
├── AGENTS.md # Agent coding guidelines
├── fish/
│ └── functions/ # Shared Fish functions
├── os/
│ ├── common/ # Cross-platform steps
│ │ ├── pre/ main/ post/ # Phase scripts
│ │ └── utilities/ # Shared helpers (dot-launcher)
│ ├── darwin/ # macOS (pre, main, utilities)
│ ├── omarchy/ # Arch/Hyprland (pre, main, utilities)
│ └── ubuntu/ # Ubuntu (pre, main, utilities)
├── mise/
│ ├── mise.toml # Tool versions (source of truth)
│ └── .default-gems # Default Ruby gems
├── nvim/
│ └── lua/sethen/
│ ├── core/ # Options, keymaps, LSP, autocmds
│ ├── plugins/ # Plugin configs
│ └── lazy.lua # lazy.nvim bootstrap
├── opencode/
│ ├── opencode.json # Opencode config
│ └── themes/ # Opencode themes
├── herdr/
│ └── config.toml # Herdr agent multiplexer config
├── starship/
│ └── starship.toml # Prompt configuration
├── kitty/
│ ├── kitty.conf # Shared terminal config
│ ├── os/ # linux.conf, darwin.conf (one is os-local.conf)
│ ├── tab_bar.py # Custom status bar
│ ├── tab-search.sh # fzf tab picker
│ ├── sessions/ # main, dotfiles, gem (work sessions are gitignored)
│ └── local/ # gitignored: machine-local overrides
├── xdg/
│ └── xdg-terminals.list # xdg-terminal-exec preference order
├── yazi/
│ ├── yazi.toml # Manager settings
│ ├── theme.toml # Flavor selection + icon table
│ └── flavors/ # Installed flavor package(s)
├── hypr/
│ ├── monitors.lua # Monitor configuration (Omarchy)
├── quickshell/
│ ├── shell.json # Bar layout + idle timings (Omarchy)
│ ├── shell.toml # Shell color overrides (Omarchy)
│ └── bar/modules/ # cpu.qml, the one custom bar widget (Omarchy)
└── assets/
├── fonts/ # SethensSuperCode.ttf
├── images/ # Screenshots
└── videos/ # Demos
Symlink already exists. Steps are idempotent and overwrite their own symlinks. To force a clean target, delete it first.
mise not found after install. Open a new shell so config.fish runs, or confirm ~/.local/bin is on PATH. config.fish only activates mise when it is present.
Neovim plugins not loading. Run :Lazy sync.
Language servers not starting. Check Mason with :Mason, and ensure the servers installed on first launch.
fish -n run.fish # syntax-check the installer
nvim --headless -c "lua require('sethen')" -c "qa" # Neovim loads cleanly
mise doctor # mise health
mise ls # installed tools
starship config validate # prompt configMade with care by Sethen