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:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Secrets — these should never be committed
|
||||||
|
.zshrc.local
|
||||||
|
.env
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
106
README.md
Normal file
106
README.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Linux Machine Provisioning
|
||||||
|
|
||||||
|
**Purpose:** Rapidly set up a new Fedora Linux machine with Julian's toolchain,
|
||||||
|
config, and customisations. Generated from audit of Pop!_OS "thinkpad" machine.
|
||||||
|
|
||||||
|
## Supported Distributions
|
||||||
|
|
||||||
|
Auto-detected — no manual config needed:
|
||||||
|
|
||||||
|
| Family | Distros | Package Manager |
|
||||||
|
|--------|---------|-----------------|
|
||||||
|
| **Debian** | Ubuntu, Pop!_OS, Debian, Linux Mint | `apt` |
|
||||||
|
| **Fedora** | Fedora Workstation, RHEL, CentOS | `dnf` |
|
||||||
|
|
||||||
|
Detection is in `lib/distro.sh`. It sets variables like `$PKG_INSTALL`,
|
||||||
|
`$PKG_UPDATE`, `$GRUB_UPDATE` etc. that all stage scripts use.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone this repo on the new machine
|
||||||
|
git clone git@github.com:julianprester/linux-provision.git ~/linux-provision
|
||||||
|
cd ~/linux-provision
|
||||||
|
|
||||||
|
# Review and edit config/shell/zshrc.local with your API keys
|
||||||
|
cp config/shell/zshrc.local.example ~/.zshrc.local
|
||||||
|
# Edit ~/.zshrc.local with real API keys
|
||||||
|
|
||||||
|
# Run the full provisioning (will prompt for sudo)
|
||||||
|
bash provision.sh --all
|
||||||
|
|
||||||
|
# Or run individual stages
|
||||||
|
bash provision.sh --stage 03-toolchains
|
||||||
|
bash provision.sh --stage 06-uv-projects
|
||||||
|
|
||||||
|
# Or source it for interactive use
|
||||||
|
source provision.sh --interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
linux-provision/
|
||||||
|
├── README.md # This file
|
||||||
|
├── provision.sh # Master orchestrator — run with --all or --stage N
|
||||||
|
├── stages/ # Modular stage scripts, sourced by provision.sh
|
||||||
|
│ ├── 00-envcheck.sh # OS/sudo/environment checks
|
||||||
|
│ ├── 01-repos.sh # DNF repos (RPM Fusion, COPR, Microsoft, etc.)
|
||||||
|
│ ├── 02-packages.sh # System packages via DNF
|
||||||
|
│ ├── 03-toolchains.sh # nvm, Node, uv, Python
|
||||||
|
│ ├── 04-shell.sh # zsh, oh-my-zsh, powerlevel10k, configs
|
||||||
|
│ ├── 05-git.sh # Git config, SSH key setup
|
||||||
|
│ ├── 06-uv-projects.sh # Clone + install Julian's uv tools from ~/Development
|
||||||
|
│ ├── 07-scripts.sh # ~/.local/bin (bw, zoom, env, etc.)
|
||||||
|
│ ├── 08-systemd.sh # User systemd services (porridge, swayidle, etc.)
|
||||||
|
│ ├── 09-desktop.sh # Keybindings, hotkeys, ghostty, fonts
|
||||||
|
│ ├── 10-docker.sh # Docker CE setup
|
||||||
|
│ ├── 11-tweaks.sh # sysctl, kernel params, TLP/powertop, modprobe
|
||||||
|
│ └── 12-other-apps.sh # Chrome, Signal, Zotero
|
||||||
|
├── config/ # Dotfiles and config files (installed by stages)
|
||||||
|
│ ├── git/gitconfig
|
||||||
|
│ ├── shell/{zshrc,zshrc.local.example,p10k.zsh}
|
||||||
|
│ ├── scripts/{bw-load-ssh.sh,idle-battery-suspend.sh,zoom.sh,env.sh}
|
||||||
|
│ ├── systemd/{porridge.service,...}
|
||||||
|
│ ├── sysctl/99-custom.conf
|
||||||
|
│ └── modprobe/{system76-power.conf,pop-default-settings-dirty-frag.conf}
|
||||||
|
├── etc/ # System-level configs (copied to /etc)
|
||||||
|
└── TODO.md # Post-provisioning manual steps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stages Overview
|
||||||
|
|
||||||
|
| # | Stage | What it does |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 00 | envcheck | Verify Fedora, sudo access, directory setup |
|
||||||
|
| 01 | repos | RPM Fusion free/nonfree, COPRs, Microsoft, Docker, Google, Signal, Tailscale |
|
||||||
|
| 02 | packages | Install all system packages (distro-mapped names) |
|
||||||
|
| 03 | toolchains | Install nvm + Node LTS, uv, Python |
|
||||||
|
| 04 | shell | Install zsh, oh-my-zsh, p10k, deploy .zshrc, .p10k.zsh |
|
||||||
|
| 05 | git | Deploy .gitconfig, generate SSH key |
|
||||||
|
| 06 | uv-projects | Clone all Julian's Python tool repos from GitHub, uv install |
|
||||||
|
| 07 | scripts | Deploy ~/.local/bin scripts |
|
||||||
|
| 08 | systemd | Deploy and enable user systemd services |
|
||||||
|
| 09 | desktop | Configure keybindings, hotkeys, ghostty, fonts |
|
||||||
|
| 10 | docker | Install Docker CE, add user to docker group |
|
||||||
|
| 11 | tweaks | sysctl, kernel cmdline, TLP/powertop, modprobe blacklists |
|
||||||
|
| 12 | other-apps | Google Chrome, Signal, Zotero |
|
||||||
|
|
||||||
|
## Post-Install Manual Steps
|
||||||
|
|
||||||
|
See `TODO.md` for things that can't be automated: restoring SSH keys
|
||||||
|
from Bitwarden, configuring Tailscale, importing GPG keys, etc.
|
||||||
|
|
||||||
|
## Design Notes
|
||||||
|
|
||||||
|
- **Distribution-agnostic** — detects Debian/Ubuntu/Pop vs Fedora via
|
||||||
|
`lib/distro.sh`. Package manager commands, repo config, and package
|
||||||
|
names adapt automatically.
|
||||||
|
- **Idempotent** — safe to run multiple times. Stages check for existing
|
||||||
|
installations before repeating work.
|
||||||
|
- **Secrets out of repo** — API keys live in `~/.zshrc.local` (gitignored).
|
||||||
|
The repo ships `zshrc.local.example` with placeholder values.
|
||||||
|
- **One stage per concern** — comment out stages you don't need in
|
||||||
|
`provision.sh` or pass `--stage` individually.
|
||||||
|
- **Hardware-specific quirks commented out** — AMD GPU kernel params,
|
||||||
|
WiFi workarounds, etc. are documented but disabled by default.
|
||||||
173
TODO.md
Normal file
173
TODO.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Post-Provisioning TODO
|
||||||
|
|
||||||
|
Things that can't be fully automated (require manual setup, credentials,
|
||||||
|
or hardware-specific configuration).
|
||||||
|
|
||||||
|
## 1. SSH Keys & GitHub
|
||||||
|
|
||||||
|
- [ ] **Load SSH keys from Bitwarden** or generate a new key:
|
||||||
|
```bash
|
||||||
|
# Option A: Generate fresh key
|
||||||
|
ssh-keygen -t ed25519 -C "hi@julianprester.com"
|
||||||
|
|
||||||
|
# Option B: Set up Bitwarden+SSH loading
|
||||||
|
bw login
|
||||||
|
bw unlock --raw > ~/.config/Bitwarden\ CLI/.session
|
||||||
|
chmod 600 ~/.config/Bitwarden\ CLI/.session
|
||||||
|
bw-load-ssh.sh
|
||||||
|
|
||||||
|
# Option C: Copy keys from old machine
|
||||||
|
# scp old-machine:~/.ssh/id_ed25519* ~/.ssh/
|
||||||
|
```
|
||||||
|
- [ ] **Add SSH public key to GitHub**: https://github.com/settings/keys
|
||||||
|
- [ ] Clone this repo and remaining repos:
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:julianprester/linux-provision.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Bitwarden & Environment Variables
|
||||||
|
|
||||||
|
API keys are loaded directly in `.zshrc` via `bw` + `jq`. No separate script.
|
||||||
|
|
||||||
|
- [ ] **Run `bw login`** to authenticate with Bitwarden
|
||||||
|
- [ ] **Unlock vault and save session:**
|
||||||
|
```bash
|
||||||
|
bw unlock --raw > ~/.config/Bitwarden\ CLI/.session
|
||||||
|
chmod 600 ~/.config/Bitwarden\ CLI/.session
|
||||||
|
```
|
||||||
|
- [ ] **Create a Bitwarden item** named "Environment" (type: Secure Note)
|
||||||
|
with custom fields for each API key:
|
||||||
|
|
||||||
|
| Field Name | Type | Example Value |
|
||||||
|
|---|---|---|
|
||||||
|
| `GROQ_API_KEY` | Hidden | `gsk_your_key` |
|
||||||
|
| `ANTHROPIC_API_KEY` | Hidden | `sk-ant-your-key` |
|
||||||
|
| `GOOGLE_API_KEY` | Hidden | `AIza_your_key` |
|
||||||
|
| `CANVAS_API_KEY` | Hidden | `3156~your_key` |
|
||||||
|
| `NC_PASSWORD` | Hidden | `your_nextcloud_password` |
|
||||||
|
| ... (22 vars total — see `config/shell/zshrc.local.example` for the full list) |
|
||||||
|
|
||||||
|
- [ ] **Open a new shell** — `.zshrc` exports them automatically
|
||||||
|
- [ ] **Verify:** `echo $GROQ_API_KEY` (should show your key)
|
||||||
|
|
||||||
|
If you prefer a plain file instead of Bitwarden:
|
||||||
|
- [ ] Edit `~/.zshrc.local` with your API keys (template in `config/shell/zshrc.local.example`)
|
||||||
|
- [ ] Uncomment the alternate `source ~/.zshrc.local` line in the deployed `.zshrc`
|
||||||
|
|
||||||
|
If you prefer a plain file instead of Bitwarden:
|
||||||
|
- [ ] Edit `~/.zshrc.local` with your API keys (template in `config/shell/zshrc.local.example`)
|
||||||
|
- [ ] Uncomment the `source ~/.zshrc.local` line in your deployed `.zshrc`
|
||||||
|
|
||||||
|
## 3. Tailscale
|
||||||
|
|
||||||
|
- [ ] Authenticate Tailscale:
|
||||||
|
```bash
|
||||||
|
sudo tailscale up
|
||||||
|
```
|
||||||
|
- [ ] Verify connection: `tailscale status`
|
||||||
|
- [ ] Note your Tailscale IP for services (Actual Budget, Nextcloud, etc.)
|
||||||
|
|
||||||
|
## 4. Nextcloud
|
||||||
|
|
||||||
|
- [ ] Install Nextcloud Desktop Client (Flatpak or RPM)
|
||||||
|
- [ ] Connect to `https://nc.julianprester.com`
|
||||||
|
- [ ] Select sync folders (especially `Nextcloud/3_bibliography/`)
|
||||||
|
- [ ] Update `PandocCiter.DefaultBib` in VS Code settings if bib path changes
|
||||||
|
|
||||||
|
## 5. Actual Budget
|
||||||
|
|
||||||
|
- [ ] Verify connection: `actualpy accounts`
|
||||||
|
- [ ] Update URL/password in `~/.config/actualpy/config.yaml`
|
||||||
|
|
||||||
|
## 6. Docker & WinBoat
|
||||||
|
|
||||||
|
- [ ] Log out and back in for docker group to take effect
|
||||||
|
- [ ] Pull WinBoat image: `docker pull ghcr.io/dockur/windows:5.14`
|
||||||
|
- [ ] Set up `~/.winboat/docker-compose.yml` (see reference in repo notes)
|
||||||
|
- [ ] Pull grobid: `docker pull grobid/grobid`
|
||||||
|
- [ ] Run grobid: `docker run -d -p 8070:8070 grobid/grobid`
|
||||||
|
|
||||||
|
## 7. Zotero
|
||||||
|
|
||||||
|
- [ ] Install Zotero (Flatpak or tarball from zotero.org)
|
||||||
|
- [ ] Sign in to sync library
|
||||||
|
- [ ] Install Zotero browser connector
|
||||||
|
- [ ] Set ZOTERO_KEY env var in `~/.zshrc.local`
|
||||||
|
|
||||||
|
## 8. GNOME Keybindings (if using GNOME)
|
||||||
|
|
||||||
|
- [ ] Verify custom shortcuts were applied:
|
||||||
|
```bash
|
||||||
|
gsettings list-recursively org.gnome.settings-daemon.plugins.media-keys.custom-keybinding
|
||||||
|
```
|
||||||
|
- [ ] Or add them manually via Settings → Keyboard → Keyboard Shortcuts
|
||||||
|
|
||||||
|
## 9. Fonts
|
||||||
|
|
||||||
|
- [ ] If Nerd Font download failed, install manually:
|
||||||
|
- Download from https://www.nerdfonts.com/font-downloads
|
||||||
|
- MesloLGS NF (recommended for Powerlevel10k)
|
||||||
|
- Extract to `~/.local/share/fonts/` and run `fc-cache -fv`
|
||||||
|
|
||||||
|
## 10. Ghostty
|
||||||
|
|
||||||
|
- [ ] Verify Ghostty runs and fonts look correct (nerd font icons in prompt)
|
||||||
|
- [ ] If not, set `font-family = "MesloLGS NF"` in `~/.config/ghostty/config`
|
||||||
|
|
||||||
|
## 11. VS Code
|
||||||
|
|
||||||
|
- [ ] Open VS Code and verify extensions are installed
|
||||||
|
- [ ] Sign in to GitHub → Settings → Sync (if you use Settings Sync)
|
||||||
|
- [ ] Verify PandocCiter path to bibliography
|
||||||
|
|
||||||
|
## 12. Solaar (Logitech Peripherals)
|
||||||
|
|
||||||
|
- [ ] Open Solaar from applications menu
|
||||||
|
- [ ] Pair your Logitech receiver or connect via Bluetooth
|
||||||
|
- [ ] The config will auto-save to `~/.config/solaar/config.yaml`
|
||||||
|
|
||||||
|
## 13. Printer / Scanning
|
||||||
|
|
||||||
|
- [ ] If using a printer, add via Settings → Printers
|
||||||
|
- [ ] If using a scanner, install `simple-scan`:
|
||||||
|
```bash
|
||||||
|
sudo dnf install simple-scan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. Reboot to Apply Kernel Changes
|
||||||
|
|
||||||
|
- [ ] `sudo reboot` — required for:
|
||||||
|
- GRUB kernel cmdline parameters (if uncommented)
|
||||||
|
- sysctl settings (most apply at runtime, but reboot ensures)
|
||||||
|
- Docker group membership
|
||||||
|
- Desktop environment changes
|
||||||
|
|
||||||
|
## 15. Verify Everything
|
||||||
|
|
||||||
|
Run a quick sanity check after reboot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development tools
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
python3 --version
|
||||||
|
uv --version
|
||||||
|
git --version
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker run --rm hello-world
|
||||||
|
|
||||||
|
# Shell
|
||||||
|
zsh --version
|
||||||
|
echo $SHELL
|
||||||
|
|
||||||
|
# Services
|
||||||
|
systemctl --user status porridge.service 2>/dev/null | head -5
|
||||||
|
|
||||||
|
# Network
|
||||||
|
tailscale status
|
||||||
|
ping -c 1 google.com
|
||||||
|
|
||||||
|
# Config files exist
|
||||||
|
ls -la ~/.zshrc ~/.zshrc.local ~/.gitconfig ~/.p10k.zsh ~/.local/bin/
|
||||||
|
```
|
||||||
67
config/cosmic/cosmic-comp-settings.ron
Normal file
67
config/cosmic/cosmic-comp-settings.ron
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// COSMIC Desktop — Compositor Settings (RON format)
|
||||||
|
// File location: ~/.config/cosmic/com.system76.CosmicComp/v1/...
|
||||||
|
// FOR REFERENCE ONLY — adapt to your target DE.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ---- XKB keyboard config ----
|
||||||
|
// Location: .../v1/xkb_config
|
||||||
|
(
|
||||||
|
rules: "",
|
||||||
|
model: "",
|
||||||
|
layout: "us",
|
||||||
|
variant: "",
|
||||||
|
options: Some("compose:ralt"), // Right Alt = Compose key
|
||||||
|
repeat_delay: 600,
|
||||||
|
repeat_rate: 25,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Touchpad ----
|
||||||
|
// Location: .../v1/input_touchpad
|
||||||
|
(
|
||||||
|
state: Enabled,
|
||||||
|
acceleration: Some((profile: Some(Flat), speed: 0.3213816999010042)),
|
||||||
|
click_method: Some(Clickfinger),
|
||||||
|
scroll_config: Some((
|
||||||
|
method: Some(TwoFinger),
|
||||||
|
natural_scroll: Some(true),
|
||||||
|
scroll_button: None,
|
||||||
|
scroll_factor: None,
|
||||||
|
)),
|
||||||
|
tap_config: Some((
|
||||||
|
enabled: true,
|
||||||
|
button_map: Some(LeftRightMiddle),
|
||||||
|
drag: true,
|
||||||
|
drag_lock: false,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Mouse ----
|
||||||
|
// Location: .../v1/input_default
|
||||||
|
(
|
||||||
|
state: Enabled,
|
||||||
|
acceleration: Some((profile: Some(Flat), speed: 0.4628044123886297)),
|
||||||
|
scroll_config: Some((
|
||||||
|
method: None,
|
||||||
|
natural_scroll: Some(false),
|
||||||
|
scroll_button: None,
|
||||||
|
scroll_factor: Some(1.0),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Workspaces ----
|
||||||
|
// Location: .../v1/workspaces
|
||||||
|
(
|
||||||
|
workspace_mode: OutputBound,
|
||||||
|
workspace_layout: Horizontal,
|
||||||
|
action_on_typing: None,
|
||||||
|
workspace_wraparound: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Autotile ----
|
||||||
|
// Location: .../v1/autotile
|
||||||
|
false
|
||||||
|
|
||||||
|
// ---- Autotile behavior ----
|
||||||
|
// Location: .../v1/autotile_behavior
|
||||||
|
PerWorkspace
|
||||||
88
config/cosmic/custom-shortcuts.ron
Normal file
88
config/cosmic/custom-shortcuts.ron
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// COSMIC Desktop — Custom Shortcuts (RON format)
|
||||||
|
// File location: ~/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom
|
||||||
|
// This is the keybinding config from the Pop!_OS machine.
|
||||||
|
// =============================================================================
|
||||||
|
// COSMIC is System76's Rust-based desktop environment (not shipped on Fedora).
|
||||||
|
// This file is FOR REFERENCE only — translate to your target DE:
|
||||||
|
//
|
||||||
|
// GNOME: Use gsettings (see stage 09-desktop.sh)
|
||||||
|
// KDE: Use kwriteconfig5 or System Settings
|
||||||
|
// Sway/Hypr: Use sway config or hyprland.conf
|
||||||
|
// swhkd: Use ~/.config/swhkd/swhkdrc
|
||||||
|
//
|
||||||
|
// The hotkey scripts (google.sh, scholar.sh, etc.) work on any DE/WM that
|
||||||
|
// supports Wayland. They use wl-clipboard, wofi, and xdg-open.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
{
|
||||||
|
// ---- Text selection searches ----
|
||||||
|
// Select text anywhere, press shortcut → action with selected text
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "e",
|
||||||
|
description: Some("Emoji"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/emoji.sh"),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "o",
|
||||||
|
description: Some("PDF"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/pdf.sh"),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "a",
|
||||||
|
description: Some("Hotstrings"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/hotstrings.sh"),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "d",
|
||||||
|
description: Some("Dictionary"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/dictionary.sh"),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "g",
|
||||||
|
description: Some("Google"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/google.sh"),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Ctrl, Alt ],
|
||||||
|
key: "s",
|
||||||
|
description: Some("Scholar"),
|
||||||
|
): Spawn("/home/julian/Development/hotkeys/scholar.sh"),
|
||||||
|
|
||||||
|
// ---- Window management ----
|
||||||
|
(
|
||||||
|
modifiers: [ Super ],
|
||||||
|
key: "n",
|
||||||
|
): Minimize,
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Super ],
|
||||||
|
key: "Escape",
|
||||||
|
): Close,
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Super, Shift ],
|
||||||
|
key: "Escape",
|
||||||
|
): Disable,
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Super ],
|
||||||
|
key: "q",
|
||||||
|
): Disable,
|
||||||
|
|
||||||
|
// ---- System actions ----
|
||||||
|
(
|
||||||
|
modifiers: [ Super ],
|
||||||
|
key: "l",
|
||||||
|
): System(LockScreen),
|
||||||
|
|
||||||
|
(
|
||||||
|
modifiers: [ Super, Shift ],
|
||||||
|
key: "l",
|
||||||
|
): System(LogOut),
|
||||||
|
}
|
||||||
11
config/ghostty/config
Normal file
11
config/ghostty/config
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ghostty terminal configuration
|
||||||
|
# =============================================================================
|
||||||
|
# This is a minimal config. Ghostty ships with sensible defaults.
|
||||||
|
# Run `ghostty +show-config --default --docs` for all options.
|
||||||
|
|
||||||
|
# Terminfo for SSH — fixes terminal issues on remote hosts
|
||||||
|
shell-integration-features = ssh-terminfo,ssh-env
|
||||||
|
|
||||||
|
# Ignore super+shift+s (conflicts with Scribe/porridge dictation)
|
||||||
|
keybind = super+shift+s=ignore
|
||||||
49
config/git/gitconfig
Normal file
49
config/git/gitconfig
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
[user]
|
||||||
|
name = Your Name
|
||||||
|
email = your.email@example.com
|
||||||
|
signingkey = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-public-key-comment
|
||||||
|
[init]
|
||||||
|
defaultBranch = main
|
||||||
|
[credential "https://git.julianprester.com"]
|
||||||
|
provider = generic
|
||||||
|
[core]
|
||||||
|
compression = 0
|
||||||
|
excludesfile = ~/.gitignore
|
||||||
|
editor = nano
|
||||||
|
[column]
|
||||||
|
ui = auto
|
||||||
|
[branch]
|
||||||
|
sort = -committerdate
|
||||||
|
[tag]
|
||||||
|
sort = version:refname
|
||||||
|
[diff]
|
||||||
|
algorithm = histogram
|
||||||
|
colorMoved = plain
|
||||||
|
mnemonicPrefix = true
|
||||||
|
renames = true
|
||||||
|
[push]
|
||||||
|
default = simple
|
||||||
|
autoSetupRemote = true
|
||||||
|
followTags = true
|
||||||
|
[pull]
|
||||||
|
rebase = true
|
||||||
|
[fetch]
|
||||||
|
prune = true
|
||||||
|
pruneTags = true
|
||||||
|
all = true
|
||||||
|
[help]
|
||||||
|
autocorrect = prompt
|
||||||
|
[commit]
|
||||||
|
verbose = true
|
||||||
|
gpgsign = true
|
||||||
|
[rerere]
|
||||||
|
enabled = true
|
||||||
|
autoupdate = true
|
||||||
|
[rebase]
|
||||||
|
autoSquash = true
|
||||||
|
autoStash = true
|
||||||
|
updateRefs = true
|
||||||
|
[merge]
|
||||||
|
conflictstyle = zdiff3
|
||||||
|
[gpg]
|
||||||
|
format = ssh
|
||||||
85
config/scripts/bw-load-ssh.sh
Normal file
85
config/scripts/bw-load-ssh.sh
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ===========================================================================
|
||||||
|
# bw-load-ssh.sh — Load SSH keys from Bitwarden into ssh-agent
|
||||||
|
# Reads SSH key items from Bitwarden vault and loads private keys into
|
||||||
|
# the running ssh-agent.
|
||||||
|
#
|
||||||
|
# Dependencies: bw (Bitwarden CLI), jq, ssh-agent running
|
||||||
|
# Usage:
|
||||||
|
# 1. First, authenticate: bw login
|
||||||
|
# 2. Unlock and save session: bw unlock --raw > ~/.config/Bitwarden\ CLI/.session
|
||||||
|
# 3. Run: ./bw-load-ssh.sh
|
||||||
|
# Or run automatically via bw-ssh-keys.service (systemd user service).
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="${HOME}/.config/Bitwarden CLI"
|
||||||
|
SESSION_FILE="${CONFIG_DIR}/.session"
|
||||||
|
|
||||||
|
# Check session file exists
|
||||||
|
if [ ! -f "$SESSION_FILE" ]; then
|
||||||
|
echo "ERROR: Session file not found at $SESSION_FILE"
|
||||||
|
echo "Run: bw unlock --raw > '$SESSION_FILE' && chmod 600 '$SESSION_FILE'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check ssh-agent is running
|
||||||
|
if ! ssh-add -l >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: ssh-agent is not running."
|
||||||
|
echo "Start it with: eval \$(ssh-agent)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read session key
|
||||||
|
export BW_SESSION=$(cat "$SESSION_FILE")
|
||||||
|
|
||||||
|
# Verify session is still valid
|
||||||
|
if ! bw status 2>/dev/null | jq -e '.status == "unlocked"' >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: Session is no longer valid (vault is locked or logged out)."
|
||||||
|
echo "Regenerate with: bw unlock --raw > '$SESSION_FILE' && chmod 600 '$SESSION_FILE'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all SSH key items
|
||||||
|
echo "Fetching SSH keys from vault..."
|
||||||
|
ITEMS=$(bw list items 2>/dev/null | jq -c '.[] | select(.type == 5)')
|
||||||
|
|
||||||
|
if [ -z "$ITEMS" ]; then
|
||||||
|
echo "No SSH keys found in vault."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOADED=0
|
||||||
|
SKIPPED=0
|
||||||
|
|
||||||
|
echo "$ITEMS" | while IFS= read -r item; do
|
||||||
|
NAME=$(echo "$item" | jq -r '.name')
|
||||||
|
PUBLIC_KEY=$(echo "$item" | jq -r '.sshKey.publicKey // ""')
|
||||||
|
PRIVATE_KEY=$(echo "$item" | jq -r '.sshKey.privateKey // ""')
|
||||||
|
|
||||||
|
if [ -z "$PRIVATE_KEY" ]; then
|
||||||
|
echo " SKIP '$NAME' — no private key found"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract comment from public key for checking if already loaded
|
||||||
|
COMMENT=$(echo "$PUBLIC_KEY" | awk '{print $3}' | tr -d '\n')
|
||||||
|
|
||||||
|
# Check if already loaded in ssh-agent
|
||||||
|
if [ -n "$COMMENT" ] && ssh-add -l 2>/dev/null | grep -q "$COMMENT"; then
|
||||||
|
echo " OK '$NAME' — already loaded"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load into ssh-agent
|
||||||
|
if echo "$PRIVATE_KEY" | ssh-add - 2>/dev/null; then
|
||||||
|
echo " LOAD '$NAME'"
|
||||||
|
LOADED=$((LOADED + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL '$NAME'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Loaded: $LOADED, Skipped (already loaded): $SKIPPED"
|
||||||
15
config/scripts/env.sh
Normal file
15
config/scripts/env.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ===========================================================================
|
||||||
|
# env — PATH helper
|
||||||
|
# Sourced from ~/.profile and ~/.bashrc to ensure ~/.local/bin is in PATH.
|
||||||
|
# Idempotent — won't duplicate PATH entries.
|
||||||
|
# ===========================================================================
|
||||||
|
# affix colons on either side of $PATH to simplify matching
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*:"$HOME/.local/bin":*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Prepending path in case a system-installed binary needs overriding
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
11
config/scripts/idle-battery-suspend.sh
Normal file
11
config/scripts/idle-battery-suspend.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===========================================================================
|
||||||
|
# idle-battery-suspend.sh — Suspend laptop only when on battery
|
||||||
|
# Checks AC power status before suspending. If on AC power, does nothing.
|
||||||
|
# Used by swayidle.service (systemd user service).
|
||||||
|
# ===========================================================================
|
||||||
|
# Only suspend if on battery (AC online = 0)
|
||||||
|
AC_ONLINE=$(cat /sys/class/power_supply/AC/online 2>/dev/null)
|
||||||
|
if [ "$AC_ONLINE" = "0" ]; then
|
||||||
|
systemctl suspend-then-hibernate
|
||||||
|
fi
|
||||||
15
config/scripts/zoom.sh
Normal file
15
config/scripts/zoom.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===========================================================================
|
||||||
|
# zoom — Zoom launcher wrapper for COSMIC/Wayland on AMD GPU
|
||||||
|
# Forces Wayland-native mode (avoids X11 event freezes via XWayland).
|
||||||
|
# Forces VA-API hardware video decoding (fixes screen share performance).
|
||||||
|
#
|
||||||
|
# Works with: AMD Radeon 680M (Rembrandt) and similar.
|
||||||
|
# For NVIDIA GPUs, use: export LIBVA_DRIVER_NAME=nvidia
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
export QT_QPA_PLATFORM=wayland
|
||||||
|
export LIBVA_DRIVER_NAME=radeonsi
|
||||||
|
export LIBVA_DRI3_DISABLE=0
|
||||||
|
|
||||||
|
exec /usr/bin/zoom "$@"
|
||||||
1713
config/shell/p10k.zsh
Normal file
1713
config/shell/p10k.zsh
Normal file
File diff suppressed because it is too large
Load Diff
75
config/shell/zshrc
Normal file
75
config/shell/zshrc
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# .zshrc — Julian's Zsh configuration
|
||||||
|
# Generated from Pop!_OS "thinkpad" machine audit.
|
||||||
|
# =============================================================================
|
||||||
|
# This file is managed by the linux-provision repo.
|
||||||
|
#
|
||||||
|
# API keys and secrets are loaded from Bitwarden on shell startup.
|
||||||
|
# If Bitwarden is unlocked, keys are available immediately.
|
||||||
|
# If locked, bw-env fails silently and you just won't have env vars
|
||||||
|
# (run `bw && bw-env` when you need them).
|
||||||
|
#
|
||||||
|
# To set up: bw-env --setup (one-time interactive)
|
||||||
|
# See config/scripts/bw-env.sh for details.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---- Powerlevel10k instant prompt ----
|
||||||
|
# Must stay close to the top.
|
||||||
|
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
|
||||||
|
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- PATH setup ----
|
||||||
|
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
|
||||||
|
|
||||||
|
# ---- Oh My Zsh ----
|
||||||
|
export ZSH="$HOME/.oh-my-zsh"
|
||||||
|
ZSH_THEME="powerlevel10k/powerlevel10zsh"
|
||||||
|
|
||||||
|
# ---- Plugins ----
|
||||||
|
plugins=(git zsh-autosuggestions)
|
||||||
|
|
||||||
|
source $ZSH/oh-my-zsh.sh
|
||||||
|
|
||||||
|
# ---- Custom Aliases ----
|
||||||
|
|
||||||
|
# Bluetooth hard reset (ath11k WiFi/Bluetooth adapter hang workaround)
|
||||||
|
alias bt-reset='rfkill block bluetooth && sleep 1 && rfkill unblock bluetooth && sleep 1 && systemctl restart bluetooth'
|
||||||
|
|
||||||
|
# ---- Key bindings ----
|
||||||
|
# Ctrl+Backspace → delete word before cursor
|
||||||
|
bindkey "^H" backward-kill-word
|
||||||
|
|
||||||
|
# ---- Powerlevel10k config ----
|
||||||
|
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
|
||||||
|
|
||||||
|
# ---- NVM (Node Version Manager) ----
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||||
|
|
||||||
|
# ---- Custom completions ----
|
||||||
|
fpath+=~/.zfunc; autoload -Uz compinit; compinit
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Secrets — API keys and tokens
|
||||||
|
# Loaded from Bitwarden on every shell start.
|
||||||
|
# Fetches the "Environment" item and exports all custom fields as env vars.
|
||||||
|
# Falls back silently if vault is locked or item doesn't exist.
|
||||||
|
#
|
||||||
|
# Prerequisites: bw (Bitwarden CLI), jq
|
||||||
|
# To set up:
|
||||||
|
# 1. bw login
|
||||||
|
# 2. bw unlock --raw > ~/.config/Bitwarden\ CLI/.session
|
||||||
|
# 3. Create a Bitwarden item named "Environment" (type: Secure Note)
|
||||||
|
# with custom fields for each API key (name = VAR_NAME, value = the_key)
|
||||||
|
# =============================================================================
|
||||||
|
if [ -z "${BW_SESSION:-}" ] && [ -f "$HOME/.config/Bitwarden CLI/.session" ]; then
|
||||||
|
export BW_SESSION=$(cat "$HOME/.config/Bitwarden CLI/.session")
|
||||||
|
fi
|
||||||
|
if [ -n "${BW_SESSION:-}" ]; then
|
||||||
|
eval "$(bw get item Environment 2>/dev/null | jq -r '
|
||||||
|
.fields[] | select(.type != 0) |
|
||||||
|
"export " + (.name | gsub(" "; "_")) + "=" + (.value | @sh)
|
||||||
|
' 2>/dev/null)" 2>/dev/null
|
||||||
|
fi
|
||||||
65
config/shell/zshrc.local.example
Normal file
65
config/shell/zshrc.local.example
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# ~/.zshrc.local — API keys and secrets (EXAMPLE)
|
||||||
|
# ===== THIS FILE IS OPTIONAL =====
|
||||||
|
# The recommended approach is to store all secrets in Bitwarden and load
|
||||||
|
# them via bw-env.sh. See config/scripts/bw-env.sh for setup.
|
||||||
|
#
|
||||||
|
# If you prefer a plain file, copy this to ~/.zshrc.local and fill in
|
||||||
|
# your real keys. Then uncomment the "source ~/.zshrc.local" line in .zshrc.
|
||||||
|
#
|
||||||
|
# DO NOT commit this file to version control.
|
||||||
|
# =============================================================================
|
||||||
|
# From the Pop machine audit, Julian stores ~17 API keys in his env.
|
||||||
|
# Using Bitwarden (bw-env.sh) is the recommended approach because:
|
||||||
|
# - Keys are encrypted at rest (not in dotfiles)
|
||||||
|
# - Syncs across machines automatically
|
||||||
|
# - One source of truth
|
||||||
|
# - Cache falls back silently if vault is locked
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---- AI API Keys ----
|
||||||
|
export GROQ_API_KEY="gsk_your_key_here"
|
||||||
|
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
|
||||||
|
export GOOGLE_API_KEY="AIza-your-key-here"
|
||||||
|
export OPENCODE_API_KEY="sk-your-key-here"
|
||||||
|
|
||||||
|
# ---- LiteLLM Proxy ----
|
||||||
|
export OPENAI_BASE_URL="https://ai.julianprester.com"
|
||||||
|
export OPENAI_API_KEY="sk-your-key-here"
|
||||||
|
|
||||||
|
# ---- Calendar (CalDAV) ----
|
||||||
|
export CALDAV_URL="https://nc.julianprester.com/remote.php/dav"
|
||||||
|
export CALDAV_USERNAME="julian.prester@sydney.edu.au"
|
||||||
|
export CALDAV_PASSWORD="your-password-here"
|
||||||
|
|
||||||
|
# ---- Thunderbird API ----
|
||||||
|
export TB_API_HOSTS="thunderbird"
|
||||||
|
|
||||||
|
# ---- Zotero ----
|
||||||
|
export ZOTERO_KEY=""
|
||||||
|
|
||||||
|
# ---- Canvas LMS ----
|
||||||
|
export CANVAS_API_KEY="3156~your-key-here"
|
||||||
|
export CANVAS_API_URL="https://canvas.sydney.edu.au/"
|
||||||
|
|
||||||
|
# ---- Nextcloud ----
|
||||||
|
export NC_URL="https://nc.julianprester.com"
|
||||||
|
export NC_USERNAME="julian.prester@sydney.edu.au"
|
||||||
|
export NC_PASSWORD="your-password-here"
|
||||||
|
|
||||||
|
# ---- Actual Budget ----
|
||||||
|
export ACTUAL_URL="https://actual.your-domain.ts.net"
|
||||||
|
export ACTUAL_PASSWORD="your-password-here"
|
||||||
|
export ACTUAL_SYNC_ID="your-sync-id-here"
|
||||||
|
|
||||||
|
# ---- Tavily (web search API) ----
|
||||||
|
export TAVILY_API_KEY="tvly-your-key-here"
|
||||||
|
|
||||||
|
# ---- FreshRSS ----
|
||||||
|
export FRESHRSS_API_KEY="your-key-here"
|
||||||
|
|
||||||
|
# ---- Semantic Scholar ----
|
||||||
|
export SEMANTIC_SCHOLAR_API_KEY="your-key-here"
|
||||||
|
|
||||||
|
# ---- OpenAlex ----
|
||||||
|
export OPENALEX_API_KEY="your-key-here"
|
||||||
13
config/systemd/bw-ssh-keys.service
Normal file
13
config/systemd/bw-ssh-keys.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Load Bitwarden SSH keys into ssh-agent
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/bin/bw-load-ssh.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
9
config/systemd/empty_downloads.service
Normal file
9
config/systemd/empty_downloads.service
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Empty Downloads folder on login
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/bash -c 'rm -rf %h/Downloads/* %h/Downloads/.* 2>/dev/null'
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
6
config/systemd/mempi-sync.service
Normal file
6
config/systemd/mempi-sync.service
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Sync mempi database to Nextcloud
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/bin/bash -c 'sqlite3 %h/.local/share/pi/pi.db "PRAGMA wal_checkpoint(TRUNCATE)" && cp %h/.local/share/pi/pi.db "%h/Nextcloud/2_resources/90-99 Misc/98 ocpa/mempi-pi.db"'
|
||||||
8
config/systemd/mempi-sync.timer
Normal file
8
config/systemd/mempi-sync.timer
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Sync mempi database on boot
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
12
config/systemd/pi-overview.service
Normal file
12
config/systemd/pi-overview.service
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pi Overview — session dashboard
|
||||||
|
After=default.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment=PATH=/home/julian/.nvm/versions/node/v24.11.1/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
ExecStart=%h/.local/bin/pi-overview --port 3000
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
13
config/systemd/porridge-dictate.service
Normal file
13
config/systemd/porridge-dictate.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Porridge dictate - push-to-talk transcription
|
||||||
|
After=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=-%h/.config/porridge/env
|
||||||
|
ExecStart=%h/.local/bin/porridge dictate
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
14
config/systemd/porridge.service
Normal file
14
config/systemd/porridge.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Porridge - Zoom meeting transcriber daemon
|
||||||
|
After=graphical-session.target pulseaudio.service pipewire.service
|
||||||
|
Wants=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=-%h/.config/porridge/env
|
||||||
|
ExecStart=%h/.local/bin/porridge daemon
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
171
lib/distro.sh
Normal file
171
lib/distro.sh
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ===========================================================================
|
||||||
|
# Distribution Detection & Common Variables
|
||||||
|
# Sourced by 00-envcheck.sh, then available to all subsequent stages.
|
||||||
|
#
|
||||||
|
# After sourcing this, use these variables instead of hardcoding commands:
|
||||||
|
# $PKG_INSTALL <packages> — Install packages
|
||||||
|
# $PKG_UPDATE — Update package cache
|
||||||
|
# $PKG_REMOVE <packages> — Remove packages
|
||||||
|
# $PKG_GROUP_INSTALL <group> — Install package group
|
||||||
|
# $DISTRO_FAMILY — "debian" or "fedora"
|
||||||
|
# $DISTRO_ID — "pop", "ubuntu", "fedora", etc.
|
||||||
|
# $DISTRO_VERSION — "24.04", "41", etc.
|
||||||
|
# See full list below.
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
# ---- Detect distribution ----
|
||||||
|
DISTRO_ID=""
|
||||||
|
DISTRO_VERSION=""
|
||||||
|
DISTRO_FAMILY="" # "debian" (apt) or "fedora" (dnf)
|
||||||
|
DISTRO_LIKE="" # from ID_LIKE in os-release
|
||||||
|
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
DISTRO_ID="$ID"
|
||||||
|
DISTRO_VERSION="$VERSION_ID"
|
||||||
|
DISTRO_LIKE="$ID_LIKE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Determine distro family ----
|
||||||
|
case "$DISTRO_ID" in
|
||||||
|
fedora|rhel|centos)
|
||||||
|
DISTRO_FAMILY="fedora"
|
||||||
|
;;
|
||||||
|
pop|ubuntu|debian|linuxmint|elementary|zorin)
|
||||||
|
DISTRO_FAMILY="debian"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Fall back to ID_LIKE
|
||||||
|
case "$DISTRO_LIKE" in
|
||||||
|
*fedora*) DISTRO_FAMILY="fedora" ;;
|
||||||
|
*debian*) DISTRO_FAMILY="debian" ;;
|
||||||
|
*) DISTRO_FAMILY="unknown" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ---- Fail early on unknown distro ----
|
||||||
|
if [ "$DISTRO_FAMILY" = "unknown" ]; then
|
||||||
|
echo "[ERROR] Unknown distribution: $DISTRO_ID (ID_LIKE=$DISTRO_LIKE)"
|
||||||
|
echo " Edit lib/distro.sh to add support."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Package Manager Commands
|
||||||
|
# ===========================================================================
|
||||||
|
if [ "$DISTRO_FAMILY" = "debian" ]; then
|
||||||
|
PKG_MGR="apt"
|
||||||
|
PKG_UPDATE="sudo apt update"
|
||||||
|
PKG_INSTALL="sudo apt install -y"
|
||||||
|
PKG_INSTALL_NO_REC="sudo apt install -y --no-install-recommends"
|
||||||
|
PKG_REMOVE="sudo apt remove -y"
|
||||||
|
PKG_PURGE="sudo apt purge -y"
|
||||||
|
PKG_AUTOREMOVE="sudo apt autoremove -y"
|
||||||
|
PKG_SEARCH="apt search"
|
||||||
|
PKG_LIST_INSTALLED="dpkg -l"
|
||||||
|
|
||||||
|
# Repo management
|
||||||
|
REPO_ADD_PPA="sudo add-apt-repository -y"
|
||||||
|
REPO_APT_KEY="sudo apt-key add -" # legacy, use signed-by when possible
|
||||||
|
REPO_ADD_LIST="sudo tee /etc/apt/sources.list.d"
|
||||||
|
|
||||||
|
# Service / boot
|
||||||
|
SERVICE_ENABLE="sudo systemctl enable --now"
|
||||||
|
GRUB_UPDATE="sudo update-grub"
|
||||||
|
GRUB_FILE="/etc/default/grub"
|
||||||
|
|
||||||
|
# Desktop environment detection
|
||||||
|
# Pop uses COSMIC; Ubuntu uses GNOME; Kubuntu uses KDE
|
||||||
|
case "$DISTRO_ID" in
|
||||||
|
pop) DEFAULT_DE="COSMIC" ;;
|
||||||
|
ubuntu|debian) DEFAULT_DE="GNOME" ;;
|
||||||
|
*) DEFAULT_DE="" ;; # unknown
|
||||||
|
esac
|
||||||
|
|
||||||
|
elif [ "$DISTRO_FAMILY" = "fedora" ]; then
|
||||||
|
PKG_MGR="dnf"
|
||||||
|
PKG_UPDATE="sudo dnf makecache"
|
||||||
|
PKG_INSTALL="sudo dnf install -y"
|
||||||
|
PKG_INSTALL_NO_REC="sudo dnf install -y" # dnf has no --no-install-recommends equivalent
|
||||||
|
PKG_REMOVE="sudo dnf remove -y"
|
||||||
|
PKG_PURGE="sudo dnf remove -y"
|
||||||
|
PKG_AUTOREMOVE="sudo dnf autoremove -y"
|
||||||
|
PKG_SEARCH="dnf search"
|
||||||
|
PKG_LIST_INSTALLED="dnf list installed"
|
||||||
|
|
||||||
|
# Repo management
|
||||||
|
REPO_ADD_COPR="sudo dnf copr enable -y"
|
||||||
|
REPO_ADD_RPM="sudo dnf config-manager --add-repo"
|
||||||
|
|
||||||
|
# Service / boot
|
||||||
|
SERVICE_ENABLE="sudo systemctl enable --now"
|
||||||
|
GRUB_UPDATE="sudo grub2-mkconfig -o /boot/grub2/grub.cfg"
|
||||||
|
GRUB_EFI_UPDATE="sudo grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg"
|
||||||
|
GRUB_FILE="/etc/default/grub"
|
||||||
|
|
||||||
|
# Desktop environment detection
|
||||||
|
DEFAULT_DE="GNOME" # Fedora Workstation defaults to GNOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Helper function: install package(s), silently skip if already installed
|
||||||
|
# ===========================================================================
|
||||||
|
pkg_install() {
|
||||||
|
if [ "$DISTRO_FAMILY" = "debian" ]; then
|
||||||
|
sudo apt install -y "$@" 2>/dev/null || return 1
|
||||||
|
else
|
||||||
|
sudo dnf install -y "$@" 2>/dev/null || return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wrapper for group installs (ignored on apt systems)
|
||||||
|
pkg_group_install() {
|
||||||
|
if [ "$DISTRO_FAMILY" = "fedora" ]; then
|
||||||
|
sudo dnf group install -y "$@" 2>/dev/null || return 1
|
||||||
|
else
|
||||||
|
# On Debian, groups aren't a thing — just skip
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Package Name Mappings (Debian → Fedora and vice versa)
|
||||||
|
# Usage: pkg_name_fedora="foo" pkg_name_debian="bar"
|
||||||
|
# pkg_name "$pkg_name_fedora" "$pkg_name_debian"
|
||||||
|
# Or use the shorthand below for known differences.
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
# Known package name differences
|
||||||
|
resolve_pkg() {
|
||||||
|
local debian_name="$1"
|
||||||
|
local fedora_name="$2"
|
||||||
|
if [ "$DISTRO_FAMILY" = "debian" ]; then
|
||||||
|
echo "$debian_name"
|
||||||
|
else
|
||||||
|
echo "$fedora_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convenience: install a package that may have different names
|
||||||
|
pkg_install_mapped() {
|
||||||
|
local debian_name="$1"
|
||||||
|
local fedora_name="$2"
|
||||||
|
if [ "$DISTRO_FAMILY" = "debian" ]; then
|
||||||
|
pkg_install "$debian_name"
|
||||||
|
else
|
||||||
|
pkg_install "$fedora_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export all variables so sourced stages can use them
|
||||||
|
export DISTRO_FAMILY DISTRO_ID DISTRO_VERSION DISTRO_LIKE
|
||||||
|
export PKG_MGR PKG_UPDATE PKG_INSTALL PKG_INSTALL_NO_REC
|
||||||
|
export PKG_REMOVE PKG_PURGE PKG_AUTOREMOVE
|
||||||
|
export PKG_SEARCH PKG_LIST_INSTALLED
|
||||||
|
export SERVICE_ENABLE GRUB_UPDATE GRUB_EFI_UPDATE GRUB_FILE
|
||||||
|
export DEFAULT_DE
|
||||||
|
|
||||||
|
# Export helpers
|
||||||
|
export -f pkg_install pkg_group_install pkg_install_mapped resolve_pkg
|
||||||
131
provision.sh
Executable file
131
provision.sh
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ===========================================================================
|
||||||
|
# Linux Machine Provisioning — Master Orchestrator
|
||||||
|
# ===========================================================================
|
||||||
|
# Usage:
|
||||||
|
# bash provision.sh --all # Run all stages in order
|
||||||
|
# bash provision.sh --stage <name> # Run a single stage
|
||||||
|
# bash provision.sh --list # List all stages
|
||||||
|
# source provision.sh --interactive # Source for manual calls
|
||||||
|
#
|
||||||
|
# Distribution-agnostic — detects Debian/Ubuntu/Pop/Fedora and adjusts
|
||||||
|
# package manager, repo config, etc. automatically.
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
STAGES_DIR="${SCRIPT_DIR}/stages"
|
||||||
|
LIBS_DIR="${SCRIPT_DIR}/lib"
|
||||||
|
CONFIG_DIR="${SCRIPT_DIR}/config"
|
||||||
|
|
||||||
|
# ---- Source distro detection first ----
|
||||||
|
source "${LIBS_DIR}/distro.sh"
|
||||||
|
|
||||||
|
# ---- Colour helpers ----
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||||
|
|
||||||
|
# ---- Available stages ----
|
||||||
|
STAGES=(
|
||||||
|
"00-envcheck" # Environment checks (uses lib/distro.sh vars)
|
||||||
|
"01-repos" # Third-party repositories
|
||||||
|
"02-packages" # System packages
|
||||||
|
"03-toolchains" # nvm, Node, uv, Python
|
||||||
|
"04-shell" # Zsh, oh-my-zsh, p10k
|
||||||
|
"05-git" # Git config, SSH
|
||||||
|
"06-uv-projects" # Clone + install Julian's Python tools
|
||||||
|
"07-scripts" # ~/.local/bin custom scripts
|
||||||
|
"08-systemd" # User systemd services
|
||||||
|
"09-desktop" # Keybindings, hotkeys, desktop config
|
||||||
|
"10-docker" # Docker CE
|
||||||
|
"11-tweaks" # sysctl, kernel, TLP/powertop
|
||||||
|
"12-other-apps" # Chrome, Signal, Zotero
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Stage Runner
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
run_stage() {
|
||||||
|
local stage_name="$1"
|
||||||
|
local stage_file="${STAGES_DIR}/${stage_name}.sh"
|
||||||
|
|
||||||
|
if [ ! -f "$stage_file" ]; then
|
||||||
|
error "Stage script not found: ${stage_file}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "=============================================="
|
||||||
|
info " Running stage: ${stage_name}"
|
||||||
|
info "=============================================="
|
||||||
|
|
||||||
|
source "$stage_file"
|
||||||
|
|
||||||
|
ok "Stage '${stage_name}' completed."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Main
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local mode="${1:-}"
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
--all)
|
||||||
|
info "Starting full provisioning on ${DISTRO_ID} ${DISTRO_VERSION} (${DISTRO_FAMILY})"
|
||||||
|
info "You may be prompted for sudo password."
|
||||||
|
for stage in "${STAGES[@]}"; do
|
||||||
|
run_stage "$stage" || warn "Stage '${stage}' failed (continuing)."
|
||||||
|
done
|
||||||
|
ok "Provisioning complete!"
|
||||||
|
warn "Review TODO.md for manual post-install steps."
|
||||||
|
;;
|
||||||
|
|
||||||
|
--stage)
|
||||||
|
local target="${2:-}"
|
||||||
|
if [ -z "$target" ]; then
|
||||||
|
error "Usage: provision.sh --stage <stage-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
run_stage "$target"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--list)
|
||||||
|
echo "Available stages (distro: ${DISTRO_ID} / ${DISTRO_FAMILY}):"
|
||||||
|
for stage in "${STAGES[@]}"; do
|
||||||
|
echo " ${stage}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
--interactive)
|
||||||
|
info "Sourcing stage functions for interactive use."
|
||||||
|
for stage in "${STAGES[@]}"; do
|
||||||
|
local f="${STAGES_DIR}/${stage}.sh"
|
||||||
|
[ -f "$f" ] && source "$f"
|
||||||
|
done
|
||||||
|
info "Stages loaded. Call: run_stage <stage-name>"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage:"
|
||||||
|
echo " bash provision.sh --all # Run everything"
|
||||||
|
echo " bash provision.sh --stage <name> # Run one stage"
|
||||||
|
echo " bash provision.sh --list # List stages"
|
||||||
|
echo " source provision.sh --interactive # Load for manual use"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
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