Initial commit: linux-provision repo

Distribution-agnostic provisioning script that sets up a new Linux machine
(Detected via lib/distro.sh - supports Debian/Ubuntu/Pop and Fedora families).

13 stages covering:
- System packages, external repos, toolchains (nvm, uv, Python)
- Shell config (zsh, oh-my-zsh, p10k), git, SSH
- Custom uv tools from ~40 git repos
- Desktop config (keybindings, hotkeys, ghostty, fonts)
- Docker, system tweaks, browser/app installs
- Custom systemd user services (porridge, swayidle, mempi-sync, etc.)
- API keys loaded from Bitwarden at shell startup
This commit is contained in:
2026-06-05 21:21:46 +10:00
commit 180c5838ea
36 changed files with 4176 additions and 0 deletions

42
stages/00-envcheck.sh Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 00: Environment Checks
# Verifies we can proceed: distro detected, sudo available, dirs exist.
# Distro detection is handled by lib/distro.sh (sourced by provision.sh).
# ===========================================================================
info "Distribution: ${DISTRO_ID} ${DISTRO_VERSION} (${DISTRO_FAMILY} family)"
info "Package manager: ${PKG_MGR}"
# ---- Sudo access ----
info "Checking sudo access..."
if ! sudo -n true 2>/dev/null; then
info "Sudo access required. You may be prompted for your password."
sudo -v || { error "Sudo required for provisioning."; exit 1; }
fi
ok "Sudo access confirmed."
# Keep sudo timestamp fresh
while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
# ---- Directory structure ----
info "Setting up directory structure..."
mkdir -p "$HOME/Development"
mkdir -p "$HOME/.local/bin"
mkdir -p "$HOME/.config"
mkdir -p "$HOME/.local/share"
ok "Directory structure ready."
# ---- Internet check ----
info "Checking internet connectivity..."
if ! ping -c 1 -W 3 google.com &>/dev/null && ! ping -c 1 -W 3 github.com &>/dev/null; then
warn "No internet detected. Some steps may fail."
else
ok "Internet connectivity confirmed."
fi
# ---- Package cache update (first time) ----
info "Updating package cache (first run)..."
$PKG_UPDATE 2>/dev/null || warn "Package cache update had issues."
ok "Stage 00 complete."

209
stages/01-repos.sh Normal file
View File

