#!/usr/bin/env bash # Strict: a failed mv or ln here used to fall through and exit 0, so a machine # could end up half-linked while the installer's summary reported the stage as # fine. Anything genuinely optional below carries its own `|| true`. set -euo pipefail # --- Helper functions --- log() { echo -e "\033[1;34m[INFO]\033[0m $*"; } # Define paths as they have not been defined by new bashrc yet! PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" PANAMA_BASH="${PANAMA_BASH:-$PANAMA_PATH/config/bash}" PANAMA_DOT="$PANAMA_PATH/config/dot" PANAMA_OLD="$PANAMA_PATH/config/old" CONFIG="$HOME/.config" # Make backup folder if it doesn't exist -- and ~/.config itself, which a # truly fresh HOME does not have yet. Before set -e above, its absence made # every symlink below fail silently while the stage still reported success. mkdir -p "$PANAMA_OLD" "$CONFIG" # --- Bashrc --- echo -e "\n--- Replacing .bashrc ---" # Backup existing .bashrc if it's a regular file if [ -f "$HOME/.bashrc" ] && [ ! -L "$HOME/.bashrc" ]; then log "Backing up existing .bashrc" mv "$HOME/.bashrc" "$PANAMA_OLD/.bashrc" fi # Remove old symlink if it exists and points somewhere else if [ -L "$HOME/.bashrc" ]; then log "Removing old .bashrc symlink" rm "$HOME/.bashrc" fi # Symlink Panama .bashrc file to ~/.bashrc log "Symlinking Panama .bashrc file to ~/.bashrc" ln -s "$PANAMA_BASH/.bashrc" "$HOME/.bashrc" # Each entry is symlinked as ~/.config/ -> $PANAMA_DOT/. # # One desktop. "forge" was here while the GNOME session was still the fallback # and Hyprland was being built beside it -- that hedge has been paid off, and a # configuration for an extension nothing installs is worse than no configuration # at all, because it reads as a supported thing. # # gnome-control-center is a separate matter and stays declared: Panama's own # Settings hands off to it for the panels it deliberately does not own. dirs=("espanso" "ghostty" "gtk-3.0" "gtk-4.0" "hypr" "kitty" "nvim" \ "quickshell" "tmux" "uwsm" "vicinae" "wofi" "xdg-desktop-portal") # --- Vim vimrc --- echo -e "\n--- Setting up vim ---" mkdir -p "$HOME/.vim" if [ -L "$HOME/.vim/vimrc" ]; then rm "$HOME/.vim/vimrc" log "Removed old symlink at ~/.vim/vimrc" fi if [ -f "$HOME/.vim/vimrc" ]; then mv "$HOME/.vim/vimrc" "$PANAMA_OLD/vimrc" log "Moved existing ~/.vim/vimrc to $PANAMA_OLD/vimrc" fi ln -s "$PANAMA_DOT/vim/vimrc" "$HOME/.vim/vimrc" log "Linked $PANAMA_DOT/vim/vimrc → ~/.vim/vimrc" for dir in "${dirs[@]}"; do echo -e "\n--- Setting up $dir ---" # Remove old symlink if it exists if [ -L "$CONFIG/$dir" ]; then rm "$CONFIG/$dir" log "Removed old symlink at $CONFIG/$dir" fi # Backup existing directory if it exists if [ -d "$CONFIG/$dir" ]; then mv "$CONFIG/$dir" "$PANAMA_OLD/$dir" log "Moved existing $dir config to $PANAMA_OLD/$dir" # A regular file at this path (not a directory, not a symlink) is missed by # the guards above, so `ln -s` below would fail with "File exists" -- back # it up the same way. elif [ -f "$CONFIG/$dir" ]; then mv "$CONFIG/$dir" "$PANAMA_OLD/$dir" log "Moved existing $dir file to $PANAMA_OLD/$dir" fi # Create symlink ln -s "$PANAMA_DOT/$dir" "$CONFIG/$dir" log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir" done # tmux.conf ends with a source-file of current-theme.conf, generated from the # color scheme rather than committed. Seed it so a fresh checkout starts themed # -- the source-file is -q, so a missing file is silent, which would leave tmux # unstyled with nothing to explain it. TMUX_THEME="$PANAMA_DOT/tmux/current-theme.conf" if [ -e "$TMUX_THEME" ]; then log "Keeping existing tmux theme at $TMUX_THEME" elif [ -d "$PANAMA_DOT/tmux/themes" ]; then tmux_scheme="dark" tmux_prefs="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" if [ -r "$tmux_prefs" ]; then tmux_stored="$(jq -r '.colorScheme // "dark"' "$tmux_prefs" 2>/dev/null || echo dark)" [ "$tmux_stored" = "light" ] && tmux_scheme="light" fi [ "$tmux_scheme" = "light" ] && tmux_name="tokyonight-day" || tmux_name="tokyonight-moon" cp "$PANAMA_DOT/tmux/themes/$tmux_name.conf" "$TMUX_THEME" log "Seeded tmux $tmux_scheme theme ($tmux_name) → $TMUX_THEME" fi # hyprlock.conf is generated from a template on every color scheme change and # is not committed. Seed it so the FIRST lock of a fresh install is themed -- # without it hyprlock falls back to its own defaults, which is a bare gray # screen with none of this desktop's identity, and the first time anyone would # find out is when they walked away from the machine. HYPRLOCK_TEMPLATE="$PANAMA_DOT/hypr/hyprlock.conf.template" HYPRLOCK_CONF="$PANAMA_DOT/hypr/hyprlock.conf" if [ -e "$HYPRLOCK_CONF" ]; then log "Keeping existing hyprlock config at $HYPRLOCK_CONF" elif [ -r "$HYPRLOCK_TEMPLATE" ]; then lock_scheme="dark" lock_prefs="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" if [ -r "$lock_prefs" ]; then lock_stored="$(jq -r '.colorScheme // "dark"' "$lock_prefs" 2>/dev/null || echo dark)" [ "$lock_stored" = "light" ] && lock_scheme="light" fi if [ "$lock_scheme" = "light" ]; then sed -e "s/@FG@/55, 96, 191/g" -e "s/@MUTED@/97, 114, 176/g" \ -e "s/@ACCENT@/46, 125, 233/g" -e "s/@ERROR@/245, 42, 101/g" \ -e "s/@BG@/225, 226, 231/g" -e "s/@FIELD@/208, 213, 227/g" \ -e "s/@MUTED_HEX@/6172b0/g" -e "s/@ERROR_HEX@/f52a65/g" \ "$HYPRLOCK_TEMPLATE" > "$HYPRLOCK_CONF" else sed -e "s/@FG@/200, 211, 245/g" -e "s/@MUTED@/130, 139, 184/g" \ -e "s/@ACCENT@/130, 170, 255/g" -e "s/@ERROR@/255, 117, 127/g" \ -e "s/@BG@/34, 36, 54/g" -e "s/@FIELD@/46, 47, 61/g" \ -e "s/@MUTED_HEX@/828bb8/g" -e "s/@ERROR_HEX@/ff757f/g" \ "$HYPRLOCK_TEMPLATE" > "$HYPRLOCK_CONF" fi log "Seeded hyprlock $lock_scheme theme → $HYPRLOCK_CONF" fi # btop reads themes from its own config directory, but OWNS btop.conf -- it # rewrites that file on exit -- so only the theme files are exposed, per file, # and the config itself is left to btop. panama-theme-apps edits the single # color_theme line in place. BTOP_THEME_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/btop/themes" mkdir -p "$BTOP_THEME_DIR" for btop_theme_src in "$PANAMA_DOT"/btop/themes/*.theme; do [ -e "$btop_theme_src" ] || continue btop_theme_dst="$BTOP_THEME_DIR/$(basename "$btop_theme_src")" if [ -L "$btop_theme_dst" ]; then rm "$btop_theme_dst" fi if [ -e "$btop_theme_dst" ]; then log "Keeping existing btop theme at $btop_theme_dst" else ln -s "$btop_theme_src" "$btop_theme_dst" log "Linked btop theme → $btop_theme_dst" fi done # GTK3 has no include mechanism, so its settings.ini is generated whole from a # template rather than layered. Without this, a fresh checkout has a template # and no settings.ini, and GTK3 applications fall back to their built-in theme. # panama-theme-apps rewrites both files on every scheme change after this. for gtk_version in 3.0 4.0; do gtk_template="$PANAMA_DOT/gtk-$gtk_version/settings.ini.template" gtk_settings="$PANAMA_DOT/gtk-$gtk_version/settings.ini" [ -r "$gtk_template" ] || continue if [ -e "$gtk_settings" ]; then log "Keeping existing GTK settings at $gtk_settings" else gtk_scheme="dark" gtk_prefs="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" if [ -r "$gtk_prefs" ]; then gtk_stored="$(jq -r '.colorScheme // "dark"' "$gtk_prefs" 2>/dev/null || echo dark)" [ "$gtk_stored" = "light" ] && gtk_scheme="light" fi if [ "$gtk_scheme" = "light" ]; then gtk_name="adw-gtk3"; gtk_dark=0 else gtk_name="adw-gtk3-dark"; gtk_dark=1 fi sed -e "s/@GTK_THEME@/$gtk_name/" -e "s/@PREFER_DARK@/$gtk_dark/" \ "$gtk_template" > "$gtk_settings" log "Generated GTK $gtk_version settings ($gtk_name) → $gtk_settings" fi done # kitty.conf ends with `include current-theme.conf`, and that file is generated # from the desktop color scheme rather than committed -- it is machine state. # A fresh checkout therefore has no such file, and kitty starts by complaining # about a missing include and falling back to its stock colors. Seed it from # the scheme in settings.json (dark unless the user has chosen otherwise) so a # first launch is themed; ColorScheme.qml overwrites it on every change after. KITTY_THEME="$PANAMA_DOT/kitty/current-theme.conf" if [ -e "$KITTY_THEME" ]; then log "Keeping existing kitty theme at $KITTY_THEME" else scheme="dark" settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" if [ -r "$settings" ]; then stored="$(jq -r '.colorScheme // "dark"' "$settings" 2>/dev/null || echo dark)" [ "$stored" = "light" ] && scheme="light" fi [ "$scheme" = "light" ] && theme="tokyonight-day" || theme="tokyonight-moon" cp "$PANAMA_DOT/kitty/themes/$theme.conf" "$KITTY_THEME" log "Seeded kitty $scheme theme ($theme) → $KITTY_THEME" fi # Vicinae 0.26 discovers user themes from its XDG data directory rather than # from ~/.config/vicinae. Keep the authored theme in Panama with the rest of # the launcher config and expose only that file at Vicinae's runtime path. VICINAE_THEME_DIR="$HOME/.local/share/vicinae/themes" mkdir -p "$VICINAE_THEME_DIR" # Every authored theme, not just the dark one: vicinae.json selects a theme per # system appearance, so linking only Moon left the launcher falling back to its # stock palette whenever Panama was in light mode. for theme_src in "$PANAMA_DOT"/vicinae/themes/*.toml; do [ -e "$theme_src" ] || continue theme_dst="$VICINAE_THEME_DIR/$(basename "$theme_src")" if [ -L "$theme_dst" ]; then rm "$theme_dst" fi if [ -e "$theme_dst" ]; then log "Keeping existing Vicinae theme at $theme_dst" else ln -s "$theme_src" "$theme_dst" log "Linked Vicinae theme → $theme_dst" fi done # Panama-native applications live in the user data directory so launchers can # discover them alongside system desktop entries. Keep each authored file in # the repository and expose it with a narrow per-file symlink. # Panama ships its own application icons. Without these the desktop entries fall # back to a generic symbolic glyph, which is drawn for 16px toolbar use and # looks wrong beside full-color application icons in the dock. PANAMA_ICON_DIR="$PANAMA_PATH/config/local/share/icons/hicolor/scalable/apps" USER_ICON_DIR="$HOME/.local/share/icons/hicolor/scalable/apps" mkdir -p "$USER_ICON_DIR" for icon_file in "$PANAMA_ICON_DIR"/*.svg; do [[ -e "$icon_file" ]] || continue icon_name="$(basename "$icon_file")" icon_target="$USER_ICON_DIR/$icon_name" if [[ -L "$icon_target" ]]; then rm "$icon_target" elif [[ -e "$icon_target" ]]; then mv "$icon_target" "$icon_target.bak" fi ln -s "$icon_file" "$icon_target" done gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true # The pointer theme. oreo_blue_cursors is packaged by no Fedora repository -- it # is a GitHub project -- so rather than download it at install time and depend on # a URL that can rot, the theme is vendored into this repository and exposed the # same way Panama's application icons are. Without it, every gsettings value, # GTK settings.ini and XCURSOR_THEME naming it resolves to nothing and the # session falls back to the default pointer. PANAMA_CURSOR_DIR="$PANAMA_PATH/config/local/share/icons" USER_CURSOR_DIR="$HOME/.local/share/icons" mkdir -p "$USER_CURSOR_DIR" for cursor_src in "$PANAMA_CURSOR_DIR"/*_cursors; do [ -d "$cursor_src" ] || continue cursor_name="$(basename "$cursor_src")" cursor_dst="$USER_CURSOR_DIR/$cursor_name" if [ -L "$cursor_dst" ]; then rm "$cursor_dst" elif [ -e "$cursor_dst" ]; then log "Keeping existing cursor theme at $cursor_dst" continue fi ln -s "$cursor_src" "$cursor_dst" log "Linked cursor theme → $cursor_dst" done # The wallpaper the desktop already believes it ships. Wallpaper.qml calls this # path `shippedPath` and substitutes it whenever wallpaperPath is unset, which is # its default; panama-lock falls back to the same file, and hyprpaper.conf names # it directly. Copied rather than symlinked, because ~/Pictures/Wallpapers is a # directory the user owns and adds their own images to -- a symlink there would # be a repository file masquerading as one of their own. PANAMA_WALLPAPER_DIR="$PANAMA_PATH/config/wallpapers" USER_WALLPAPER_DIR="$HOME/Pictures/Wallpapers" if [ -d "$PANAMA_WALLPAPER_DIR" ]; then mkdir -p "$USER_WALLPAPER_DIR" for wallpaper_src in "$PANAMA_WALLPAPER_DIR"/*; do [ -f "$wallpaper_src" ] || continue wallpaper_dst="$USER_WALLPAPER_DIR/$(basename "$wallpaper_src")" if [ -e "$wallpaper_dst" ]; then log "Keeping existing wallpaper at $wallpaper_dst" else cp "$wallpaper_src" "$wallpaper_dst" log "Copied wallpaper → $wallpaper_dst" fi done fi # The Firefox chrome. Edge-Frfox, vendored in config/firefox -- see the README # there for what it is and whose it is. Both halves have to land or neither # works: chrome/ holds the CSS, and user.js sets the preference that makes # Firefox read chrome/ at all. # # Firefox has moved to the XDG directories, so a current build keeps profiles in # ~/.config/mozilla/firefox and an older one in ~/.mozilla/firefox. Both are # looked for rather than assumed -- assuming picked the wrong one on the machine # this was written on. PANAMA_FIREFOX_DIR="$PANAMA_PATH/config/firefox" firefox_root="" find_firefox_root() { local candidate for candidate in "${XDG_CONFIG_HOME:-$HOME/.config}/mozilla/firefox" "$HOME/.mozilla/firefox"; do if [ -f "$candidate/profiles.ini" ]; then firefox_root="$candidate" return 0 fi done return 1 } if [ -d "$PANAMA_FIREFOX_DIR" ] && command -v firefox >/dev/null 2>&1; then echo -e "\n--- Setting up Firefox chrome ---" # A profile does not exist until Firefox has been run once. -CreateProfile is # a documented flag that creates one and exits rather than opening a window, # so a fresh machine gets the theme on the first install instead of the # second. MOZ_HEADLESS in case a future build decides otherwise. if ! find_firefox_root; then log "No Firefox profile yet; creating one" MOZ_HEADLESS=1 firefox -CreateProfile panama >/dev/null 2>&1 || true find_firefox_root || true fi if [ -z "$firefox_root" ]; then log "Could not find or create a Firefox profile; skipping the chrome" else # Every profile, not just the default. profiles.ini records each one as a # path that is relative to the root unless it says otherwise. while read -r profile; do [ -n "$profile" ] || continue [ -d "$profile" ] || continue for piece in chrome user.js; do target="$profile/$piece" if [ -L "$target" ]; then rm "$target" elif [ -e "$target" ]; then backup="$PANAMA_OLD/firefox-$(basename "$profile")-$piece" mv "$target" "$backup" log "Moved existing $target to $backup" fi ln -s "$PANAMA_FIREFOX_DIR/$piece" "$target" log "Linked $PANAMA_FIREFOX_DIR/$piece → $target" done done < <(awk -F= -v root="$firefox_root" ' /^\[/ { relative = 1; next } /^IsRelative=/ { relative = $2; next } /^Path=/ { print (relative == 1 ? root "/" $2 : $2) } ' "$firefox_root/profiles.ini") fi fi PANAMA_APPLICATION_DIR="$PANAMA_PATH/config/local/share/applications" USER_APPLICATION_DIR="$HOME/.local/share/applications" mkdir -p "$USER_APPLICATION_DIR" for desktop_file in "$PANAMA_APPLICATION_DIR"/*.desktop; do [[ -e "$desktop_file" ]] || continue desktop_name="$(basename "$desktop_file")" desktop_target="$USER_APPLICATION_DIR/$desktop_name" if [[ -L "$desktop_target" ]]; then rm "$desktop_target" elif [[ -e "$desktop_target" ]]; then log "Keeping existing desktop entry at $desktop_target" continue fi ln -s "$desktop_file" "$desktop_target" log "Linked $desktop_name → $desktop_target" done # Curate what files open with, now that Panama's own entries are linked. # # Left alone, a machine decides this by installation order -- whichever # application registered for image/png last wins it -- which is how a pixel-art # editor ends up owning screenshots and a video transcoder ends up owning MP3s. # Seeding only fills roles nobody has chosen for; an existing "Open With" choice # is always kept. # Panama's own systemd user units -- currently the Flatpak update timer, which # the Updates page switches on and off. Linked rather than copied so an edit in # the repository is the edit that runs. PANAMA_UNIT_DIR="$PANAMA_PATH/config/local/share/systemd/user" USER_UNIT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/systemd/user" if [[ -d "$PANAMA_UNIT_DIR" ]]; then mkdir -p "$USER_UNIT_DIR" for unit_file in "$PANAMA_UNIT_DIR"/*; do [[ -e "$unit_file" ]] || continue unit_name="$(basename "$unit_file")" unit_target="$USER_UNIT_DIR/$unit_name" if [[ -L "$unit_target" ]]; then rm "$unit_target" elif [[ -e "$unit_target" ]]; then log "Keeping existing unit at $unit_target" continue fi ln -s "$unit_file" "$unit_target" log "Linked $unit_name → $unit_target" done systemctl --user daemon-reload 2>/dev/null || true fi # Quadlets: container definitions systemd turns into units at boot. Linked # per-file into the directory the podman generator reads, rather than by # symlinking ~/.config/containers wholesale -- podman keeps its registries, # storage and login credentials in there, and that directory is the user's. PANAMA_QUADLET_DIR="$PANAMA_PATH/config/containers" USER_QUADLET_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/containers/systemd" if [ -d "$PANAMA_QUADLET_DIR" ]; then mkdir -p "$USER_QUADLET_DIR" for quadlet in "$PANAMA_QUADLET_DIR"/*/*.container; do [ -e "$quadlet" ] || continue quadlet_dst="$USER_QUADLET_DIR/$(basename "$quadlet")" if [ -L "$quadlet_dst" ]; then rm "$quadlet_dst" elif [ -e "$quadlet_dst" ]; then log "Keeping existing quadlet at $quadlet_dst" continue fi ln -s "$quadlet" "$quadlet_dst" log "Linked quadlet → $quadlet_dst" done systemctl --user daemon-reload 2>/dev/null || true fi # Nautilus loads Python extensions from this directory. Linked per file rather # than by symlinking the directory itself, the same way the quadlets and # desktop entries are: Nautilus writes nothing here today, but a directory # symlink into the repository is how machine state ends up in a tracked path. PANAMA_NAUTILUS_DIR="$PANAMA_PATH/config/local/share/nautilus-python/extensions" USER_NAUTILUS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/nautilus-python/extensions" if [ -d "$PANAMA_NAUTILUS_DIR" ]; then mkdir -p "$USER_NAUTILUS_DIR" for extension in "$PANAMA_NAUTILUS_DIR"/*.py; do [ -e "$extension" ] || continue extension_dst="$USER_NAUTILUS_DIR/$(basename "$extension")" if [ -L "$extension_dst" ]; then rm "$extension_dst" elif [ -e "$extension_dst" ]; then log "Keeping existing Nautilus extension at $extension_dst" continue fi ln -s "$extension" "$extension_dst" log "Linked Nautilus extension → $extension_dst" done fi DEFAULT_APPS_HELPER="$PANAMA_PATH/config/dot/quickshell/scripts/panama-default-apps" if [[ -x "$DEFAULT_APPS_HELPER" ]] && command -v xdg-mime >/dev/null 2>&1; then update-desktop-database "$USER_APPLICATION_DIR" >/dev/null 2>&1 || true while IFS= read -r seeded_line; do log "Default applications: $seeded_line" done < <("$DEFAULT_APPS_HELPER" seed 2>&1 || true) fi