Skip to content

ADR-012 — Terminal Maturity on macOS

Date: 2026-04-15 Status: Research / Proposed Context: Current setup is a near-empty .zshrc, iTerm2, and basic SSH config. Goal is to mature the terminal experience with privacy, productivity, and homelab management in mind.


1. AI Interaction: VS Code vs Terminal

Aspect VS Code (Claude Code extension) Terminal (claude CLI)
Context Sees open files, selections, workspace Sees only what you pipe or describe
Editing Inline diffs, click-to-apply Writes files directly
Multitasking Side-by-side with code Full-screen focus, tmux-friendly
Homelab ops Awkward for SSH/infra tasks Natural for SSH, ansible, scripts

Verdict: Use both. VS Code for code-heavy work (IaC, playbooks). Terminal for infra ops, debugging, SSH sessions. They complement each other; the CLI is better when you're already in a terminal flow.


2. How People Use Their Terminal Effectively

Core Productivity Stack (install all of these)

Tool What it does Install
Starship Fast, informative prompt (git branch, k8s context, etc.) brew install starship
fzf Fuzzy finder for files, history, everything brew install fzf
zoxide Smart cd - learns your directories, z proj jumps there brew install zoxide
bat cat with syntax highlighting and line numbers brew install bat
eza Modern ls with git status, icons, tree view brew install eza
fd Fast find replacement brew install fd
ripgrep Fast grep replacement (already used by Claude Code) brew install ripgrep
atuin Shell history stored in SQLite, full-text search, sync across machines brew install atuin
tldr Simplified man pages with practical examples brew install tldr

Homelab-Specific Tools

Tool What it does Install
pvetui TUI for Proxmox management (multi-cluster, SSH integration) Go binary from GitHub
lazydocker TUI for Docker container management over SSH brew install lazydocker
sshtmux SSH terminal manager with tmux integration pip install sshtmux
k9s TUI for Kubernetes (if you ever move to k8s) brew install k9s

Workflow Patterns

  • tmux/zellij sessions: persistent terminal sessions that survive disconnects. Name sessions per-task (tmux new -s vault-debug).
  • Aliases for common ops: alias lxc='ssh [email protected] pct', alias logs='ssh [email protected] journalctl'.
  • fzf + SSH: fuzzy-select which host to SSH into from your config.
  • Shell scripts in ~/bin/: you already do this (backup-to-cajita.sh, mount scripts). Keep going.

3. Self-Hosted Terminal Solutions

For accessing your terminal from anywhere (browser-based):

Solution Notes
Termix Best option. Self-hosted, open-source, Docker deploy. SSH terminal + tunneling + file editing + system monitoring. Split-screen up to 4 terminals.
Nexterm SSH + VNC + RDP in one interface. Good for mixed protocol homelab. Open-source.
WebSSH (bifrost0x) Lightweight web-based SSH with SFTP file manager. Single Docker container.
Sshwifty Minimal web SSH/Telnet client. Very lightweight.
ttyd Share any CLI tool as a web page. Dead simple.

Recommendation: Termix is the most feature-complete for a homelab. Deploy it on an LXC behind Caddy + NetBird VPN for secure remote access without exposing SSH to the internet.

Note: You already have NetBird VPN, so you can access any self-hosted terminal securely from anywhere without Cloudflare tunnels.


Your current .zshrc is essentially empty. Here's a structured approach:

# --- Package manager ---
eval "$(/opt/homebrew/bin/brew shellenv)"

# --- Prompt ---
eval "$(starship init zsh)"

# --- Smart tools ---
eval "$(zoxide init zsh)"         # z <partial-dir>
eval "$(fzf --zsh)"               # Ctrl+R history, Ctrl+T files
eval "$(atuin init zsh)"          # better shell history

# --- Aliases: general ---
alias ls='eza --icons --group-directories-first'
alias ll='eza -la --icons --group-directories-first'
alias cat='bat --paging=never'
alias tree='eza --tree --icons'
alias grep='rg'
alias find='fd'

# --- Aliases: homelab ---
alias chizuru='ssh [email protected]'
alias cajita='ssh [email protected]'
alias lxcs='ssh [email protected] pct list'
alias vsh='vault-ssh'  # custom function

# --- Aliases: git ---
alias gs='git status'
alias gd='git diff'
alias gl='git log --oneline -20'
alias gp='git push'

