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:
42
stages/00-envcheck.sh
Normal file
42
stages/00-envcheck.sh
Normal 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
209
stages/01-repos.sh
Normal 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
125
stages/02-packages.sh
Normal 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
102
stages/03-toolchains.sh
Normal 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
101
stages/04-shell.sh
Normal 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
101
stages/05-git.sh
Normal 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
81
stages/06-uv-projects.sh
Normal 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
86
stages/07-scripts.sh
Normal 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
96
stages/08-systemd.sh
Normal 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
199
stages/09-desktop.sh
Normal 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
68
stages/10-docker.sh
Normal 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
37
stages/11-tweaks.sh
Normal 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
58
stages/12-other-apps.sh
Normal 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."
|
||||
Reference in New Issue
Block a user