@@ -0,0 +1,209 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 01: Third-Party Repositories
# Adds external package repos. Completely different per distro family.
#
# Debian/Ubuntu/Pop: uses PPAs and .list/.sources files in
# /etc/apt/sources.list.d/
# Fedora: uses .repo files in /etc/yum.repos.d/ and COPRs
# ===========================================================================
# ==================================================================
# COMMON: Add GPG keys (shared helper)
# ==================================================================
add_gpg_key() {
local url="$1"
local dest="$2"
if [ ! -f "$dest" ]; then
sudo curl -fsSL "$url" -o "$dest" 2>/dev/null || {
warn "Failed to download GPG key: $url"
return 1
}
sudo chmod 644 "$dest"
fi
}
# ==================================================================
# DEBIAN / UBUNTU / POP — APT-based
# ==================================================================
if [ "$DISTRO_FAMILY" = "debian" ]; then
info "Configuring APT repositories..."
# ---- VS Code ----
info " Adding VS Code repo..."
if [ ! -f /etc/apt/sources.list.d/vscode.sources ]; then
add_gpg_key "https://packages.microsoft.com/keys/microsoft.asc" \
"/usr/share/keyrings/microsoft.gpg"
cat << 'EOF' | sudo tee /etc/apt/sources.list.d/vscode.sources > /dev/null
Types: deb
URIs: https://packages.microsoft.com/repos/code
Suites: stable
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/microsoft.gpg
EOF
ok " VS Code repo added."
fi
# ---- Google Chrome ----
info " Adding Google Chrome repo..."
if [ ! -f /etc/apt/sources.list.d/google-chrome.sources ]; then
add_gpg_key "https://dl.google.com/linux/linux_signing_key.pub" \
"/usr/share/keyrings/google-chrome.gpg"
cat << 'EOF' | sudo tee /etc/apt/sources.list.d/google-chrome.sources > /dev/null
Types: deb
URIs: https://dl.google.com/linux/chrome/deb/
Suites: stable
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/google-chrome.gpg
EOF
ok " Google Chrome repo added."
fi
# ---- Docker CE ----
info " Adding Docker CE repo..."
if [ ! -f /etc/apt/sources.list.d/docker.list ]; then
add_gpg_key "https://download.docker.com/linux/${DISTRO_ID}/gpg" \
"/etc/apt/keyrings/docker.asc"
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DISTRO_ID} ${DISTRO_VERSION_CODENAME:-$(. /etc/os-release && echo "$VERSION_CODENAME")} stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
ok " Docker CE repo added."
fi
# ---- Tailscale ----
info " Adding Tailscale repo..."
if [ ! -f /etc/apt/sources.list.d/tailscale.list ]; then
add_gpg_key "https://pkgs.tailscale.com/stable/${DISTRO_ID}/$(. /etc/os-release && echo "$VERSION_CODENAME").gz" \
"/usr/share/keyrings/tailscale-archive-keyring.gpg" 2>/dev/null || \
add_gpg_key "https://pkgs.tailscale.com/stable/${DISTRO_ID}/repo.gpg" \
"/usr/share/keyrings/tailscale-archive-keyring.gpg"
echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/${DISTRO_ID} $(. /etc/os-release && echo "$VERSION_CODENAME") main" | \
sudo tee /etc/apt/sources.list.d/tailscale.list > /dev/null
ok " Tailscale repo added."
fi
# ---- Signal Desktop ----
info " Adding Signal repo..."
if [ ! -f /etc/apt/sources.list.d/signal-desktop.sources ]; then
add_gpg_key "https://updates.signal.org/desktop/apt/keys.asc" \
"/usr/share/keyrings/signal-desktop-keyring.gpg"
cat << 'EOF' | sudo tee /etc/apt/sources.list.d/signal-desktop.sources > /dev/null
Types: deb
URIs: https://updates.signal.org/desktop/apt
Suites: xenial
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/signal-desktop-keyring.gpg
EOF
ok " Signal repo added."
fi
# ---- Papirus icon theme (PPA) ----
info " Adding Papirus PPA..."
if [ ! -f /etc/apt/sources.list.d/papirus-ubuntu-papirus-*.sources ]; then
$REPO_ADD_PPA papirus/papirus 2>/dev/null && ok " Papirus PPA added." || warn " Papirus PPA failed."
fi
# ---- Solaar (Logitech) PPA ----
info " Adding Solaar PPA..."
if [ ! -f /etc/apt/sources.list.d/solaar-unifying-ubuntu-stable-*.sources ]; then
$REPO_ADD_PPA solaar-unifying/stable 2>/dev/null && ok " Solaar PPA added." || warn " Solaar PPA failed."
fi
# ---- Ghostty terminal PPA ----
info " Adding Ghostty PPA..."
$REPO_ADD_PPA ghostty/ghostty 2>/dev/null && ok " Ghostty PPA added." || warn " Ghostty PPA failed."
# ---- Zotero repo ----
info " Adding Zotero repo..."
if [ ! -f /etc/apt/sources.list.d/zotero.list ]; then
add_gpg_key "https://zotero.retorque.re/file/apt-package-archive/pubkey.gpg" \
"/usr/share/keyrings/zotero-archive-keyring.gpg"
echo "deb [signed-by=/usr/share/keyrings/zotero-archive-keyring.gpg by-hash=force] https://zotero.retorque.re/file/apt-package-archive ./" | \
sudo tee /etc/apt/sources.list.d/zotero.list > /dev/null
ok " Zotero repo added."
fi
# ---- Yubico PPA ----
info " Adding Yubico PPA..."
$REPO_ADD_PPA yubico/stable 2>/dev/null && ok " Yubico PPA added." || warn " Yubico PPA failed."
# ==================================================================
# FEDORA / RHEL — DNF-based
# ==================================================================
elif [ "$DISTRO_FAMILY" = "fedora" ]; then
info "Configuring DNF repositories..."
# ---- RPM Fusion (free + nonfree) ----
info " Enabling RPM Fusion..."
$PKG_INSTALL \
"https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" \
"https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" \
2>/dev/null && ok " RPM Fusion configured." || warn " RPM Fusion install failed."
# ---- VS Code ----
info " Adding VS Code repo..."
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc 2>/dev/null || true
cat << 'EOF' | sudo tee /etc/yum.repos.d/vscode.repo > /dev/null
[code]
name=Visual Studio Code
baseurl=https://packages.microsoft.com/yumrepos/vscode
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc
EOF
ok " VS Code repo added."
# ---- Google Chrome ----
info " Adding Google Chrome repo..."
cat << 'EOF' | sudo tee /etc/yum.repos.d/google-chrome.repo > /dev/null
[google-chrome]
name=Google Chrome
baseurl=https://dl.google.com/linux/chrome/rpm/stable/x86_64
enabled=1
gpgcheck=1
gpgkey=https://dl.google.com/linux/linux_signing_key.pub
EOF
ok " Google Chrome repo added."
# ---- Docker CE ----
info " Adding Docker CE repo..."
$REPO_ADD_RPM https://download.docker.com/linux/fedora/docker-ce.repo 2>/dev/null && \
ok " Docker repo added." || warn " Docker repo add failed."
# ---- Tailscale ----
info " Adding Tailscale repo..."
$REPO_ADD_RPM https://pkgs.tailscale.com/stable/fedora/tailscale.repo 2>/dev/null && \
ok " Tailscale repo added." || warn " Tailscale repo add failed."
# ---- Signal Desktop ----
info " Adding Signal repo..."
cat << 'EOF' | sudo tee /etc/yum.repos.d/signal-desktop.repo > /dev/null
[signal-desktop]
name=Signal Desktop
baseurl=https://updates.signal.org/desktop/yum
enabled=1
gpgcheck=1
gpgkey=https://updates.signal.org/desktop/signal_pubkey.gpg
EOF
ok " Signal repo added."
# ---- COPRs for extra packages ----
# Papirus icon theme is in RPM Fusion nonfree.
# Solaar is in RPM Fusion.
# Yubico tools: use COPR
info " Adding COPR repos..."
# $REPO_ADD_COPR atim/papirus-icon-theme 2>/dev/null || true
# $REPO_ADD_COPR sergiomb/Solaar 2>/dev/null || true
# ---- Zotero — no DNF repo, use Flatpak (handled in stage 13) ----
info " Note: Zotero will be installed via Flatpak or tarball in stage 13."
fi
# ---- Update package cache after adding repos ----
info "Updating package cache..."
$PKG_UPDATE 2>/dev/null || warn "Package cache update had issues."
ok "Stage 01 complete: repositories configured."

125
stages/02-packages.sh Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 02: System Packages
# Installs all non-default packages — distro-agnostic.
#
# Package names that differ between Debian and Fedora are handled via
# pkg_install_mapped() or conditional branches.
# Same-name packages are installed with pkg_install().
# ===========================================================================
info "Installing system packages (this may take a while)..."
# ===========================================================================
# A. Development tools & compilers
# ===========================================================================
echo " Development tools..."
pkg_group_install "Development Tools" # Fedora only
# Common dev packages (same name on both)
pkg_install cmake
# Differently-named dev packages
pkg_install_mapped "build-essential" "@development-tools"
pkg_install_mapped "g++" "gcc-c++"
# Kernel headers
if [ "$DISTRO_FAMILY" = "debian" ]; then
pkg_install "linux-headers-$(uname -r)" 2>/dev/null || pkg_install linux-headers-generic
else
pkg_install kernel-devel kernel-headers
fi
pkg_install dkms
# ===========================================================================
# B. CLI utilities
# ===========================================================================
echo " CLI utility packages..."
# Same-name CLI tools
pkg_install \
ripgrep \
fd-find \
jq \
just \
ffmpeg \
wl-clipboard \
wtype \
wofi
# fd-find may need a symlink (binary is fdfind on both distros)
if command -v fdfind &>/dev/null && ! command -v fd &>/dev/null; then
if [ "$DISTRO_FAMILY" = "debian" ]; then
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd 2>/dev/null || true
else
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd 2>/dev/null || true
fi
fi
# Differently-named
pkg_install_mapped "imagemagick" "ImageMagick"
# ===========================================================================
# C. Media & graphics
# ===========================================================================
echo " Media & graphics packages..."
pkg_install gimp vlc
# ===========================================================================
# D. Fonts
# ===========================================================================
echo " Font packages..."
if [ "$DISTRO_FAMILY" = "debian" ]; then
pkg_install fonts-powerline fonts-noto-cjk fonts-noto-cjk-extra \
fonts-noto-core fonts-noto-ui-core 2>/dev/null || true
else
pkg_install powerline-fonts google-noto-cjk-fonts \
google-noto-fonts-common 2>/dev/null || true
fi
# ===========================================================================
# E. System tools
# ===========================================================================
echo " System tool packages..."
pkg_install \
powertop \
smartmontools \
solaar 2>/dev/null || warn " Some system tools failed."
# TLP — laptop power management
if [ "$DISTRO_FAMILY" = "debian" ]; then
pkg_install tlp 2>/dev/null || warn " tlp not available."
else
pkg_install tlp tlp-rdw 2>/dev/null || warn " tlp not available."
fi
# ===========================================================================
# F. General utilities
# ===========================================================================
echo " General utility packages..."
pkg_install \
rsync \
curl \
wget \
unzip \
p7zip 2>/dev/null || true
# ---- Ghostty terminal emulator ----
# Fedora: in official repos since F40
# Ubuntu/Pop: via PPA (added in stage 01)
echo " Ghostty terminal emulator..."
pkg_install ghostty 2>/dev/null || warn " ghostty install failed."
# ---- VS Code ----
echo " VS Code..."
pkg_install code 2>/dev/null || warn " code install failed."
# ===========================================================================
# Start enabled services
# ===========================================================================
if command -v tlp &>/dev/null; then
$SERVICE_ENABLE tlp 2>/dev/null || true
fi
ok "Stage 02 complete: system packages installed."

