commit f8d61dd49e2499a7c80091876e7bb3f051a866cb
Author: Dasho <git@dasho.dev>
Date: Tue, 27 Jan 2026 06:17:54 +0000
Early Dotfile Fun
Diffstat:
| A | .gitignore | | | 2 | ++ |
| A | Brewfile | | | 179 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | Justfile | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | README.md | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | bin/backup | | | 14 | ++++++++++++++ |
| A | bin/backup-secrets | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | bin/bootstrap | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
| A | bin/restore-secrets | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | bin/wizard | | | 1038 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | brew/leaves.txt | | | 60 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | config/direnv/direnvrc | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | config/git/config | | | 22 | ++++++++++++++++++++++ |
| A | config/nvim/init.lua | | | 353 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | config/redbrick/config.toml | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | zsh/secrets.zsh | | | 119 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | zsh/zshrc | | | 187 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
16 files changed, 2402 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+private
+password
diff --git a/Brewfile b/Brewfile
@@ -0,0 +1,179 @@
+tap "antoniorodr/lexy"
+tap "charmbracelet/tap"
+tap "danielgatis/imgcat"
+tap "steipete/tap"
+# Clone of cat(1) with syntax highlighting and Git integration
+brew "bat"
+# Draw boxes around text
+brew "boxes"
+# GNU compiler collection
+brew "gcc"
+# Resource monitor. C++ version and continuation of bashtop and bpytop
+brew "btop"
+# Statistics utility to count lines of code
+brew "cloc"
+# Securely send things from one computer to another
+brew "croc"
+# Load/unload environment variables based on $PWD
+brew "direnv"
+# Perl lib for reading and writing EXIF metadata
+brew "exiftool"
+# Modern, maintained replacement for ls
+brew "eza"
+# Command-line fuzzy finder written in Go
+brew "fzf"
+# Open-source, cross-platform JavaScript runtime environment
+brew "node"
+# Interact with Google Gemini AI models from the command-line
+brew "gemini-cli"
+# GitHub command-line tool
+brew "gh"
+# Render markdown on the CLI
+brew "glow"
+# Open source programming language to build simple/reliable/efficient software
+brew "go"
+# Tool for glamorous shell scripts
+brew "gum"
+# CLI email client written in Rust
+brew "himalaya"
+# Tools and libraries to manipulate images in select formats
+brew "imagemagick"
+# Modular IRC client
+brew "irssi"
+# Handy way to save and run project-specific commands
+brew "just"
+# Rainbows and unicorns in your console!
+brew "lolcat"
+# Securely transfers data between computers
+brew "magic-wormhole"
+# CLI tool for saving complete web pages as a single HTML file
+brew "monolith"
+# Fast, highly customisable system info script
+brew "neofetch"
+# Ambitious Vim-fork focused on extensibility and agility
+brew "neovim"
+# General-purpose speech recognition model
+brew "openai-whisper"
+# Search tool like grep and The Silver Searcher
+brew "ripgrep"
+# AI coding agent, built for the terminal
+brew "opencode"
+# Password manager
+brew "pass"
+# Animated pipes terminal screensaver
+brew "pipes-sh"
+# Send emails from your terminal
+brew "pop"
+# Hook preloader
+brew "proxychains-ng"
+# Run a command when files change
+brew "reflex"
+# Safe, concurrent, practical language
+brew "rust"
+# Spreadsheet program for the terminal, using ncurses
+brew "sc-im"
+# 7-Zip is a file archiver with a high compression ratio
+brew "sevenzip"
+# Editor of encrypted files
+brew "sops"
+# Fast, collaborative live terminal sharing over the web
+brew "sshx"
+# Tool Command Language
+brew "tcl-tk"
+# Terminal multiplexer
+brew "tmux"
+# Anonymizing overlay network for TCP
+brew "tor", restart_service: :changed
+# Use SOCKS-friendly applications with Tor
+brew "torsocks"
+# Command-line tool for sharing terminal over the web
+brew "ttyd"
+# Command-line unarchiving tools supporting multiple formats
+brew "unar"
+# Extremely fast Python package installer and resolver, written in Rust
+brew "uv"
+# Your CLI home video recorder
+brew "vhs"
+# Extensible IRC client
+brew "weechat"
+# Pluggable terminal workspace, with terminal multiplexer as the base feature
+brew "zellij"
+# Shell extension to navigate your filesystem faster
+brew "zoxide"
+# UNIX shell (command interpreter)
+brew "zsh"
+# CLI tool that fetches programming tutorials from "Learn X in Y Minutes" directly into your terminal.
+brew "antoniorodr/lexy/lexy"
+# A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.
+brew "charmbracelet/tap/crush"
+# AI on the command line
+brew "charmbracelet/tap/mods"
+# A personal key value store πΌ
+brew "charmbracelet/tap/skate"
+# A tasty, self-hostable Git server for the command lineπ¦
+brew "charmbracelet/tap/soft-serve"
+# The SSH directory
+brew "charmbracelet/tap/wishlist"
+# Display images and gifs in your terminal
+brew "danielgatis/imgcat/imgcat"
+# Grep the GIF. Stick the landing
+brew "steipete/tap/gifgrep"
+# Google CLI for Gmail, Calendar, Drive, and Contacts
+brew "steipete/tap/gogcli"
+# Send and read iMessage / SMS from the terminal
+brew "steipete/tap/imsg"
+cask "font-monaspace"
+cask "font-ubuntu-nerd-font"
+vscode "benjaminbenais.copilot-theme"
+vscode "bmewburn.vscode-intelephense-client"
+vscode "github.copilot"
+vscode "github.copilot-chat"
+vscode "github.remotehub"
+vscode "golang.go"
+vscode "ms-azuretools.vscode-containers"
+vscode "ms-python.debugpy"
+vscode "ms-python.python"
+vscode "ms-python.vscode-pylance"
+vscode "ms-python.vscode-python-envs"
+vscode "ms-toolsai.jupyter"
+vscode "ms-toolsai.jupyter-keymap"
+vscode "ms-toolsai.jupyter-renderers"
+vscode "ms-toolsai.vscode-jupyter-cell-tags"
+vscode "ms-toolsai.vscode-jupyter-slideshow"
+vscode "ms-vscode-remote.remote-containers"
+vscode "ms-vscode-remote.remote-ssh"
+vscode "ms-vscode-remote.remote-ssh-edit"
+vscode "ms-vscode-remote.remote-wsl"
+vscode "ms-vscode-remote.vscode-remote-extensionpack"
+vscode "ms-vscode.azure-repos"
+vscode "ms-vscode.cmake-tools"
+vscode "ms-vscode.cpptools"
+vscode "ms-vscode.cpptools-extension-pack"
+vscode "ms-vscode.cpptools-themes"
+vscode "ms-vscode.makefile-tools"
+vscode "ms-vscode.remote-explorer"
+vscode "ms-vscode.remote-repositories"
+vscode "ms-vscode.remote-server"
+vscode "openai.chatgpt"
+vscode "rust-lang.rust-analyzer"
+go "github.com/air-verse/air"
+go "golang.org/x/tools/gopls"
+go "github.com/golang-migrate/migrate/v4/cmd/migrate"
+go "github.com/cespare/reflex"
+go "github.com/sqlc-dev/sqlc/cmd/sqlc"
+go "honnef.co/go/tools/cmd/staticcheck"
+cargo "cross"
+cargo "freenet"
+flatpak "app.zen_browser.zen"
+flatpak "chat.simplex.simplex"
+flatpak "com.discordapp.Discord"
+flatpak "com.protonvpn.www"
+flatpak "com.vivaldi.Vivaldi"
+flatpak "io.github.celluloid_player.Celluloid"
+flatpak "io.github.flattool.Warehouse"
+flatpak "io.github.peazip.PeaZip"
+flatpak "io.github.picocrypt.Picocrypt"
+flatpak "it.mijorus.smile"
+flatpak "org.gimp.GIMP"
+flatpak "org.kiwix.desktop"
+flatpak "org.telegram.desktop"
diff --git a/Justfile b/Justfile
@@ -0,0 +1,91 @@
+# Justfile
+# ------------------------------------------------------------------------------
+# Dotfiles + secrets workflow
+#
+# Quick start:
+# just bootstrap
+# just backup
+# just backup-secrets
+#
+# Notes:
+# - This repo is intended to be cloned to: ~/.dotfiles
+# - Encrypted archives should be committed ONLY to a private remote
+# ------------------------------------------------------------------------------
+
+default := "help"
+
+dotdir := env_var_or_default("DOTFILES_DIR", "~/.dotfiles")
+private_dir := "private"
+
+help:
+ @echo ""
+ @echo "Dotfiles & Secrets Commands"
+ @echo "--------------------------"
+ @echo "wizard π§ Interactive wizard for all operations (recommended!)"
+ @echo "bootstrap Install Homebrew (if needed), brew bundle install, link configs."
+ @echo "backup Update Brewfile and brew/leaves.txt from current machine."
+ @echo "backup-secrets Create encrypted archive (GPG/SSH/age/Skate) into private/."
+ @echo "restore-secrets A Decrypt and restore from archive A."
+ @echo "link Symlink dotfiles into expected locations (~/.zshrc, ~/.config/direnv/, etc)."
+ @echo "check Verify core tools exist (direnv, skate, age, just)."
+ @echo ""
+ @echo "When to use:"
+ @echo "- RECOMMENDED: just wizard (interactive, guided experience)"
+ @echo "- On a NEW machine: just bootstrap && just restore-secrets private/keys-YYYYMMDD.tar.gz.age"
+ @echo "- After changes: just backup && git commit"
+ @echo "- Periodically: just backup-secrets && git commit (private repo!)"
+ @echo ""
+
+check:
+ @command -v direnv >/dev/null 2>&1 || (echo "Missing: direnv" && exit 1)
+ @command -v skate >/dev/null 2>&1 || (echo "Missing: skate" && exit 1)
+ @command -v age >/dev/null 2>&1 || (echo "Missing: age" && exit 1)
+ @command -v just >/dev/null 2>&1 || (echo "Missing: just" && exit 1)
+ @echo "OK"
+
+# π§ Interactive wizard - the magical way to manage dotfiles
+wizard:
+ @zsh {{justfile_directory()}}/bin/wizard
+
+# Alias for wizard
+w: wizard
+easy: wizard
+
+# Link configs into expected locations.
+link:
+ @mkdir -p ~/.config/direnv
+ @mkdir -p ~/.config/git
+ @mkdir -p ~/.config/nvim
+ @mkdir -p ~/.config/redbrick
+
+ @ln -snf {{justfile_directory()}}/zsh/zshrc ~/.zshrc
+ @echo "Linked ~/.zshrc"
+ @ln -snf {{justfile_directory()}}/config/direnv/direnvrc ~/.config/direnv/direnvrc
+ @echo "Linked ~/.config/direnv/direnvrc"
+ @ln -snf {{justfile_directory()}}/config/git/config ~/.gitconfig
+ @echo "Linked ~/.gitconfig"
+ @ln -snf {{justfile_directory()}}/config/git/config ~/.config/git/config
+ @echo "Linked ~/.config/git/config"
+ @ln -snf {{justfile_directory()}}/config/nvim/init.lua ~/.config/nvim/init.lua
+ @echo "Linked ~/.config/nvim/init.lua"
+ @ln -snf {{justfile_directory()}}/config/redbrick ~/.config/redbrick
+ @echo "Linked ~/.config/redbrick/*"
+ @echo "Linked all dotfiles."
+
+# Full new-machine setup.
+bootstrap:
+ @bash {{justfile_directory()}}/bin/bootstrap
+
+# Update Brewfile + leaves list.
+backup:
+ @bash {{justfile_directory()}}/bin/backup
+
+# Create encrypted backup archive in private/
+backup-secrets:
+ @mkdir -p {{justfile_directory()}}/{{private_dir}}
+ @bash {{justfile_directory()}}/bin/backup-secrets
+
+# Restore from an encrypted archive
+restore-secrets archive:
+ @bash {{justfile_directory()}}/bin/restore-secrets {{archive}}
+
diff --git a/README.md b/README.md
@@ -0,0 +1,56 @@
+# π§ Dasho's Dotfiles
+
+My personal dotfiles, managed by a magical interactive wizard.
+
+This repository contains my personal configuration for `zsh`, `git`, `nvim`, and other tools. It's designed to be portable and easy to set up on any new machine.
+
+> **Pro tip**: My single init.lua file for Neovim is [here](config/nvim/init.lua) and it is awesome. I personally really like the look of it and it is very fast, yet inuitive for newer users.
+
+## Quick Start
+
+The easiest way to use this repository is with the interactive wizard. It handles everything from initial setup to backups and git operations.
+
+Just a warning though, it can be a little broken now and then. I might improve it soon but if you want to, feel free to open an issue or PR!
+
+```bash
+# Clone the repo (if you haven't already)
+git clone <your-repo-url> ~/.dotfiles
+cd ~/.dotfiles
+
+# Run the wizard!
+just wizard
+```
+
+The wizard will guide you through:
+- **Bootstrapping** a new machine (installing Homebrew, packages, etc.)
+- **Backing up** installed packages and secrets.
+- **Restoring** secrets from an encrypted archive.
+- **Linking** all configuration files to their correct locations.
+- **Managing** this git repository with a friendly UI.
+
+## π Secrets Management
+
+This repository has a built-in workflow for securely managing sensitive files like GPG keys, SSH keys, and other credentials using `age` encryption.
+
+### How it Works
+
+1. **Backup (`just wizard` -> `Backup secrets`)**:
+ - The `backup-secrets` script gathers credentials from common locations:
+ - GPG keys (`~/.gnupg`)
+ - SSH keys (`~/.ssh`)
+ - `age` identities (`~/.config/age/keys.txt`)
+ - `skate` database entries
+ - Oh-My-Zsh custom configurations (`~/.oh-my-zsh/custom`)
+ - It bundles them into a `tar.gz` archive.
+ - It encrypts this archive using `age` into a file like `private/keys-YYYYMMDD.tar.gz.age`.
+
+2. **Storage**:
+ - The encrypted archive is saved in the `private/` directory.
+ - **β οΈ IMPORTANT**: The `private/` directory is intended to be committed to a **private Git repository only**. Do not expose your encrypted secrets on a public repository.
+
+3. **Restore (`just wizard` -> `Restore secrets`)**:
+ - The `restore-secrets` script prompts you to choose an archive from the `private/` directory.
+ - It decrypts the archive using your `age` identity.
+ - It restores the files to their original locations on the new machine.
+
+This system allows you to safely version control your secrets and easily provision a new machine with all your configurations and credentials.
diff --git a/bin/backup b/bin/backup
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+mkdir -p "$ROOT/brew"
+
+if command -v brew >/dev/null 2>&1; then
+ brew bundle dump --force --describe --file "$ROOT/Brewfile"
+ brew leaves > "$ROOT/brew/leaves.txt"
+ echo "Wrote Brewfile and brew/leaves.txt"
+else
+ echo "brew not found; skipping Brewfile dump" >&2
+fi
+
diff --git a/bin/backup-secrets b/bin/backup-secrets
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+OUT="${1:-$ROOT/private/keys-$(date +%Y%m%d).tar.gz.age}"
+mkdir -p "$(dirname "$OUT")"
+
+TMP="$(mktemp -d)"
+echo "Using temporary directory: $TMP"
+trap 'rm -rf "$TMP"' EXIT
+
+mkdir -p "$TMP/gpg" "$TMP/ssh" "$TMP/age" "$TMP/skate" "$TMP/omz"
+
+# --- GPG export ---
+if command -v gpg >/dev/null 2>&1; then
+ gpg --export --armor > "$TMP/gpg/public-keys.asc" || true
+ gpg --export-secret-keys --armor > "$TMP/gpg/secret-keys.asc" || true
+ gpg --export-ownertrust > "$TMP/gpg/ownertrust.txt" || true
+fi
+
+# --- SSH backup (selected files) ---
+if [[ -d "$HOME/.ssh" ]]; then
+ rsync -a \
+ --include='config' \
+ --include='known_hosts' \
+ --include='id_*' \
+ --exclude='*' \
+ "$HOME/.ssh/" "$TMP/ssh/" || true
+fi
+
+# --- AGE identities (common locations) ---
+for f in \
+ "$HOME/.config/age/keys.txt" \
+ "$HOME/.config/age/key.txt" \
+ "$HOME/.config/age/age.key" \
+ "$HOME/key.txt" \
+ "$HOME/age.key"
+do
+ [[ -f "$f" ]] && cp -p "$f" "$TMP/age/"
+done
+
+# --- Skate export (JSONL; values base64) ---
+if command -v skate >/dev/null 2>&1; then
+ python3 - <<'PY' > "$TMP/skate/skate.jsonl"
+import base64, json, subprocess
+def sh(*cmd):
+ return subprocess.check_output(list(cmd))
+dbs = sh("skate","list-dbs").decode().splitlines()
+for db in dbs:
+ keys = sh("skate","list","-k",f"{db}").decode().splitlines()
+ for k in keys:
+ v = sh("skate","get",f"{k}{db}")
+ print(json.dumps({"db": db, "key": k, "b64": base64.b64encode(v).decode()}))
+PY
+fi
+
+# --- Oh-My-Zsh custom configs ---
+if [[ -d "$HOME/.oh-my-zsh/custom" ]]; then
+ echo "Backing up Oh-My-Zsh custom configs..."
+ rsync -a "$HOME/.oh-my-zsh/custom/" "$TMP/omz/custom/" || true
+fi
+
+# --- Encrypt archive ---
+echo "Creating encrypted archive: $OUT"
+if [[ -n "${AGE_RECIPIENT:-}" ]]; then
+ tar -C "$TMP" -czf - . | age -r "$AGE_RECIPIENT" -o "$OUT"
+else
+ tar -C "$TMP" -czf - . | age -p -o "$OUT"
+fi
+
+echo "Wrote: $OUT"
+
diff --git a/bin/bootstrap b/bin/bootstrap
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+DOT="$HOME/.dotfiles"
+
+mkdir -p "$HOME/.config/direnv"
+mkdir -p "$HOME/.config/nvim"
+mkdir -p "$HOME/.config/redbrick"
+mkdir -p "$HOME/.config/git"
+
+# Install Homebrew if missing
+if ! command -v brew >/dev/null 2>&1; then
+ echo "Installing Homebrew..."
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+
+ # Make brew available in this script run
+ if [[ -x /opt/homebrew/bin/brew ]]; then
+ eval "$(/opt/homebrew/bin/brew shellenv)"
+ elif [[ -x /usr/local/bin/brew ]]; then
+ eval "$(/usr/local/bin/brew shellenv)"
+ elif [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
+ fi
+fi
+
+# Install packages
+if [[ -f "$ROOT/Brewfile" ]]; then
+ echo "Installing Brewfile packages..."
+ brew bundle install --file "$ROOT/Brewfile"
+else
+ echo "No Brewfile found; skipping brew bundle install" >&2
+fi
+
+# Symlink configs
+echo "Linking configs..."
+ln -snf "$ROOT/zsh/zshrc" "$HOME/.zshrc"
+ln -snf "$ROOT/zsh/secret.zsh" "$DOT/zsh/secret.zsh" 2>/dev/null || true
+ln -snf "$ROOT/config/direnv/direnvrc" "$HOME/.config/direnv/direnvrc"
+ln -snf "$ROOT/config/git/config" "$HOME/.gitconfig"
+ls -snf "$ROOT/config/git/config" "$HOME/.config/git/config" 2>/dev/null || true
+ln -snf "$ROOT/config/nvim/init.lua" "$HOME/.config/nvim/init.lua"
+ln -snf "$ROOT/config/redbrick" "$HOME/.config/redbrick" 2>/dev/null || true
+
+echo "Bootstrap complete. Restart your shell."
+
diff --git a/bin/restore-secrets b/bin/restore-secrets
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ARCHIVE="${1:?Usage: restore-secrets path/to/keys-YYYYMMDD.tar.gz.age}"
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+
+age -d "$ARCHIVE" | tar -xz -C "$TMP"
+
+# GPG restore
+if command -v gpg >/dev/null 2>&1 && [[ -f "$TMP/gpg/secret-keys.asc" ]]; then
+ gpg --import "$TMP/gpg/public-keys.asc" 2>/dev/null || true
+ gpg --import "$TMP/gpg/secret-keys.asc"
+ [[ -f "$TMP/gpg/ownertrust.txt" ]] && gpg --import-ownertrust "$TMP/gpg/ownertrust.txt" || true
+fi
+
+# SSH restore
+if [[ -d "$TMP/ssh" ]]; then
+ mkdir -p "$HOME/.ssh"
+ rsync -a "$TMP/ssh/" "$HOME/.ssh/"
+ chmod 700 "$HOME/.ssh"
+ chmod 600 "$HOME/.ssh"/id_* 2>/dev/null || true
+ chmod 644 "$HOME/.ssh"/*.pub 2>/dev/null || true
+fi
+
+# AGE restore
+if [[ -d "$TMP/age" ]]; then
+ mkdir -p "$HOME/.config/age"
+ rsync -a "$TMP/age/" "$HOME/.config/age/"
+ chmod 700 "$HOME/.config/age"
+ chmod 600 "$HOME/.config/age"/* 2>/dev/null || true
+fi
+
+# Skate restore
+if command -v skate >/dev/null 2>&1 && [[ -f "$TMP/skate/skate.jsonl" ]]; then
+ python3 - "$TMP" <<'PY'
+import base64, json, subprocess, sys, pathlib
+tmp = pathlib.Path(sys.argv[1])
+path = tmp / "skate" / "skate.jsonl"
+for line in path.read_text().splitlines():
+ rec = json.loads(line)
+ val = base64.b64decode(rec["b64"])
+ subprocess.check_call(["skate","set",f'{rec["key"]}@{rec["db"]}'], input=val)
+PY
+fi
+
+# Oh-My-Zsh custom configs restore
+if [[ -d "$TMP/omz/custom" ]]; then
+ echo "Restoring Oh-My-Zsh custom configs..."
+ mkdir -p "$HOME/.oh-my-zsh/custom"
+ rsync -a "$TMP/omz/custom/" "$HOME/.oh-my-zsh/custom/"
+fi
+
+echo "Restore complete."
+
diff --git a/bin/wizard b/bin/wizard
@@ -0,0 +1,1038 @@
+#!/usr/bin/env zsh
+# Dasho's Dotfiles Wizard π§
+# A magical, self-contained interactive wizard for managing dotfiles
+# Works on fresh Linux/macOS systems with just internet access
+
+set -eo pipefail
+
+# Color Palette (Soft Gradients & Pastels)
+# ----------------------------------------
+# 183 - Soft Lavender (headers, prompts)
+# 219 - Light Pink (selected items, cursor)
+# 147 - Muted Purple (list items)
+# 117 - Pale Cyan (info messages)
+# 114 - Soft Green (success messages)
+# 210 - Coral (error messages)
+# 222 - Peach (warnings)
+# 242 - Warm Gray (subtle text)
+
+# Colors for fallback (before gum is available)
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+MAGENTA='\033[0;35m'
+CYAN='\033[0;36m'
+BOLD='\033[1m'
+RESET='\033[0m'
+
+# Paths
+SCRIPT_DIR="$(cd "$(dirname "${(%):-%x}")" && pwd)"
+ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+PRIVATE_DIR="$ROOT/private"
+
+# ============================================================================
+# SELF-BOOTSTRAP: Install wizard dependencies
+# ============================================================================
+
+has_command() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+install_homebrew() {
+ echo "${BLUE}π¦ Installing Homebrew...${RESET}"
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+ if [[ -x /opt/homebrew/bin/brew ]]; then
+ eval "$(/opt/homebrew/bin/brew shellenv)"
+ elif [[ -x /usr/local/bin/brew ]]; then
+ eval "$(/usr/local/bin/brew shellenv)"
+ fi
+ else
+ # Linux
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+ if [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
+ fi
+ fi
+}
+
+wizard_bootstrap() {
+ echo "${MAGENTA}${BOLD}"
+ echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ echo "β β"
+ echo "β π§ Dasho's Dotfiles Wizard π§ β"
+ echo "β β"
+ echo "β Making magic happen on your machine... β"
+ echo "β β"
+ echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ echo "${RESET}"
+ echo ""
+ echo "${CYAN}Checking for wizard dependencies...${RESET}"
+ echo ""
+
+ # Check/install Homebrew first
+ if ! has_command brew; then
+ echo "${YELLOW}β οΈ Homebrew not found. Installing...${RESET}"
+ install_homebrew
+ else
+ echo "${GREEN}β${RESET} Homebrew"
+ fi
+
+ # Install essential tools
+ local tools=("gum" "glow" "age" "jq")
+ local to_install=()
+
+ for tool in "${tools[@]}"; do
+ if ! has_command "$tool"; then
+ echo "${YELLOW}β οΈ $tool not found${RESET}"
+ to_install+=("$tool")
+ else
+ echo "${GREEN}β${RESET} $tool"
+ fi
+ done
+
+ if [[ ${#to_install[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BLUE}π¦ Installing missing tools: ${to_install[*]}${RESET}"
+ brew install "${to_install[@]}"
+ echo ""
+ echo "${GREEN}β All wizard dependencies installed!${RESET}"
+ else
+ echo ""
+ echo "${GREEN}β All wizard dependencies present!${RESET}"
+ fi
+
+ echo ""
+ sleep 1
+}
+
+# ============================================================================
+# GUM-POWERED UI HELPERS
+# ============================================================================
+
+show_header() {
+ clear
+ gum style \
+ --border double \
+ --border-foreground 183 \
+ --padding "1 2" \
+ --margin "1 0" \
+ --align center \
+ --foreground 219 \
+ "π§ Dasho's Dotfiles Wizard π§" \
+ "" \
+ "Your magical companion for dotfiles mastery"
+}
+
+show_success() {
+ gum style \
+ --foreground 114 \
+ "β $1"
+}
+
+show_error() {
+ gum style \
+ --foreground 210 \
+ "β $1"
+}
+
+show_info() {
+ gum style \
+ --foreground 117 \
+ "βΉ $1"
+}
+
+spinner_run() {
+ local title="$1"
+ shift
+ gum spin --spinner dot --title "$title" -- "$@"
+}
+
+wait_for_key() {
+ gum style --foreground 242 --italic "Press any key to continue..."
+ read -k1 -s
+ return 0
+}
+
+confirm() {
+ command gum confirm \
+ --prompt.foreground 183 \
+ --selected.foreground 219 \
+ "$@"
+}
+
+# ============================================================================
+# ERROR HANDLING WITH RETRY/SKIP OPTIONS
+# ============================================================================
+
+handle_error() {
+ local error_msg="$1"
+ local context="$2"
+
+ show_error "$error_msg"
+ echo ""
+
+ gum style --foreground 183 --italic "What would you like to do?"
+
+ local choice=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β " \
+ "π Retry" \
+ "βοΈ Skip and continue" \
+ "π Exit wizard" \
+ "π Show error details")
+
+ case "$choice" in
+ *Retry*)
+ return 0
+ ;;
+ *Skip*)
+ show_info "Skipping: $context"
+ sleep 1
+ return 1
+ ;;
+ *Exit*)
+ echo ""
+ gum style --foreground 9 "Exiting wizard. See you next time! π"
+ exit 1
+ ;;
+ *details*)
+ gum style --foreground 8 "$context"
+ echo ""
+ wait_for_key
+ handle_error "$error_msg" "$context"
+ return $?
+ ;;
+ esac
+}
+
+safe_run() {
+ local description="$1"
+ shift
+
+ local output
+ local exit_code
+
+ while true; do
+ output=$("$@" 2>&1) && exit_code=0 || exit_code=$?
+
+ if [[ $exit_code -eq 0 ]]; then
+ return 0
+ else
+ if handle_error "$description failed" "$output"; then
+ continue # Retry
+ else
+ return 1 # Skip
+ fi
+ fi
+ done
+}
+
+# ============================================================================
+# CORE OPERATIONS
+# ============================================================================
+
+run_bootstrap() {
+ show_header
+ gum style --foreground 183 --bold "π Bootstrap: Fresh Machine Setup"
+ echo ""
+
+ gum style --foreground 183 "This will install Homebrew (if needed), install packages from Brewfile,"
+ gum style --foreground 183 "and link all your dotfiles to the right places."
+ echo ""
+
+ if ! confirm "Ready to bootstrap this machine?"; then
+ return
+ fi
+
+ echo ""
+ spinner_run "Installing Homebrew if needed..." bash "$ROOT/bin/bootstrap"
+
+ if [[ $? -eq 0 ]]; then
+ echo ""
+ show_success "Bootstrap completed successfully!"
+ echo ""
+ gum style --foreground 222 "β οΈ Remember to restart your shell or run: source ~/.zshrc"
+ else
+ safe_run "Bootstrap" bash "$ROOT/bin/bootstrap"
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+run_backup() {
+ show_header
+ gum style --foreground 183 --bold "πΎ Backup: Save Brewfile & Package List"
+ echo ""
+
+ gum style --foreground 183 "Updates Brewfile and brew/leaves.txt with currently installed packages."
+ echo ""
+
+ if ! confirm "Run backup now?"; then
+ return
+ fi
+
+ echo ""
+ spinner_run "Backing up Brewfile and brew leaves..." bash "$ROOT/bin/backup"
+
+ echo ""
+ show_success "Backup completed!"
+
+ if has_command glow && [[ -f "$ROOT/Brewfile" ]]; then
+ echo ""
+ if confirm "View the updated Brewfile?"; then
+ glow "$ROOT/Brewfile" -p
+ fi
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+run_backup_secrets() {
+ show_header
+ gum style --foreground 183 --bold "π Backup Secrets: Encrypt GPG/SSH/Age/Skate/Oh-My-Zsh"
+ echo ""
+
+ gum style --foreground 183 "Creates an encrypted archive with:"
+ gum style --foreground 147 " β’ GPG keys"
+ gum style --foreground 147 " β’ SSH keys and config"
+ gum style --foreground 147 " β’ Age identities"
+ gum style --foreground 147 " β’ Skate database"
+ gum style --foreground 147 " β’ Oh-My-Zsh custom configs"
+ echo ""
+
+ local default_name="keys-$(date +%Y%m%d).tar.gz.age"
+ local archive_name=$(gum input \
+ --placeholder "$default_name" \
+ --prompt "Archive name: " \
+ --value "$default_name" \
+ --prompt.foreground 183 \
+ --cursor.foreground 219)
+
+ [[ -z "$archive_name" ]] && archive_name="$default_name"
+
+ local archive_path="$PRIVATE_DIR/$archive_name"
+
+ echo ""
+ spinner_run "Creating encrypted backup..." bash "$ROOT/bin/backup-secrets" "$archive_path"
+
+ echo ""
+ show_success "Secrets backed up to: $archive_name"
+
+ echo ""
+ wait_for_key
+}
+
+run_restore_secrets() {
+ show_header
+ gum style --foreground 183 --bold "π Restore Secrets: Decrypt & Restore"
+ echo ""
+
+ if [[ ! -d "$PRIVATE_DIR" ]] || [[ -z "$(ls -A "$PRIVATE_DIR"/*.age 2>/dev/null)" ]]; then
+ show_error "No encrypted archives found in $PRIVATE_DIR"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ local archives=($(ls "$PRIVATE_DIR"/*.age 2>/dev/null))
+
+ if [[ ${#archives[@]} -eq 0 ]]; then
+ show_error "No .age archives found"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ gum style --foreground 183 "Select an archive to restore:"
+ echo ""
+
+ local selected=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β " \
+ "${archives[@]##*/}")
+ local archive_path="$PRIVATE_DIR/$selected"
+
+ echo ""
+ gum style --foreground 222 "β οΈ This will restore GPG, SSH, Age, Skate, and Oh-My-Zsh configs"
+
+ if ! confirm "Restore from $selected?"; then
+ return
+ fi
+
+ echo ""
+ spinner_run "Restoring secrets..." bash "$ROOT/bin/restore-secrets" "$archive_path"
+
+ echo ""
+ show_success "Secrets restored successfully!"
+
+ echo ""
+ wait_for_key
+}
+
+run_link() {
+ show_header
+ gum style --foreground 183 --bold "π Link: Symlink Dotfiles"
+ echo ""
+
+ gum style --foreground 183 "Creates symlinks for:"
+ gum style --foreground 147 " β’ ~/.zshrc"
+ gum style --foreground 147 " β’ ~/.config/direnv/direnvrc"
+ gum style --foreground 147 " β’ ~/.gitconfig"
+ gum style --foreground 147 " β’ ~/.config/nvim/init.lua"
+ gum style --foreground 147 " β’ ~/.config/redbrick/*"
+ echo ""
+
+ if ! confirm "Create/update symlinks?"; then
+ return
+ fi
+
+ echo ""
+
+ mkdir -p "$HOME/.config/direnv"
+ mkdir -p "$HOME/.config/git"
+ mkdir -p "$HOME/.config/nvim"
+ mkdir -p "$HOME/.config/redbrick"
+
+ ln -snf "$ROOT/zsh/zshrc" "$HOME/.zshrc" && show_success "Linked ~/.zshrc"
+ ln -snf "$ROOT/config/direnv/direnvrc" "$HOME/.config/direnv/direnvrc" && show_success "Linked direnvrc"
+ ln -snf "$ROOT/config/git/config" "$HOME/.gitconfig" && show_success "Linked gitconfig"
+ ln -snf "$ROOT/config/nvim/init.lua" "$HOME/.config/nvim/init.lua" && show_success "Linked nvim config"
+ ln -snf "$ROOT/config/redbrick" "$HOME/.config/redbrick" && show_success "Linked redbrick config"
+
+ echo ""
+ show_success "All dotfiles linked!"
+
+ echo ""
+ wait_for_key
+}
+
+run_check() {
+ show_header
+ gum style --foreground 183 --bold "π Check: Verify Dependencies"
+ echo ""
+
+ local tools=("direnv" "skate" "age" "just" "brew" "gum" "glow" "git" "zsh")
+ local missing=()
+
+ for tool in "${tools[@]}"; do
+ if has_command "$tool"; then
+ show_success "$tool"
+ else
+ show_error "$tool (missing)"
+ missing+=("$tool")
+ fi
+ done
+
+ echo ""
+
+ if [[ ${#missing[@]} -eq 0 ]]; then
+ gum style --foreground 10 --bold "β¨ All dependencies present! You're all set!"
+ else
+ gum style --foreground 222 "Missing tools: ${missing[*]}"
+ echo ""
+ if confirm "Install missing tools via Homebrew?"; then
+ echo ""
+ spinner_run "Installing missing tools..." brew install "${missing[@]}"
+ echo ""
+ show_success "Tools installed!"
+ fi
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+# ============================================================================
+# GIT OPERATIONS
+# ============================================================================
+
+check_git_repo() {
+ if ! git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ show_header
+ show_error "Not a git repository"
+ gum style --foreground 147 "The directory $ROOT is not a git repository."
+ gum style --foreground 147 "Please initialize a repository first."
+ echo ""
+ if confirm "Initialize a new git repository here?"; then
+ git -C "$ROOT" init
+ show_success "Git repository initialized at $ROOT"
+ fi
+ echo ""
+ wait_for_key
+ return 1
+ fi
+ return 0
+}
+
+git_menu() {
+ if ! check_git_repo; then
+ return
+ fi
+
+ while true; do
+ show_header
+ gum style --foreground 183 --bold "π³ Git Operations"
+ echo ""
+
+ local choice=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β " \
+ "π Status" \
+ "πΎ Commit changes" \
+ "π Sync (pull + push)" \
+ "β¬οΈ Push" \
+ "β¬οΈ Pull" \
+ "π§ Configure remote" \
+ "π GitHub authentication" \
+ "β Back to main menu")
+
+ case "$choice" in
+ *Status*)
+ git_status
+ ;;
+ *Commit*)
+ git_commit
+ ;;
+ *Sync*)
+ git_sync
+ ;;
+ *Push*)
+ git_push
+ ;;
+ *Pull*)
+ git_pull
+ ;;
+ *remote*)
+ git_configure_remote
+ ;;
+ *authentication*)
+ github_auth
+ ;;
+ *Back*)
+ break
+ ;;
+ esac
+ done
+}
+
+git_status() {
+ show_header
+ gum style --foreground 183 --bold "π Git Status"
+ echo ""
+
+ cd "$ROOT"
+ git status
+
+ echo ""
+ wait_for_key
+}
+
+git_commit() {
+ show_header
+ gum style --foreground 183 --bold "πΎ Commit Changes"
+ echo ""
+
+ cd "$ROOT"
+
+ local status_output=$(git status --porcelain)
+
+ if [[ -z "$status_output" ]]; then
+ show_info "No changes to commit"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo "Changed files:"
+ git status --short
+ echo ""
+
+ if ! confirm "Stage all changes?"; then
+ show_info "Commit cancelled"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ git add -A
+
+ echo ""
+ local commit_msg=$(gum input \
+ --placeholder "Enter commit message" \
+ --prompt "Message: " \
+ --prompt.foreground 183 \
+ --cursor.foreground 219)
+
+ if [[ -z "$commit_msg" ]]; then
+ show_error "Commit message required"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo ""
+ spinner_run "Committing changes..." git commit -m "$commit_msg"
+
+ echo ""
+ show_success "Changes committed!"
+
+ echo ""
+ if confirm "Push to remote?"; then
+ git_push
+ else
+ wait_for_key
+ fi
+}
+
+git_sync() {
+ show_header
+ gum style --foreground 183 --bold "π Sync: Pull + Push"
+ echo ""
+
+ cd "$ROOT"
+
+ # Check if remote is configured
+ if ! git remote get-url origin >/dev/null 2>&1; then
+ show_error "No remote repository configured"
+ gum style --foreground 147 "Configure a remote first in the Git menu."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ if spinner_run "Pulling from remote..." git pull 2>&1; then
+ show_success "Pulled successfully"
+ else
+ echo ""
+ show_error "Git pull failed"
+ gum style --foreground 147 "Check your internet connection and authentication."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo ""
+
+ local status_output=$(git status --porcelain)
+
+ if [[ -n "$status_output" ]]; then
+ gum style --foreground 222 "You have uncommitted changes."
+ if confirm "Commit and push?"; then
+ git_commit
+ return
+ fi
+ fi
+
+ if spinner_run "Pushing to remote..." git push 2>&1; then
+ echo ""
+ show_success "Synced successfully!"
+ else
+ echo ""
+ show_error "Git push failed"
+ gum style --foreground 147 "Check your authentication and permissions."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+git_push() {
+ show_header
+ gum style --foreground 183 --bold "β¬οΈ Push to Remote"
+ echo ""
+
+ cd "$ROOT"
+
+ # Check if remote is configured
+ if ! git remote get-url origin >/dev/null 2>&1; then
+ show_error "No remote repository configured"
+ gum style --foreground 147 "Configure a remote first in the Git menu."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ if spinner_run "Pushing to remote..." git push 2>&1; then
+ echo ""
+ show_success "Pushed successfully!"
+ else
+ echo ""
+ show_error "Git push failed"
+ gum style --foreground 147 "Check authentication and permissions."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+git_pull() {
+ show_header
+ gum style --foreground 183 --bold "β¬οΈ Pull from Remote"
+ echo ""
+
+ cd "$ROOT"
+
+ # Check if remote is configured
+ if ! git remote get-url origin >/dev/null 2>&1; then
+ show_error "No remote repository configured"
+ gum style --foreground 147 "Configure a remote first in the Git menu."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ if spinner_run "Pulling from remote..." git pull 2>&1; then
+ echo ""
+ show_success "Pulled successfully!"
+ else
+ echo ""
+ show_error "Git pull failed"
+ gum style --foreground 147 "Check your internet connection and authentication."
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+git_configure_remote() {
+ show_header
+ gum style --foreground 183 --bold "π§ Configure Git Remote"
+ echo ""
+
+ cd "$ROOT"
+
+ local current_remote=$(git remote get-url origin 2>/dev/null || echo "")
+
+ if [[ -n "$current_remote" ]]; then
+ gum style "Current remote: $current_remote"
+ echo ""
+ else
+ show_info "No remote configured"
+ echo ""
+ fi
+
+ local action=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β " \
+ "β Add/Update remote" \
+ "ποΈ Remove remote" \
+ "β Back")
+
+ case "$action" in
+ *Add*)
+ echo ""
+ local remote_url=$(gum input \
+ --placeholder "https://github.com/username/repo.git" \
+ --prompt "Remote URL: " \
+ --prompt.foreground 183 \
+ --cursor.foreground 219)
+
+ if [[ -z "$remote_url" ]]; then
+ show_error "Remote URL required"
+ else
+ if [[ -n "$current_remote" ]]; then
+ git remote set-url origin "$remote_url"
+ else
+ git remote add origin "$remote_url"
+ fi
+ show_success "Remote configured: $remote_url"
+ fi
+ ;;
+ *Remove*)
+ git remote remove origin 2>/dev/null
+ show_success "Remote removed"
+ ;;
+ esac
+
+ echo ""
+ wait_for_key
+}
+
+github_auth() {
+ show_header
+ gum style --foreground 183 --bold "π GitHub Authentication"
+ echo ""
+
+ gum style --foreground 183 "Choose authentication method:"
+ echo ""
+
+ local method=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β " \
+ "π Device flow (browser)" \
+ "π Personal Access Token" \
+ "π Check current auth" \
+ "β Back")
+
+ case "$method" in
+ *Device*)
+ github_device_flow
+ ;;
+ *Token*)
+ github_token_setup
+ ;;
+ *Check*)
+ github_check_auth
+ ;;
+ esac
+}
+
+github_device_flow() {
+ show_header
+ gum style --foreground 183 --bold "π GitHub Device Flow Authentication"
+ echo ""
+
+ if ! has_command gh; then
+ gum style --foreground 222 "GitHub CLI (gh) not found"
+ echo ""
+ if confirm "Install GitHub CLI?"; then
+ spinner_run "Installing gh..." brew install gh
+ echo ""
+ else
+ return
+ fi
+ fi
+
+ # Check if GITHUB_TOKEN is set
+ if [[ -n "${GITHUB_TOKEN:-}" ]]; then
+ show_info "GITHUB_TOKEN environment variable is set"
+ echo ""
+ gum style --foreground 147 "GitHub CLI will use this token automatically."
+ gum style --foreground 147 "To use device flow instead, unset GITHUB_TOKEN first:"
+ gum style --foreground 242 " unset GITHUB_TOKEN"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ gum style --foreground 183 "This will open GitHub in your browser for authentication."
+ echo ""
+
+ if ! confirm "Continue?"; then
+ return
+ fi
+
+ echo ""
+
+ if gh auth login 2>&1; then
+ echo ""
+ show_success "Authentication complete!"
+
+ echo ""
+ if confirm "Configure git to use gh credentials?"; then
+ gh auth setup-git 2>/dev/null && show_success "Git configured to use gh credentials" || show_info "Git already configured"
+ fi
+ else
+ local gh_exit=$?
+ echo ""
+ show_error "GitHub authentication failed"
+ gum style --foreground 147 "You may already be authenticated, or there was a network issue."
+ echo ""
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+github_token_setup() {
+ show_header
+ gum style --foreground 183 --bold "π GitHub Personal Access Token Setup"
+ echo ""
+
+ gum style --foreground 183 "To create a token:"
+ gum style --foreground 147 " 1. Go to: https://github.com/settings/tokens"
+ gum style --foreground 147 " 2. Click 'Generate new token (classic)'"
+ gum style --foreground 147 " 3. Select scopes: repo, workflow, write:packages"
+ gum style --foreground 147 " 4. Generate and copy the token"
+ echo ""
+
+ if ! confirm "Have you created a token?"; then
+ return
+ fi
+
+ echo ""
+ local token=$(gum input \
+ --password \
+ --placeholder "ghp_..." \
+ --prompt "Token: " \
+ --prompt.foreground 183 \
+ --cursor.foreground 219)
+
+ if [[ -z "$token" ]]; then
+ show_error "Token required"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ # Store in git config
+ cd "$ROOT"
+ local remote_url=$(git remote get-url origin 2>/dev/null || echo "")
+
+ if [[ -z "$remote_url" ]]; then
+ show_error "No git remote configured. Please configure remote first."
+ else
+ # Update URL to use token
+ if [[ "$remote_url" =~ ^https://github.com/ ]]; then
+ local new_url=$(echo "$remote_url" | sed "s|https://github.com/|https://${token}@github.com/|")
+ git remote set-url origin "$new_url"
+ show_success "Token configured for this repository"
+ else
+ show_error "Remote is not an HTTPS GitHub URL"
+ fi
+ fi
+
+ echo ""
+ wait_for_key
+}
+
+github_check_auth() {
+ show_header
+ gum style --foreground 183 --bold "π GitHub Authentication Status"
+ echo ""
+
+ if has_command gh; then
+ gh auth status
+ else
+ show_info "GitHub CLI not installed"
+ fi
+
+ echo ""
+ echo "Git config:"
+ git config --get user.name && show_success "Name: $(git config --get user.name)" || show_error "No user.name set"
+ git config --get user.email && show_success "Email: $(git config --get user.email)" || show_error "No user.email set"
+
+ echo ""
+ wait_for_key
+}
+
+# ============================================================================
+# INSTALL OH-MY-ZSH
+# ============================================================================
+
+install_omz() {
+ show_header
+ gum style --foreground 183 --bold "π¨ Install Oh-My-Zsh"
+ echo ""
+
+ if [[ -d "$HOME/.oh-my-zsh" ]]; then
+ show_info "Oh-My-Zsh already installed"
+ echo ""
+ wait_for_key
+ return
+ fi
+
+ gum style --foreground 183 "This will install Oh-My-Zsh with recommended settings."
+ echo ""
+
+ if ! confirm "Install Oh-My-Zsh?"; then
+ return
+ fi
+
+ echo ""
+ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
+
+ echo ""
+ show_success "Oh-My-Zsh installed!"
+
+ echo ""
+ wait_for_key
+}
+
+# ============================================================================
+# MAIN MENU
+# ============================================================================
+
+main_menu() {
+ while true; do
+ show_header
+
+ echo ""
+ gum style --foreground 183 --italic "What magical task shall we perform today?"
+ echo ""
+
+ local choice=$(gum choose \
+ --cursor.foreground 219 \
+ --selected.foreground 183 \
+ --cursor "β¨ " \
+ --height 12 \
+ "π Bootstrap (fresh machine setup)" \
+ "πΎ Backup (Brewfile & packages)" \
+ "π Backup secrets (GPG/SSH/Age/Skate/OMZ)" \
+ "π Restore secrets" \
+ "π Link dotfiles" \
+ "π Check dependencies" \
+ "π³ Git operations" \
+ "π¨ Install Oh-My-Zsh" \
+ "πͺ Exit")
+
+ case "$choice" in
+ *Bootstrap*)
+ run_bootstrap
+ ;;
+ *"Backup ("*)
+ run_backup
+ ;;
+ *"Backup secrets"*)
+ run_backup_secrets
+ ;;
+ *Restore*)
+ run_restore_secrets
+ ;;
+ *Link*)
+ run_link
+ ;;
+ *Check*)
+ run_check
+ ;;
+ *Git*)
+ git_menu
+ ;;
+ *Oh-My-Zsh*)
+ install_omz
+ ;;
+ *Exit*)
+ clear
+ gum style \
+ --foreground 219 \
+ --align center \
+ --padding "1 2" \
+ --border rounded \
+ --border-foreground 183 \
+ "β¨ Thank you for using Dasho's Dotfiles Wizard! β¨" \
+ "" \
+ "May your configs be ever in your favor! π§"
+ echo ""
+ exit 0
+ ;;
+ esac
+ done
+}
+
+# ============================================================================
+# ENTRY POINT
+# ============================================================================
+
+# First-time bootstrap of wizard itself
+wizard_bootstrap
+
+# Launch main menu
+main_menu
diff --git a/brew/leaves.txt b/brew/leaves.txt
@@ -0,0 +1,60 @@
+antoniorodr/lexy/lexy
+bat
+boxes
+btop
+charmbracelet/tap/crush
+charmbracelet/tap/mods
+charmbracelet/tap/skate
+charmbracelet/tap/soft-serve
+charmbracelet/tap/wishlist
+cloc
+croc
+danielgatis/imgcat/imgcat
+direnv
+exiftool
+eza
+fzf
+gemini-cli
+gh
+glow
+go
+gum
+himalaya
+imagemagick
+irssi
+just
+liblqr
+libraw
+librsvg
+libultrahdr
+lolcat
+magic-wormhole
+monolith
+neofetch
+neovim
+openai-whisper
+opencode
+pass
+pipes-sh
+pop
+proxychains-ng
+reflex
+rust
+sc-im
+sevenzip
+sops
+sshx
+steipete/tap/gifgrep
+steipete/tap/gogcli
+steipete/tap/imsg
+tmux
+tor
+torsocks
+tree-sitter
+unar
+uv
+vhs
+weechat
+zellij
+zoxide
+zsh
diff --git a/config/direnv/direnvrc b/config/direnv/direnvrc
@@ -0,0 +1,67 @@
+# direnv/direnvrc
+# -----------------------------------------------------------------------------
+# direnv helpers for loading secrets from Skate.
+#
+# In your project's .envrc:
+# use_secrets dev GITHUB_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
+#
+# Resolution order (per variable):
+# 1) repo.<repo-slug>.<env> (only if inside a git repo)
+# 2) global.<env>
+#
+# If a variable is missing, direnv will fail the load (safe default).
+# -----------------------------------------------------------------------------
+
+_secrets_repo_slug() {
+ local top remote slug
+
+ top="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null)" || return 1
+ remote="$(git -C "$top" remote get-url origin 2>/dev/null || echo "$top")"
+
+ # normalize + trim
+ remote="${remote%.git}"
+ remote="${remote#git@}"
+ remote="${remote#https://}"
+ remote="${remote#http://}"
+ remote="${remote/:/\/}" # ssh form host:owner/repo -> host/owner/repo
+
+ slug="${remote//\//_}" # slashes -> underscores
+ slug="$(printf '%s' "$slug" | tr -c 'A-Za-z0-9_.-' '_' )"
+
+ # remove any trailing underscores that can come from hidden chars/newlines
+ slug="${slug%%_}"
+ # Finally, change all double underscores to single underscores
+ slug="${slug//__/_}"
+
+ printf '%s\n' "$slug"
+}
+
+use_secrets() {
+ local env="${1:-dev}"
+ shift || true
+
+ command -v skate >/dev/null 2>&1 || { log_error "Missing: skate"; return 1; }
+
+ local global_db="global.${env}"
+ local repo_db=""
+
+ if git -C "$PWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ local slug
+ slug="$(_secrets_repo_slug 2>/dev/null)" && repo_db="repo.${slug}.${env}"
+ fi
+
+ local var val
+ for var in "$@"; do
+ if [ -n "$repo_db" ] && val="$(skate get "${var}@${repo_db}" 2>/dev/null)"; then
+ export "$var=$val"
+ continue
+ fi
+ if val="$(skate get "${var}@${global_db}" 2>/dev/null)"; then
+ export "$var=$val"
+ continue
+ fi
+ log_error "Missing secret: ${var} (looked in ${repo_db:-<no-repo-db>} then ${global_db})"
+ return 1
+ done
+}
+
diff --git a/config/git/config b/config/git/config
@@ -0,0 +1,22 @@
+[user]
+ name = Dasho
+ email = git@dasho.dev
+ signingkey = ~/.ssh/id_ed25519.pub
+
+[core]
+ compression = 9
+ whitespace = error
+ preloadindex = true
+ excludesfile = ~/.gitignore
+
+[commit]
+ gpgsign = true
+
+[gpg]
+ format = ssh
+
+[init]
+ defaultBranch = main
+
+[url "git@github.com:"]
+ insteadOf = https://github.com/
diff --git a/config/nvim/init.lua b/config/nvim/init.lua
@@ -0,0 +1,353 @@
+-- Neovim init.lua
+
+-- Basic options
+vim.opt.number = true -- enable absolute line numbers
+vim.opt.relativenumber = true -- enable relative line numbers
+vim.opt.clipboard = "unnamedplus" -- use system clipboard
+vim.opt.mouse = "a" -- enable mouse
+vim.opt.termguicolors = true -- true color support
+vim.opt.signcolumn = "yes" -- always show signcolumn
+vim.opt.showmode = false -- don't show mode (we use statusline)
+vim.opt.cursorline = true -- highlight current line
+vim.opt.hidden = true -- allow buffer switching without saving
+
+-- Indentation
+vim.opt.expandtab = true -- spaces instead of tabs
+vim.opt.shiftwidth = 2 -- size of an indent
+vim.opt.tabstop = 2 -- number of spaces tabs count for
+vim.opt.softtabstop = 2 -- spaces when hitting <Tab>
+
+-- Searching
+vim.opt.ignorecase = true -- ignore case
+vim.opt.smartcase = true -- unless uppercase present
+vim.opt.incsearch = true -- incremental search
+vim.opt.hlsearch = false -- no persistent highlight
+
+-- Splits & Windows
+vim.opt.splitbelow = true -- horizontal splits go below
+vim.opt.splitright = true -- vertical splits go right
+vim.opt.equalalways = true -- auto-resize splits
+
+-- Backups & Undo
+vim.opt.backup = false
+vim.opt.writebackup = false
+vim.opt.swapfile = false
+vim.opt.undofile = true -- enable persistent undo
+vim.opt.undodir = vim.fn.stdpath('state') .. '/undo'
+
+-- Timing
+vim.opt.updatetime = 300 -- faster CursorHold
+vim.opt.timeoutlen = 500 -- faster mapped sequences
+
+-- Leader Key
+vim.g.mapleader = ' '
+vim.g.maplocalleader = ' '
+
+-- Bootstrap lazy.nvim
+local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
+if not vim.loop.fs_stat(lazypath) then
+ vim.fn.system({
+ 'git', 'clone', '--filter=blob:none',
+ 'https://github.com/folke/lazy.nvim.git',
+ '--branch=stable', lazypath,
+ })
+end
+vim.opt.rtp:prepend(lazypath)
+
+-- Plugin setup
+require('lazy').setup({
+ -- lazy.nvim manager
+ { 'folke/lazy.nvim', version = '*' },
+
+ -- Lualine statusline
+ {
+ 'nvim-lualine/lualine.nvim',
+ event = 'VeryLazy',
+ dependencies = { 'kyazdani42/nvim-web-devicons', opt = true },
+ config = function()
+ require('lualine').setup {
+ options = {
+ icons_enabled = true,
+ theme = 'auto',
+ component_separators = { left = 'ξ±', right = 'ξ³' },
+ section_separators = { left = 'ξ°', right = 'ξ²' },
+ },
+ sections = {
+ lualine_a = {'mode'},
+ lualine_b = {'branch', 'diff', 'diagnostics'},
+ lualine_c = {'filename'},
+ lualine_x = {'encoding', 'fileformat', 'filetype'},
+ lualine_y = {'progress'},
+ lualine_z = {'location'},
+ },
+ }
+ end,
+ },
+ {
+ "zbirenbaum/copilot.lua",
+ event = "VeryLazy",
+ config = function()
+ require("copilot").setup({
+ suggestion = {
+ enabled = true,
+ auto_trigger = true,
+ accept = false,
+ },
+ panel = {
+ enabled = false
+ },
+ filetypes = {
+ markdown = true,
+ help = true,
+ html = true,
+ javascript = true,
+ typescript = true,
+ ["*"] = true
+ },
+ })
+
+ vim.keymap.set("i", '<Tab>', function()
+ if require("copilot.suggestion").is_visible() then
+ require("copilot.suggestion").accept()
+ else
+ vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Tab>", true, false, true), "n", false)
+ end
+ end, {
+ silent = true,
+ })
+ end,
+ },
+ -- Telescope fuzzy finder
+ {
+ 'nvim-telescope/telescope.nvim',
+ cmd = 'Telescope',
+ dependencies = { 'nvim-lua/plenary.nvim' },
+ config = function()
+ require('telescope').setup {
+ defaults = {
+ layout_strategy = 'flex',
+ file_ignore_patterns = {'node_modules'}
+ }
+ }
+ end,
+ },
+
+ -- File explorer
+ {
+ 'nvim-tree/nvim-tree.lua',
+ cmd = { 'NvimTreeToggle', 'NvimTreeFocus' },
+ dependencies = { 'kyazdani42/nvim-web-devicons' },
+ config = function()
+ require('nvim-tree').setup {
+ view = { width = 30 },
+ update_focused_file = { enable = true },
+ }
+ end,
+ },
+
+ -- Treesitter for syntax
+ {
+ 'nvim-treesitter/nvim-treesitter',
+ build = ':TSUpdate',
+ event = { 'BufReadPost', 'BufNewFile' },
+ config = function()
+ require('nvim-treesitter.configs').setup {
+ ensure_installed = { 'lua', 'python', 'javascript', 'go', 'rust' },
+ highlight = { enable = true },
+ indent = { enable = true },
+ }
+ end,
+ },
+
+ -- Mason LSP installer
+ { 'williamboman/mason.nvim', cmd = 'Mason', config = true },
+ {
+ 'williamboman/mason-lspconfig.nvim',
+ dependencies = 'neovim/nvim-lspconfig',
+ config = function()
+ require('mason-lspconfig').setup {
+ ensure_installed = { 'pyright', 'ts_ls', 'lua_ls' },
+ automatic_enable = false,
+ }
+ vim.lsp.config('lua_ls', {
+ settings = {
+ Lua = {
+ runtime = { version = 'LuaJIT' },
+ diagnostics = { globals = {'vim'} },
+ workspace = { library = vim.api.nvim_get_runtime_file('', true) },
+ telemetry = { enable = false },
+ },
+ },
+ })
+ vim.lsp.enable({ 'pyright', 'ts_ls', 'lua_ls' })
+ end,
+ },
+
+ -- Completion
+ {
+ 'hrsh7th/nvim-cmp',
+ event = 'InsertEnter',
+ dependencies = {
+ 'hrsh7th/cmp-nvim-lsp', 'hrsh7th/cmp-path',
+ 'L3MON4D3/LuaSnip', 'saadparwaiz1/cmp_luasnip',
+ },
+ config = function()
+ local cmp = require('cmp')
+ cmp.setup {
+ snippet = { expand = function(args) require('luasnip').lsp_expand(args.body) end },
+ sources = cmp.config.sources({{ name = 'nvim_lsp' },{ name = 'path' },{ name = 'luasnip' }}),
+ }
+ end,
+ },
+
+ -- Git signs
+ {
+ 'lewis6991/gitsigns.nvim',
+ event = 'BufReadPre',
+ config = function() require('gitsigns').setup {} end,
+ },
+
+ -- Autopairs
+ {
+ 'windwp/nvim-autopairs',
+ event = 'InsertEnter',
+ config = function() require('nvim-autopairs').setup {} end,
+ },
+
+ -- Commenting
+ {
+ 'numToStr/Comment.nvim',
+ keys = {
+ { '<Leader>/', function() require('Comment.api').toggle.linewise.current() end, desc = 'Toggle comment (line)' },
+ { '<Leader>/', '<Esc><cmd>lua require("Comment.api").toggle.blockwise(vim.fn.visualmode())<CR>', mode = 'v', desc = 'Toggle comment (block)' },
+ },
+ config = function()
+ require('Comment').setup({
+ -- Add any custom config here; these are the defaults:
+ padding = true, -- add a space b/w comment and line
+ sticky = true, -- cursor stays put
+ mappings = {
+ basic = false, -- disable builtin mappings because we're using our own
+ extra = false,
+ },
+ toggler = {
+ line = '<Leader>/', -- won't be set by default since basic=false
+ block = '<Leader>/', -- same here
+ },
+ })
+ end,
+ },
+
+
+ -- Which-key
+ {
+ 'folke/which-key.nvim',
+ event = 'VeryLazy',
+ config = function() require('which-key').setup {} end,
+ },
+
+ -- Bufferline
+ {
+ 'akinsho/bufferline.nvim',
+ event = 'BufWinEnter',
+ dependencies = 'kyazdani42/nvim-web-devicons',
+ config = function()
+ require('bufferline').setup { options = { separator_style = 'thick' } }
+ end,
+ },
+
+ -- Hop
+ {
+ 'phaazon/hop.nvim',
+ branch = 'v2',
+ keys = { 'f', 'F', 't', 'T' },
+ config = function() require('hop').setup {} end,
+ },
+
+ {
+ 'akinsho/toggleterm.nvim', -- terminal toggling plugin
+ version = '*',
+ keys = { { '<Leader>t', '<cmd>ToggleTerm<CR>', desc = 'Toggle floating terminal' } },
+ opts = {
+ size = 20, -- height of split if not floating
+ open_mapping = [[<Leader>t]], -- map <Leader>t to toggle
+ direction = 'float', -- open as floating window
+ float_opts = {
+ border = 'curved', -- single, double, rounded, curved, or none
+ winblend = 0,
+ width = function() return math.floor(vim.o.columns * 0.8) end,
+ height = function() return math.floor(vim.o.lines * 0.8) end,
+ -- row and col will center the float
+ row = 0.5,
+ col = 0.5,
+ },
+ -- hide line numbers and start in insert mode
+ hide_numbers = true,
+ start_in_insert= true,
+ persist_size = true,
+ },
+ config = function(_, opts)
+ require('toggleterm').setup(opts) -- apply settings
+ end,
+ },
+})
+
+local map = vim.keymap.set
+local opts = { silent = true, noremap = true }
+
+-- Better window navigation
+map('n', '<Leader>h', '<C-w>h', { desc = 'Move to left split', unpack(opts) })
+map('n', '<Leader>j', '<C-w>j', { desc = 'Move to below split', unpack(opts) })
+map('n', '<Leader>k', '<C-w>k', { desc = 'Move to above split', unpack(opts) })
+map('n', '<Leader>l', '<C-w>l', { desc = 'Move to right split', unpack(opts) })
+
+-- Resize splits with arrows
+map('n', '<Leader><Up>', ':resize -2<CR>', { desc = 'Decrease split height', unpack(opts) })
+map('n', '<Leader><Down>', ':resize +2<CR>', { desc = 'Increase split height', unpack(opts) })
+map('n', '<Leader><Left>', ':vertical resize -2<CR>', { desc = 'Decrease split width', unpack(opts) })
+map('n', '<Leader><Right>', ':vertical resize +2<CR>', { desc = 'Increase split width', unpack(opts) })
+
+-- Buffer navigation
+map('n', '<Leader>bn', ':bnext<CR>', { desc = 'Next buffer', unpack(opts) })
+map('n', '<Leader>bp', ':bprevious<CR>', { desc = 'Previous buffer', unpack(opts) })
+map('n', '<Leader>bc', ':bdelete<CR>', { desc = 'Close buffer', unpack(opts) })
+
+-- Quick save & quit
+map('n', '<Leader>w', ':write<CR>', { desc = 'Save current file', unpack(opts) })
+map('n', '<Leader>q', ':quit<CR>', { desc = 'Quit current window', unpack(opts) })
+map('n', '<Leader>WQ', ':wqall<CR>', { desc = 'Save all and quit', unpack(opts) })
+
+-- Move lines up/down in visual mode
+map('v', '<Leader>j', ":m '>+1<CR>gv=gv", { desc = 'Move selection down', unpack(opts) })
+map('v', '<Leader>k', ":m '<-2<CR>gv=gv", { desc = 'Move selection up', unpack(opts) })
+
+-- Yank to system clipboard
+map('n', '<Leader>y', '"+y', { desc = 'Yank to system clipboard', unpack(opts) })
+map('v', '<Leader>y', '"+y', { desc = 'Yank selection to system clipboard', unpack(opts) })
+map('n', '<Leader>Y', '"+Y', { desc = 'Yank entire line to clipboard', unpack(opts) })
+
+-- Paste over visual selection without losing register
+map('v', '<Leader>p', '"_dP', { desc = 'Paste over selection', unpack(opts) })
+
+-- Clear search highlights
+map('n', '<Leader>c', ':nohlsearch<CR>', { desc = 'Clear search highlights',unpack(opts) })
+
+-- Quick Telescope pickers
+map('n', '<Leader>ff', '<cmd>Telescope find_files<CR>', { desc = 'Find files', unpack(opts) })
+map('n', '<Leader>fg', '<cmd>Telescope live_grep<CR>', { desc = 'Live grep', unpack(opts) })
+map('n', '<Leader>fb', '<cmd>Telescope buffers<CR>', { desc = 'List open buffers', unpack(opts) })
+map('n', '<Leader>fh', '<cmd>Telescope help_tags<CR>', { desc = 'Find help tags', unpack(opts) })
+
+-- Toggle NvimTree
+map('n', '<Leader>e', ':NvimTreeToggle<CR>', { desc = 'Toggle file explorer', unpack(opts) })
+
+-- Make ESC faster (jk/ kj in insert mode)
+map('i', 'jk', '<Esc>', { desc = 'Exit insert mode', unpack(opts) })
+map('i', 'kj', '<Esc>', { desc = 'Exit insert mode', unpack(opts) })
+
+-- Quick comment toggling (using Comment.nvim)
+-- map('n', '<Leader>/', '<cmd>CommentToggle<CR>', opts)
+-- map('v', '<Leader>/', '<esc><cmd>CommentToggle<CR>', opts)
+
+
+-- End of init.lua
diff --git a/config/redbrick/config.toml b/config/redbrick/config.toml
@@ -0,0 +1,40 @@
+# Redbrick config.
+#
+# For a complete list of available options,
+# please visit https://redbrick.chat/configuration/
+
+[servers.404]
+nickname = "Dasho"
+nick_password_file = "~/.config/redbrick/404/password"
+server = "irc.4-0-4.io"
+port = 6667
+channels = ["#chat", "#general", "#help", "#redbrick"]
+use_tls = false
+
+[servers.404.sasl.plain]
+username = "Dasho"
+password_file = "~/.config/redbrick/404/password"
+
+[servers.test-404]
+nickname = "Dasho"
+nick_password_file = "~/.config/redbrick/404/password"
+server = "127.0.0.1"
+port = 6667
+channels = ["#test"]
+use_tls = false
+
+[servers.bhc]
+nickname = "Dasho"
+nick_password_file = "~/.config/redbrick/bhc/password"
+server = "blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion"
+port = 6667
+channels = ["#lobby", "#staff"]
+use_tls = false
+
+[servers.cheshire]
+nickname = "Dasho"
+nick_password_file = "~/.config/redbrick/cheshire/password"
+server = "34vnln24rlakgbk6gpityvljieayyw7q4bhdbbgs6zp2v5nbh345zgad.onion"
+port = 6667
+channels =["#chucky"]
+use_tls = false
diff --git a/zsh/secrets.zsh b/zsh/secrets.zsh
@@ -0,0 +1,119 @@
+# ---------------------------- Secret Management ------------------------------
+
+# direnv integration (must be in interactive shells)
+if command -v direnv >/dev/null 2>&1; then
+ eval "$(direnv hook zsh)"
+fi
+
+# -----------------------------------------------------------------------------
+# Secret helpers for Skate + gum.
+#
+# Usage:
+# secret [-g] [-e env] KEY
+# - By default: uses repo scope if inside a git repo, otherwise global.
+# - -g forces global scope.
+# - -e chooses an environment name (default: dev).
+#
+# secret-get [-e env] KEY
+# - Prints the secret to stdout.
+# - Resolution order: repo DB first (if in a repo), then global DB.
+#
+# Notes:
+# - Skate stores items as: KEY@DB
+# - DBs are named like:
+# global.<env>
+# repo.<repo-slug>.<env>
+# -----------------------------------------------------------------------------
+
+_secrets_repo_slug() {
+ local top remote slug
+ top="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null)" || return 1
+ remote="$(git -C "$top" remote get-url origin 2>/dev/null || echo "$top")"
+
+ remote="${remote%.git}"
+ remote="${remote#git@}"
+ remote="${remote#https://}"
+ remote="${remote#http://}"
+ remote="${remote/:/\/}"
+
+ slug="${remote//\//_}"
+ slug="${slug//[^A-Za-z0-9_.-]/_}"
+ slug="${slug%%_}" # strip trailing underscore
+ # Finally change all double underscores to single
+ slug="${slug//__/_}"
+
+ print -r -- "$slug"
+}
+
+_secrets_db_global() { print -r -- "global.$1"; }
+
+_secrets_db_repo() {
+ local env="$1" slug
+ slug="$(_secrets_repo_slug)" || return 1
+ print -r -- "repo.${slug}.${env}"
+}
+
+secret() {
+ emulate -L zsh
+ setopt localoptions pipefail
+
+ local env="dev" scope="auto"
+ local OPTIND=1 opt
+ while getopts ":ge:" opt; do
+ case "$opt" in
+ g) scope="global" ;;
+ e) env="$OPTARG" ;;
+ *) echo "Usage: secret [-g] [-e env] KEY" >&2; return 1 ;;
+ esac
+ done
+ shift $((OPTIND-1))
+
+ local key="$1"
+ [[ -z "$key" ]] && { echo "Usage: secret [-g] [-e env] KEY" >&2; return 1; }
+
+ command -v skate >/dev/null 2>&1 || { echo "Missing: skate" >&2; return 1; }
+ command -v gum >/dev/null 2>&1 || { echo "Missing: gum" >&2; return 1; }
+
+ local db
+ if [[ "$scope" == "global" ]]; then
+ db="$(_secrets_db_global "$env")"
+ else
+ db="$(_secrets_db_repo "$env" 2>/dev/null)" || db="$(_secrets_db_global "$env")"
+ fi
+
+ local value
+ value="$(gum input --password --prompt "Enter secret for $key ($db): ")" || return 1
+ printf "\n" >&2
+ [[ -z "$value" ]] && { echo "Empty value, aborting." >&2; return 1; }
+
+ skate set "${key}@${db}" "$value"
+ echo "Stored ${key}@${db}"
+}
+
+secret-get() {
+ emulate -L zsh
+ setopt localoptions pipefail
+
+ local env="dev"
+ local OPTIND=1 opt
+ while getopts ":e:" opt; do
+ case "$opt" in
+ e) env="$OPTARG" ;;
+ *) echo "Usage: secret-get [-e env] KEY" >&2; return 1 ;;
+ esac
+ done
+ shift $((OPTIND-1))
+
+ local key="$1"
+ [[ -z "$key" ]] && { echo "Usage: secret-get [-e env] KEY" >&2; return 1; }
+ command -v skate >/dev/null 2>&1 || { echo "Missing: skate" >&2; return 1; }
+
+ local repo_db global_db val
+ repo_db="$(_secrets_db_repo "$env" 2>/dev/null)" || repo_db=""
+ global_db="$(_secrets_db_global "$env")"
+
+ if [[ -n "$repo_db" ]] && val="$(skate get "${key}@${repo_db}" 2>/dev/null)"; then
+ print -r -- "$val"; return 0
+ fi
+ skate get "${key}@${global_db}"
+}
diff --git a/zsh/zshrc b/zsh/zshrc
@@ -0,0 +1,187 @@
+# direnv integration (must be in interactive shells)
+if command -v direnv >/dev/null 2>&1; then
+ eval "$(direnv hook zsh)"
+fi
+
+# Secret helpers
+if [[ -f "$HOME/.dotfiles/zsh/secrets.zsh" ]]; then
+ source "$HOME/.dotfiles/zsh/secrets.zsh"
+fi
+
+# Path to your Oh My Zsh installation.
+export ZSH_DISABLE_COMPFIX=true
+export ZSH="$HOME/.oh-my-zsh"
+eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
+#Set name of the theme to load --- if set to "random", it will
+# load a random theme each time Oh My Zsh is loaded, in which case,
+# to know which specific one was loaded, run: echo $RANDOM_THEME
+# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
+ZSH_THEME="robbyrussell"
+
+# Uncomment one of the following lines to change the auto-update behavior
+# zstyle ':omz:update' mode disabled # disable automatic updates
+# zstyle ':omz:update' mode auto # update automatically without asking
+ zstyle ':omz:update' mode reminder # just remind me to update when it's time
+
+# Uncomment the following line to change how often to auto-update (in days).
+ zstyle ':omz:update' frequency 1
+
+# Uncomment the following line if pasting URLs and other text is messed up.
+# DISABLE_MAGIC_FUNCTIONS="true"
+
+# Uncomment the following line to disable colors in ls.
+# DISABLE_LS_COLORS="true"
+
+# Uncomment the following line to disable auto-setting terminal title.
+# DISABLE_AUTO_TITLE="true"
+
+# Uncomment the following line to enable command auto-correction.
+# ENABLE_CORRECTION="true"
+
+# Uncomment the following line to display red dots whilst waiting for completion.
+# You can also set it to another string to have that shown instead of the default red dots.
+# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
+# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
+# COMPLETION_WAITING_DOTS="true"
+
+# Uncomment the following line if you want to disable marking untracked files
+# under VCS as dirty. This makes repository status check for large repositories
+# much, much faster.
+# DISABLE_UNTRACKED_FILES_DIRTY="true"
+
+# Uncomment the following line if you want to change the command execution time
+# stamp shown in the history command output.
+# You can set one of the optional three formats:
+# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
+# or set a custom format using the strftime function format specifications,
+# see 'man strftime' for details.
+# HIST_STAMPS="mm/dd/yyyy"
+
+# Would you like to use another custom folder than $ZSH/custom?
+# ZSH_CUSTOM=/path/to/new-custom-folder
+
+# Which plugins would you like to load?
+# Standard plugins can be found in $ZSH/plugins/
+# Custom plugins may be added to $ZSH_CUSTOM/plugins/
+# Example format: plugins=(rails git textmate ruby lighthouse)
+# Add wisely, as too many plugins slow down shell startup.
+plugins=(fzf git)
+
+source $ZSH/oh-my-zsh.sh
+
+# User configuration
+
+# export MANPATH="/usr/local/man:$MANPATH"
+
+# You may need to manually set your language environment
+# export LANG=en_US.UTF-8
+
+# Preferred editor for local and remote sessions
+# if [[ -n $SSH_CONNECTION ]]; then
+# export EDITOR='vim'
+# else
+# export EDITOR='nvim'
+# fi
+
+# Compilation flags
+# export ARCHFLAGS="-arch $(uname -m)"
+
+# Set personal aliases, overriding those provided by Oh My Zsh libs,
+# plugins, and themes. Aliases can be placed here, though Oh My Zsh
+# users are encouraged to define aliases within a top-level file in
+# the $ZSH_CUSTOM folder, with .zsh extension. Examples:
+# - $ZSH_CUSTOM/aliases.zsh
+# - $ZSH_CUSTOM/macos.zsh
+# For a full list of active aliases, run `alias`.
+#
+# Example aliases
+# alias zshconfig="mate ~/.zshrc"
+# alias ohmyzsh="mate ~/.oh-my-zsh"
+autoload -U compinit; compinit
+eval "$(zoxide init zsh)"
+
+export EDITOR='nvim'
+# Aliases
+## -- File Navigation --
+alias cd="z"
+alias ci="zi"
+alias ls="eza --icons"
+
+## -- Applications --
+alias e="nvim"
+alias dotedit="chezmoi edit"
+alias dotfiles="chezmoi"
+alias ..="cd .."
+alias ...="cd ../.."
+
+alias bhcli="/home/dasho/dev/bhcli-new/target/release/bhcli --refresh-rate 2 -m"
+alias bhcli2="/home/dasho/dev/bhcli-new/target/release/bhcli --refresh-rate 2 --url http://blkh4ylofapg42tj6ht565klld5i42dhjtysvsnnswte4xt4uvnfj5qd.onion -m"
+alias 404="/home/dasho/dev/bhcli-new/target/release/bhcli --refresh-rate 2 --url https://4-0-4.io/chat/min"
+alias 404tor="/home/dasho/dev/bhcli-new/target/release/bhcli --refresh-rate 2 --url http://4o4o4hn4hsujpnbsso7tqigujuokafxys62thulbk2k3mf46vq22qfqd.onion/chat/min"
+alias tb="cd /home/dasho/Downloads/tor-browser && ./start-tor-browser.desktop"
+
+alias -s md="glow"
+alias -s py="$EDITOR"
+alias -s txt="bat"
+alias -s log="bat"
+alias -s json="bat"
+alias -s xml="bat"
+alias -s csv="bat"
+alias -s yml="$EDITOR"
+alias -s yaml="$EDITOR"
+alias -s html="$EDITOR"
+alias -s js="$EDITOR"
+alias -s css="$EDITOR"
+# alias -s sh="$EDITOR"
+# alias -s zsh="$EDITOR"
+alias -s conf="bat"
+alias -s toml="bat"
+alias -s rs="$EDITOR"
+alias -s go="$EDITOR"
+alias -s c="$EDITOR"
+alias -s cpp="$EDITOR"
+alias -s h="$EDITOR"
+alias -s hpp="$EDITOR"
+alias -s mov="open"
+alias -s mp4="open"
+alias -s mkv="open"
+alias -s jpg="open"
+alias -s png="open"
+alias -s gif="open"
+
+alias -g NE="2>/dev/null"
+alias -g ND=">/dev/null"
+alias -g NULL=">/dev/null 2>1"
+alias -g F="| fzf"
+alias -g T="| tail"
+alias -g JQ="| jq"
+alias -g G="| grep"
+alias -g L="| less"
+alias -g H="| head"
+
+# Source and export variables from ~/.env
+if [ -f ~/.env ]; then
+ set -a
+ source ~/.env
+ set +a
+fi
+export PATH=$HOME/.local/bin:$PATH
+export PATH="$HOME/.config/emacs/bin:$PATH"
+export PATH="$HOME/Applications/halloy/bin:$PATH"
+
+chpwd() {
+ ls
+}
+
+clear_buffer_screen() {
+ zle clear-screen
+}
+zle -N clear_buffer_screen
+bindkey '^Xl' clear_buffer_screen
+
+# --- Auto-start Zellij for interactive shells ---
+# if [[ -o interactive ]] && [[ -z "$ZELLIJ" ]] && command -v zellij >/dev/null 2>&1; then
+ # Attach to "main" if it exists, otherwise create it
+ # exec zellij attach --create main
+# fi
+# --- End Auto-start Zellij ---