diff --git a/dotfiles/.config/quickshell/modules/bar/Bar.qml b/dotfiles/.config/quickshell/modules/bar/Bar.qml index 149caa7..42827f2 100644 --- a/dotfiles/.config/quickshell/modules/bar/Bar.qml +++ b/dotfiles/.config/quickshell/modules/bar/Bar.qml @@ -46,7 +46,7 @@ PanelWindow { rightMargin: Appearance.spacing } spacing: Appearance.spacing - //Tray {} + PinnedTray {} TailscaleIndicator { barWindow: bar } VpnIndicator { barWindow: bar } NetworkIndicator { barWindow: bar } diff --git a/dotfiles/.config/quickshell/modules/bar/PinnedTray.qml b/dotfiles/.config/quickshell/modules/bar/PinnedTray.qml new file mode 100644 index 0000000..dc424ea --- /dev/null +++ b/dotfiles/.config/quickshell/modules/bar/PinnedTray.qml @@ -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 + } + } + } + } + } +}