#!/usr/bin/env bash set -euo pipefail info() { printf '\n==> %s\n' "$1" } warn() { printf 'WARN: %s\n' "$1" >&2 } append_if_missing() { local line="$1" local file="$2" touch "$file" grep -qxF "$line" "$file" || echo "$line" >>"$file" } command_exists() { command -v "$1" >/dev/null 2>&1 } AUTO_INSTALL_DEPS=0 find_ssh_key_in_dir() { local ssh_dir="$1" local key_file="" [[ -d "$ssh_dir" ]] || return 1 while IFS= read -r key_file; do case "$(basename "$key_file")" in *.pub|authorized_keys|known_hosts|config) continue ;; esac if [[ -f "$key_file.pub" ]]; then printf '%s' "$key_file" return 0 fi done < <(find "$ssh_dir" -maxdepth 1 -type f | sort) return 1 } wait_for_ssh_key() { local preferred_path="$1" local ssh_dir="$HOME_DIR/.ssh" local detected_key="" mkdir -p "$ssh_dir" chmod 700 "$ssh_dir" if [[ -f "$preferred_path" && -f "$preferred_path.pub" ]]; then printf '%s' "$preferred_path" return 0 fi if detected_key="$(find_ssh_key_in_dir "$ssh_dir")"; then printf '%s' "$detected_key" return 0 fi info "Waiting for an SSH key to appear in $ssh_dir" printf 'Create the key in another terminal, then press Enter here to continue.\n' printf 'Expected path: %s (with matching .pub file)\n' "$preferred_path" read -r -p "Press Enter once you have generated the SSH key... " printf '%s' "$preferred_path" } prompt_yes_no() { local prompt="$1" local default="${2:-Y}" local reply="" while true; do if [[ "$default" == "Y" ]]; then read -r -p "$prompt [Y/n] " reply reply="${reply:-Y}" else read -r -p "$prompt [y/N] " reply reply="${reply:-N}" fi case "${reply,,}" in y|yes) return 0 ;; n|no) return 1 ;; esac done } prompt_dependency_yes_no() { local prompt="$1" local default="${2:-Y}" if [[ "$AUTO_INSTALL_DEPS" -eq 1 ]]; then printf '%s\n' "$prompt [auto-yes]" return 0 fi prompt_yes_no "$prompt" "$default" } prompt_value() { local prompt="$1" local default="${2:-}" local reply="" if [[ -n "$default" ]]; then read -r -p "$prompt [$default] " reply printf '%s' "${reply:-$default}" else read -r -p "$prompt " reply printf '%s' "$reply" fi } prompt_required_value() { local prompt="$1" local default="${2:-}" local value="" while true; do value="$(prompt_value "$prompt" "$default")" if [[ -n "$value" ]]; then printf '%s' "$value" return 0 fi done } prompt_passphrase() { local pass1="" local pass2="" while true; do IFS= read -r -s -p "Enter SSH key passphrase: " pass1 printf '\n' IFS= read -r -s -p "Confirm SSH key passphrase: " pass2 printf '\n' if [[ "$pass1" == "$pass2" ]]; then printf '%s' "$pass1" return 0 fi warn "Passphrases did not match. Try again." done } sanitize_slug() { local value="$1" value="${value,,}" value="${value// /-}" value="$(printf '%s' "$value" | tr -cd 'a-z0-9_-')" printf '%s' "$value" } extract_host_from_git_url() { local url="$1" if [[ "$url" =~ ^ssh://([^/@]+@)?([^/:]+) ]]; then printf '%s' "${BASH_REMATCH[2]}" return 0 fi if [[ "$url" =~ ^([^/@]+@)?([^:]+): ]]; then printf '%s' "${BASH_REMATCH[2]}" return 0 fi if [[ "$url" =~ ^https?://([^/]+) ]]; then printf '%s' "${BASH_REMATCH[1]}" return 0 fi return 1 } add_host_to_known_hosts() { local host="$1" if [[ -z "$host" ]]; then return 1 fi if ! command_exists ssh-keyscan; then warn "ssh-keyscan is not installed; skipping known_hosts update for $host" return 1 fi mkdir -p "$HOME_DIR/.ssh" touch "$HOME_DIR/.ssh/known_hosts" if ssh-keygen -F "$host" -f "$HOME_DIR/.ssh/known_hosts" >/dev/null 2>&1; then return 0 fi ssh-keyscan -H "$host" >>"$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true } install_pacman_package() { local package="$1" sudo pacman -S --needed --noconfirm "$package" } install_yay() { if command_exists yay; then info "yay already installed" return 0 fi info "Installing yay" git clone https://aur.archlinux.org/yay.git "$TMP_DIR/yay" ( cd "$TMP_DIR/yay" makepkg -si --noconfirm ) } write_element_desktop_entry() { local desktop_file="$1" local display_name="$2" local exec_line="$3" mkdir -p "$HOME_DIR/.local/share/applications" cat >"$desktop_file" </dev/null <<'EOF' [ids] 0001:0001:09b4e68d [main] leftalt = leftmeta leftmeta = leftalt EOF } HOME_DIR="$HOME" BASHRC="$HOME_DIR/.bashrc" TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT cd "$HOME_DIR" info "Interactive Arch post-install" if prompt_yes_no "Auto-install dependency packages and tooling without per-item prompts?" "N"; then AUTO_INSTALL_DEPS=1 fi if prompt_dependency_yes_no "Update the system first?" "Y"; then info "Updating system" sudo pacman -Syu --noconfirm fi declare -a PACMAN_PACKAGES=( git which base-devel ttf-jetbrains-mono-nerd cliphist wl-clipboard neovim starship lazygit openssh alacritty hyprlock hyprpaper cava waybar keyd grim chromium uv npm rofi slurp brightnessctl playerctl nautilus network-manager-applet pavucontrol discord fastfetch ripgrep fd xdg-desktop-portal-hyprland element-desktop ) info "Package selection" for package in "${PACMAN_PACKAGES[@]}"; do if prompt_dependency_yes_no "Install package '$package'?" "Y"; then install_pacman_package "$package" fi done if prompt_dependency_yes_no "Install AUR helper 'yay'?" "Y"; then install_yay fi if command_exists yay && prompt_dependency_yes_no "Install AUR package 'zen-browser-bin'?" "Y"; then info "Installing zen-browser-bin" yay -S --needed --noconfirm zen-browser-bin fi if prompt_dependency_yes_no "Install ble.sh?" "Y"; then if [[ ! -f "$HOME_DIR/.local/share/blesh/ble.sh" ]]; then info "Installing ble.sh" git clone --recursive --depth 1 --shallow-submodules https://github.com/akinomyoga/ble.sh.git "$TMP_DIR/ble.sh" make -C "$TMP_DIR/ble.sh" install PREFIX="$HOME_DIR/.local" else info "ble.sh already installed" fi append_if_missing 'source -- ~/.local/share/blesh/ble.sh' "$BASHRC" touch "$HOME_DIR/.blerc" append_if_missing '[ -f "$HOME/.config/blerc" ] && source "$HOME/.config/blerc"' "$HOME_DIR/.blerc" fi if prompt_dependency_yes_no "Enable starship in ~/.bashrc?" "Y"; then info "Configuring shell" append_if_missing 'eval "$(starship init bash)"' "$BASHRC" fi stty sane || true if prompt_yes_no "Configure global git identity?" "Y"; then info "Configuring git" git_name="$(prompt_required_value "Git user name:")" git_email="$(prompt_required_value "Git user email:")" git config --global user.name "$git_name" git config --global user.email "$git_email" fi if prompt_yes_no "Wait for an existing SSH key for git access?" "Y"; then info "Waiting for SSH key" ssh_key_path="$(prompt_required_value "SSH key path:" "$HOME_DIR/.ssh/id_ed25519")" ssh_key_path="$(wait_for_ssh_key "$ssh_key_path")" if [[ -f "$ssh_key_path" ]]; then chmod 600 "$ssh_key_path" else warn "Private key '$ssh_key_path' was not found; continuing without changing permissions" fi if [[ -f "$ssh_key_path.pub" ]]; then chmod 644 "$ssh_key_path.pub" else warn "Public key '$ssh_key_path.pub' was not found; clipboard copy will be skipped" fi if prompt_yes_no "Add a git host to ~/.ssh/known_hosts?" "Y"; then git_host="$(prompt_required_value "Git host (for ssh-keyscan):")" add_host_to_known_hosts "$git_host" fi if prompt_yes_no "Copy the public SSH key to the clipboard?" "Y"; then if [[ ! -f "$ssh_key_path.pub" ]]; then warn "Public key '$ssh_key_path.pub' is not available yet" elif command_exists wl-copy; then wl-copy <"$ssh_key_path.pub" info "Your public SSH key has been copied to the clipboard" else warn "wl-copy is not installed; printing the key instead" cat "$ssh_key_path.pub" fi fi printf '\nPublic key: %s.pub\n' "$ssh_key_path" fi if prompt_yes_no "Enable the keyd system service?" "Y"; then if command_exists keyd; then info "Enabling keyd" sudo systemctl enable --now keyd.service else warn "keyd is not installed; skipping service enablement" fi fi if prompt_yes_no "Remap ThinkPad Alt to Super with keyd?" "Y"; then if command_exists keyd; then info "Writing /etc/keyd/remap.conf" write_keyd_remap_conf sudo systemctl enable --now keyd.service sudo systemctl restart keyd.service else warn "keyd is not installed; skipping remap configuration" fi fi if prompt_dependency_yes_no "Set up uv with Python 3.14 and global tools?" "Y"; then if command_exists uv; then info "Configuring uv" uv python install 3.14 uv python pin --global 3.14 uv tool install ruff uv tool install python-lsp-server else warn "uv is not installed; skipping Python and tool setup" fi fi if prompt_yes_no "Clone a dotfiles repository into ~/.config?" "Y"; then info "Cloning dotfiles" dotfiles_repo="$(prompt_required_value "Dotfiles repository URL:")" config_target="$(prompt_required_value "Target config directory:" "$HOME_DIR/.config")" if [[ -d "$config_target" ]]; then if prompt_yes_no "Back up the existing '$config_target' before replacing it?" "Y"; then config_backup="${config_target}.backup.$(date +%Y%m%d-%H%M%S)" mv "$config_target" "$config_backup" info "Existing config moved to $config_backup" elif prompt_yes_no "Delete the existing '$config_target' and replace it?" "N"; then rm -rf "$config_target" else warn "Skipping dotfiles clone because target already exists" dotfiles_repo="" fi fi if [[ -n "$dotfiles_repo" ]]; then if dotfiles_host="$(extract_host_from_git_url "$dotfiles_repo")"; then if prompt_yes_no "Add the dotfiles git host to ~/.ssh/known_hosts before cloning?" "Y"; then add_host_to_known_hosts "$dotfiles_host" fi fi git clone "$dotfiles_repo" "$config_target" if ! dotfiles_host="$(extract_host_from_git_url "$dotfiles_repo")"; then warn "Could not determine a host from '$dotfiles_repo'" fi fi fi if prompt_yes_no "Clone a wallpapers repository?" "Y"; then info "Cloning wallpapers" wallpapers_repo="$(prompt_required_value "Wallpapers repository URL:")" wallpaper_target="$(prompt_required_value "Wallpaper target directory:" "$HOME_DIR/.local/share/wallpapers")" mkdir -p "$(dirname "$wallpaper_target")" if [[ -d "$wallpaper_target" ]]; then if prompt_yes_no "Delete the existing wallpaper directory '$wallpaper_target' first?" "Y"; then rm -rf "$wallpaper_target" else warn "Skipping wallpapers clone because target already exists" wallpapers_repo="" fi fi if [[ -n "$wallpapers_repo" ]]; then git clone "$wallpapers_repo" "$wallpaper_target" fi fi if prompt_yes_no "Create two custom Element desktop launchers?" "Y"; then info "Creating Element desktop entries" element_name_default="$(prompt_required_value "Name for the default Element profile launcher:")" element_name_secondary="$(prompt_required_value "Name for the second Element profile launcher:")" element_slug_default="$(sanitize_slug "$element_name_default")" element_slug_secondary="$(sanitize_slug "$element_name_secondary")" element_profile_secondary="$(prompt_required_value "Secondary Element profile id:" "profile1")" write_element_desktop_entry \ "$HOME_DIR/.local/share/applications/element-${element_slug_default}.desktop" \ "$element_name_default" \ 'element-desktop --ozone-platform=x11' write_element_desktop_entry \ "$HOME_DIR/.local/share/applications/element-${element_slug_secondary}.desktop" \ "$element_name_secondary" \ "element-desktop --profile $element_profile_secondary" update-desktop-database "$HOME_DIR/.local/share/applications" >/dev/null 2>&1 || true fi if prompt_yes_no "Enable the sshd system service?" "Y"; then info "Enabling sshd" sudo systemctl enable --now sshd.service fi if prompt_yes_no "Configure SDDM autologin?" "N"; then info "Configuring SDDM autologin" autologin_user="$(prompt_required_value "Autologin user:")" autologin_session="$(prompt_required_value "Autologin session:" "hyprland-uwsm")" sudo mkdir -p /etc/sddm.conf.d sudo tee /etc/sddm.conf.d/autologin.conf >/dev/null <