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:
@@ -46,7 +46,7 @@ PanelWindow {
|
|||||||
rightMargin: Appearance.spacing
|
rightMargin: Appearance.spacing
|
||||||
}
|
}
|
||||||
spacing: Appearance.spacing
|
spacing: Appearance.spacing
|
||||||
//Tray {}
|
PinnedTray {}
|
||||||
TailscaleIndicator { barWindow: bar }
|
TailscaleIndicator { barWindow: bar }
|
||||||
VpnIndicator { barWindow: bar }
|
VpnIndicator { barWindow: bar }
|
||||||
NetworkIndicator { barWindow: bar }
|
NetworkIndicator { barWindow: bar }
|
||||||
|
|||||||
101
dotfiles/.config/quickshell/modules/bar/PinnedTray.qml
Normal file
101
dotfiles/.config/quickshell/modules/bar/PinnedTray.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user