Skip to content

Instantly share code, notes, and snippets.

@ara303
Created March 25, 2026 00:27
Show Gist options
  • Select an option

  • Save ara303/328252ad77cf4c945a9bd863c114c774 to your computer and use it in GitHub Desktop.

Select an option

Save ara303/328252ad77cf4c945a9bd863c114c774 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# =============================================================================
# Arch Linux + KDE Plasma — Automated Setup Script
# =============================================================================
# Run as your normal user (NOT root). sudo will be invoked as needed.
# Usage: bash arch-setup.sh
# =============================================================================
set -euo pipefail
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
section() { echo -e "\n${BOLD}${GREEN}══ $* ══${RESET}"; }
manual() { echo -e "${YELLOW}[MANUAL]${RESET} $*"; }
# ── Sanity checks ─────────────────────────────────────────────────────────────
if [[ "$EUID" -eq 0 ]]; then
echo -e "${RED}Do not run this script as root. Run it as your normal user.${RESET}"
exit 1
fi
if ! grep -q "Arch Linux" /etc/os-release 2>/dev/null; then
warn "This script is designed for Arch Linux. Proceed anyway? (y/N)"
read -r ans; [[ "$ans" =~ ^[Yy]$ ]] || exit 1
fi
# =============================================================================
# SECTION 1 — System update & AUR helper (paru)
# =============================================================================
section "System update & AUR helper"
info "Updating system packages..."
sudo pacman -Syu --noconfirm
if ! command -v paru &>/dev/null; then
info "Installing paru (AUR helper)..."
sudo pacman -S --noconfirm --needed base-devel git
TMPDIR=$(mktemp -d)
git clone https://aur.archlinux.org/paru.git "$TMPDIR/paru"
(cd "$TMPDIR/paru" && makepkg -si --noconfirm)
rm -rf "$TMPDIR"
success "paru installed."
else
success "paru already installed."
fi
# =============================================================================
# SECTION 2 — Install packages via pacman
# =============================================================================
section "Installing pacman packages"
PACMAN_PACKAGES=(
# Terminal & shell
ghostty
# Code editor
code # Visual Studio Code (OSS build; see note below)
# Communication
discord
# Development — GitHub CLI
github-cli
# Development — PHP 8.5 + common extensions
php
php-fpm
php-gd
php-intl
php-sqlite
php-snmp
php-embed
php-redis
php-cgi
xdebug # Xdebug (packaged for current php)
# Development — MariaDB
mariadb
# Development — composer (useful alongside PHP)
composer
# Fonts & utilities
noto-fonts
ttf-liberation
wget
curl
unzip
jq
git
base-devel
)
info "Installing pacman packages (this may take a while)..."
sudo pacman -S --noconfirm --needed "${PACMAN_PACKAGES[@]}"
success "pacman packages installed."
# NOTE: 'code' is the open-source VS Code build. If you want the official
# Microsoft binary with the proprietary marketplace, replace 'code' with the
# AUR package 'visual-studio-code-bin' installed in the next section.
# =============================================================================
# SECTION 3 — Install AUR packages
# =============================================================================
section "Installing AUR packages"
AUR_PACKAGES=(
google-chrome # Google Chrome (official binary)
helium-browser-bin # Helium Browser
visual-studio-code-bin # Official Microsoft VS Code binary
# (remove if you kept 'code' from pacman above)
)
info "Importing Helium's PGP signing key..."
gpg --keyserver hkps://keyserver.ubuntu.com \
--recv-keys BE677C1989D35EAB2C5F26C9351601AD01D6378E || \
warn "Could not import Helium key from keyserver — install may prompt you."
info "Installing AUR packages..."
paru -S --noconfirm --needed "${AUR_PACKAGES[@]}"
success "AUR packages installed."
# =============================================================================
# SECTION 4 — nvm + Node + npm
# =============================================================================
section "nvm → Node & npm"
if [[ ! -d "$HOME/.nvm" ]]; then
info "Installing nvm..."
# Fetch latest nvm version tag from GitHub
NVM_VERSION=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest \
| grep '"tag_name"' | sed 's/.*"tag_name": "\(.*\)".*/\1/')
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash
success "nvm ${NVM_VERSION} installed."
else
success "nvm already installed."
fi
# Source nvm for the rest of this script
export NVM_DIR="$HOME/.nvm"
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
info "Installing latest LTS Node via nvm..."
nvm install --lts
nvm use --lts
nvm alias default 'lts/*'
success "Node $(node -v) / npm $(npm -v) active."
# Ensure nvm is sourced in future shells
for RC in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [[ -f "$RC" ]] && ! grep -q 'NVM_DIR' "$RC"; then
cat >> "$RC" << 'EOF'
# nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
success "nvm bootstrap appended to $RC."
fi
done
# =============================================================================
# SECTION 5 — MariaDB: initialise & enable passwordless root
# =============================================================================
section "MariaDB setup"
info "Initialising MariaDB data directory..."
sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
info "Enabling & starting MariaDB..."
sudo systemctl enable --now mariadb
info "Configuring passwordless root access (unix_socket auth)..."
# unix_socket plugin means 'root on the local machine = no password needed'
sudo mariadb -u root << 'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;
FLUSH PRIVILEGES;
SQL
success "MariaDB root login: just run 'mariadb' (or 'sudo mariadb') — no password."
# =============================================================================
# SECTION 6 — PHP: enable extensions & Xdebug config
# =============================================================================
section "PHP 8.5 configuration"
PHP_INI="/etc/php/php.ini"
PHP_CONFD="/etc/php/conf.d"
# Enable common extensions in php.ini (uncomment their lines)
EXTENSIONS=(bcmath curl gd intl mbstring pdo_mysql mysqli sqlite3 zip)
for EXT in "${EXTENSIONS[@]}"; do
sudo sed -i "s/^;extension=${EXT}/extension=${EXT}/" "$PHP_INI" || true
sudo sed -i "s/^;extension=${EXT}.so/extension=${EXT}.so/" "$PHP_INI" || true
done
# OPcache is enabled by default in PHP 8.5 — just verify the ini block exists
if ! grep -q "opcache.enable=1" "$PHP_INI"; then
sudo tee -a "$PHP_INI" > /dev/null << 'EOF'
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
EOF
fi
# Xdebug config for VS Code (listen mode)
XDEBUG_INI="${PHP_CONFD}/xdebug.ini"
if [[ ! -f "$XDEBUG_INI" ]]; then
sudo tee "$XDEBUG_INI" > /dev/null << 'EOF'
zend_extension=xdebug.so
[xdebug]
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
EOF
success "Xdebug configured at ${XDEBUG_INI}."
else
success "Xdebug ini already exists, skipping."
fi
success "PHP extensions enabled. Run 'php -m' to verify."
# =============================================================================
# SECTION 7 — VS Code extensions (PHP + Xdebug)
# =============================================================================
section "VS Code extensions"
# Wait for 'code' binary to be available (handles both 'code' and 'code-oss')
CODE_BIN=""
for BIN in code code-oss; do
if command -v "$BIN" &>/dev/null; then CODE_BIN="$BIN"; break; fi
done
if [[ -n "$CODE_BIN" ]]; then
VSCODE_EXTENSIONS=(
xdebug.php-debug # PHP Debug (Xdebug adapter)
bmewburn.vscode-intelephense-client # PHP Intelephense
github.vscode-pull-request-github # GitHub Pull Requests & Issues
github.copilot # GitHub Copilot (optional — comment out if unwanted)
)
for EXT in "${VSCODE_EXTENSIONS[@]}"; do
info "Installing VS Code extension: $EXT"
"$CODE_BIN" --install-extension "$EXT" --force 2>/dev/null || warn "Could not install $EXT"
done
success "VS Code extensions installed."
else
warn "VS Code binary not found — extensions skipped. Run manually after install."
fi
# =============================================================================
# SECTION 8 — Google Sans Code font
# =============================================================================
section "Google Sans Code font"
FONT_DIR="$HOME/.local/share/fonts/GoogleSansCode"
if [[ ! -d "$FONT_DIR" ]]; then
info "Cloning Google Sans Code font repo..."
git clone --depth=1 https://github.com/google/google-sans-code.git \
"$HOME/.cache/google-sans-code-src"
mkdir -p "$FONT_DIR"
find "$HOME/.cache/google-sans-code-src" -name "*.ttf" -o -name "*.otf" \
| xargs -I{} cp {} "$FONT_DIR/"
fc-cache -fv "$FONT_DIR" &>/dev/null
success "Google Sans Code installed to ${FONT_DIR}."
else
success "Google Sans Code already installed."
fi
# =============================================================================
# SECTION 9 — Ghostty configuration
# =============================================================================
section "Ghostty configuration"
GHOSTTY_CFG_DIR="$HOME/.config/ghostty"
GHOSTTY_CFG="$GHOSTTY_CFG_DIR/config"
mkdir -p "$GHOSTTY_CFG_DIR"
cat > "$GHOSTTY_CFG" << 'EOF'
# ── Ghostty config ─────────────────────────────────────────────────────────
# Font
font-family = "Google Sans Code"
font-size = 10
# Dracula colour theme
# (Ghostty ships Dracula built-in as of v1.x)
theme = dracula
# ── Key bindings ────────────────────────────────────────────────────────────
# Ctrl+C → COPY (not SIGINT)
keybind = ctrl+c=copy_to_clipboard
# Ctrl+V → PASTE
keybind = ctrl+v=paste_from_clipboard
# Super+C (Windows key + C) → send SIGINT to the running process
keybind = super+c=esc:c
# Ctrl+Shift+C → send SIGINT (fallback if Super+C doesn't work on your WM)
keybind = ctrl+shift+c=esc:c
# Standard new tab / window shortcuts preserved
keybind = ctrl+t=new_tab
keybind = ctrl+n=new_window
EOF
success "Ghostty config written to ${GHOSTTY_CFG}."
warn "IMPORTANT — Ctrl+C is now COPY; Super+C / Ctrl+Shift+C sends SIGINT."
warn "If Ghostty's built-in 'dracula' theme name differs in your version,"
warn "check 'ghostty +list-themes' and update the theme= line accordingly."
# =============================================================================
# SECTION 10 — KDE: system-wide Ctrl+C copy / Ctrl+V paste
# =============================================================================
section "KDE system-wide Ctrl+C/V clipboard shortcuts"
# KDE Plasma 6 stores global shortcuts in kglobalshortcutsrc.
# We patch klipper (the KDE clipboard daemon) to map Ctrl+C → copy, Ctrl+V → paste.
# These are ADDITIONAL to Ghostty's own rebinding above.
KGLOBAL="$HOME/.config/kglobalshortcutsrc"
patch_kde_shortcut() {
local group="$1" key="$2" value="$3"
# If the file / group / key doesn't exist, kwriteconfig6 will create them.
kwriteconfig6 --file kglobalshortcutsrc --group "$group" --key "$key" "$value" 2>/dev/null || true
}
if command -v kwriteconfig6 &>/dev/null; then
# Ensure Ctrl+C is the global "copy" shortcut in KDE
patch_kde_shortcut "klipper" "copy_clipboard_history" "ctrl+c,ctrl+c,Copy"
patch_kde_shortcut "klipper" "paste_clipboard_history" "ctrl+v,ctrl+v,Paste"
# Reload KDE shortcuts
qdbus6 org.kde.kglobalaccel /kglobalaccel org.kde.kglobalaccel.reloadConfig 2>/dev/null || true
success "KDE Ctrl+C/V shortcuts patched."
else
warn "kwriteconfig6 not found — KDE shortcut patch skipped (run after login)."
fi
# =============================================================================
# SECTION 11 — Set default applications (xdg-mime)
# =============================================================================
section "Default applications"
if command -v xdg-mime &>/dev/null; then
info "Setting Google Chrome as default browser..."
xdg-settings set default-web-browser google-chrome.desktop 2>/dev/null \
|| xdg-mime default google-chrome.desktop x-scheme-handler/http \
&& xdg-mime default google-chrome.desktop x-scheme-handler/https
info "Setting Ghostty as default terminal..."
kwriteconfig6 --file kdeglobals --group General \
--key TerminalApplication ghostty 2>/dev/null || true
success "Default apps set."
fi
# =============================================================================
# SECTION 12 — GitHub CLI authentication
# =============================================================================
section "GitHub CLI (gh) authentication"
if ! gh auth status &>/dev/null; then
info "Launching GitHub CLI auth flow (browser will open)..."
gh auth login --web --git-protocol https
success "GitHub CLI authenticated."
else
success "GitHub CLI already authenticated."
fi
# Also authenticate VS Code's GitHub extension via the CLI token
if [[ -n "$CODE_BIN" ]]; then
info "VS Code GitHub sign-in: open VS Code → Accounts (bottom-left) → Sign in with GitHub."
manual "VS Code GitHub auth cannot be scripted — you must click through the UI once."
fi
# =============================================================================
# SECTION 13 — Things that MUST be done manually
# =============================================================================
section "Manual steps remaining"
cat << 'MANUAL'
The following steps require interactive UI and cannot be fully automated:
1. GOOGLE CHROME — Sign in to your Google account:
Open Chrome → click the profile icon (top-right) → Sign in to Chrome.
2. VS CODE — Sign in with GitHub (for Settings Sync etc.):
Open VS Code → click Accounts icon (bottom-left) → "Sign in with GitHub".
(The gh CLI token is separate from VS Code's own GitHub session.)
3. GHOSTTY CTRL+C SIGINT — Verify the keybind works:
Open Ghostty, run a long process (e.g. sleep 100), then press Super+C.
If Super+C is intercepted by KDE before Ghostty, use Ctrl+Shift+C instead —
both are configured in the Ghostty config.
4. DISCORD — Log in interactively when you first open Discord.
5. HELIUM BROWSER — First launch shows a "Helium Services" setup screen;
choose your preferred privacy settings there.
6. XDEBUG IN VS CODE — Create a .vscode/launch.json in your project:
{
"version": "0.2.0",
"configurations": [{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003
}]
}
MANUAL
success "Setup complete! Reboot or re-login to ensure all services and env vars are active."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment