diff options
| -rw-r--r-- | .chezmoiignore | 1 | ||||
| -rw-r--r-- | dot_config/zsh/dot_zshrc | 31 | ||||
| -rw-r--r-- | remote-dev/README.md | 69 | ||||
| -rwxr-xr-x | remote-dev/bootstrap.sh | 82 | ||||
| -rw-r--r-- | remote-dev/flake.lock | 49 | ||||
| -rw-r--r-- | remote-dev/flake.nix | 28 | ||||
| -rw-r--r-- | remote-dev/home.nix | 106 |
7 files changed, 362 insertions, 4 deletions
diff --git a/.chezmoiignore b/.chezmoiignore index 7ccf1be..acca5f0 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -6,6 +6,7 @@ systemd-units/ etc/ firefox/ thunderbird/ +remote-dev/ justfile just-lib.sh selene.toml diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc index 552f9eb..7a9538d 100644 --- a/dot_config/zsh/dot_zshrc +++ b/dot_config/zsh/dot_zshrc @@ -31,7 +31,13 @@ bindkey -e # ── Prompt ──────────────────────────────────────────────────────────────────── autoload -Uz colors && colors -source /usr/share/git/completion/git-prompt.sh +# git-prompt.sh: distro-managed on Arch, nix-store-managed on the VM +for _f in \ + /usr/share/git/completion/git-prompt.sh \ + $HOME/.nix-profile/share/git/contrib/completion/git-prompt.sh; do + [[ -r $_f ]] && { source "$_f"; break; } +done +unset _f PROMPT='%B%{$fg[green]%}%n%{$reset_color%}@%{$fg[cyan]%}%m%{$reset_color%}:%b%{$fg[yellow]%}%~%{$reset_color%}$(__git_ps1 " (%s)")%(?..[%{$fg[red]%}%?%{$reset_color%}]) %(!.#.>) ' # ── Completion ──────────────────────────────────────────────────────────────── @@ -375,15 +381,32 @@ _fzf_compgen_path() { fd --hidden --follow --exclude ".git" . "$1" } _fzf_compgen_dir() { fd --type d --hidden --follow --exclude ".git" . "$1" } # ── Plugins (must be sourced last) ──────────────────────────────────────────── +# Plugin locations: Arch system path first, ~/.nix-profile fallback for VM HM. +_source_first() { + local f + for f in "$@"; do + [[ -r $f ]] && { source "$f"; return 0; } + done + return 1 +} + # Highlight config must be set BEFORE sourcing the plugin ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern) typeset -A ZSH_HIGHLIGHT_STYLES ZSH_HIGHLIGHT_STYLES[comment]='fg=yellow' -source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh \ + $HOME/.nix-profile/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh -source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh \ + $HOME/.nix-profile/share/zsh-autosuggestions/zsh-autosuggestions.zsh bindkey '^[[Z' autosuggest-accept # Shift-Tab to accept suggestion -source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh +_source_first \ + /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh \ + $HOME/.nix-profile/share/zsh-history-substring-search/zsh-history-substring-search.zsh [[ -n "${key[Up]}" ]] && bindkey -- "${key[Up]}" history-substring-search-up [[ -n "${key[Down]}" ]] && bindkey -- "${key[Down]}" history-substring-search-down + +unfunction _source_first diff --git a/remote-dev/README.md b/remote-dev/README.md new file mode 100644 index 0000000..a274d19 --- /dev/null +++ b/remote-dev/README.md @@ -0,0 +1,69 @@ +# remote-dev + +Headless dev environment for an Ubuntu 22.04 VM I SSH into. Deployed with +Nix + Home-Manager. Shares the host's neovim, zellij, and zsh configs from +the same repo — no duplication. + +## Bootstrap + +On a fresh VM, as the dev user (must have sudo): + +```sh +curl -fsSL https://raw.githubusercontent.com/ruifm/dotfiles/master/remote-dev/bootstrap.sh | sh +``` + +Then log out and back in. Run `nvim` once to let it fetch plugins from +GitHub on first launch. + +## What it does + +1. Installs Nix (Determinate Systems multi-user installer). +2. Clones this repo to `~/.local/share/dotfiles`. +3. Runs `home-manager switch --flake .../remote-dev#vm`, which: + - Installs the CLI tool subset (see `home.nix`). + - Symlinks `~/.config/{nvim,zellij,zsh,direnv,ghostty}` at the cloned + working tree via `mkOutOfStoreSymlink`, so `git pull` is enough to + pick up config edits — no rebuild needed for config-only changes. + - Sets `ZDOTDIR=$HOME/.config/zsh` so the shared zshrc/zprofile load. +4. Appends the nix-store zsh to `/etc/shells` and `chsh`'s to it. + +## Updating + +```sh +cd ~/.local/share/dotfiles +git pull +nix run home-manager/master -- switch --flake ./remote-dev#vm +``` + +## Adding a tool + +Edit `home.nix`, add to `home.packages`, then `home-manager switch`. + +## Caveats + +- **GPG / pass**: HM installs `gnupg` and `pass` but does _not_ import any + private key. Bring your key separately if you need signed commits or + `pass`-backed env vars on the VM. +- **Disk usage**: Nix store + nvim plugins consumes ~3-5 GB. Check the + VM's root partition size first. +- **Network for first nvim launch**: `vim.pack.add` fetches plugins from + GitHub on first start. +- **Ubuntu apt collisions**: Nix-installed binaries appear first in PATH. + If you need a specific apt-version of something, install it manually + and prefix with the full path. + +## How it's wired + +`home.nix` uses `config.lib.file.mkOutOfStoreSymlink` so the symlinks +point at the **live working tree** at `~/.local/share/dotfiles/...`, not +at copies in `/nix/store`. This means: + +- Editing `dot_config/nvim/init.lua` in the cloned repo takes effect on + the next `nvim` launch with no rebuild. +- `home-manager switch` only needs to re-run when adding/removing a + package or changing what's symlinked. + +The zsh plugins (`zsh-syntax-highlighting`, etc.) live in +`$HOME/.nix-profile/share/`. The shared `dot_zshrc` probes Arch system +paths first, then falls back to the nix-profile path, so the same file +works on both host and VM unchanged. diff --git a/remote-dev/bootstrap.sh b/remote-dev/bootstrap.sh new file mode 100755 index 0000000..c1e95df --- /dev/null +++ b/remote-dev/bootstrap.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env sh +# Bootstrap a headless dev environment on a fresh Ubuntu 22.04 VM. +# Idempotent: safe to re-run. +# +# curl -fsSL https://raw.githubusercontent.com/<user>/dotfiles/master/remote-dev/bootstrap.sh | sh +# +# Steps: +# 1. Install Nix (Determinate Systems installer, multi-user). +# 2. Clone (or fast-forward) the dotfiles repo to ~/.local/share/dotfiles. +# 3. Run `home-manager switch --flake .../remote-dev#vm`. +# 4. Add Nix-store zsh to /etc/shells and chsh the user. +# +# Environment overrides: +# DOTFILES_REPO Git URL (default: https://github.com/ruifm/dotfiles) +# DOTFILES_REF Branch/tag/sha (default: master) +# DOTFILES_DIR Checkout path (default: $HOME/.local/share/dotfiles) + +set -eu + +REPO="${DOTFILES_REPO:-https://github.com/ruifm/dotfiles}" +REF="${DOTFILES_REF:-master}" +DIR="${DOTFILES_DIR:-$HOME/.local/share/dotfiles}" + +log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; } + +# ── 1. Nix ──────────────────────────────────────────────────────────────────── +if ! command -v nix >/dev/null 2>&1; then + log "Installing Nix (Determinate Systems installer)…" + curl --proto '=https' --tlsv1.2 -sSf -L \ + https://install.determinate.systems/nix | + sh -s -- install linux --no-confirm +else + log "Nix already installed, skipping installer." +fi + +# Source nix env for the rest of this script (installer writes +# /etc/profile.d/nix.sh but the current shell hasn't sourced it). +if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then + # shellcheck disable=SC1091 + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh +fi + +# ── 2. Repo checkout ───────────────────────────────────────────────────────── +if ! command -v git >/dev/null 2>&1; then + log "Bootstrapping git via nix profile…" + nix profile install nixpkgs#git +fi + +if [ -d "$DIR/.git" ]; then + log "Updating existing checkout at $DIR…" + git -C "$DIR" fetch origin "$REF" + git -C "$DIR" checkout "$REF" + git -C "$DIR" pull --ff-only +else + log "Cloning $REPO ($REF) → $DIR…" + mkdir -p "$(dirname "$DIR")" + git clone --branch "$REF" "$REPO" "$DIR" +fi + +# ── 3. Home-Manager switch ─────────────────────────────────────────────────── +log "Running home-manager switch (this can take a while on first run)…" +nix --extra-experimental-features 'nix-command flakes' \ + run home-manager/master -- \ + switch --impure --flake "$DIR/remote-dev#vm" -b backup + +# ── 4. chsh to nix-store zsh ───────────────────────────────────────────────── +NIX_ZSH="$HOME/.nix-profile/bin/zsh" +if [ -x "$NIX_ZSH" ]; then + if ! grep -qxF "$NIX_ZSH" /etc/shells 2>/dev/null; then + log "Appending $NIX_ZSH to /etc/shells (requires sudo)…" + echo "$NIX_ZSH" | sudo tee -a /etc/shells >/dev/null + fi + current_shell="$(getent passwd "$USER" | cut -d: -f7)" + if [ "$current_shell" != "$NIX_ZSH" ]; then + log "Changing login shell to $NIX_ZSH (requires sudo)…" + sudo chsh -s "$NIX_ZSH" "$USER" + fi +fi + +log "Done. Log out and back in for the new shell to take effect." +log "Then run 'nvim' once to let it fetch plugins on first launch." diff --git a/remote-dev/flake.lock b/remote-dev/flake.lock new file mode 100644 index 0000000..349d5fd --- /dev/null +++ b/remote-dev/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778628724, + "narHash": "sha256-VNG6hJ146VEenXcDrB3t6MVnrMx+gtyCWTCDkzOp9Qs=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "6a0bbd6b4720da1c9ce7ebf35ff5c41a82db367a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "master", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/remote-dev/flake.nix b/remote-dev/flake.nix new file mode 100644 index 0000000..6622a72 --- /dev/null +++ b/remote-dev/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Headless dev environment for remote Ubuntu VMs."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager/master"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, home-manager, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; config.allowUnfree = false; }; + in + { + homeConfigurations.vm = home-manager.lib.homeManagerConfiguration { + inherit pkgs; + modules = [ ./home.nix ]; + # Path to the cloned dotfiles checkout — passed in so home.nix can + # symlink shared configs (nvim, zellij, zsh) from the same repo. + extraSpecialArgs = { + dotfilesRoot = ../.; + }; + }; + }; +} diff --git a/remote-dev/home.nix b/remote-dev/home.nix new file mode 100644 index 0000000..a2b9392 --- /dev/null +++ b/remote-dev/home.nix @@ -0,0 +1,106 @@ +{ config, pkgs, lib, dotfilesRoot, ... }: + +let + # The dotfiles checkout is cloned to ~/.local/share/dotfiles by bootstrap.sh. + # We do NOT use `dotfilesRoot` as a Nix store path because that would copy + # the entire repo into the store on every rebuild. Instead, we symlink + # config dirs at runtime via `config.lib.file.mkOutOfStoreSymlink`, which + # points at the live working tree so edits take effect without a rebuild. + dotfiles = "${config.home.homeDirectory}/.local/share/dotfiles"; + link = path: config.lib.file.mkOutOfStoreSymlink "${dotfiles}/${path}"; +in +{ + home.username = builtins.getEnv "USER"; + home.homeDirectory = builtins.getEnv "HOME"; + home.stateVersion = "25.05"; + + # ── Packages ──────────────────────────────────────────────────────────────── + # Mirrors the dev-tool subset of `meta/base.txt` on the Arch host. Tools that + # only make sense on a workstation (procs/gdu/duf for sysadmin, lazygit + # unused, node/yarn only needed for markdown-preview on GUI) are excluded. + home.packages = with pkgs; [ + # Editor + multiplexer + neovim + zellij + tree-sitter + + # Search / move + ripgrep + fd + fzf + sd + choose + + # Viewers + bat + lsd + glow + + # Git stack + git + gh + delta + + # JSON / YAML + jq + yq-go + + # System + htop + fastfetch + + # Net + curl + curlie + wget + dog + rsync + openssh + + # Docs + tldr + man-db + man-pages + + # Secrets (user can bring their key separately) + gnupg + pass + + # Zsh and plugins (sourced from $HOME/.nix-profile/share/... by the shared zshrc) + zsh + zsh-syntax-highlighting + zsh-autosuggestions + zsh-history-substring-search + ]; + + # ── direnv + nix-direnv ───────────────────────────────────────────────────── + programs.direnv = { + enable = true; + nix-direnv.enable = true; + enableZshIntegration = false; # zshrc already calls `eval "$(direnv hook zsh)"` + }; + + # ── Shared config symlinks ────────────────────────────────────────────────── + # Live symlinks back into the cloned working tree so `git pull` is enough + # to update configs — no `home-manager switch` required after every edit. + xdg.configFile = { + "nvim".source = link "dot_config/nvim"; + "zellij".source = link "dot_config/zellij"; + "zsh/.zshrc".source = link "dot_config/zsh/dot_zshrc"; + "zsh/.zprofile".source = link "dot_config/zsh/dot_zprofile"; + "ghostty".source = link "dot_config/ghostty"; # for terminfo refs only + "direnv/direnvrc".source = link "dot_config/direnv/direnvrc"; + }; + + # ZDOTDIR redirect so login shells find ~/.config/zsh/.zprofile etc. + home.file.".zshenv".text = '' + export ZDOTDIR="$HOME/.config/zsh" + [[ -r "$ZDOTDIR/.zshenv" ]] && source "$ZDOTDIR/.zshenv" + ''; + + # ── XDG base dirs (Ubuntu doesn't set these in /etc/profile.d by default) ── + xdg.enable = true; + + # ── Enable HM-managed activation messages ────────────────────────────────── + programs.home-manager.enable = true; +} |