102
stages/03-toolchains.sh Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 03: Language Toolchains
# Installs nvm + Node.js LTS, uv (Python), and sets up the toolchain.
# ===========================================================================
# On Pop/Ubuntu these are post-system-package installs.
# Fedora packages for Node/python are available but we use version managers
# for flexibility (nvm for Node, uv for Python).
# ===========================================================================
# ---- nvm + Node.js ----
# nvm is installed to ~/.nvm. Node LTS is installed and set as default.
# This matches the Pop machine which had v24.11.1 via nvm.
info "Installing nvm (Node Version Manager)..."
if [ -d "$HOME/.nvm" ] && [ -f "$HOME/.nvm/nvm.sh" ]; then
ok "nvm already installed."
else
# Install latest nvm
# curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
# Use the install script from nvm's GitHub
export NVM_DIR="$HOME/.nvm"
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
ok "nvm installed."
fi
# Source nvm for this script
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
# Install latest LTS Node
info "Installing Node.js LTS via nvm..."
nvm install --lts 2>/dev/null || {
warn "nvm install --lts failed. Trying specific version..."
nvm install 24 2>/dev/null || warn "Could not install Node via nvm."
}
nvm alias default 'lts/*' 2>/dev/null || true
ok "Node.js $(node --version 2>/dev/null || echo 'installed')"
# Install/update npm to latest
npm install -g npm@latest 2>/dev/null || true
ok "npm $(npm --version 2>/dev/null || echo 'installed')"
# ---- npm global packages ----
# Julian's daily tools: pi agent, browser automation, subagents
info "Installing npm global packages..."
npm install -g \
@earendil-works/pi-coding-agent \
agent-browser \
pi-subagents \
2>/dev/null || warn "Some npm global packages failed."
ok "npm global packages installed."
# ---- uv (Python toolchain) ----
# uv is the recommended Python package + project manager.
# On Pop: uv was installed standalone from astral.sh — binary lives in ~/.local/bin
# The binary is self-updating via 'uv self update'.
info "Installing uv (Python project manager)..."
if command -v uv &>/dev/null; then
ok "uv already installed: $(uv --version)"
# Self-update to latest
uv self update 2>/dev/null || true
else
curl -fsSL https://astral.sh/uv/install.sh | bash
# Ensure ~/.local/bin is in PATH
export PATH="$HOME/.local/bin:$PATH"
ok "uv installed: $(uv --version)"
fi
# Install system Python if not present (Fedora usually has it)
if ! command -v python3 &>/dev/null; then
info "Installing Python..."
pkg_install python3 python3-pip python3-devel 2>/dev/null || true
fi
# ---- uv tool installs (from PyPI) ----
# Julian's daily Python CLI tools, installed via uv.
info "Installing uv tools from PyPI..."
if command -v uv &>/dev/null; then
uv tool install markitdown 2>/dev/null && echo " markitdown" || warn " markitdown install failed."
uv tool install pre-commit 2>/dev/null && echo " pre-commit" || warn " pre-commit install failed."
uv tool install yq 2>/dev/null && echo " yq (tomlq, xq)" || warn " yq install failed."
ok "PyPI uv tools installed."
fi
# ---- (Optional) Rust toolchain ----
# The Pop machine did NOT have Rust installed. Uncomment if you want it.
# info "Installing Rust via rustup..."
# if command -v rustc &>/dev/null; then
# ok "Rust already installed: $(rustc --version)"
# else
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# . "$HOME/.cargo/env"
# ok "Rust installed: $(rustc --version)"
# fi
# ---- (Optional) Go toolchain ----
# The Pop machine did NOT have Go installed. Uncomment if you want it.
# info "Installing Go..."
# sudo dnf install -y golang 2>/dev/null || warn "Go install failed."
# ok "Go installed: $(go version)"
ok "Stage 03 complete: language toolchains installed."

