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
This commit is contained in:
Gabriel Brown
2026-08-18 06:54:43 -04:00
parent 71ee18048e
commit 8c4dee0ca8
8 changed files with 231 additions and 3 deletions
@@ -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",
@@ -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 });
}
}
}
@@ -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;
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Panama Settings.
A gear, because a settings icon has to be recognisable at 48px in a dock
before it is anything else - but rendered in the Prism identity rather than
the flat monochrome symbolic icon this replaces. Blue leads into orchid, the
same pair that marks the focused window, the active workspace, and the
hairline along every glass surface.
The gear is a real toothed outline. An earlier version drew a ring with radial
strokes; at dock size the strokes merged into the ring and it read as an X.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<defs>
<linearGradient id="prism" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#82aaff"/>
<stop offset="55%" stop-color="#9b8ed8"/>
<stop offset="100%" stop-color="#b172b0"/>
</linearGradient>
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2b2e45"/>
<stop offset="100%" stop-color="#1e2030"/>
</linearGradient>
<linearGradient id="edge" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#82aaff" stop-opacity="0"/>
<stop offset="22%" stop-color="#82aaff" stop-opacity="0.9"/>
<stop offset="78%" stop-color="#b172b0" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#b172b0" stop-opacity="0"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="120" height="120" rx="28" fill="url(#tile)"/>
<rect x="4.5" y="4.5" width="119" height="119" rx="27.5" fill="none"
stroke="#c8d3f5" stroke-opacity="0.10" stroke-width="1"/>
<path d="M34 6.5 H94" stroke="url(#edge)" stroke-width="1.6" stroke-linecap="round"/>
<path d="M 52.53 29.88 L 56.45 18.62 L 71.55 18.62 L 75.47 29.88 L 80.02 31.76 L 90.75 26.57 L 101.43 37.25 L 96.24 47.98 L 98.12 52.53 L 109.38 56.45 L 109.38 71.55 L 98.12 75.47 L 96.24 80.02 L 101.43 90.75 L 90.75 101.43 L 80.02 96.24 L 75.47 98.12 L 71.55 109.38 L 56.45 109.38 L 52.53 98.12 L 47.98 96.24 L 37.25 101.43 L 26.57 90.75 L 31.76 80.02 L 29.88 75.47 L 18.62 71.55 L 18.62 56.45 L 29.88 52.53 L 31.76 47.98 L 26.57 37.25 L 37.25 26.57 L 47.98 31.76 Z" fill="url(#prism)" stroke="url(#prism)" stroke-width="5"
stroke-linejoin="round"/>
<circle cx="64" cy="64" r="15" fill="#1e2030"/>
<circle cx="64" cy="64" r="15" fill="none" stroke="#c8d3f5" stroke-opacity="0.18" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Panama Settings.
A gear, because a settings icon has to be recognisable at 48px in a dock
before it is clever — but rendered in the Prism identity rather than the flat
monochrome of the symbolic icon this replaces. Blue leads into orchid across
the gradient, the same pair that marks the focused window, the active
workspace and the hairline on every glass surface.
Drawn on the dark tile (#222436) so it reads as an application icon among
full-colour ones, rather than as a UI glyph that wandered out of a toolbar.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<defs>
<linearGradient id="prism" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#82aaff"/>
<stop offset="55%" stop-color="#9b8ed8"/>
<stop offset="100%" stop-color="#b172b0"/>
</linearGradient>
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2b2e45"/>
<stop offset="100%" stop-color="#1e2030"/>
</linearGradient>
<linearGradient id="edge" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#82aaff" stop-opacity="0"/>
<stop offset="22%" stop-color="#82aaff" stop-opacity="0.85"/>
<stop offset="78%" stop-color="#b172b0" stop-opacity="0.85"/>
<stop offset="100%" stop-color="#b172b0" stop-opacity="0"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="120" height="120" rx="28" fill="url(#tile)"/>
<rect x="4.5" y="4.5" width="119" height="119" rx="27.5" fill="none"
stroke="#c8d3f5" stroke-opacity="0.10" stroke-width="1"/>
<!-- The prism hairline, as it appears along the top edge of every glass
surface in the shell. -->
<path d="M32 6.5 H96" stroke="url(#edge)" stroke-width="1.5" stroke-linecap="round"/>
<!-- Gear: eight teeth, drawn as a ring with radial spokes so it stays legible
when the icon is scaled down to a dock tile. -->
<g transform="translate(64 66)">
<g fill="none" stroke="url(#prism)" stroke-width="9" stroke-linecap="round">
<circle r="24"/>
<g>
<path d="M0 -38 V-30"/>
<path d="M0 38 V30"/>
<path d="M-38 0 H-30"/>
<path d="M38 0 H30"/>
<path d="M-26.9 -26.9 L-21.2 -21.2"/>
<path d="M26.9 26.9 L21.2 21.2"/>
<path d="M-26.9 26.9 L-21.2 21.2"/>
<path d="M26.9 -26.9 L21.2 -21.2"/>
</g>
</g>
<circle r="7.5" fill="#c8d3f5" fill-opacity="0.92"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+19
View File
@@ -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"
+68
View File
@@ -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")"
View File