diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3ae2508 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "themes/catppuccin-iterm"] + path = themes/catppuccin-iterm + url = https://github.com/catppuccin/iterm diff --git a/README.md b/README.md index cdd0967..048411a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,61 @@ -# Dotfiles +# dotfiles -Filesystem overlay. Structure mirrors actual paths on disk. +Centralized database of how my machines should be set up. All Macs are identical to each other, all Arch machines are identical to each other. OS is the only axis of variation. ``` dotfiles/ - home/benk/ -> symlinked into $HOME - .config/nvim/ - .config/fish/ - .config/wezterm/ - .tmux.conf - .vimrc - ... - etc/ -> copied into /etc (prompted, needs sudo) - fstab - nginx/nginx.conf - packages/ -> package lists for brew/yay - bootstrap.sh -> does everything + home/benk/ mirrors $HOME — files here get synced to the machine + etc/ mirrors /etc — applied with sudo on Linux + packages/ + brew.txt Homebrew formulae (all Macs) + brew-casks.txt Homebrew casks (all Macs) + arch.txt yay packages (all Arch machines) + themes/ + catppuccin-iterm/ submodule: catppuccin/iterm + setup.toml declarative tooling config (fish + tmux plugins) + bootstrap.py the manager — run this ``` -## Usage +## setup -``` -git clone git@github.com:benkyd/dotfiles.git ~/dotfiles +```bash +git clone --recurse-submodules git@github.com:benkyd/dotfiles.git ~/dotfiles cd ~/dotfiles -./bootstrap.sh +python3 bootstrap.py ``` -The bootstrap will: -1. Detect OS (macOS / Arch) -2. Optionally install packages (Homebrew or yay) -3. Install oh-my-fish, fisher, tmux plugin manager -4. Symlink `home/benk/` into `$HOME` -5. Optionally copy `etc/` to `/etc` (prompted) +Dependencies (`rich`, `questionary`) are auto-installed on first run. -Existing files are backed up to `~/dotfiles.bak/`. +## usage + +Running `python3 bootstrap.py` drops you into a TUI. Pick a mode: + +- **Set up this machine** — install packages, shell tooling, sync dotfiles. use this on a fresh machine. +- **Apply dotfiles** — push repo → `$HOME`. skips packages. +- **Save changes** — pull edits you made on this machine back into the repo. +- **Check status** — see what's drifted. nothing is changed. + +Or pass a mode directly to skip the TUI: + +```bash +python3 bootstrap.py install +python3 bootstrap.py copy +python3 bootstrap.py pull +python3 bootstrap.py status +python3 bootstrap.py add ~/.config/something.conf # start tracking a new file +``` + +## per-file variation + +If you've edited something on a work machine and don't want to clobber it or pull it back, the sync steps show a per-file checkbox for any file that's newer on the machine than in the repo. just uncheck what you want to skip. + +## OS-specific paths + +Some paths only make sense on one OS and are excluded automatically: + +- `Library/` — macOS only (iTerm2 prefs, etc.) +- `.config/wezterm/` — Linux only (iTerm2 is used on Mac) + +## themes + +Catppuccin Mocha is the colour scheme. The iTerm2 colour preset lives at `themes/catppuccin-iterm/colors/catppuccin-mocha.itermcolors`. Import it via iTerm2 → Profiles → Colors → Color Presets → Import. diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 0000000..52458b6 --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,1068 @@ +#!/usr/bin/env python3 +""" +bootstrap.py — Dotfiles manager for Ben's machines. + +OVERVIEW +-------- +This script is the single entry point for setting up and maintaining any of +Ben's machines (macOS or Arch Linux). It handles three concerns: + + 1. Packages — installing formulae/casks (brew) or AUR packages (yay) + 2. Tooling — shell plugins, tmux plugins, etc. declared in setup.toml + 3. Dotfiles — rsyncing the overlay directory (home//) into $HOME, + and optionally /etc on Linux + +DESIGN DECISIONS +---------------- +- OS is the only variation axis. All Macs are identical; all Arch machines + are identical. There are no per-hostname overrides. +- The repo's overlay directory IS the manifest. Any file placed under + home// will be synced to $HOME. Use `add` to track new files. +- Backups are always taken before overwriting ($HOME/dotfiles.bak/). +- When syncing, files that are NEWER at the destination (system) than in the + repo are flagged for per-file review rather than silently overwritten. + This protects edits made on a work machine that haven't been pulled yet. +- setup.toml holds declarative tooling config (fish/tmux plugins). + Packages stay in packages/*.txt so they're easy to grep and diff. + +USAGE +----- + python3 bootstrap.py → interactive TUI + python3 bootstrap.py install → full setup (non-interactive) + python3 bootstrap.py copy → sync dotfiles only + python3 bootstrap.py pull → save local edits back to repo + python3 bootstrap.py status → check drift, no changes made + python3 bootstrap.py add → start tracking a new file + +STRUCTURE +--------- + packages/brew.txt formulae for all Macs + packages/brew-casks.txt cask apps for all Macs + packages/arch.txt packages for all Arch machines + setup.toml fish/tmux plugin declarations + home// dotfile overlay (mirrors $HOME structure) + etc/ optional /etc overlay (applied with sudo) + themes/ colour schemes (catppuccin submodule etc.) +""" +from __future__ import annotations + +import os, sys, subprocess, shutil, platform +from pathlib import Path + +# ── dependency bootstrap ────────────────────────────────────────────────────── +# This block runs before any third-party imports so the script is self-contained +# on a fresh machine. It installs rich, questionary, and (on Python < 3.11) tomli, +# then re-execs the current process so the new packages are on sys.path. +# We cannot simply importlib.reload after pip because sys.path is already frozen. + +def _ensure_deps() -> None: + """Install TUI/TOML deps if missing, then re-exec so they're importable.""" + import importlib.util + needed = ["rich", "questionary"] + if sys.version_info < (3, 11): + # tomllib was added to stdlib in 3.11; older Pythons need the backport + needed.append("tomli") + missing = [p for p in needed if not importlib.util.find_spec(p)] + if not missing: + return + print(f"[bootstrap] Installing missing deps: {', '.join(missing)} ...") + try: + # --user avoids needing root; --quiet suppresses pip's noisy output + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "--quiet", "--user"] + missing, + stderr=subprocess.DEVNULL, + ) + except subprocess.CalledProcessError: + # PEP 668 (Arch, Ubuntu 23+) marks the system Python as externally + # managed, requiring --break-system-packages for user-level installs + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "--quiet", + "--break-system-packages", "--user"] + missing + ) + # Re-exec this process with the same args; now deps will be importable + os.execv(sys.executable, [sys.executable] + sys.argv) + +_ensure_deps() + +# tomllib is stdlib on 3.11+; fall back to the tomli backport below that +try: + import tomllib +except ImportError: + import tomli as tomllib # type: ignore + +from dataclasses import dataclass +from typing import Callable +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +import questionary + +console = Console() + +# ── paths ───────────────────────────────────────────────────────────────────── +DOTFILES_DIR = Path(__file__).parent.resolve() +BACKUP_DIR = Path.home() / "dotfiles.bak" # originals go here before overwrite +USERNAME = os.environ.get("USER", os.environ.get("LOGNAME", "benk")) + +# ── setup.toml ──────────────────────────────────────────────────────────────── +# setup.toml holds tooling that doesn't belong in a plain text package list: +# fish plugins, tmux plugins, and future post-install hooks. +# Packages stay in packages/*.txt so they remain easy to diff and grep. + +def _load_setup() -> dict: + """Load setup.toml from the repo root. Returns {} if the file doesn't exist.""" + p = DOTFILES_DIR / "setup.toml" + if not p.exists(): + return {} + with open(p, "rb") as f: + return tomllib.load(f) + +SETUP = _load_setup() + +# ── OS detection ────────────────────────────────────────────────────────────── + +def detect_os() -> str: + """ + Detect the current operating system. + Returns one of: 'macos', 'arch', 'linux', 'unknown'. + """ + if platform.system() == "Darwin": + return "macos" + if Path("/etc/arch-release").exists(): + return "arch" + if Path("/etc/os-release").exists(): + return "linux" + return "unknown" + +OS = detect_os() + +# ── overlay resolution ──────────────────────────────────────────────────────── +# OS is the ONLY variation axis — all Macs are identical, all Arch machines +# are identical. We fall back through home// → home/benk/. + +def overlay_home() -> Path: + """ + Return the dotfile overlay directory for the current user. + Checks home// first, then falls back to home/benk/. + Creates home// if neither exists. + """ + for candidate in (USERNAME, "benk"): + p = DOTFILES_DIR / "home" / candidate + if p.is_dir(): + return p + # No overlay found — create one for this user rather than silently using benk/ + p = DOTFILES_DIR / "home" / USERNAME + p.mkdir(parents=True, exist_ok=True) + return p + +# ── OS-specific path exclusions ─────────────────────────────────────────────── +# Some paths in the overlay only make sense on a specific OS. +# These are excluded from syncing when we're on a different OS. +# +# Format: { "owner_os": ["path/to/exclude/"] } +# +# Current rules: +# macOS only → Library/ (iTerm2 prefs, macOS app support) +# Linux only → .config/wezterm/ (WezTerm is the Linux terminal; +# iTerm2 is used on macOS instead) + +OS_ONLY: dict[str, list[str]] = { + "macos": ["Library/"], + "linux": [".config/wezterm/"], + "arch": [".config/wezterm/"], +} + +def home_excludes() -> list[str]: + """ + Build rsync --exclude flags for paths that don't belong on this OS. + For each entry in OS_ONLY, if we're NOT on that OS, exclude those paths. + """ + flags: list[str] = [] + for owner_os, paths in OS_ONLY.items(): + if OS != owner_os: + for p in paths: + flags += ["--exclude", p] + return flags + +# ── rsync helpers ───────────────────────────────────────────────────────────── +# We use rsync for all file syncing because it handles: +# - incremental transfers (only changed files) +# - directory trees +# - backups (--backup --backup-dir) +# - dry-run previews (--dry-run --itemize-changes) +# +# The --itemize-changes format is: YXcstpogba path +# Y = update type: > (transfer to dst), < (transfer from dst) +# X = file type: f (file), d (dir), L (symlink), ... +# flags = 9 chars; all '+' means new file; any other char means attribute changed +# +# We parse this output to distinguish new files from overwrites, +# and to decide which overwrites need per-file confirmation. + +def _parse_changes(rsync_output: str) -> list[tuple[str, str]]: + """ + Parse rsync --itemize-changes output into (kind, path) tuples. + Only regular files transferred TO the destination ('>f') are included. + + kind values: + 'new' — file doesn't exist at dst yet (flags are all '+') + 'changed' — file exists at dst and will be overwritten + """ + results = [] + for line in rsync_output.splitlines(): + # Only care about files being sent to the destination ('>f...') + if not line.startswith(">f"): + continue + parts = line.split(None, 1) + if len(parts) != 2: + continue + flag, path = parts + # All-'+' flags = brand new file; any variation = existing file updated + kind = "new" if "+++++++++" in flag else "changed" + results.append((kind, path)) + return results + +def confirm_rsync(src: str, dst: str, flags: list[str], *, sudo: bool = False, label: str = "") -> bool: + """ + Smart rsync wrapper that protects against accidentally overwriting newer work. + + Flow: + 1. Dry-run rsync to find what would change + 2. For files that already exist at dst: + - If dst is NEWER than src → add to per-file confirmation list + (unchecked by default, so the user must actively choose to overwrite) + - If dst is OLDER than src → overwrite silently (repo wins) + 3. Run the real rsync, excluding any files the user declined to overwrite + + Returns True if the sync ran, False if the user cancelled. + """ + all_args = flags + [src, dst] + + # Dry run first — no files are touched + dry = subprocess.run( + ["rsync", "--dry-run", "--itemize-changes"] + all_args, + capture_output=True, text=True, + ) + changes = _parse_changes(dry.stdout) + overwrites = [p for kind, p in changes if kind == "changed"] + + extra_excludes: list[str] = [] + + if overwrites: + src_p = Path(src.rstrip("/")) + dst_p = Path(dst.rstrip("/")) + + newer_at_dst: list[str] = [] # dst is newer → needs review + auto_count = 0 # dst is older → safe to overwrite + + for rel in overwrites: + src_f, dst_f = src_p / rel, dst_p / rel + if (dst_f.exists() and src_f.exists() + and dst_f.stat().st_mtime > src_f.stat().st_mtime): + newer_at_dst.append(rel) + else: + auto_count += 1 + + if newer_at_dst: + # Inform the user about silent overwrites before showing the checkbox + if auto_count: + console.print( + f" [dim]Auto-overwriting {auto_count} older file(s) — " + f"repo is newer, no action needed[/dim]" + ) + console.print() + # Show a checkbox for files where the system has newer edits. + # All unchecked by default — the user must opt IN to overwrite. + prefix = f"[{label}] " if label else "" + selected = questionary.checkbox( + f"{prefix}These files are newer on this machine than in the repo.\n" + f" Check any you want to overwrite with the repo version:", + choices=[ + questionary.Choice( + title=f"{r} [dim](system is newer)[/dim]", + value=r, + checked=False, # opt-in, not opt-out + ) + for r in newer_at_dst + ], + ).ask() + if selected is None: + console.print("[yellow] Cancelled.[/yellow]") + return False + # Exclude files the user chose NOT to overwrite + for rel in newer_at_dst: + if rel not in selected: + extra_excludes += ["--exclude", rel] + + cmd = (["sudo"] if sudo else []) + ["rsync"] + extra_excludes + all_args + subprocess.run(cmd, check=True) + return True + +# ── directory bootstrap ─────────────────────────────────────────────────────── + +def create_home_dirs() -> None: + """ + Ensure standard XDG and home directories exist before syncing. + rsync will fail if a parent directory doesn't exist. + .ssh must be 700 or SSH will refuse to use it. + """ + for d in [ + Path.home() / ".config", + Path.home() / ".local" / "bin", + Path.home() / ".local" / "share", + Path.home() / ".local" / "state", + Path.home() / ".ssh", + ]: + d.mkdir(parents=True, exist_ok=True) + (Path.home() / ".ssh").chmod(0o700) + +# ── package file helpers ────────────────────────────────────────────────────── + +def _read_packages(path: Path) -> list[str]: + """ + Read a package list file. Returns an empty list if the file doesn't exist. + Lines starting with '#' and blank lines are ignored. + """ + if not path.exists(): + return [] + return [ + ln.strip() for ln in path.read_text().splitlines() + if ln.strip() and not ln.startswith("#") + ] + +def _install_one(cmd: list[str], pkg: str) -> None: + """ + Run `cmd pkg` and prompt to continue if it fails. + Exits the whole script if the user says no — a half-installed machine + is worse than an aborted one. + """ + console.print(f" [dim]{' '.join(cmd)} {pkg}[/dim]") + result = subprocess.run(cmd + [pkg], capture_output=True, text=True) + if result.returncode != 0: + console.print(f" [red][!] Failed to install:[/red] {pkg}") + if not questionary.confirm(" Continue anyway?", default=False).ask(): + sys.exit(1) + +# ── package installation ────────────────────────────────────────────────────── + +def install_packages() -> None: + """Dispatch to the correct package manager based on OS.""" + if OS == "macos": + _install_brew() + elif OS == "arch": + _install_arch() + else: + console.print("[yellow]Unknown OS — skipping package install.[/yellow]") + console.print("[dim]Install packages manually from packages/*.txt[/dim]") + +def _install_brew() -> None: + """ + Install Homebrew if not present, then install all formulae from brew.txt + and all casks from brew-casks.txt. + + brew.txt → `brew install ` (CLI tools, libraries) + brew-casks.txt → `brew install --cask ` (GUI apps, fonts) + """ + if not shutil.which("brew"): + console.print(" [bold]Homebrew not found — installing...[/bold]") + subprocess.run( + ["/bin/bash", "-c", + "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"], + check=True, + ) + pkgs = _read_packages(DOTFILES_DIR / "packages" / "brew.txt") + console.print(f" Installing [bold]{len(pkgs)}[/bold] formulae...") + for pkg in pkgs: + _install_one(["brew", "install"], pkg) + casks = _read_packages(DOTFILES_DIR / "packages" / "brew-casks.txt") + if casks: + console.print(f" Installing [bold]{len(casks)}[/bold] casks...") + for cask in casks: + _install_one(["brew", "install", "--cask"], cask) + +def _install_arch() -> None: + """ + Bootstrap yay (AUR helper) if not present, then install all packages + from arch.txt using yay (which also handles official repo packages). + """ + if not shutil.which("yay"): + console.print(" [bold]yay not found — bootstrapping from AUR...[/bold]") + subprocess.run( + ["sudo", "pacman", "-S", "--needed", "--noconfirm", "git", "base-devel"], + check=True, + ) + tmp = Path(subprocess.check_output(["mktemp", "-d"]).decode().strip()) + subprocess.run( + ["git", "clone", "https://aur.archlinux.org/yay.git", str(tmp / "yay")], + check=True, + ) + subprocess.run(["makepkg", "-si", "--noconfirm"], cwd=tmp / "yay", check=True) + shutil.rmtree(tmp) + pkgs = _read_packages(DOTFILES_DIR / "packages" / "arch.txt") + console.print(f" Installing [bold]{len(pkgs)}[/bold] packages...") + for pkg in pkgs: + _install_one(["yay", "-S", "--needed", "--noconfirm"], pkg) + +# ── shell tooling ───────────────────────────────────────────────────────────── +# "Tooling" means things that aren't packages but need to be set up once: +# tmux plugin manager, oh-my-fish, fisher, and the plugins declared in setup.toml. + +def setup_shell_tooling() -> None: + """Set up tmux and fish tooling in sequence.""" + _setup_tmux() + _setup_fish() + +def _setup_tmux() -> None: + """ + Clone tpm (Tmux Plugin Manager) if not present, then install any tmux + plugins listed in setup.toml [tmux] plugins via tpm's headless install script. + + tpm lives at ~/.tmux/plugins/tpm. + Plugins are installed to ~/.tmux/plugins/. + The headless install script is at ~/.tmux/plugins/tpm/bin/install_plugins. + """ + tpm = Path.home() / ".tmux" / "plugins" / "tpm" + if not tpm.exists(): + console.print(" Installing tmux plugin manager (tpm)...") + subprocess.run( + ["git", "clone", "https://github.com/tmux-plugins/tpm", str(tpm)], + check=True, + ) + else: + console.print(" [green]✓[/green] tmux plugin manager") + + plugins = SETUP.get("tmux", {}).get("plugins", []) + if plugins: + install_script = tpm / "bin" / "install_plugins" + if install_script.exists(): + console.print(f" Installing {len(plugins)} tmux plugin(s) headlessly...") + subprocess.run([str(install_script)], capture_output=True) + console.print(" [green]✓[/green] tmux plugins") + else: + # Fallback: manual instruction if the script isn't present + console.print(" [yellow]![/yellow] Run [dim]I[/dim] inside tmux to install plugins") + +def _setup_fish() -> None: + """ + Set up the fish shell plugin ecosystem: + 1. oh-my-fish (OMF) — theming and utility functions + 2. fisher — fish plugin manager (manages plugins declared in setup.toml) + 3. fish plugins from setup.toml [fish] plugins + + Skipped entirely if fish is not installed on this machine. + Fisher install is idempotent — it only installs plugins not already present. + """ + if not shutil.which("fish"): + console.print(" [dim]fish not installed — skipping fish setup[/dim]") + return + + omf = Path.home() / ".local" / "share" / "omf" + if not omf.exists(): + console.print(" Installing oh-my-fish...") + subprocess.run(["fish", "-c", "curl -L https://get.oh-my.fish | fish"], check=True) + else: + console.print(" [green]✓[/green] oh-my-fish") + + # Check if fisher is available as a fish function + result = subprocess.run(["fish", "-c", "type -q fisher"], capture_output=True) + if result.returncode != 0: + console.print(" Installing fisher...") + subprocess.run( + ["fish", "-c", + "curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/" + "functions/fisher.fish | source && fisher install jorgebucaran/fisher"], + check=True, + ) + else: + console.print(" [green]✓[/green] fisher") + + # Install any fish plugins declared in setup.toml that aren't already installed + plugins = SETUP.get("fish", {}).get("plugins", []) + if plugins: + installed_r = subprocess.run( + ["fish", "-c", "fisher list"], capture_output=True, text=True + ) + installed = set(installed_r.stdout.strip().splitlines()) + to_install = [p for p in plugins if p not in installed] + if to_install: + console.print(f" Installing {len(to_install)} fish plugin(s)...") + subprocess.run( + ["fish", "-c", f"fisher install {' '.join(to_install)}"], + check=True, + ) + console.print(" [green]✓[/green] fish plugins") + +# ── sync: repo → home (copy / install) ─────────────────────────────────────── + +def sync_to_home() -> None: + """ + Sync the overlay directory (home//) into $HOME. + Original files are backed up to ~/dotfiles.bak/home/ before overwriting. + OS-specific paths are excluded via home_excludes(). + """ + src = overlay_home() + dst = Path.home() + (BACKUP_DIR / "home").mkdir(parents=True, exist_ok=True) + flags = [ + "-av", "--itemize-changes", + "--backup", f"--backup-dir={BACKUP_DIR / 'home'}", + *home_excludes(), + ] + console.print( + f" [bold]Syncing[/bold] [cyan]{src.relative_to(DOTFILES_DIR)}[/cyan]" + f" → [cyan]~[/cyan]" + ) + confirm_rsync(f"{src}/", f"{dst}/", flags, label="repo → home") + +# ── sync: home → repo (pull) ───────────────────────────────────────────────── + +def sync_to_repo() -> None: + """ + Pull live config from $HOME back into the overlay (home//). + Only files that already exist in the overlay are candidates — we build + an explicit file list from the overlay and pass it via --files-from. + + Why --files-from instead of --existing: + --existing tells rsync "only update files at dst that exist there", but + rsync still traverses the ENTIRE source tree to find candidates. On a + large home directory this is slow, verbose, and can create ghost + directories in the overlay that become "tracked" on the next run. + --files-from is an explicit manifest: rsync only touches those paths. + + Also pulls /etc if an etc/ overlay directory exists in the repo. + """ + import tempfile + + src, dst = Path.home(), overlay_home() + + # Build explicit file list from the overlay (relative to overlay root) + tracked = [ + str(f.relative_to(dst)) + for f in dst.rglob("*") + if f.is_file() and not any( + str(f.relative_to(dst)).startswith(excl.lstrip("/")) + for excl in home_excludes()[1::2] # every other element is the path + ) + ] + + if not tracked: + console.print(" [dim]No files tracked in overlay — nothing to pull.[/dim]") + return + + console.print( + f" [bold]Pulling[/bold] [cyan]~[/cyan]" + f" → [cyan]{dst.relative_to(DOTFILES_DIR)}[/cyan]" + f" [dim]({len(tracked)} tracked files)[/dim]" + ) + + # Write the manifest to a temp file and pass it to rsync + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as fh: + fh.write("\n".join(tracked)) + manifest = fh.name + + try: + flags = [ + "-av", "--itemize-changes", + f"--files-from={manifest}", # only sync these exact paths + "--update", # skip files where dst (overlay) is newer than src (home) + ] + confirm_rsync(f"{src}/", f"{dst}/", flags, label="home → repo") + finally: + Path(manifest).unlink(missing_ok=True) + + etc_dir = DOTFILES_DIR / "etc" + if etc_dir.is_dir(): + etc_tracked = [ + str(f.relative_to(etc_dir)) + for f in etc_dir.rglob("*") if f.is_file() + ] + console.print(f" [bold]Pulling[/bold] [cyan]/etc[/cyan] → [cyan]etc/[/cyan]") + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as fh: + fh.write("\n".join(etc_tracked)) + etc_manifest = fh.name + try: + confirm_rsync( + "/etc/", f"{etc_dir}/", + ["-av", "--itemize-changes", f"--files-from={etc_manifest}", "--update"], + sudo=True, label="/etc → repo", + ) + finally: + Path(etc_manifest).unlink(missing_ok=True) + +# ── sync: etc/ → /etc ───────────────────────────────────────────────────────── + +def sync_etc() -> None: + """ + Apply the etc/ overlay to /etc on the system (requires sudo). + Original /etc files are backed up to ~/dotfiles.bak/etc/ before overwriting. + This is exposed as an optional stage in mode_install; it's excluded from + mode_copy because /etc changes are usually intentional and should be reviewed. + """ + etc_dir = DOTFILES_DIR / "etc" + files = [f for f in etc_dir.rglob("*") if f.is_file()] + console.print(" [bold]System config files to apply:[/bold]") + for f in files: + console.print(f" [dim]/{f.relative_to(DOTFILES_DIR)}[/dim]") + console.print() + (BACKUP_DIR / "etc").mkdir(parents=True, exist_ok=True) + confirm_rsync( + f"{etc_dir}/", "/etc/", + ["-av", "--itemize-changes", "--backup", f"--backup-dir={BACKUP_DIR / 'etc'}"], + sudo=True, label="etc → /etc", + ) + +def _has_etc() -> bool: + """Return True if the repo has an etc/ overlay with at least one file.""" + etc_dir = DOTFILES_DIR / "etc" + return etc_dir.is_dir() and any(f for f in etc_dir.rglob("*") if f.is_file()) + +# ── status command ──────────────────────────────────────────────────────────── +# Status is read-only — it never modifies any files. +# It answers the question: "how does this machine differ from the repo?" + +def cmd_status() -> None: + """ + Show a full drift report: packages, dotfiles, and tooling. + No files are written or modified. + """ + console.print(Panel( + "[bold]Status[/bold] [dim]comparing this machine against the repo — " + "no changes will be made[/dim]", + border_style="bright_blue", padding=(0, 1), + )) + + console.rule("[bold]Packages[/bold]", style="bright_blue") + _status_packages() + + console.rule("[bold]Dotfiles[/bold]", style="bright_blue") + _status_dotfiles() + + console.rule("[bold]Tooling[/bold]", style="bright_blue") + _status_tooling() + + console.rule(style="bright_blue") + +def _status_packages() -> None: + """ + Compare wanted packages (from packages/*.txt) against what's installed. + Shows missing packages individually; installed ones are summarised as a count. + """ + console.print() + if OS == "macos": + if not shutil.which("brew"): + console.print(" [red]✗[/red] Homebrew is not installed") + console.print() + return + installed_formulae = set( + subprocess.check_output(["brew", "list", "--formula"], text=True).splitlines() + ) + installed_casks = set( + subprocess.check_output(["brew", "list", "--cask"], text=True).splitlines() + ) + _status_pkg_list( + "formulae", + _read_packages(DOTFILES_DIR / "packages" / "brew.txt"), + installed_formulae, + ) + _status_pkg_list( + "casks", + _read_packages(DOTFILES_DIR / "packages" / "brew-casks.txt"), + installed_casks, + ) + elif OS in ("arch", "linux"): + installed = set(subprocess.check_output(["pacman", "-Qq"], text=True).splitlines()) + _status_pkg_list( + "packages", + _read_packages(DOTFILES_DIR / "packages" / "arch.txt"), + installed, + ) + else: + console.print(" [dim]Package status not supported on this OS[/dim]") + console.print() + +def _status_pkg_list(label: str, wanted: list[str], installed: set[str]) -> None: + """Print missing packages individually and summarise installed ones as a count.""" + missing = [p for p in wanted if p not in installed] + ok_count = sum(1 for p in wanted if p in installed) + for p in missing: + console.print(f" [red]✗[/red] {p} [dim]({label} — not installed)[/dim]") + console.print(f" [green]✓[/green] {ok_count}/{len(wanted)} {label} installed") + +def _status_dotfiles() -> None: + """ + Dry-run rsync repo→home and classify each changed file as: + new — in repo, not yet on this machine (copy will add it) + repo ahead — repo is newer (copy will update it) + system ahead — system is newer (pull to capture, or leave it) + + Shows up to 5 examples per category with a count for the rest. + """ + console.print() + src = overlay_home() + dst = Path.home() + + result = subprocess.run( + ["rsync", "--dry-run", "--itemize-changes", "-av", + *home_excludes(), f"{src}/", f"{dst}/"], + capture_output=True, text=True, + ) + changes = _parse_changes(result.stdout) + + if not changes: + console.print(" [green]✓[/green] All dotfiles are up to date") + console.print() + return + + new_files = [p for kind, p in changes if kind == "new"] + changed_files = [p for kind, p in changes if kind == "changed"] + + # For each changed file, decide which side is "ahead" by mtime + repo_ahead = [] + system_ahead = [] + for rel in changed_files: + dst_f = dst / rel + src_f = src / rel + if dst_f.exists() and src_f.exists() and dst_f.stat().st_mtime > src_f.stat().st_mtime: + system_ahead.append(rel) + else: + repo_ahead.append(rel) + + def _show_files(files: list[str], limit: int = 5) -> None: + for p in files[:limit]: + console.print(f" [dim]{p}[/dim]") + if len(files) > limit: + console.print(f" [dim]... and {len(files) - limit} more[/dim]") + + if new_files: + console.print( + f" [blue]+[/blue] {len(new_files)} file(s) in repo, " + f"not yet on this machine [dim](run Copy to apply)[/dim]" + ) + _show_files(new_files) + + if repo_ahead: + console.print( + f" [cyan]↓[/cyan] {len(repo_ahead)} file(s) where repo is newer " + f"[dim](run Copy to apply)[/dim]" + ) + _show_files(repo_ahead) + + if system_ahead: + console.print( + f" [yellow]↑[/yellow] {len(system_ahead)} file(s) where this machine is newer " + f"[dim](run Pull to save, or leave as-is)[/dim]" + ) + _show_files(system_ahead) + + console.print() + +def _status_tooling() -> None: + """ + Check whether tmux tpm, oh-my-fish, fisher, and declared plugins are installed. + Cross-references setup.toml for expected plugin lists. + """ + console.print() + + # tmux + tpm = Path.home() / ".tmux" / "plugins" / "tpm" + _status_line("tmux plugin manager (tpm)", tpm.exists()) + for plugin in SETUP.get("tmux", {}).get("plugins", []): + plugin_dir = Path.home() / ".tmux" / "plugins" / plugin.split("/")[-1] + _status_line(f" tmux: {plugin}", plugin_dir.exists()) + + # fish + if shutil.which("fish"): + omf = Path.home() / ".local" / "share" / "omf" + _status_line("oh-my-fish", omf.exists()) + fisher_ok = subprocess.run(["fish", "-c", "type -q fisher"], capture_output=True) + _status_line("fisher", fisher_ok.returncode == 0) + if fisher_ok.returncode == 0: + installed_r = subprocess.run( + ["fish", "-c", "fisher list"], capture_output=True, text=True + ) + installed = set(installed_r.stdout.strip().splitlines()) + for plugin in SETUP.get("fish", {}).get("plugins", []): + _status_line(f" fish: {plugin}", plugin in installed) + else: + console.print(" [dim]fish not installed — skipping fish tooling check[/dim]") + + console.print() + +def _status_line(label: str, ok: bool) -> None: + """Print a single ✓ / ✗ status line.""" + icon = "[green]✓[/green]" if ok else "[red]✗[/red]" + console.print(f" {icon} {label}") + +# ── add command ─────────────────────────────────────────────────────────────── + +def cmd_add(path_str: str) -> None: + """ + Start tracking a new file by copying it into the dotfile overlay. + + The file must be under $HOME. Its path relative to $HOME is preserved + in the overlay, so the sync will deploy it back to the same location. + + Example: + python3 bootstrap.py add ~/.config/starship.toml + → copies to home/benk/.config/starship.toml + → `git add` and commit to make it permanent + """ + path = Path(path_str).expanduser().resolve() + if not path.exists(): + console.print(f"[red]File not found:[/red] {path}") + sys.exit(1) + + try: + rel = path.relative_to(Path.home()) + except ValueError: + console.print( + f"[red]Error:[/red] path must be under $HOME\n" + f" Got: {path}\n" + f" $HOME: {Path.home()}" + ) + sys.exit(1) + + dst = overlay_home() / rel + if dst.exists(): + console.print(f"[yellow]Already tracked:[/yellow] {rel}") + console.print(f" [dim]Currently at: {dst}[/dim]") + if not questionary.confirm(" Overwrite the repo copy?", default=False).ask(): + sys.exit(0) + + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, dst) + console.print(f" [green]✓[/green] Tracked: [cyan]~/{rel}[/cyan]") + console.print( + f" [dim]Repo path: {dst.relative_to(DOTFILES_DIR)}[/dim]\n" + f" [dim]Commit: git add {dst.relative_to(DOTFILES_DIR)} && git commit[/dim]" + ) + +# ── stage runner ────────────────────────────────────────────────────────────── +# A Stage is a named, describable unit of work within a mode. +# run_stages() shows a checkbox so the user can skip stages they don't need, +# then runs each selected stage with a clear progress header. + +@dataclass +class Stage: + """A discrete unit of work within a mode (e.g. 'Install packages').""" + key: str # unique identifier used for checkbox selection + title: str # short display name shown in the checkbox and header + desc: str # one-line description shown alongside the title + fn: Callable[[], None] # the function to call when this stage runs + +# Set once at import time: True if a CLI argument was passed (non-interactive mode). +# run_stages() skips the checkbox and runs all stages when this is True. +NON_INTERACTIVE = len(sys.argv) > 1 + +def run_stages(mode_title: str, mode_desc: str, stages: list[Stage]) -> None: + """ + Display a mode panel, let the user select which stages to run via checkbox, + then execute each selected stage with a numbered progress header. + + In non-interactive mode (CLI argument provided) all stages run without prompting. + + The stage rule format is: Stage N/M Title description + This makes it easy to see at a glance where you are in a multi-stage run. + """ + console.print(Panel( + f"[bold]{mode_title}[/bold]\n[dim]{mode_desc}[/dim]", + border_style="bright_blue", padding=(0, 1), + )) + console.print() + + if NON_INTERACTIVE: + # Skip the checkbox when called from the command line (scripted / CI use) + to_run = stages + else: + selected_keys = questionary.checkbox( + "Select which stages to run " + "[dim](space to toggle, enter to confirm, ctrl-c to cancel)[/dim]", + choices=[ + questionary.Choice( + title=f"{s.title} [dim]— {s.desc}[/dim]", + value=s.key, + checked=True, # all stages selected by default + ) + for s in stages + ], + ).ask() + + if selected_keys is None: + # questionary returns None on Ctrl-C + console.print("\n[yellow]Cancelled.[/yellow]") + sys.exit(0) + if not selected_keys: + console.print("[yellow]No stages selected — nothing to do.[/yellow]") + return + + to_run = [s for s in stages if s.key in selected_keys] + + total = len(to_run) + for i, stage in enumerate(to_run, 1): + console.print() + console.rule( + f"[bold bright_blue]Stage {i} of {total}[/bold bright_blue]" + f" [bold]{stage.title}[/bold] [dim]{stage.desc}[/dim]", + style="bright_blue", + ) + console.print() + stage.fn() + + console.print() + console.rule("[bold green]All done[/bold green]", style="green") + console.print() + +# ── modes ───────────────────────────────────────────────────────────────────── +# Each mode is a curated list of stages suited to a particular intent. +# Modes are presented in the TUI select and can also be called from the CLI. + +def _stage_sync_dotfiles() -> None: + """Ensure home dirs exist then sync dotfiles. Used in both install and copy.""" + create_home_dirs() + sync_to_home() + +def mode_install() -> None: + """ + Full machine setup: packages → tooling → dotfiles → /etc (if present). + Intended for bootstrapping a new machine from scratch. + All stages are shown in the checkbox; deselect any you want to skip. + """ + pkg_mgr = {"macos": "brew", "arch": "yay"}.get(OS, "unknown") + stages = [ + Stage("packages", "Install packages", f"via {pkg_mgr}", install_packages), + Stage("tooling", "Shell tooling", "tmux tpm · fish plugins", setup_shell_tooling), + Stage("dotfiles", "Sync dotfiles", "repo → home", _stage_sync_dotfiles), + ] + if _has_etc(): + stages.append(Stage("etc", "Sync /etc", "requires sudo", sync_etc)) + run_stages( + "Set up this machine", + "Install packages, shell tooling, and sync dotfiles from the repo", + stages, + ) + +def mode_copy() -> None: + """ + Apply dotfiles from the repo to this machine. + Skips packages and shell tooling — use this on a machine that's already set up + when you just want to push a config change out. + """ + run_stages( + "Apply dotfiles", + "Sync repo → home (skip packages and shell tooling)", + [Stage("dotfiles", "Sync dotfiles", "repo → home", _stage_sync_dotfiles)], + ) + +def mode_pull() -> None: + """ + Pull edits made on this machine back into the repo. + Only files already tracked in the overlay are updated (no new files are added). + Use `add` to start tracking a new file. + Files newer on this machine will be shown for per-file confirmation. + """ + run_stages( + "Save changes", + "Pull edits from this machine back into the repo", + [Stage("pull", "Pull config", "home + /etc → repo", sync_to_repo)], + ) + +# ── mode registry ───────────────────────────────────────────────────────────── +# Modes are keyed by their CLI name. Each entry is (display_title, description, fn). +# The TUI select and the CLI dispatcher both read from this dict, +# so adding a new mode here is the only change needed. + +MODES: dict[str, tuple[str, str, Callable[[], None]]] = { + "install": ( + "Set up this machine", + "install packages, shell tooling, and sync dotfiles", + mode_install, + ), + "copy": ( + "Apply dotfiles", + "push repo changes to this machine (skip packages)", + mode_copy, + ), + "pull": ( + "Save changes", + "pull edits made on this machine back into the repo", + mode_pull, + ), + "status": ( + "Check status", + "see what's different between this machine and the repo", + cmd_status, + ), +} + +# ── entry point ─────────────────────────────────────────────────────────────── + +def main() -> None: + """ + Parse CLI arguments or show the interactive TUI. + + CLI usage (non-interactive, skips all checkboxes): + bootstrap.py install | copy | pull | status + bootstrap.py add + + Interactive usage (no arguments): + Shows a mode-select menu, then a stage-select checkbox. + """ + # ── header ── + console.print(Panel( + Text.assemble( + ("dotfiles", "bold cyan"), "\n", + (" repo ", "dim"), str(DOTFILES_DIR), "\n", + (" user ", "dim"), USERNAME, "\n", + (" os ", "dim"), OS, + ), + border_style="bright_blue", + padding=(0, 1), + )) + console.print() + + # ── CLI: bootstrap.py add ── + if len(sys.argv) > 1 and sys.argv[1] == "add": + if len(sys.argv) < 3: + console.print( + "[red]Usage:[/red] bootstrap.py add [cyan][/cyan]\n" + " Example: bootstrap.py add ~/.config/starship.toml" + ) + sys.exit(1) + cmd_add(sys.argv[2]) + return + + # ── CLI: bootstrap.py ── + if len(sys.argv) > 1: + key = sys.argv[1].lstrip("-") # accept --install as well as install + if key in MODES: + MODES[key][2]() + return + console.print( + f"[red]Unknown command:[/red] {sys.argv[1]}\n" + f" Valid commands: add | {' | '.join(MODES)}" + ) + sys.exit(1) + + # ── TUI: interactive mode select ── + console.print("[dim]Use arrow keys to navigate, enter to select.[/dim]\n") + choice = questionary.select( + "What would you like to do?", + choices=[ + questionary.Choice( + title=f"{title} [dim]— {desc}[/dim]", + value=key, + ) + for key, (title, desc, _) in MODES.items() + ], + ).ask() + + if choice is None: + # Ctrl-C + console.print("\n[yellow]Cancelled.[/yellow]") + sys.exit(0) + + MODES[choice][2]() + +if __name__ == "__main__": + main() diff --git a/bootstrap.sh b/bootstrap.sh index 282d5d5..39b3389 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -5,6 +5,25 @@ DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" BACKUP_DIR="$HOME/dotfiles.bak" USER="$(whoami)" +# --- Detect OS early (needed by sync functions) --- +OS="unknown" +if [[ "$(uname)" == "Darwin" ]]; then + OS="macos" +elif [[ -f /etc/arch-release ]]; then + OS="arch" +elif [[ -f /etc/os-release ]]; then + OS="linux" +fi + +# Files/dirs in the home overlay that are macOS-only +MACOS_ONLY=(Library/) +HOME_EXCLUDES=() +if [[ "$OS" != "macos" ]]; then + for item in "${MACOS_ONLY[@]}"; do + HOME_EXCLUDES+=(--exclude "$item") + done +fi + usage() { echo "Usage: ./bootstrap.sh [--pull | --copy]" echo "" @@ -14,6 +33,34 @@ usage() { exit 0 } +# --- rsync wrapper: prompts for confirmation if existing files would be overwritten --- +# Usage: rsync_confirm [--sudo] +rsync_confirm() { + local rsync_cmd=(rsync) + if [[ "${1:-}" == "--sudo" ]]; then + rsync_cmd=(sudo rsync) + shift + fi + + local overwrites + overwrites=$("${rsync_cmd[@]}" --dry-run --itemize-changes "$@" 2>/dev/null \ + | awk '/^>f/ && !/\+{9}/ { print $2 }') + + if [ -n "$overwrites" ]; then + echo "The following existing files would be overwritten:" + echo "$overwrites" | sed 's/^/ /' + echo "" + read -p "Proceed with overwrite? [y/N] " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo " Aborted." + return 1 + fi + fi + + "${rsync_cmd[@]}" "$@" +} + # --- Pull mode: copy live files back into the repo --- pull_back() { OVERLAY_HOME="$DOTFILES_DIR/home/$USER" @@ -22,13 +69,13 @@ pull_back() { fi echo "==> Pulling home files back into repo..." - rsync -av --itemize-changes --existing --update \ - "$HOME/" "$OVERLAY_HOME/" + rsync_confirm -av --itemize-changes --existing --update \ + "${HOME_EXCLUDES[@]}" "$HOME/" "$OVERLAY_HOME/" echo "" if [ -d "$DOTFILES_DIR/etc" ]; then echo "==> Pulling /etc files back into repo..." - sudo rsync -av --itemize-changes --existing --update \ + rsync_confirm --sudo -av --itemize-changes --existing --update \ /etc/ "$DOTFILES_DIR/etc/" echo "" fi @@ -56,8 +103,8 @@ copy_home() { echo "==> Syncing $OVERLAY_HOME -> $HOME" mkdir -p "$BACKUP_DIR/home" - rsync -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/home" \ - "$OVERLAY_HOME/" "$HOME/" + rsync_confirm -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/home" \ + "${HOME_EXCLUDES[@]}" "$OVERLAY_HOME/" "$HOME/" echo "" echo "Done. Backups at ~/dotfiles.bak/" exit 0 @@ -77,15 +124,6 @@ echo "Repo: $DOTFILES_DIR" echo "User: $USER" echo "" -# --- Detect OS --- -OS="unknown" -if [[ "$(uname)" == "Darwin" ]]; then - OS="macos" -elif [[ -f /etc/arch-release ]]; then - OS="arch" -elif [[ -f /etc/os-release ]]; then - OS="linux" -fi echo "Detected OS: $OS" echo "" @@ -205,8 +243,8 @@ chmod 700 "$HOME/.ssh" echo "==> Syncing $OVERLAY_HOME -> $HOME" mkdir -p "$BACKUP_DIR/home" -rsync -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/home" \ - "$OVERLAY_HOME/" "$HOME/" +rsync_confirm -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/home" \ + "${HOME_EXCLUDES[@]}" "$OVERLAY_HOME/" "$HOME/" echo "" # --- Overlay: etc/ -> /etc --- @@ -224,7 +262,7 @@ if [ -d "$DOTFILES_DIR/etc" ]; then if [[ $REPLY =~ ^[Yy]$ ]]; then mkdir -p "$BACKUP_DIR/etc" - sudo rsync -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/etc" \ + rsync_confirm --sudo -av --itemize-changes --backup --backup-dir="$BACKUP_DIR/etc" \ "$DOTFILES_DIR/etc/" /etc/ echo " Originals backed up to ~/dotfiles.bak/etc/" else diff --git a/home/benk/Library/Preferences/com.googlecode.iterm2.plist b/home/benk/Library/Preferences/com.googlecode.iterm2.plist new file mode 100644 index 0000000..8c0cb2f --- /dev/null +++ b/home/benk/Library/Preferences/com.googlecode.iterm2.plist @@ -0,0 +1,2479 @@ + + + + + AIFeatureFunctionCalling + + AIFeatureHostedCodeInterpeter + + AIFeatureHostedFileSearch + + AIFeatureHostedWebSearch + + AIFeatureStreamingResponses + + AITermAPI + 2 + AIVectorStore + 0 + AiMaxTokens + 400000 + AiModel + gpt-5.2 + AiResponseMaxTokens + 128000 + AitermURL + https://api.openai.com/v1/responses + AllowClipboardAccess + + AppleAntiAliasingThreshold + 1 + ApplePressAndHoldEnabled + + AppleScrollAnimationEnabled + 0 + AppleSmoothFixedFontsSizeThreshold + 1 + AppleWindowTabbingMode + manual + Custom Color Presets + + catppuccin-mocha + + Ansi 0 Color + + Alpha Component + 1 + Blue Component + 0.35294117647058826 + Color Space + sRGB + Green Component + 0.27843137254901962 + Red Component + 0.27058823529411763 + + Ansi 1 Color + + Alpha Component + 1 + Blue Component + 0.6588235294117647 + Color Space + sRGB + Green Component + 0.54509803921568623 + Red Component + 0.95294117647058818 + + Ansi 10 Color + + Alpha Component + 1 + Blue Component + 0.54509803921568623 + Color Space + sRGB + Green Component + 0.84705882352941175 + Red Component + 0.53725490196078429 + + Ansi 11 Color + + Alpha Component + 1 + Blue Component + 0.56862745098039214 + Color Space + sRGB + Green Component + 0.82745098039215681 + Red Component + 0.92156862745098034 + + Ansi 12 Color + + Alpha Component + 1 + Blue Component + 0.9882352941176471 + Color Space + sRGB + Green Component + 0.6588235294117647 + Red Component + 0.45490196078431372 + + Ansi 13 Color + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.68235294117647061 + Red Component + 0.94901960784313721 + + Ansi 14 Color + + Alpha Component + 1 + Blue Component + 0.792156862745098 + Color Space + sRGB + Green Component + 0.84313725490196079 + Red Component + 0.41960784313725491 + + Ansi 15 Color + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.72941176470588232 + + Ansi 2 Color + + Alpha Component + 1 + Blue Component + 0.63137254901960782 + Color Space + sRGB + Green Component + 0.8901960784313725 + Red Component + 0.65098039215686276 + + Ansi 3 Color + + Alpha Component + 1 + Blue Component + 0.68627450980392157 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.97647058823529409 + + Ansi 4 Color + + Alpha Component + 1 + Blue Component + 0.98039215686274506 + Color Space + sRGB + Green Component + 0.70588235294117652 + Red Component + 0.53725490196078429 + + Ansi 5 Color + + Alpha Component + 1 + Blue Component + 0.90588235294117647 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.96078431372549022 + + Ansi 6 Color + + Alpha Component + 1 + Blue Component + 0.83529411764705885 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.58039215686274515 + + Ansi 7 Color + + Alpha Component + 1 + Blue Component + 0.78431372549019607 + Color Space + sRGB + Green Component + 0.67843137254901964 + Red Component + 0.65098039215686276 + + Ansi 8 Color + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + Ansi 9 Color + + Alpha Component + 1 + Blue Component + 0.59999999999999998 + Color Space + sRGB + Green Component + 0.46666666666666667 + Red Component + 0.95294117647058818 + + Background Color + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Bold Color + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Cursor Color + + Alpha Component + 1 + Blue Component + 0.86274509803921573 + Color Space + sRGB + Green Component + 0.8784313725490196 + Red Component + 0.96078431372549022 + + Cursor Guide Color + + Alpha Component + 0.070000000000000007 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Cursor Text Color + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Foreground Color + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Link Color + + Alpha Component + 1 + Blue Component + 0.92156862745098034 + Color Space + sRGB + Green Component + 0.86274509803921573 + Red Component + 0.53725490196078429 + + Selected Text Color + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Selection Color + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + + + Default Bookmark Guid + 67999E29-F912-49FB-8963-71295D790777 + HapticFeedbackForEsc + + HotkeyMigratedFromSingleToMulti + + NSAutoFillHeuristicControllerEnabled + + NSNavPanelExpandedSizeForOpenMode + {880, 448} + NSOSPLastRootDirectory + + Ym9vayQDAAAAAAUQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAQCAAAEAAAAAwMAAAAAACAFAAAAAQEAAFVzZXJzAAAABAAAAAEB + AABiZW5rCAAAAAEBAABkb3RmaWxlcwYAAAABAQAAdGhlbWVzAAAQAAAAAQEAAGNhdHBw + dWNjaW4taXRlcm0GAAAAAQEAAGNvbG9ycwAAGAAAAAEGAAAQAAAAIAAAACwAAAA8AAAA + TAAAAGQAAAAIAAAABAMAAPBHAAAAAAAACAAAAAQDAACkUwYAAAAAAAgAAAAEAwAAyzgO + AAAAAAAIAAAABAMAAGJWRgAAAAAACAAAAAQDAABjVkYAAAAAAAgAAAAEAwAAvFZGAAAA + AAAYAAAAAQYAAJQAAACkAAAAtAAAAMQAAADUAAAA5AAAAAgAAAAABAAAQce63sR1/PEY + AAAAAQIAAAIAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAABBQAACAAAAAQDAAAEAAAA + AAAAAAQAAAADAwAA9gEAAAgAAAABCQAAZmlsZTovLy8MAAAAAQEAAE1hY2ludG9zaCBI + RAgAAAAEAwAAAFCBGHMAAAAIAAAAAAQAAEHHrgGqAAAAJAAAAAEBAAA5NUZEOEU1NC0z + NTQzLTQ0RjUtODY2OS0yMEI1MDRBNUNBRDQYAAAAAQIAAIEAAAABAAAA7xMAAAEAAAAA + AAAAAAAAAAEAAAABAQAALwAAANgAAAD+////AQAAAAAAAAARAAAABBAAAHQAAAAAAAAA + BRAAAPQAAAAAAAAAEBAAACQBAAAAAAAAQBAAABQBAAAAAAAAAiAAAPgBAAAAAAAABSAA + AGgBAAAAAAAAECAAAHgBAAAAAAAAESAAAKwBAAAAAAAAEiAAAIwBAAAAAAAAEyAAAJwB + AAAAAAAAICAAANgBAAAAAAAAMCAAAEQBAAAAAAAAAcAAAEwBAAAAAAAAEcAAACAAAAAA + AAAAEsAAAFwBAAAAAAAAAdAAAEQBAAAAAAAAENAAAAQAAAAAAAAA + + NSOverlayScrollersFallBackForAccessoryViews + + NSQuotedKeystrokeBinding + + NSRepeatCountBinding + + NSScrollAnimationEnabled + + NSScrollViewShouldScrollUnderTitlebar + + New Bookmarks + + + ASCII Anti Aliased + + Ambiguous Double Width + + Ansi 0 Color + + Alpha Component + 1 + Blue Component + 0.11764705926179886 + Color Space + sRGB + Green Component + 0.098039217293262482 + Red Component + 0.078431375324726105 + + Ansi 0 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.35294117647058826 + Color Space + sRGB + Green Component + 0.27843137254901962 + Red Component + 0.27058823529411763 + + Ansi 0 Color (Light) + + Alpha Component + 1 + Blue Component + 0.35294117647058826 + Color Space + sRGB + Green Component + 0.27843137254901962 + Red Component + 0.27058823529411763 + + Ansi 1 Color + + Alpha Component + 1 + Blue Component + 0.16300037503242493 + Color Space + sRGB + Green Component + 0.23660069704055786 + Red Component + 0.7074432373046875 + + Ansi 1 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.6588235294117647 + Color Space + sRGB + Green Component + 0.54509803921568623 + Red Component + 0.95294117647058818 + + Ansi 1 Color (Light) + + Alpha Component + 1 + Blue Component + 0.6588235294117647 + Color Space + sRGB + Green Component + 0.54509803921568623 + Red Component + 0.95294117647058818 + + Ansi 10 Color + + Alpha Component + 1 + Blue Component + 0.56541937589645386 + Color Space + sRGB + Green Component + 0.9042816162109375 + Red Component + 0.3450070321559906 + + Ansi 10 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.54509803921568623 + Color Space + sRGB + Green Component + 0.84705882352941175 + Red Component + 0.53725490196078429 + + Ansi 10 Color (Light) + + Alpha Component + 1 + Blue Component + 0.54509803921568623 + Color Space + sRGB + Green Component + 0.84705882352941175 + Red Component + 0.53725490196078429 + + Ansi 11 Color + + Alpha Component + 1 + Blue Component + 0.0 + Color Space + sRGB + Green Component + 0.8833775520324707 + Red Component + 0.9259033203125 + + Ansi 11 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.56862745098039214 + Color Space + sRGB + Green Component + 0.82745098039215681 + Red Component + 0.92156862745098034 + + Ansi 11 Color (Light) + + Alpha Component + 1 + Blue Component + 0.56862745098039214 + Color Space + sRGB + Green Component + 0.82745098039215681 + Red Component + 0.92156862745098034 + + Ansi 12 Color + + Alpha Component + 1 + Blue Component + 0.9485321044921875 + Color Space + sRGB + Green Component + 0.67044717073440552 + Red Component + 0.65349078178405762 + + Ansi 12 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.9882352941176471 + Color Space + sRGB + Green Component + 0.6588235294117647 + Red Component + 0.45490196078431372 + + Ansi 12 Color (Light) + + Alpha Component + 1 + Blue Component + 0.9882352941176471 + Color Space + sRGB + Green Component + 0.6588235294117647 + Red Component + 0.45490196078431372 + + Ansi 13 Color + + Alpha Component + 1 + Blue Component + 0.8821563720703125 + Color Space + sRGB + Green Component + 0.4927266538143158 + Red Component + 0.8821563720703125 + + Ansi 13 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.68235294117647061 + Red Component + 0.94901960784313721 + + Ansi 13 Color (Light) + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.68235294117647061 + Red Component + 0.94901960784313721 + + Ansi 14 Color + + Alpha Component + 1 + Blue Component + 1 + Color Space + sRGB + Green Component + 0.99263292551040649 + Red Component + 0.37597531080245972 + + Ansi 14 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.792156862745098 + Color Space + sRGB + Green Component + 0.84313725490196079 + Red Component + 0.41960784313725491 + + Ansi 14 Color (Light) + + Alpha Component + 1 + Blue Component + 0.792156862745098 + Color Space + sRGB + Green Component + 0.84313725490196079 + Red Component + 0.41960784313725491 + + Ansi 15 Color + + Alpha Component + 1 + Blue Component + 1 + Color Space + sRGB + Green Component + 1 + Red Component + 0.99999600648880005 + + Ansi 15 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.72941176470588232 + + Ansi 15 Color (Light) + + Alpha Component + 1 + Blue Component + 0.87058823529411766 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.72941176470588232 + + Ansi 2 Color + + Alpha Component + 1 + Blue Component + 0.0 + Color Space + sRGB + Green Component + 0.7607843279838562 + Red Component + 0.0 + + Ansi 2 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.63137254901960782 + Color Space + sRGB + Green Component + 0.8901960784313725 + Red Component + 0.65098039215686276 + + Ansi 2 Color (Light) + + Alpha Component + 1 + Blue Component + 0.63137254901960782 + Color Space + sRGB + Green Component + 0.8901960784313725 + Red Component + 0.65098039215686276 + + Ansi 3 Color + + Alpha Component + 1 + Blue Component + 0.0 + Color Space + sRGB + Green Component + 0.76959484815597534 + Red Component + 0.78058648109436035 + + Ansi 3 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.68627450980392157 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.97647058823529409 + + Ansi 3 Color (Light) + + Alpha Component + 1 + Blue Component + 0.68627450980392157 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.97647058823529409 + + Ansi 4 Color + + Alpha Component + 1 + Blue Component + 0.78216177225112915 + Color Space + sRGB + Green Component + 0.26474356651306152 + Red Component + 0.15404300391674042 + + Ansi 4 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.98039215686274506 + Color Space + sRGB + Green Component + 0.70588235294117652 + Red Component + 0.53725490196078429 + + Ansi 4 Color (Light) + + Alpha Component + 1 + Blue Component + 0.98039215686274506 + Color Space + sRGB + Green Component + 0.70588235294117652 + Red Component + 0.53725490196078429 + + Ansi 5 Color + + Alpha Component + 1 + Blue Component + 0.74494361877441406 + Color Space + sRGB + Green Component + 0.24931684136390686 + Red Component + 0.752197265625 + + Ansi 5 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.90588235294117647 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.96078431372549022 + + Ansi 5 Color (Light) + + Alpha Component + 1 + Blue Component + 0.90588235294117647 + Color Space + sRGB + Green Component + 0.76078431372549016 + Red Component + 0.96078431372549022 + + Ansi 6 Color + + Alpha Component + 1 + Blue Component + 0.78166204690933228 + Color Space + sRGB + Green Component + 0.77425903081893921 + Red Component + 0.0 + + Ansi 6 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.83529411764705885 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.58039215686274515 + + Ansi 6 Color (Light) + + Alpha Component + 1 + Blue Component + 0.83529411764705885 + Color Space + sRGB + Green Component + 0.88627450980392153 + Red Component + 0.58039215686274515 + + Ansi 7 Color + + Alpha Component + 1 + Blue Component + 0.78104829788208008 + Color Space + sRGB + Green Component + 0.78105825185775757 + Red Component + 0.7810397744178772 + + Ansi 7 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.78431372549019607 + Color Space + sRGB + Green Component + 0.67843137254901964 + Red Component + 0.65098039215686276 + + Ansi 7 Color (Light) + + Alpha Component + 1 + Blue Component + 0.78431372549019607 + Color Space + sRGB + Green Component + 0.67843137254901964 + Red Component + 0.65098039215686276 + + Ansi 8 Color + + Alpha Component + 1 + Blue Component + 0.4078223705291748 + Color Space + sRGB + Green Component + 0.40782788395881653 + Red Component + 0.40781760215759277 + + Ansi 8 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + Ansi 8 Color (Light) + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + Ansi 9 Color + + Alpha Component + 1 + Blue Component + 0.45833224058151245 + Color Space + sRGB + Green Component + 0.47524076700210571 + Red Component + 0.8659515380859375 + + Ansi 9 Color (Dark) + + Alpha Component + 1 + Blue Component + 0.59999999999999998 + Color Space + sRGB + Green Component + 0.46666666666666667 + Red Component + 0.95294117647058818 + + Ansi 9 Color (Light) + + Alpha Component + 1 + Blue Component + 0.59999999999999998 + Color Space + sRGB + Green Component + 0.46666666666666667 + Red Component + 0.95294117647058818 + + BM Growl + + Background Color + + Alpha Component + 1 + Blue Component + 0.97999999999999998 + Color Space + sRGB + Green Component + 0.97999999999999998 + Red Component + 0.97999999999999998 + + Background Color (Dark) + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Background Color (Light) + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Background Image Location + + Badge Color + + Alpha Component + 0.5 + Blue Component + 0.11610633134841919 + Color Space + sRGB + Green Component + 0.11610633134841919 + Red Component + 0.74613857269287109 + + Badge Color (Dark) + + Alpha Component + 0.5 + Blue Component + 0.13960540294647217 + Color Space + P3 + Green Component + 0.25479039549827576 + Red Component + 0.92929404973983765 + + Badge Color (Light) + + Alpha Component + 0.5 + Blue Component + 0.13960540294647217 + Color Space + P3 + Green Component + 0.25479039549827576 + Red Component + 0.92929404973983765 + + Blinking Cursor + + Blur + + Bold Color + + Alpha Component + 1 + Blue Component + 0.062745101749897003 + Color Space + sRGB + Green Component + 0.062745101749897003 + Red Component + 0.062745101749897003 + + Bold Color (Dark) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Bold Color (Light) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Character Encoding + 4 + Close Sessions On End + + Columns + 80 + Command + /opt/homebrew/bin/fish + Cursor Color + + Alpha Component + 1 + Blue Component + 0.0 + Color Space + sRGB + Green Component + 0.0 + Red Component + 0.0 + + Cursor Color (Dark) + + Alpha Component + 1 + Blue Component + 0.86274509803921573 + Color Space + sRGB + Green Component + 0.8784313725490196 + Red Component + 0.96078431372549022 + + Cursor Color (Light) + + Alpha Component + 1 + Blue Component + 0.86274509803921573 + Color Space + sRGB + Green Component + 0.8784313725490196 + Red Component + 0.96078431372549022 + + Cursor Guide Color + + Alpha Component + 0.25 + Blue Component + 0.85319280624389648 + Color Space + sRGB + Green Component + 0.77217715978622437 + Red Component + 0.52338260412216187 + + Cursor Guide Color (Dark) + + Alpha Component + 0.070000000000000007 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Cursor Guide Color (Light) + + Alpha Component + 0.070000000000000007 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Cursor Text Color + + Alpha Component + 1 + Blue Component + 1 + Color Space + sRGB + Green Component + 1 + Red Component + 1 + + Cursor Text Color (Dark) + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Cursor Text Color (Light) + + Alpha Component + 1 + Blue Component + 0.1803921568627451 + Color Space + sRGB + Green Component + 0.11764705882352941 + Red Component + 0.11764705882352941 + + Custom Command + Custom Shell + Custom Directory + No + Default Bookmark + No + Description + Default + Disable Window Resizing + + Flashing Bell + + Foreground Color + + Alpha Component + 1 + Blue Component + 0.062745101749897003 + Color Space + sRGB + Green Component + 0.062745101749897003 + Red Component + 0.062745101749897003 + + Foreground Color (Dark) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Foreground Color (Light) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Guid + 67999E29-F912-49FB-8963-71295D790777 + Horizontal Spacing + 1 + Idle Code + 0 + Jobs to Ignore + + rlogin + ssh + slogin + telnet + + Keyboard Map + + Link Color + + Alpha Component + 1 + Blue Component + 0.9337158203125 + Color Space + sRGB + Green Component + 0.55789834260940552 + Red Component + 0.19802422821521759 + + Link Color (Dark) + + Alpha Component + 1 + Blue Component + 0.92156862745098034 + Color Space + sRGB + Green Component + 0.86274509803921573 + Red Component + 0.53725490196078429 + + Link Color (Light) + + Alpha Component + 1 + Blue Component + 0.92156862745098034 + Color Space + sRGB + Green Component + 0.86274509803921573 + Red Component + 0.53725490196078429 + + Load Shell Integration Automatically + + Match Background Color (Dark) + + Alpha Component + 1 + Blue Component + 0.32116127014160156 + Color Space + P3 + Green Component + 0.98600882291793823 + Red Component + 0.99697142839431763 + + Match Background Color (Light) + + Alpha Component + 1 + Blue Component + 0.32116127014160156 + Color Space + P3 + Green Component + 0.98600882291793823 + Red Component + 0.99697142839431763 + + Mouse Reporting + + Name + Default + Non Ascii Font + Monaco 12 + Non-ASCII Anti Aliased + + Normal Font + Monaco 12 + Option Key Sends + 0 + Prompt Before Closing 2 + + Right Option Key Sends + 0 + Rows + 25 + Screen + -1 + Scrollback Lines + 1000 + Selected Text Color + + Alpha Component + 1 + Blue Component + 0.0 + Color Space + sRGB + Green Component + 0.0 + Red Component + 0.0 + + Selected Text Color (Dark) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Selected Text Color (Light) + + Alpha Component + 1 + Blue Component + 0.95686274509803926 + Color Space + sRGB + Green Component + 0.83921568627450982 + Red Component + 0.80392156862745101 + + Selection Color + + Alpha Component + 1 + Blue Component + 1 + Color Space + sRGB + Green Component + 0.84313726425170898 + Red Component + 0.70196080207824707 + + Selection Color (Dark) + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + Selection Color (Light) + + Alpha Component + 1 + Blue Component + 0.4392156862745098 + Color Space + sRGB + Green Component + 0.35686274509803922 + Red Component + 0.34509803921568627 + + Send Code When Idle + + Shortcut + + Silence Bell + + Sync Title + + Tags + + Terminal Type + xterm-256color + Transparency + 0.0 + Unlimited Scrollback + + Use Bold Font + + Use Bright Bold + + Use Italic Font + + Use Non-ASCII Font + + Use Separate Colors for Light and Dark Mode + + Vertical Spacing + 1 + Visual Bell + + Window Type + 0 + Working Directory + /Users/benk + + + NoSyncAllAppVersions + + 3.6.9 + + NoSyncCommandHistoryHasEverBeenUsed + + NoSyncFrame_SharedPreferences + + screenFrame + {{0, 0}, {1637, 1024}} + topLeft + {88, 698} + + NoSyncIgnoreSystemWindowRestoration + + NoSyncInstallationId + 5AB01265-0CDC-42D1-B79A-8F5D276B0E90 + NoSyncLastOSVersion + Version 26.3.2 (Build 25D2140) + NoSyncLastSystemPythonVersionRequirement + 1.17 + NoSyncLaunchExperienceControllerRunCount + 1 + NoSyncNeverAllowPaste + + NoSyncNextAnnoyanceTime + 796416221.05094695 + NoSyncRecordedVariables + + 0 + + + isTerminal + + name + + nonterminalContext + 0 + + + 1 + + + isTerminal + + name + presentationName + nonterminalContext + 0 + + + isTerminal + + name + tmuxRole + nonterminalContext + 0 + + + isTerminal + + name + lastCommand + nonterminalContext + 0 + + + isTerminal + + name + profileName + nonterminalContext + 0 + + + isTerminal + + name + showingAlternateScreen + nonterminalContext + 0 + + + isTerminal + + name + id + nonterminalContext + 0 + + + isTerminal + + name + termid + nonterminalContext + 0 + + + isTerminal + + name + homeDirectory + nonterminalContext + 0 + + + isTerminal + + name + jobName + nonterminalContext + 0 + + + isTerminal + + name + columns + nonterminalContext + 0 + + + isTerminal + + name + uname + nonterminalContext + 0 + + + isTerminal + + name + tab.tmuxWindowTitle + nonterminalContext + 0 + + + isTerminal + + name + processTitle + nonterminalContext + 0 + + + isTerminal + + name + tmuxClientName + nonterminalContext + 0 + + + isTerminal + + name + hostname + nonterminalContext + 0 + + + isTerminal + + name + selectionLength + nonterminalContext + 0 + + + isTerminal + + name + mouseInfo + nonterminalContext + 0 + + + isTerminal + + name + path + nonterminalContext + 0 + + + isTerminal + + name + shell + nonterminalContext + 0 + + + isTerminal + + name + triggerName + nonterminalContext + 0 + + + isTerminal + + name + parentSession + nonterminalContext + 1 + + + isTerminal + + name + terminalIconName + nonterminalContext + 0 + + + isTerminal + + name + tmuxWindowPane + nonterminalContext + 0 + + + isTerminal + + name + mouseReportingMode + nonterminalContext + 0 + + + isTerminal + + name + name + nonterminalContext + 0 + + + isTerminal + + name + tmuxStatusRight + nonterminalContext + 0 + + + isTerminal + + name + iterm2 + nonterminalContext + 4 + + + isTerminal + + name + tmuxPaneTitle + nonterminalContext + 0 + + + isTerminal + + name + rows + nonterminalContext + 0 + + + isTerminal + + name + tmuxWindowPaneIndex + nonterminalContext + 0 + + + isTerminal + + name + tty + nonterminalContext + 0 + + + isTerminal + + name + autoLogId + nonterminalContext + 0 + + + isTerminal + + name + badge + nonterminalContext + 0 + + + isTerminal + + name + username + nonterminalContext + 0 + + + isTerminal + + name + logFilename + nonterminalContext + 0 + + + isTerminal + + name + effective_root_pid + nonterminalContext + 0 + + + isTerminal + + name + sshIntegrationLevel + nonterminalContext + 0 + + + isTerminal + + name + tab.tmuxWindowName + nonterminalContext + 0 + + + isTerminal + + name + tab + nonterminalContext + 2 + + + isTerminal + + name + tmuxStatusLeft + nonterminalContext + 0 + + + isTerminal + + name + selection + nonterminalContext + 0 + + + isTerminal + + name + bellCount + nonterminalContext + 0 + + + isTerminal + + name + autoNameFormat + nonterminalContext + 0 + + + isTerminal + + name + autoName + nonterminalContext + 0 + + + isTerminal + + name + terminalWindowName + nonterminalContext + 0 + + + isTerminal + + name + creationTimeString + nonterminalContext + 0 + + + isTerminal + + name + commandLine + nonterminalContext + 0 + + + isTerminal + + name + applicationKeypad + nonterminalContext + 0 + + + isTerminal + + name + jobPid + nonterminalContext + 0 + + + isTerminal + + name + pid + nonterminalContext + 0 + + + 16 + + + isTerminal + + name + style + nonterminalContext + 0 + + + isTerminal + + name + frame + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.pid + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.mouseInfo + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.termid + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.terminalWindowName + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.terminalIconName + nonterminalContext + 0 + + + isTerminal + + name + isHotkeyWindow + nonterminalContext + 0 + + + isTerminal + + name + currentTab + nonterminalContext + 2 + + + isTerminal + + name + currentTab.currentSession.processTitle + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.lastCommand + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession + nonterminalContext + 0 + + + isTerminal + + name + id + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.name + nonterminalContext + 0 + + + isTerminal + + name + titleOverride + nonterminalContext + 0 + + + isTerminal + + name + number + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.commandLine + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.effective_root_pid + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.path + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.hostname + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.tty + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.username + nonterminalContext + 0 + + + isTerminal + + name + iterm2 + nonterminalContext + 4 + + + isTerminal + + name + titleOverrideFormat + nonterminalContext + 0 + + + isTerminal + + name + currentTab.currentSession.jobName + nonterminalContext + 0 + + + 2 + + + isTerminal + + name + currentSession + nonterminalContext + 0 + + + isTerminal + + name + currentSession.commandLine + nonterminalContext + 0 + + + isTerminal + + name + title + nonterminalContext + 1 + + + isTerminal + + name + title + nonterminalContext + 0 + + + isTerminal + + name + tmuxWindowTitle + nonterminalContext + 0 + + + isTerminal + + name + currentSession.terminalIconName + nonterminalContext + 0 + + + isTerminal + + name + currentSession.effective_root_pid + nonterminalContext + 0 + + + isTerminal + + name + tmuxWindowName + nonterminalContext + 0 + + + isTerminal + + name + window + nonterminalContext + 16 + + + isTerminal + + name + currentSession.tty + nonterminalContext + 0 + + + isTerminal + + name + currentSession.jobName + nonterminalContext + 0 + + + isTerminal + + name + currentSession.name + nonterminalContext + 0 + + + isTerminal + + name + currentSession.processTitle + nonterminalContext + 0 + + + isTerminal + + name + currentSession.mouseInfo + nonterminalContext + 0 + + + isTerminal + + name + currentSession.lastCommand + nonterminalContext + 0 + + + isTerminal + + name + id + nonterminalContext + 0 + + + isTerminal + + name + titleOverride + nonterminalContext + 0 + + + isTerminal + + name + currentSession.username + nonterminalContext + 0 + + + isTerminal + + name + currentSession.termid + nonterminalContext + 0 + + + isTerminal + + name + iterm2 + nonterminalContext + 4 + + + isTerminal + + name + titleOverrideFormat + nonterminalContext + 0 + + + isTerminal + + name + currentSession.hostname + nonterminalContext + 0 + + + isTerminal + + name + currentSession.pid + nonterminalContext + 0 + + + isTerminal + + name + currentSession.path + nonterminalContext + 0 + + + isTerminal + + name + tmuxWindow + nonterminalContext + 0 + + + isTerminal + + name + currentSession + nonterminalContext + 1 + + + isTerminal + + name + currentSession.terminalWindowName + nonterminalContext + 0 + + + 4 + + + isTerminal + + name + pid + nonterminalContext + 0 + + + isTerminal + + name + localhostName + nonterminalContext + 0 + + + isTerminal + + name + effectiveTheme + nonterminalContext + 0 + + + + NoSyncRemoveDeprecatedKeyMappings + 1 + NoSyncRestoreWindowsCount + 0 + NoSyncTipOfTheDayEligibilityBeganTime + 796243421.05083001 + NoSyncUserHasSelectedCommand + + NoSyncWindowRestoresWorkspaceAtLaunch + + PreventEscapeSequenceFromClearingHistory + + SUFeedAlternateAppNameKey + iTerm + SUFeedURL + https://iterm2.com/appcasts/final_modern.xml?shard=71 + SUHasLaunchedBefore + + SoundForEsc + + TabStyleWithAutomaticOption + 5 + VisualIndicatorForEsc + + iTerm Version + 3.6.9 + + diff --git a/packages/arch.txt b/packages/arch.txt index d8a7a46..ae5a419 100644 --- a/packages/arch.txt +++ b/packages/arch.txt @@ -2,6 +2,7 @@ fish tmux neovim wezterm +fastfetch btop ripgrep fd @@ -18,3 +19,5 @@ nodejs python nginx base-devel +obsidian +syncthing diff --git a/packages/brew-casks.txt b/packages/brew-casks.txt new file mode 100644 index 0000000..b98fced --- /dev/null +++ b/packages/brew-casks.txt @@ -0,0 +1,4 @@ +# casks — installed via `brew install --cask` +iterm2 +obsidian +thunderbird diff --git a/packages/brew.txt b/packages/brew.txt index 8b8795b..95e9532 100644 --- a/packages/brew.txt +++ b/packages/brew.txt @@ -1,7 +1,7 @@ +# formulae — installed via `brew install` fish tmux -nvim -wezterm +neovim btop ripgrep fd @@ -9,11 +9,12 @@ git curl wget rsync +fastfetch lua-language-server typescript-language-server pyright bash-language-server cmake node -python3 -xclip +python@3.14 +syncthing diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a894ac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +rich>=13.0.0 +questionary>=2.0.0 diff --git a/setup.toml b/setup.toml new file mode 100644 index 0000000..2cd65eb --- /dev/null +++ b/setup.toml @@ -0,0 +1,12 @@ +# setup.toml — declarative tooling config +# Packages live in packages/brew.txt and packages/arch.txt. +# This file covers everything that isn't a package manager install. + +[fish] +# Fisher plugins to install. Run `fisher list` to see what's currently installed. +plugins = [] + +[tmux] +# TPM plugins to install headlessly via ~/.tmux/plugins/tpm/bin/install_plugins. +# Format: "owner/repo" matching the TPM convention. +plugins = [] diff --git a/themes/catppuccin-iterm b/themes/catppuccin-iterm new file mode 160000 index 0000000..b2936a6 --- /dev/null +++ b/themes/catppuccin-iterm @@ -0,0 +1 @@ +Subproject commit b2936a6e55270fcc55421b1bb7d5fd194a489591