101
stages/04-shell.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 04: Shell Configuration (zsh, oh-my-zsh, powerlevel10k)
# Deploys .zshrc, .p10k.zsh, and .zshrc.local (from config/shell/).
# ===========================================================================
# On the Pop machine, Julian uses:
# - zsh as default shell
# - oh-my-zsh with powerlevel10k theme (lean style, 1 line)
# - Plugin: git, zsh-autosuggestions
# - Custom aliases: bt-reset (Bluetooth)
# - 17 environment variables (API keys)
# - NVM integration
# - Ctrl+Backspace → backward-kill-word
#
# API keys go into ~/.zshrc.local (NOT tracked in this repo).
# See config/shell/zshrc.local.example for the template.
# ===========================================================================
# ---- Install zsh ----
info "Installing zsh..."
if command -v zsh &>/dev/null; then
ok "zsh already installed: $(zsh --version | head -1)"
else
pkg_install zsh 2>/dev/null
fi
# ---- Install oh-my-zsh ----
info "Installing Oh My Zsh..."
if [ -d "$HOME/.oh-my-zsh" ]; then
ok "Oh My Zsh already installed."
else
# Non-interactive install (no chsh prompt)
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended 2>/dev/null || {
warn "Oh My Zsh install failed; trying without unattended flag."
# Fallback: set ZSH and clone directly
export ZSH="$HOME/.oh-my-zsh"
git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "$ZSH" 2>/dev/null || true
}
ok "Oh My Zsh installed."
fi
# ---- Install Powerlevel10k theme ----
info "Installing Powerlevel10k theme..."
if [ -d "${ZSH:-$HOME/.oh-my-zsh}/custom/themes/powerlevel10k" ]; then
ok "Powerlevel10k already installed."
else
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \
"${ZSH:-$HOME/.oh-my-zsh}/custom/themes/powerlevel10k" 2>/dev/null || warn "p10k clone failed."
fi
# ---- Install zsh-autosuggestions plugin ----
info "Installing zsh-autosuggestions plugin..."
if [ -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" ]; then
ok "zsh-autosuggestions already installed."
else
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions \
"${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" 2>/dev/null || warn "clone failed."
fi
# ---- Deploy .zshrc ----
info "Deploying .zshrc..."
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
if [ -f "$HOME/.zshrc" ]; then
# Backup existing
cp "$HOME/.zshrc" "$HOME/.zshrc.bak.$(date +%Y%m%d)" 2>/dev/null
warn "Backed up existing .zshrc to .zshrc.bak.$(date +%Y%m%d)"
fi
cp "${CONFIG_DIR}/shell/zshrc" "$HOME/.zshrc"
ok ".zshrc deployed."
# ---- Deploy .p10k.zsh ----
info "Deploying .p10k.zsh..."
if [ -f "$HOME/.p10k.zsh" ]; then
cp "$HOME/.p10k.zsh" "$HOME/.p10k.zsh.bak.$(date +%Y%m%d)" 2>/dev/null
warn "Backed up existing .p10k.zsh"
fi
cp "${CONFIG_DIR}/shell/p10k.zsh" "$HOME/.p10k.zsh"
ok ".p10k.zsh deployed."
# ---- Deploy .zshrc.local (secrets template) ----
# This file should contain your API keys. The example file has placeholders.
# It is NOT sourced by default. To enable, uncomment the "source ~/.zshrc.local"
# line in your deployed .zshrc.
info "Installing .zshrc.local example..."
if [ ! -f "$HOME/.zshrc.local" ]; then
cp "${CONFIG_DIR}/shell/zshrc.local.example" "$HOME/.zshrc.local"
warn "Created ~/.zshrc.local from example. EDIT IT with your API keys."
else
ok ".zshrc.local already exists (keeping existing)."
fi
# ---- Change default shell to zsh ----
info "Setting zsh as default shell..."
if [ "$SHELL" != "$(which zsh)" ]; then
chsh -s "$(which zsh)" 2>/dev/null || warn "Could not change shell (chsh)."
ok "Default shell set to zsh. Log out and back in to activate."
else
ok "zsh is already the default shell."
fi
ok "Stage 04 complete: shell configured."

101
stages/05-git.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 05: Git Configuration & SSH Keys
# Deploys .gitconfig and optionally generates SSH keys.
# ===========================================================================
# The Pop machine's .gitconfig is well-optimised:
# - SSH key signing (gpg.format = ssh)
# - zdiff3 conflict style, histogram diff algorithm
# - rerere.enabled, autoSquash, autoStash
# - push.autoSetupRemote, pull.rebase, fetch.prune
# ===========================================================================
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
# ---- Deploy .gitconfig ----
info "Deploying .gitconfig..."
if [ -f "$HOME/.gitconfig" ]; then
cp "$HOME/.gitconfig" "$HOME/.gitconfig.bak.$(date +%Y%m%d)" 2>/dev/null
warn "Backed up existing .gitconfig"
fi
# Use the template from config/git/gitconfig
# NOTE: This template does NOT contain your signing key or email.
# Edit it after deployment to set:
# [user]
# name = Your Name
# email = your.email@example.com
# signingkey = <your-ssh-public-key>
cp "${CONFIG_DIR}/git/gitconfig" "$HOME/.gitconfig"
ok ".gitconfig deployed."
warn "REMINDER: Edit ~/.gitconfig to set your name, email, and signingkey."
# ---- Deploy .gitignore_global ----
info "Deploying global .gitignore..."
if [ -f "$HOME/.gitignore" ]; then
warn "Global .gitignore already exists (keeping)."
else
# A sensible global gitignore for common OS + editor files
cat > "$HOME/.gitignore" << 'EOF'
# OS files
.DS_Store
Thumbs.db
Desktop.ini
# Editor/IDE
*.swp
*.swo
*~
.vscode/
.idea/
*.sublime-*
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
.eggs/
# Node
node_modules/
.npm/
# Rust
target/
EOF
ok "Global .gitignore deployed."
fi
# ---- Git SSH key ----
# The Pop machine has a single SSH key: id_ed25519_github
# Loading is handled by bw-load-ssh.sh (stage 07) from Bitwarden.
# This stage can generate a new key if one doesn't exist.
info "Checking SSH keys..."
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
warn "No SSH key found."
warn "Options:"
warn " 1. Generate a new key (recommended for new machine):"
warn " ssh-keygen -t ed25519 -C 'hi@julianprester.com'"
warn " 2. Restore from backup/Bitwarden (see TODO.md)"
warn " 3. Skip for now (run bw-load-ssh.sh later to load from Bitwarden)"
echo ""
read -r -p "Generate a new SSH key now? [y/N] " response
if [[ "$response" =~ ^[Yy]$ ]]; then
ssh-keygen -t ed25519 -C "hi@julianprester.com" -f "$HOME/.ssh/id_ed25519" -N "" 2>/dev/null || {
warn "Key generation skipped or failed."
}
ok "SSH key generated: ~/.ssh/id_ed25519.pub"
cat "$HOME/.ssh/id_ed25519.pub"
warn "Add this key to GitHub: https://github.com/settings/keys"
fi
else
ok "SSH key already exists."
fi
# Ensure proper SSH permissions
chmod 700 "$HOME/.ssh" 2>/dev/null || true
find "$HOME/.ssh" -type f -name "id_*" -exec chmod 600 {} \; 2>/dev/null || true
find "$HOME/.ssh" -type f -name "*.pub" -exec chmod 644 {} \; 2>/dev/null || true
ok "Stage 05 complete: Git configured."

81
stages/06-uv-projects.sh Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 06: Julian's uv Python Tools — Clone & Install
# Clones all custom Python tool repos (from GitHub) into ~/Development/
# and installs them via 'uv tool install' (editable mode from local path).
# ===========================================================================
# These are Julian's own CLI tools — the ones installed on Pop via uv.
# Each has a remote on github.com/julianprester/ (or re3-work/) and a
# pyproject.toml defining the package.
#
# Tools without a public remote (oracle, panac, skill-eval, mondada) are
# noted — you'll need to push them to GitHub or copy them manually.
#
# Order: tools that depend on other tools should come after. Most are
# independent Python packages.
# ===========================================================================
# Ensure uv is in PATH
export PATH="$HOME/.local/bin:$PATH"
# ---- Define tool repos ----
# Format: "repo_name:github_org:has_pyproject:has_package_json"
# repo_name = directory name under ~/Development/
# github_org = GitHub org (julianprester or re3-work)
# has_pyproject = true if it has pyproject.toml and should be uv-installed
# has_package_json = true if it has package.json and should be npm-linked
TOOLS=(
"porridge:julianprester:true:false" # Zoom meeting transcriber daemon
"deepis:julianprester:true:false" # Literature discovery CLI
"pi-persist:julianprester:true:false" # Memory persistence (mempi, pi-overview)
"panac:julianprester:true:false" # Pandoc wrapper CLI
"gromd:julianprester:true:false" # Gromd tool
"kannwas:julianprester:true:false" # Kannwas tool
"tb-api:julianprester:false:false" # Thunderbird REST API (not a Python/npm pkg — Firefox addon)
"hotkeys:julianprester:false:false" # Shell scripts for Wayland hotkeys (no install needed)
"ocpa:julianprester:true:false" # OpenCode pi agent Python package
)
# ===========================================================================
info "Cloning & installing Julian's Python tools..."
mkdir -p "$HOME/Development"
# ---- Clone and install each tool ----
for tool_entry in "${TOOLS[@]}"; do
IFS=':' read -r name org has_pyproject has_package_json <<< "$tool_entry"
target_dir="$HOME/Development/$name"
if [ -d "$target_dir" ]; then
ok "Repo '$name' already cloned. Pulling latest..."
git -C "$target_dir" pull --ff-only 2>/dev/null || warn "Could not pull $name."
else
info "Cloning $org/$name..."
git clone "git@github.com:${org}/${name}.git" "$target_dir" 2>/dev/null || {
warn "Clone failed for ${org}/${name}. Trying HTTPS fallback..."
git clone "https://github.com/${org}/${name}.git" "$target_dir" 2>/dev/null || {
warn "Could not clone ${org}/${name}. SSH keys not set up? Skipping."
continue
}
}
ok "Cloned $org/$name$target_dir"
fi
# Install via uv if it has pyproject.toml
if [ "$has_pyproject" = "true" ] && [ -f "$target_dir/pyproject.toml" ]; then
info "Installing '$name' via uv tool..."
# Try editable install from local path; fall back to non-editable
uv tool install --editable "$target_dir" 2>/dev/null || \
uv tool install "$target_dir" 2>/dev/null || \
warn "uv tool install failed for '$name'. Check pyproject.toml."
ok "'$name' installed via uv."
fi
done
# ---- Verify installations ----
echo ""
info "Verifying uv tool installations..."
uv tool list 2>/dev/null || warn "No uv tools installed."
ok "Stage 06 complete: uv tools installed."

86
stages/07-scripts.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 07: Custom Scripts (~/.local/bin/)
# Deploys Julian's custom scripts: Bitwarden SSH loader, Zoom wrapper,
# idle battery suspend, env PATH helper, and more.
# ===========================================================================
# These are the "glue" scripts that make the desktop work the way Julian
# expects. They were found in ~/.local/bin/ on the Pop machine.
#
# Config templates are in config/scripts/
# ===========================================================================
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
SCRIPTS_DIR="${CONFIG_DIR}/scripts"
TARGET_DIR="$HOME/.local/bin"
mkdir -p "$TARGET_DIR"
info "Deploying custom scripts to $TARGET_DIR..."
# ---- 1. env.sh — PATH helper (sourced by .profile / .bashrc) ----
# Ensures ~/.local/bin is in PATH without duplicate entries.
if [ -f "$SCRIPTS_DIR/env.sh" ]; then
cp "$SCRIPTS_DIR/env.sh" "$TARGET_DIR/env"
chmod +x "$TARGET_DIR/env"
ok "env deployed."
fi
# ---- 2. bw-load-ssh.sh — Load SSH keys from Bitwarden ----
# Script that fetches SSH keys from Bitwarden vault and loads into ssh-agent.
# Depends on: bw (Bitwarden CLI), jq, ssh-agent running.
if [ -f "$SCRIPTS_DIR/bw-load-ssh.sh" ]; then
cp "$SCRIPTS_DIR/bw-load-ssh.sh" "$TARGET_DIR/bw-load-ssh.sh"
chmod +x "$TARGET_DIR/bw-load-ssh.sh"
ok "bw-load-ssh.sh deployed."
fi
# ---- 3. zoom.sh — Zoom wrapper for Wayland + AMD GPU ----
# Forces Wayland native mode and VAAPI hardware video decoding on AMD Radeon.
# Without this, Zoom would use XWayland and software decoding (bad perf).
if [ -f "$SCRIPTS_DIR/zoom.sh" ]; then
cp "$SCRIPTS_DIR/zoom.sh" "$TARGET_DIR/zoom"
chmod +x "$TARGET_DIR/zoom"
ok "zoom wrapper deployed."
else
# Create default Zoom wrapper
cat > "$TARGET_DIR/zoom" << 'SCRIPT'
#!/bin/bash
# Zoom wrapper — forces Wayland + HW acceleration on AMD GPU
export QT_QPA_PLATFORM=wayland
export LIBVA_DRIVER_NAME=radeonsi
export LIBVA_DRI3_DISABLE=0
exec /usr/bin/zoom "$@"
SCRIPT
chmod +x "$TARGET_DIR/zoom"
ok "zoom wrapper created (default)."
fi
# ---- 4. idle-battery-suspend.sh — Suspend on battery after idle ----
# Checks if AC is disconnected before suspending. Prevents suspend on desktop.
# Used by swayidle.service (stage 08).
if [ -f "$SCRIPTS_DIR/idle-battery-suspend.sh" ]; then
cp "$SCRIPTS_DIR/idle-battery-suspend.sh" "$TARGET_DIR/idle-battery-suspend.sh"
chmod +x "$TARGET_DIR/idle-battery-suspend.sh"
ok "idle-battery-suspend.sh deployed."
fi
# ---- 5. Bitwarden CLI (bw) ----
# On Pop: ~/.local/bin/bw (138 MB standalone binary)
if ! command -v bw &>/dev/null; then
info "Installing Bitwarden CLI..."
# bw is a standalone binary — download it
BW_LATEST=$(curl -fsSL "https://api.github.com/repos/bitwarden/clients/releases?per_page=1" | grep -oP '"tag_name":\s*"cli-v\K[^"]+' | head -1) || BW_LATEST="2025.1.0"
curl -fsSL "https://github.com/bitwarden/clients/releases/download/cli-v${BW_LATEST}/bw-linux-${BW_LATEST}.zip" -o /tmp/bw.zip 2>/dev/null && {
unzip -o /tmp/bw.zip -d /tmp/bw-extract 2>/dev/null
cp /tmp/bw-extract/bw "$TARGET_DIR/bw"
chmod +x "$TARGET_DIR/bw"
rm -rf /tmp/bw.zip /tmp/bw-extract
ok "Bitwarden CLI installed."
} || warn "Bitwarden CLI download failed. Install manually: https://bitwarden.com/help/cli/"
fi
# Ensure permissions
chmod -R 755 "$TARGET_DIR" 2>/dev/null || true
ok "Stage 07 complete: custom scripts deployed to ~/.local/bin."

96
stages/08-systemd.sh Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 08: User Systemd Services
# Deploys and enables Julian's custom user systemd services.
# ===========================================================================
# On the Pop machine, Julian runs several custom services:
# - porridge.service : Zoom meeting transcriber daemon
# - porridge-dictate.service : Push-to-talk transcription
# - pi-overview.service : Session dashboard on port 3000
# - bw-ssh-keys.service : Load Bitwarden SSH keys at boot
# - mempi-sync.service : Sync memory DB to Nextcloud
# - mempi-sync.timer : Run mempi-sync on boot +5min
# - empty_downloads.service : Clear Downloads folder at login
# ===========================================================================
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
SERVICES_DIR="${CONFIG_DIR}/systemd"
UNIT_DIR="$HOME/.config/systemd/user"
mkdir -p "$UNIT_DIR"
info "Deploying user systemd services..."
# ---- Helper: install service file ----
install_service_file() {
local src="$1"
local name="$2"
if [ -f "$src" ]; then
cp "$src" "$UNIT_DIR/$name"
ok "Installed: $name"
else
warn "Service file not found: $src (skipping)"
fi
}
# ---- 1. porridge.service — Zoom transcriber daemon ----
install_service_file "$SERVICES_DIR/porridge.service" "porridge.service"
# ---- 2. porridge-dictate.service — Push-to-talk transcription ----
install_service_file "$SERVICES_DIR/porridge-dictate.service" "porridge-dictate.service"
# ---- 3. pi-overview.service — Session dashboard ----
install_service_file "$SERVICES_DIR/pi-overview.service" "pi-overview.service"
# ---- 4. bw-ssh-keys.service — Load Bitwarden SSH keys at boot ----
install_service_file "$SERVICES_DIR/bw-ssh-keys.service" "bw-ssh-keys.service"
# ---- 5. mempi-sync.service + timer — Sync memory DB to Nextcloud ----
install_service_file "$SERVICES_DIR/mempi-sync.service" "mempi-sync.service"
install_service_file "$SERVICES_DIR/mempi-sync.timer" "mempi-sync.timer"
# ---- 6. empty_downloads.service — Clear Downloads at login ----
install_service_file "$SERVICES_DIR/empty_downloads.service" "empty_downloads.service"
# ---- Enable and start services ----
info "Enabling and starting services..."
# Services that should start automatically (enabled)
systemctl --user daemon-reload
# Check which scripts from stage 07 are available before enabling services.
# This avoids failures when running stages out of order.
if [ -x "$HOME/.local/bin/porridge" ]; then
systemctl --user enable --now porridge.service 2>/dev/null && ok "porridge.service enabled"
else
warn "porridge.service skipped (binary not found — run stage 07 first)."
fi
if [ -x "$HOME/.local/bin/porridge" ]; then
systemctl --user enable --now porridge-dictate.service 2>/dev/null && ok "porridge-dictate.service enabled"
else
warn "porridge-dictate.service skipped (binary not found — run stage 07 first)."
fi
if [ -x "$HOME/.local/bin/pi-overview" ]; then
systemctl --user enable --now pi-overview.service 2>/dev/null && ok "pi-overview.service enabled"
else
warn "pi-overview.service skipped (binary not found — run stage 06-uv-projects first)."
fi
if [ -f "$HOME/.local/bin/bw-load-ssh.sh" ]; then
systemctl --user enable bw-ssh-keys.service 2>/dev/null && ok "bw-ssh-keys.service enabled"
else
warn "bw-ssh-keys.service skipped (script not found — run stage 07 first)."
fi
systemctl --user enable --now empty_downloads.service 2>/dev/null && ok "empty_downloads.service enabled" || warn "empty_downloads.service not started."
# Timers
systemctl --user enable --now mempi-sync.timer 2>/dev/null && ok "mempi-sync.timer enabled" || warn "mempi-sync.timer not started."
info "===== Service Status ====="
systemctl --user list-units --type=service --state=running 2>/dev/null | grep -E "(porridge|swayidle|pi-overview|mempi|bw-ssh|empty)" || true
ok "Stage 08 complete: user systemd services deployed."

199
stages/09-desktop.sh Normal file
View File

@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 09: Desktop Configuration
# Keybindings, hotkey scripts, Ghostty, fonts, and desktop theme.
# ===========================================================================
# On Pop!_OS, Julian uses COSMIC desktop (Rust-based, System76's own DE).
# Fedora ships with GNOME by default. This stage provides keybinding config
# for both GNOME (via gsettings) and a fallback swhkd/keyd approach.
#
# If you install a different DE (KDE, COSMIC via COPR, Sway/Hyprland),
# adjust this stage accordingly.
# ===========================================================================
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
# Detect desktop environment
DE="${XDG_CURRENT_DESKTOP:-unknown}"
# ===========================================================================
# 1. Hotkey Scripts (from hotkeys repo)
# ===========================================================================
# These are shell scripts that use selected text for quick actions.
# They rely on: wl-clipboard, wtype, wofi, jq, xdg-open
# Installed from ~/Development/hotkeys/ (cloned in stage 06).
info "Setting up hotkey scripts..."
HOTKEYS_DIR="$HOME/Development/hotkeys"
if [ -d "$HOTKEYS_DIR" ]; then
# Create a symlink or copy to ~/.local/bin for PATH access
for script in google.sh scholar.sh dictionary.sh pdf.sh emoji.sh hotstrings.sh; do
if [ -f "$HOTKEYS_DIR/$script" ]; then
ln -sf "$HOTKEYS_DIR/$script" "$HOME/.local/bin/${script%.sh}" 2>/dev/null || true
fi
done
ok "Hotkey scripts linked to ~/.local/bin/"
else
warn "hotkeys repo not cloned yet. Run stage 06 first."
fi
# ===========================================================================
# 2. Desktop Environment Keybindings
# ===========================================================================
# Map Julian's COSMIC keybindings to the current DE.
case "$DE" in
*GNOME*|*gnome*)
info "Configuring GNOME keybindings..."
# ---- Custom keybindings (hotkey scripts) ----
# Map Ctrl+Alt+{G,S,D,E,A,O} to the hotkey scripts
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "[]"
_add_gnome_kb() {
local name="$1"
local binding="$2"
local command="$3"
local path="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/${name}/"
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"${path}" \
name "${name}" 2>/dev/null || true
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"${path}" \
binding "${binding}" 2>/dev/null || true
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"${path}" \
command "${command}" 2>/dev/null || true
}
_add_gnome_kb "google" "<Control><Alt>g" "$HOME/.local/bin/google"
_add_gnome_kb "scholar" "<Control><Alt>s" "$HOME/.local/bin/scholar"
_add_gnome_kb "dictionary" "<Control><Alt>d" "$HOME/.local/bin/dictionary"
_add_gnome_kb "emoji" "<Control><Alt>e" "$HOME/.local/bin/emoji"
_add_gnome_kb "hotstrings" "<Control><Alt>a" "$HOME/.local/bin/hotstrings"
_add_gnome_kb "pdf" "<Control><Alt>o" "$HOME/.local/bin/pdf"
# ---- Collect all custom paths into array for gsettings ----
KB_PATHS="["
for n in google scholar dictionary emoji hotstrings pdf; do
KB_PATHS+="'/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/${n}/', "
done
KB_PATHS="${KB_PATHS%, }]"
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "${KB_PATHS}"
# ---- GNOME-specific shortcuts ----
# Super+L → lock (default on GNOME too)
# Super+Q → close window (default is Alt+F4; set Super+Q too)
gsettings set org.gnome.desktop.wm.keybindings close "['<Super>q', '<Alt>F4']" 2>/dev/null || true
ok "GNOME keybindings configured."
;;
*COSMIC*)
info "Configuring COSMIC keybindings..."
# On COSMIC, keybindings are stored in:
# ~/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom
# This is a RON (Rust Object Notation) file.
# We deploy our custom bindings from the config template.
if [ -f "$CONFIG_DIR/cosmic/custom-shortcuts.ron" ]; then
mkdir -p "$HOME/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1"
cp "$CONFIG_DIR/cosmic/custom-shortcuts.ron" \
"$HOME/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom"
# Set terminal command (COSMIC desktop)
mkdir -p "$HOME/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1"
echo '{
Terminal: "/usr/bin/ghostty --gtk-single-instance=true",
}' > "$HOME/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions"
ok "COSMIC keybindings deployed."
else
warn "COSMIC shortcuts template not found. Run 'cp config/cosmic/* ~/.config/cosmic/' manually."
fi
;;
*Hyprland*|*sway*|*Sway*)
info "Configuring Wayland compositor keybindings..."
warn "No automatic config for ${DE}. Apply manually from config/hotkeys/hyprland.conf or config/hotkeys/sway.config"
warn "See hotkeys/README.md for config examples."
;;
*KDE*|*Plasma*)
info "KDE Plasma detected. Applying keybindings via kwriteconfig..."
warn "KDE keybinding automation not yet implemented. Configure manually via System Settings > Shortcuts."
;;
*)
warn "Unknown DE: ${DE}. Keybindings must be configured manually."
warn "Reference: Ctrl+Alt+G/S/D/E/A/O → hotkey scripts in ~/Development/hotkeys/"
;;
esac
# ===========================================================================
# 4. Ghostty terminal config
# ===========================================================================
info "Deploying Ghostty config..."
mkdir -p "$HOME/.config/ghostty"
if [ -f "$CONFIG_DIR/ghostty/config" ]; then
cp "$CONFIG_DIR/ghostty/config" "$HOME/.config/ghostty/config"
ok "Ghostty config deployed."
fi
# ===========================================================================
# 5. Fonts (Nerd Fonts for terminal + dev)
# ===========================================================================
info "Installing Nerd Fonts..."
FONT_DIR="$HOME/.local/share/fonts"
mkdir -p "$FONT_DIR"
# MesloLGS NF — recommended for Powerlevel10k
# This is the font used on the Pop machine (MesloLGS NF Regular).
install_nerd_font() {
local font_name="$1"
local repo_url="$2"
local font_dir="$FONT_DIR"
# Check if already installed
if ls "$font_dir"/*"$font_name"* 2>/dev/null | grep -q .; then
ok "Font '$font_name' already installed."
return
fi
info "Downloading $font_name Nerd Font..."
local tmpdir
tmpdir=$(mktemp -d)
# Download from Nerd Fonts GitHub releases
curl -fsSL "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${font_name}.zip" \
-o "$tmpdir/${font_name}.zip" 2>/dev/null && {
unzip -o "$tmpdir/${font_name}.zip" -d "$tmpdir" 2>/dev/null
cp "$tmpdir"/*.ttf "$font_dir"/ 2>/dev/null || true
ok "Font '$font_name' installed."
} || warn "Font download failed for $font_name."
rm -rf "$tmpdir"
}
# On Pop, these were found:
# MesloLGS NF (Regular, Bold, Italic, Bold Italic)
# FiraCode Nerd Font Propo
# ApercuMonoPro-Regular.otf (proprietary — not distributed)
install_nerd_font "Meslo" ""
install_nerd_font "FiraCode" ""
# Rebuild font cache
fc-cache -f "$FONT_DIR" 2>/dev/null || true
ok "Font cache rebuilt."
# ===========================================================================
# 6. GTK Theme (dark mode + Papirus icons)
# ===========================================================================
info "Setting GTK theme and icons..."
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' 2>/dev/null || true
gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3-dark' 2>/dev/null || true
gsettings set org.gnome.desktop.interface icon-theme 'Papirus-Dark' 2>/dev/null || true
ok "GTK dark theme + Papirus icons set."
# ===========================================================================
# 7. Solaar (Logitech peripherals) config
# ===========================================================================
# The Pop machine had config for MX Keys Mini + MX Master 3.
# Solaar config is auto-generated when you pair devices. The config
# template (if supplied) can be placed at ~/.config/solaar/config.yaml
info "Solaar config will be generated automatically when you pair devices."
ok "Stage 09 complete: desktop configured."

68
stages/10-docker.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 10: Docker CE
# Installs Docker CE, enables on boot, and adds user to docker group.
# ===========================================================================
# On the Pop machine, Julian uses Docker extensively:
# - grobid (PDF extraction server)
# - rocker/rstudio (R environment)
# - pandoc/extra (document conversion)
#
# This stage covers Docker CE install only. Docker images are pulled
# on demand (too large to pre-pull in provisioning).
# ===========================================================================
info "Installing Docker CE..."
# Docker repo was added in stage 01. Install from it.
if command -v docker &>/dev/null; then
ok "Docker already installed: $(docker --version 2>/dev/null)"
else
$PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || {
error "Docker install failed. Check repo config in stage 01."
exit 1
}
ok "Docker CE packages installed."
# Enable and start Docker
$SERVICE_ENABLE docker 2>/dev/null
ok "Docker service enabled and started."
fi
# ---- Add user to docker group ----
if groups | grep -q docker; then
ok "User is already in the docker group."
else
info "Adding user to docker group (required to run Docker without sudo)..."
sudo usermod -aG docker "$USER" 2>/dev/null || warn "Could not add user to docker group."
warn "You'll need to log out and back in for docker group changes to take effect."
warn "Alternatively, run: newgrp docker (temporary, current shell only)."
fi
# ---- Verify Docker works ----
info "Verifying Docker installation..."
# Use a simple test (may need re-login for group changes)
if sg docker -c "docker run --rm hello-world" 2>/dev/null; then
ok "Docker verified: hello-world ran successfully."
else
warn "Docker verification failed. You may need to log out and back in."
warn "Or run: sudo usermod -aG docker $USER && newgrp docker"
fi
# ---- Install Docker Compose (plugin) ----
# docker compose (v2, plugin) was installed with docker-compose-plugin above.
if docker compose version 2>/dev/null; then
ok "Docker Compose v2 plugin ready: $(docker compose version --short 2>/dev/null)"
fi
# ---- (Optional) Pre-pull commonly used images ----
# Uncomment to pre-pull images Julian uses frequently.
# Note: grobid image is large (~2GB each).
info "Note: Docker images are pulled on demand when you run containers."
info "Common images Julian uses:"
info " - grobid/grobid (PDF extraction)"
info " - rocker/tidyverse or rocker/verse (RStudio)"
info " - pandoc/extra"
echo ""
ok "Stage 10 complete: Docker installed."

37
stages/11-tweaks.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 11: System Tweaks
# sysctl tuning, kernel cmdline parameters, TLP/powertop, modprobe.
# Uses distro-agnostic variables from lib/distro.sh.
# ===========================================================================
# CAUTION: GPU kernel parameters are hardware-specific (AMD Radeon 680M).
# They are COMMENTED OUT by default. Uncomment only if you have the same GPU.
# ===========================================================================
CONFIG_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config"
# ===========================================================================
# 1. TLP / PowerTOP
# ===========================================================================
info "Configuring power management..."
$SERVICE_ENABLE tlp 2>/dev/null && ok "TLP enabled." || warn "TLP not available."
if command -v powertop &>/dev/null; then
# Enable powertop auto-tune via systemd service
if [ ! -f /etc/systemd/system/powertop.service ]; then
sudo tee /etc/systemd/system/powertop.service > /dev/null << 'EOF'
[Unit]
Description=PowerTOP auto tune
[Service]
Type=oneshot
ExecStart=/usr/sbin/powertop --auto-tune
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
fi
$SERVICE_ENABLE powertop 2>/dev/null && ok "PowerTOP auto-tune enabled." || true
fi
ok "Stage 11 complete: system tweaks applied."

58
stages/12-other-apps.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# ===========================================================================
# Stage 12: Other Applications
# Depends on: stage 01 (repos), stage 02 (packages)
# Chrome, Signal, Zotero, Obsidian, Nextcloud client, FreeRDP.
# Distro-agnostic — uses pkg_install / Flatpak / tarball.
# ===========================================================================
# ---- Google Chrome ----
info "Installing Google Chrome..."
pkg_install google-chrome-stable 2>/dev/null && ok "Chrome installed." \
|| warn "Chrome not available. Install from https://www.google.com/chrome/"
# ---- Signal Desktop ----
info "Installing Signal Desktop..."
pkg_install signal-desktop 2>/dev/null && ok "Signal installed." \
|| warn "Signal not available. Check repo in stage 01."
# ---- Zotero (reference manager) ----
info "Installing Zotero..."
if command -v zotero &>/dev/null || flatpak list 2>/dev/null | grep -qi "zotero"; then
ok "Zotero already installed."
elif command -v flatpak &>/dev/null; then
flatpak install -y flathub org.zotero.Zotero 2>/dev/null && ok "Zotero installed via Flatpak." \
|| warn "Zotero Flatpak failed. Try manual tarball from zotero.org."
else
warn "Zotero not installed. Get it from https://www.zotero.org/download/"
fi
# ---- Obsidian (knowledge base) ----
info "Installing Obsidian..."
if command -v obsidian &>/dev/null || flatpak list 2>/dev/null | grep -qi "obsidian"; then
ok "Obsidian already installed."
elif command -v flatpak &>/dev/null; then
flatpak install -y flathub md.obsidian.Obsidian 2>/dev/null && ok "Obsidian installed." \
|| warn "Obsidian Flatpak failed. Download from https://obsidian.md/"
else
warn "Obsidian not installed."
fi
# ---- Nextcloud Desktop Client ----
info "Installing Nextcloud Desktop Client..."
if [ "$DISTRO_FAMILY" = "debian" ]; then
pkg_install nextcloud-desktop 2>/dev/null && ok "Nextcloud client installed." || \
warn "Nextcloud client not available."
else
pkg_install nextcloud-client 2>/dev/null && ok "Nextcloud client installed." || \
warn "Nextcloud client not available."
fi
# ---- FreeRDP (for WinBoat RDP) ----
info "Installing FreeRDP (Flatpak)..."
if command -v flatpak &>/dev/null; then
flatpak install -y flathub com.freerdp.FreeRDP 2>/dev/null && ok "FreeRDP installed." \
|| warn "FreeRDP Flatpak failed."
fi
ok "Stage 12 complete: additional applications installed."