# --- Functions ---
# SSH into any LXC by ID
lxc-ssh() {
  ssh [email protected] "pct exec $1 -- bash"
}

# Quick Vault read
vault-read() {
  VAULT_ADDR=https://vault.eva-00.network vault kv get "$1"
}

# --- Zsh plugins (via Zinit or manual) ---
# zsh-autosuggestions: suggests commands as you type (grey text)
# zsh-syntax-highlighting: colors valid/invalid commands
# fzf-tab: replaces default tab completion with fzf

# --- PATH ---
export PATH="$HOME/bin:$PATH"
export PATH="$PATH:$HOME/.rvm/bin"
export PATH="/opt/homebrew/opt/ruby/bin:$PATH"

Plugin Management

Skip Oh My Zsh (bloated, slow). Use Zinit to cherry-pick just the plugins you want:

# Install: bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)"

zinit light zsh-users/zsh-autosuggestions
zinit light zsh-users/zsh-syntax-highlighting
zinit light Aloxaf/fzf-tab

File Organization

File Purpose
~/.zshrc Main config (prompt, tools, plugins)
~/.zshenv Environment variables (PATH, EDITOR, etc.)
~/.config/starship.toml Prompt customization
~/.config/ghostty/config Terminal emulator config
~/bin/ Custom scripts (already using this)
~/.ssh/config SSH host definitions (see section 5)

5. SSH Config Keywords

Your current SSH config is minimal. Here's how to level it up:

Expanded ~/.ssh/config

# --- Global defaults ---
Host *
  AddKeysToAgent yes
  UseKeyChain yes
  IdentityFile ~/.ssh/id_ed25519_github
  ServerAliveInterval 60
  ServerAliveCountMax 3
  Compression yes

# --- Forgejo ---
Host forgejo
  HostName code.eva-00.network
  User git
  IdentityFile ~/.ssh/id_ed25519_forgejo

# --- Proxmox host ---
Host chizuru
  HostName 192.168.1.125
  User root

# --- PBS ---
Host cajita
  HostName 192.168.1.196
  User root

# --- LXC pattern: ssh lxc-100, lxc-107, etc. ---
# (requires ProxyJump through Proxmox host)
Host lxc-*
  User root
  ProxyJump chizuru
  # Proxmox pct exec handles the actual LXC connection
  # Alternative: if LXCs have their own IPs, map them directly

# --- Homelab wildcard ---
Host 192.168.1.*
  User root
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  LogLevel ERROR

Key SSH Keywords

Keyword What it does
Host Alias name (e.g., ssh chizuru instead of ssh [email protected])
HostName Real IP/hostname
ProxyJump Bounce through a bastion host (e.g., ProxyJump chizuru to reach internal hosts)
LocalForward Port forwarding (e.g., access a service on LXC via localhost:8080)
IdentityFile Per-host SSH key
ServerAliveInterval Keep connections alive (critical for homelab SSH sessions)
Match Conditional config (e.g., different settings when on VPN vs local)

6. Best Terminal Emulator for macOS

Comparison

Terminal GPU Accel Native macOS Open Source Privacy Config Format Tabs/Splits
Ghostty Yes (Metal) Yes (Swift/AppKit) Yes (MIT) No telemetry key=value Native
iTerm2 No Yes Yes (GPLv2) No telemetry GUI + plist Yes
WezTerm Yes Cross-platform Yes (MIT) No telemetry Lua Built-in mux
Kitty Yes Cross-platform Yes (GPLv3) No telemetry key=value Yes (kittens)
Alacritty Yes Cross-platform Yes (Apache) No telemetry TOML No (use tmux)
Warp Yes Yes No Account required, telemetry GUI Yes

Recommendation: Ghostty

Why Ghostty over iTerm2 (your current terminal): - GPU-accelerated via Metal (smooth 120Hz on ProMotion displays) - Feels truly native macOS (Swift/AppKit, not Electron) - Fast startup, low memory - Simple ~/.config/ghostty/config file (version-controllable) - Built by Mitchell Hashimoto (HashiCorp founder) - actively maintained - 1M+ downloads/week as of 2026 - Zero telemetry, no account, no subscription

Ghostty is the closest thing to Warp's polish without any of the privacy concerns. It won't have Warp's AI features built-in, but you already have Claude Code for that.

