Feature: Add pinned tray for Bitwarden and Sunshine

Replaces the disabled generic system tray with a curated PinnedTray
showing just Bitwarden and Sunshine. Each shows its live tray icon
(and forwards clicks to it) while running; when not running it shows
a dimmed static icon and launches the app on click instead.

Right-clicking Sunshine specifically opens its web UI in the browser
rather than forwarding to secondaryActivate(), since that's how it's
actually configured/used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 22:10:21 +01:00
parent a0b9ea29fe
commit 14b7e748d4
2 changed files with 102 additions and 1 deletions

View File

@@ -46,7 +46,7 @@ PanelWindow {
rightMargin: Appearance.spacing
}
spacing: Appearance.spacing
//Tray {}
PinnedTray {}
TailscaleIndicator { barWindow: bar }
VpnIndicator { barWindow: bar }
NetworkIndicator { barWindow: bar }

View File

@@ -0,0 +1,101 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
// A curated stand-in for the full system tray (see Tray.qml) — just the
// couple of apps worth one click away, rather than every SNI item any
// background app happens to register. Each entry shows that app's own live
// tray icon (and forwards clicks to it, same as Tray.qml) while it's
// running; when it isn't, it falls back to the app's static icon, dimmed,
// and a click launches it instead of doing nothing.
RowLayout {
id: root
spacing: Appearance.spacing * 0.6
// name: matched case-insensitively against a tray item's id/title to
// find this app's live icon, once it's running.
// icon: themed icon name shown in place of a live tray icon.
// launch: command run on left-click while not running.
// secondary: optional command run on right-click, regardless of running
// state, in place of forwarding to the tray item's own
// secondaryActivate() (which for most apps' tray icons does
// nothing useful anyway).
readonly property var pinned: [
{
name: "Bitwarden",
icon: "bitwarden",
launch: ["bitwarden-desktop", "--ozone-platform-hint=auto", "--silent"],
},
{
name: "Sunshine",
icon: "dev.lizardbyte.app.Sunshine",
launch: ["systemctl", "--user", "start", "app-dev.lizardbyte.app.Sunshine.service"],
// Sunshine is configured entirely through its web UI — there's
// no separate desktop app to "open" here.
secondary: ["xdg-open", "https://localhost:47990"],
},
]
function findTrayItem(name) {
const needle = name.toLowerCase()
for (const item of SystemTray.items.values) {
if (item.id.toLowerCase().includes(needle) || item.title.toLowerCase().includes(needle))
return item
}
return null
}
Repeater {
model: root.pinned
delegate: Item {
id: slot
required property var modelData
readonly property var trayItem: root.findTrayItem(modelData.name)
readonly property bool running: trayItem !== null
implicitWidth: Appearance.iconSize
implicitHeight: Appearance.iconSize
IconImage {
anchors.fill: parent
source: slot.running ? slot.trayItem.icon : Quickshell.iconPath(slot.modelData.icon)
// Not-running is the only cue we give beyond the click
// behavior itself — dimmed rather than hidden or badged, so
// the icon stays put and clickable either way.
opacity: slot.running ? 1.0 : 0.5
}
Process { id: launcher }
Process { id: secondaryProc }
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (slot.modelData.secondary) {
if (!secondaryProc.running) {
secondaryProc.command = slot.modelData.secondary
secondaryProc.running = true
}
} else if (slot.running) {
slot.trayItem.secondaryActivate()
}
} else if (slot.running) {
slot.trayItem.activate()
} else if (!launcher.running) {
launcher.command = slot.modelData.launch
launcher.running = true
}
}
}
}
}
}