diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml
index 495397d..b4f76b9 100644
--- a/config/dot/quickshell/config/PreferenceSchema.qml
+++ b/config/dot/quickshell/config/PreferenceSchema.qml
@@ -534,13 +534,18 @@ Singleton {
// A "json" value: the ordered list of desktop entry ids pinned to the
// dock. Kept in the shared store so that reordering the dock is covered
// by Restore defaults like everything else, rather than living in its
- // own file. The shipped order is the GNOME dash it replaced.
+ // own file. The shipped order is the GNOME dash it replaced, with one
+ // deliberate substitution: Panama Settings takes the first slot rather
+ // than GNOME Settings. Panama now covers what GNOME Settings did for
+ // this desktop and delegates the remainder to it by name, so pinning
+ // the thing it delegates TO put the fallback in front of the real one.
+ // GNOME Settings stays installed and searchable in the launcher.
{
key: "dockPinned", type: "json", group: "dock",
label: "Pinned applications",
detail: "Applications that stay in the Dock whether or not they are running",
def: [
- "org.gnome.Settings", "kitty", "org.gnome.Nautilus",
+ "panama-settings", "kitty", "org.gnome.Nautilus",
"com.bitwarden.desktop", "org.gnome.Software", "helium",
"org.mozilla.thunderbird_esr", "com.slack.Slack",
"app.bluebubbles.BlueBubbles", "rustdesk",
diff --git a/config/dot/quickshell/dock-pin-harness.qml b/config/dot/quickshell/dock-pin-harness.qml
new file mode 100644
index 0000000..e27f763
--- /dev/null
+++ b/config/dot/quickshell/dock-pin-harness.qml
@@ -0,0 +1,35 @@
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import qs.config
+
+ShellRoot {
+ // DesktopEntries populates asynchronously. Reading `applications.values`
+ // here rather than calling byId() blind is the documented way to wait for
+ // it -- and is exactly the trap noted in modules/settings/README.md.
+ readonly property var ids: {
+ const out = {};
+ for (const entry of DesktopEntries.applications.values)
+ out[entry.id] = entry;
+ return out;
+ }
+
+ IpcHandler {
+ target: "dock-pin-test"
+
+ function count(): int { return DesktopEntries.applications.values.length; }
+
+ function resolve(): string {
+ const pinned = DesktopPreferences.get("dockPinned");
+ const missing = [];
+ let first = null;
+ for (const id of pinned) {
+ const entry = DesktopEntries.byId(id);
+ if (!entry) missing.push(id);
+ if (first === null)
+ first = { id: id, found: !!entry, name: entry ? entry.name : "", icon: entry ? entry.icon : "" };
+ }
+ return JSON.stringify({ total: pinned.length, first: first, missing: missing });
+ }
+ }
+}
diff --git a/config/local/share/applications/panama-settings.desktop b/config/local/share/applications/panama-settings.desktop
index d730fbe..f4c2713 100644
--- a/config/local/share/applications/panama-settings.desktop
+++ b/config/local/share/applications/panama-settings.desktop
@@ -4,7 +4,7 @@ Name=Panama Settings
GenericName=System Settings
Comment=Configure the Panama Hyprland desktop
Exec=qs ipc call settings open
-Icon=preferences-system-symbolic
+Icon=panama-settings
Terminal=false
StartupNotify=false
Categories=Settings;
diff --git a/config/local/share/icons/hicolor/scalable/apps/panama-settings.svg b/config/local/share/icons/hicolor/scalable/apps/panama-settings.svg
new file mode 100644
index 0000000..a2858c1
--- /dev/null
+++ b/config/local/share/icons/hicolor/scalable/apps/panama-settings.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/config/local/share/icons/panama-settings.svg b/config/local/share/icons/panama-settings.svg
new file mode 100644
index 0000000..5db09a5
--- /dev/null
+++ b/config/local/share/icons/panama-settings.svg
@@ -0,0 +1,59 @@
+
+
+
diff --git a/setup/scripts/link-dotfiles b/setup/scripts/link-dotfiles
index 34ab909..e3eb5a3 100755
--- a/setup/scripts/link-dotfiles
+++ b/setup/scripts/link-dotfiles
@@ -91,6 +91,25 @@ fi
# 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-colour 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
+
PANAMA_APPLICATION_DIR="$PANAMA_PATH/config/local/share/applications"
USER_APPLICATION_DIR="$HOME/.local/share/applications"
mkdir -p "$USER_APPLICATION_DIR"
diff --git a/tests/quickshell/dock-pins-contract.sh b/tests/quickshell/dock-pins-contract.sh
new file mode 100755
index 0000000..0fc3780
--- /dev/null
+++ b/tests/quickshell/dock-pins-contract.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+
+# Every pinned application must resolve to an installed desktop entry.
+#
+# DockBody drops a pin it cannot resolve rather than drawing a broken icon,
+# which is the right behaviour at runtime and a terrible one to debug: a typo in
+# the shipped list, or an application that changed its desktop id, simply
+# removes an icon from the dock with nothing logged.
+#
+# Also asserts the Settings entry resolves and carries Panama's own icon, since
+# it is the first pin and the one most likely to be wrong after a rename.
+
+set -euo pipefail
+
+repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+harness="$repo_dir/config/dot/quickshell/dock-pin-harness.qml"
+config_home="$(mktemp -d /tmp/panama-dockpins.XXXXXX)"
+
+fail() {
+ printf 'dock pins contract: %s\n' "$1" >&2
+ exit 1
+}
+
+run() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"; }
+harness_pid=""
+
+cleanup() {
+ # By PID, never `pkill -f dock-pin-harness`: that pattern also matches the
+ # shell running this script.
+ [[ -n "$harness_pid" ]] && kill "$harness_pid" >/dev/null 2>&1 || true
+ rm -rf "$config_home"
+}
+trap cleanup EXIT
+
+XDG_CONFIG_HOME="$config_home" qs -p "$harness" --daemonize >/dev/null
+for _ in $(seq 1 40); do
+ run ipc show 2>/dev/null | rg -q '^target dock-pin-test$' && break
+ sleep 0.1
+done
+run ipc show 2>/dev/null | rg -q '^target dock-pin-test$' || fail 'test IPC target did not start'
+harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
+
+# DesktopEntries populates asynchronously; asking before it has finished reports
+# every pin as missing, which is a false failure rather than a real one.
+loaded=0
+for _ in $(seq 1 60); do
+ loaded="$(run ipc call dock-pin-test count 2>/dev/null || printf 0)"
+ [[ "$loaded" =~ ^[0-9]+$ ]] && (( loaded > 0 )) && break
+ sleep 0.25
+done
+(( loaded > 0 )) || fail 'no desktop entries were discovered at all'
+
+state="$(run ipc call dock-pin-test resolve)"
+
+missing="$(jq -r '.missing | join(", ")' <<<"$state")"
+[[ -z "$missing" ]] || fail "these pinned applications do not resolve and would vanish from the dock: $missing"
+
+jq -e '.first.id == "panama-settings" and .first.found == true' <<<"$state" >/dev/null \
+ || fail "Panama Settings is not the first pin, or does not resolve: $(jq -c .first <<<"$state")"
+jq -e '.first.icon == "panama-settings"' <<<"$state" >/dev/null \
+ || fail "Settings is not using Panama's own icon: $(jq -r .first.icon <<<"$state")"
+
+icon="$HOME/.local/share/icons/hicolor/scalable/apps/panama-settings.svg"
+[[ -r "$icon" ]] || fail "the icon it names is not installed at $icon"
+
+trap - EXIT
+cleanup
+printf 'dock pins contract: PASS (%s pins, all resolve)\n' "$(jq -r .total <<<"$state")"
diff --git a/tests/quickshell/osd-ui-contract.sh b/tests/quickshell/osd-ui-contract.sh
old mode 100644
new mode 100755