Install: brew install --cask ghostty

Basic Ghostty Config (~/.config/ghostty/config)

font-family = JetBrains Mono
font-size = 14
theme = catppuccin-mocha
window-padding-x = 8
window-padding-y = 8
cursor-style = bar
shell-integration = zsh

7. File Sync: rsync, rclone, and the Ecosystem

rsync is powerful but the flag soup (-avzhP --delete --exclude ...) is hard to remember. There's no killer TUI/wrapper the community has rallied around — the ecosystem is fragmented. Here's the real landscape and what to actually use.

The state of rsync extensibility (as of 2026)

rsync itself hasn't meaningfully evolved its UX in decades. It's single-threaded, flag-heavy, and there's no built-in profile/task system. The community response:

Tool What it is Status
rclone Modern rsync alternative with parallelism, web GUI, 70+ backends Actively maintained, widely adopted
RsyncUI Native macOS SwiftUI GUI for rsync with saved profiles Actively maintained
rsyncy Progress bar wrapper for rsync Small but maintained
tui-rsync Python TUI for rsync profiles Barely maintained
backupmenu Bash menu for tar/rsync backups Basic, Linux-only
Timeshift Linux snapshot tool (rsync + hardlinks under the hood) Linux-only, proves rsync needs a wrapper

The gap: There is no good terminal-native rsync profile manager or TUI. This is a genuine hole in the ecosystem.

Use the right tool for each job

Scenario Best tool Why
Ad-hoc local/external drive sync Shell functions (below) or rclone Don't memorize flags
Many files over fast network (LAN/10Gbps) rclone 4x faster than rsync — parallel transfers saturate bandwidth
Large files that change slightly rsync Delta transfer — only sends changed bytes, rclone sends whole files
Recurring sync profiles (click-to-run) RsyncUI Saved tasks with GUI
Scripted server backups rsync (raw) Your senku script is correct — delta-based, hardlink-friendly
Cloud storage sync rclone 70+ backends (S3, GDrive, etc.), rsync can't do this
Bidirectional sync rclone bisync rsync is one-way only

rclone on macOS

rclone works natively on macOS. Install and use:

brew install rclone

Why rclone over rsync for local/external drive transfers: - Parallel transfers: --transfers=16 processes 16 files at once (rsync does 1 at a time) - On Jeff Geerling's 10Gbps LAN benchmark: rsync = 128 MB/s, rclone = 1 GB/s (4x faster) - Built-in web GUI: rclone rcd --rc-web-gui — browser-based file manager - Progress bars built-in (no wrapper needed) - Same syntax for local, remote, and cloud transfers

The tradeoff: rclone transfers whole files, not deltas. For a 10GB VM image where 1MB changed, rsync sends 1MB while rclone re-sends 10GB. For many small/medium files (photos, documents, music), rclone wins.

Common rclone commands:

# Copy with progress (like rsync -avhP)
rclone copy ~/Photos /Volumes/USB/Photos --progress --transfers=16

# Sync (mirror, like rsync --delete)
rclone sync ~/Music /Volumes/Backup/Music --progress --transfers=16

# Dry run first (like rsync -n)
rclone sync ~/Music /Volumes/Backup/Music --dry-run

# Bidirectional sync (rsync can't do this)
rclone bisync ~/Documents /Volumes/USB/Documents --progress

# Web GUI — browse and manage transfers visually
rclone rcd --rc-web-gui

Shell functions for when you do use rsync

For delta-heavy or server-to-server transfers where rsync is still better:

# --- rsync helpers (no flags to remember) ---

# Copy files with progress (the one you'll use most)
# Usage: rscopy ~/Documents/taxes /Volumes/USB-Drive/
rscopy() {
  rsync -avhP "$1" "$2"
}

# Mirror a folder exactly (deletes files on dest that aren't on source)
# Usage: rsmirror ~/Music /Volumes/Backup/Music
rsmirror() {
  echo "DRY RUN first — showing what would change:"
  rsync -avhn --delete "$1/" "$2/"
  echo ""
  read -q "REPLY?Proceed? (y/n) " && echo && rsync -avh --delete --info=progress2 "$1/" "$2/"
}

