Files
Panama/setup/scripts/link-dotfiles
T
Gabriel Brown 7cd4131327 Hold a key, speak, and the words are typed
Super+D holds the microphone open, releasing it transcribes on the GPU and
types the result wherever the cursor is. Roughly 150ms for a normal utterance
once the model is resident, measured rather than hoped for.

Getting there meant discarding two approaches. Fedora 44 cannot install any
GPU-capable Whisper for Python -- openai-whisper needs a numba that needs an
llvmlite that does not exist for 3.14, and faster-whisper needs a ctranslate2
nobody packaged. The whisper-cpp package IS built with HIP but ships libraries
with no binary and no bindings, and hand-writing ctypes for a large by-value
struct is a segfault waiting for a version bump. So a container, as suggested.

Vulkan rather than ROCm, and upstream's image rather than one built here. ROCm
is seven gigabytes and serves AMD alone; Vulkan compute runs on the AMD, Intel
and NVIDIA machines this config is used on, in a twentieth of the space. The
Vulkan tag already contains whisper-server, so there is no Containerfile to keep
working -- an earlier draft of this commit had one, and it was strictly worse.

Two bugs found by using it rather than by reading it. Whisper describes silence
as the literal text "[BLANK_AUDIO]", and the first working version pasted that
string into the clipboard; a transcription that is nothing but such markers is
now discarded. And the server answers with a line per segment, which typed into
a window is an Enter press -- sending the half-written message, submitting the
form. Whitespace is collapsed to one line.

Neither the image nor the model is installed by ./install. Together they are
over two gigabytes that want the network, and Settings offers both as one
action instead. Nothing starts at login either: whisper-server holds the model
from the moment it starts, so the first press of the key is what brings it up.

The contract pins both text bugs, that the server stays on loopback, and that it
does not start at login. Reverting the [BLANK_AUDIO] guard did not fail it at
first -- the check was still correct, it had simply stopped being called -- so
it now checks the call site too.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-21 12:22:19 -04:00

437 lines
18 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>.
#
# 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
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