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