# Sync to/from a homelab host
# Usage: push chizuru ~/scripts /opt/scripts
push() { rsync -avhP "$2" "root@$1:$3"; }
# Usage: pull chizuru /var/log/syslog ~/Desktop/
pull() { rsync -avhP "root@$1:$2" "$3"; }

# Resume a large interrupted transfer
# Usage: rsresume ~/big-folder /Volumes/External/big-folder
rsresume() {
  rsync -avhP --partial --append-verify "$1" "$2"
}

Key design decisions: - rsmirror always does a dry-run first and asks for confirmation (because --delete is destructive) - rscopy uses -P (progress + partial resume) so you can Ctrl+C and re-run without starting over - push/pull use your SSH config aliases so push chizuru just works - Trailing slash handling: rsmirror adds / to source automatically so it mirrors contents, not the folder itself - Functions prefixed with rs to avoid collisions with system commands

fzf-powered interactive picker

For when you want to pick what to sync interactively (works with either rsync or rclone):

# Interactive sync — fuzzy-pick source and destination
# Usage: rspick (then pick from mounted volumes and common dirs)
rspick() {
  local dirs=()
  # Add mounted volumes (external drives, NFS, SMB)
  for vol in /Volumes/*(N); do
    [[ "$vol" != "/Volumes/Macintosh HD" ]] && dirs+=("$vol")
  done
  # Add common directories
  dirs+=("$HOME/Documents" "$HOME/Desktop" "$HOME/Downloads" "$HOME/git" "$HOME/Music" "$HOME/Pictures")

  echo "Select SOURCE:"
  local src=$(printf '%s\n' "${dirs[@]}" | fzf --prompt="Source > ")
  [[ -z "$src" ]] && return 1

  echo "Select DESTINATION:"
  local dst=$(printf '%s\n' "${dirs[@]}" | fzf --prompt="Dest > ")
  [[ -z "$dst" ]] && return 1

  echo ""
  echo "  From: $src"
  echo "  To:   $dst"
  echo ""

  # Use rclone for local-to-local (faster), rsync for remote
  if [[ "$src" == /* && "$dst" == /* ]]; then
    echo "Using rclone (parallel, faster for local transfers):"
    rclone sync "$src/" "$dst/" --dry-run --progress
    echo ""
    read -q "REPLY?Proceed? (y/n) " && echo && rclone sync "$src/" "$dst/" --progress --transfers=16
  else
    echo "DRY RUN (rsync):"
    rsync -avhn "$src/" "$dst/"
    echo ""
    read -q "REPLY?Proceed? (y/n) " && echo && rsync -avh --info=progress2 "$src/" "$dst/"
  fi
}

RsyncUI for saved profiles

If you have recurring syncs (e.g., "Photos to USB every week"):

brew install --cask rsyncui

Native macOS SwiftUI app. Save named profiles, run them with a click. Free, open-source, no telemetry. Good complement to the shell functions — not a replacement.

Quick reference card

I want to... Command
Copy many files fast (local/external) rclone copy ~/src /dest -P --transfers=16
Mirror folder exactly (local) rclone sync ~/src /dest -P --transfers=16
Bidirectional sync rclone bisync ~/src /dest -P
Web GUI for transfers rclone rcd --rc-web-gui
Copy with delta (large files, remote) rscopy ~/src host:/dest
Mirror with safety check (rsync) rsmirror ~/src /dest
Push files to homelab host push chizuru ~/file /remote/path
Pull files from homelab host pull chizuru /remote/path ~/local/
Resume interrupted transfer rsresume ~/big-folder /dest
Interactive pick source/dest rspick
Recurring sync profiles (GUI) RsyncUI app

Decision

Phase 1 - Immediate (low effort, high impact)

  1. Install Ghostty, replace iTerm2 as daily driver
  2. Install core CLI tools: brew install starship fzf zoxide bat eza fd ripgrep atuin tldr rclone
  3. Set up .zshrc with the structure above
  4. Expand ~/.ssh/config with host aliases

Phase 2 - Short term

  1. Install zsh plugins via Zinit (autosuggestions, syntax-highlighting, fzf-tab)
  2. Install homelab tools: lazydocker, pvetui
  3. Customize Starship prompt for homelab context

Phase 3 - Optional

  1. Deploy Termix on an LXC for browser-based SSH access
  2. Set up Atuin sync across machines
  3. Dotfiles repo to version-control all config

Sources