From 8c4dee0ca8f90216f93f9457d12ab3b800d4feef Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 06:54:43 -0400 Subject: [PATCH] Give Settings its own icon and the first dock slot Panama Settings shipped with Icon=preferences-system-symbolic, a monochrome glyph drawn for 16px toolbar use. Beside full-colour application icons in a 48px dock it reads as a missing icon rather than a quiet one. It now has its own: a gear, because a settings icon has to be recognisable before it is clever, rendered in the Prism gradient on the dark tile so it belongs to this desktop. An earlier attempt drew the gear as a ring with radial strokes; at dock size the strokes merged into the ring and it read as an X. The shipped version is a real toothed outline, checked at 48px rather than only at 128. The dock also pinned GNOME Settings first. 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. link-dotfiles installs icons alongside desktop entries, so this survives a fresh setup rather than being a file that happens to exist here. The new contract asserts every pinned application resolves to an installed desktop entry. DockBody drops an unresolvable pin rather than drawing a broken icon, which is right at runtime and invisible to debug: a typo or a renamed desktop id just removes an icon with nothing logged. It also cost me a false negative while writing it -- DesktopEntries populates asynchronously, and asking too early reports every pin as missing. Also makes tests/quickshell/osd-ui-contract.sh executable. It was committed mode 644, the only test in the suite that was, so the runner could not invoke it. It passes. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L --- .../quickshell/config/PreferenceSchema.qml | 9 ++- config/dot/quickshell/dock-pin-harness.qml | 35 ++++++++++ .../applications/panama-settings.desktop | 2 +- .../hicolor/scalable/apps/panama-settings.svg | 42 ++++++++++++ config/local/share/icons/panama-settings.svg | 59 ++++++++++++++++ setup/scripts/link-dotfiles | 19 ++++++ tests/quickshell/dock-pins-contract.sh | 68 +++++++++++++++++++ tests/quickshell/osd-ui-contract.sh | 0 8 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 config/dot/quickshell/dock-pin-harness.qml create mode 100644 config/local/share/icons/hicolor/scalable/apps/panama-settings.svg create mode 100644 config/local/share/icons/panama-settings.svg create mode 100755 tests/quickshell/dock-pins-contract.sh mode change 100644 => 100755 tests/quickshell/osd-ui-contract.sh 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