Files
Panama/setup/scripts/link-dotfiles
T
Gabriel Brown 88497826ec Let a machine say what it is for, and give Firefox its face back
Phase 4: the optional application categories, and the Firefox chrome.

Everything Panama installed until now was what every machine gets, which meant a
work laptop acquired emulators and a desktop that wanted Steam had to be told
about it by hand. The interview now offers the categories in
setup/packages/extras/ as a checklist -- gaming, creative, communication,
virtualization -- and nothing is preselected, because a default here installs
applications nobody chose on a machine whose owner answered a question they
thought was about something else.

A category is one file, and a category mixes both package managers because the
applications do: Steam is in RPM Fusion, Slack publishes only a flatpak. So a
bare line is a dnf package and a flatpak: line is a Flathub ID, and one file
holds the whole answer rather than splitting each category across two. The menu
is read from the directory rather than written down, so adding a category is
adding a file. Every name in all four was resolved against the actual
repositories before being written down, and the contract re-resolves them --
the point of admitting applications one at a time is that they stay installable,
and a typo here fails on somebody else's machine, not this one.

Firefox is declared, and its chrome is Edge-Frfox, vendored into config/firefox.
sunhat carried that theme with no license and no attribution; it is MIT, and now
it says so and says whose it is.

It is the only piece of Panama's configuration that does not go to a path this
repository chooses. Firefox owns the profile directory, names it with a random
salt, and does not create one until the browser has been run -- so link-dotfiles
finds or creates a profile and links both halves into it. Both, or neither works:
chrome/ is the CSS and user.js sets the preference that makes Firefox read chrome/
at all, without which the theme is a directory of dead files.

Two assumptions there were wrong, and the contract exists for both. Firefox has
moved to the XDG directories -- the profile root is ~/.config/mozilla/firefox on
this build, not ~/.mozilla/firefox, and writing to the wrong one themes nothing
and says nothing about it. And -CreateProfile turns out to be non-interactive, so
a fresh machine gets the theme on the first install rather than the second. The
contract runs link-dotfiles for real against a throwaway home with no profile in
it and looks at what came out; it was checked by pointing the search at the
legacy path only and watching it fail.

Also: the enrolment/enrollment spellings from the last commit are corrected. This
repository is US-spelled everywhere else -- color 1131 times against colour never
-- and consistency in prose is worth as much as it is in code.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:24:15 -04:00

409 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
# --- 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
mkdir -p "$PANAMA_OLD"
# --- 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/<name> -> $PANAMA_DOT/<name>.
# "forge" is the GNOME tiling extension; "hypr"/"quickshell"/"wofi" are the
# Hyprland desktop. They coexist deliberately — the GNOME session keeps working
# while Hyprland is being used, so you can log back into either.
dirs=("espanso" "forge" "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
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