aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.chezmoiignore1
-rw-r--r--dot_config/zsh/dot_zshrc31
-rw-r--r--remote-dev/README.md69
-rwxr-xr-xremote-dev/bootstrap.sh82
-rw-r--r--remote-dev/flake.lock49
-rw-r--r--remote-dev/flake.nix28
-rw-r--r--remote-dev/home.nix106
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;
+}