nexoniarz-config-nixos/dotfiles/quickshell/RightBar.qml
Nexoniarz ea7fefbba8 Fix wallpaper persistence, tray icons, Booru bugs, session launcher
- Persist last-set wallpaper and restore it via exec-once, since
  hyprpaper/mpvpaper start every session with nothing loaded
- Fix Vesktop's system tray icon rendering as a broken texture: only
  resolve tray icons through the icon theme when they're actual names,
  not raw pixmap-sourced image:// URLs
- Launch Hyprland via start-hyprland (its official watchdog launcher)
  instead of the raw binary, removing the startup warning and adding
  hang recovery
- Fix Booru panel: Lewds toggle now actually persists, tag autocomplete
  no longer drops/races overlapping requests, and pagination wraps
  instead of overflowing off-screen
- Remove the redundant gap between windows and sidebars/tiles
  (gaps_in/gaps_out, and duplicate flyout margins stacked on top of
  Hyprland's automatic exclusive-zone inset)
2026-08-30 15:48:06 +02:00

230 lines
8.9 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Quickshell.Wayland
PanelWindow {
id: root
required property var screen
property string activePanel: ""
property bool expanded: false
// Keeps the default sink's PwNode subscribed so audio properties
// (volume/muted) actually receive live updates.
PwObjectTracker {
objects: Pipewire.defaultAudioSink ? [Pipewire.defaultAudioSink] : []
}
anchors { top: true; right: true; bottom: true }
// Collapsed = zero reserved space; expand/collapse is triggered from
// the taskbar (BottomBar), not a button on this bar itself.
implicitWidth: expanded ? 220 : 0
exclusiveZone: expanded ? 220 : 0
color: Theme.bg
// Never take keyboard focus — this bar has no text input, and leaving
// it focusable was stealing the keyboard grab from things like slurp's
// screenshot selection overlay.
focusable: false
// Only "network" needs the popup's keyboard grab (hotspot/wifi
// password fields).
function needsGrab(panel) { return panel === "network"; }
function toggle(panel) {
if (root.activePanel === panel) {
root.activePanel = "";
return;
}
if (root.activePanel !== "" && root.needsGrab(root.activePanel) !== root.needsGrab(panel)) {
// See LeftBar.qml's toggle() for why: an xdg_popup's grab
// can't be toggled on an already-mapped surface, only
// established fresh — so force a close+reopen when crossing
// that boundary instead of just changing the panel in place.
root.activePanel = "";
Qt.callLater(function () { root.activePanel = panel; });
return;
}
root.activePanel = panel;
}
Column {
visible: root.expanded
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 14
spacing: 14
width: parent.width - 24
DigitalClock {
anchors.horizontalCenter: parent.horizontalCenter
}
// System tray (StatusNotifierItem) — background/status apps like
// Steam, Discord, KDE Connect that register a tray icon. Sits
// right under the clock, separate from the categorized sections
// below the divider.
Row {
id: trayRow
visible: SystemTray.items.values.length > 0
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Repeater {
model: SystemTray.items.values
delegate: IconImage {
id: trayIcon
implicitSize: 18
// Only resolve through the icon theme when this is
// actually a themed icon name (e.g. Steam's
// "steam_tray_mono"). Apps that only ever supply a raw
// IconPixmap over the SNI protocol and no IconName at
// all (confirmed via busctl: Vesktop's tray item has no
// IconName property) get an already-usable image:// URL
// back from modelData.icon instead — running that
// through iconPath(), a name/path resolver, mangled it
// into nothing, which is what rendered as a broken-
// texture checkerboard instead of the actual icon.
source: modelData.icon.includes("://")
? modelData.icon
: Quickshell.iconPath(modelData.icon, "")
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (modelData.hasMenu) {
var pos = trayIcon.mapToItem(null, 0, trayIcon.height);
modelData.display(root, pos.x, pos.y);
} else {
modelData.secondaryActivate();
}
} else {
modelData.activate();
}
}
}
}
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Column {
width: parent.width
spacing: 6
Text {
text: "CONNECTIVITY"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Wi-Fi & Network"
active: root.activePanel === "network"
onClicked: root.toggle("network")
}
SidebarRow {
label: "Bluetooth"
active: root.activePanel === "bluetooth"
onClicked: root.toggle("bluetooth")
}
SidebarRow {
label: "Messages"
active: root.activePanel === "messages"
onClicked: root.toggle("messages")
}
}
Column {
width: parent.width
spacing: 6
Text {
text: "SYSTEM"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Screenshot"
active: root.activePanel === "screenshot"
onClicked: root.toggle("screenshot")
}
SidebarRow {
label: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio && Pipewire.defaultAudioSink.audio.muted)
? "Volume (muted)" : "Volume"
active: root.activePanel === "volume"
onClicked: root.toggle("volume")
}
SidebarRow {
label: "Brightness"
active: root.activePanel === "brightness"
onClicked: root.toggle("brightness")
}
SidebarRow {
label: "Devices"
active: root.activePanel === "devices"
onClicked: root.toggle("devices")
}
}
}
SidebarRow {
visible: root.expanded
anchors.bottom: parent.bottom
anchors.bottomMargin: 14
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 24
label: "Power"
fg: Theme.danger
active: root.activePanel === "power"
onClicked: root.toggle("power")
}
PanelWindow {
id: flyout
screen: root.screen
anchors { top: true; right: true }
// No margin here on purpose: Hyprland already insets layer-shell
// surfaces past other surfaces' exclusive zones on the same edge
// (confirmed via `hyprctl layers` — BottomBar gets the same
// treatment for free, spanning right up to x=1700 with zero
// margin of its own). A margin of 220 here was stacking on top
// of that automatic inset, pushing the flyout 220px further left
// than intended and leaving a ~220px gap between it and the
// sidebar.
visible: root.activePanel !== ""
implicitWidth: 320
implicitHeight: content.implicitHeight
exclusiveZone: 0
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
// On-demand layer-shell focus, not an xdg_popup grab: this used to
// be a PopupWindow with grabFocus, but an xdg_popup grab gets
// dismissed by the compositor the instant anything else so much as
// touches focus/surface state elsewhere (confirmed: even an
// unrelated hyprpaper/mpvpaper surface change was enough), which
// silently closed the whole flyout AND dropped keyboard input —
// that's what let keystrokes fall through to whatever window was
// focused before the flyout opened instead of reaching the text
// field. OnDemand only claims focus when something inside is
// actually clicked, and isn't torn down by unrelated surfaces.
// Panels with no text input don't need it at all.
WlrLayershell.keyboardFocus: root.needsGrab(root.activePanel)
? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
RightFlyout {
id: content
anchors.fill: parent
activePanel: root.activePanel
onRequestClose: root.activePanel = ""
}
}
}