//@ pragma UseQApplication import Quickshell import Quickshell.Io import Quickshell.Hyprland import Quickshell.Wayland import Quickshell.Widgets import Quickshell.Services.SystemTray import Quickshell.Services.Pipewire import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts import QtQuick.Controls ShellRoot { id: root // ───────────── SETTINGS & THEMING (from ~/.config/foot/foot.ini) ───────────── readonly property string mono: "Maple Mono, JetBrainsMono Nerd Font, monospace" readonly property int barWidth: 45 readonly property int wsCount: 5 // Foot colorscheme palette: no harsh pure white, cohesive dark aesthetic readonly property color cBar: "#151515" // background readonly property color cPanel: "#181818" // opaque Foot-black popup surface readonly property color cSurface: "#202020" // card / elevated item readonly property color cSurfaceHover: "#282828" // hover state readonly property color cBorder: "#2f2f2f" // subtle border readonly property color cBorderFocus: "#454545" // active border readonly property color cText: "#D0D0D0" // foreground text readonly property color cDim: "#505050" // bright0 (dim / muted) readonly property color cSubtle: "#757575" // secondary text readonly property color cBright: "#F5F5F5" // selection-foreground (soft crisp white) readonly property color cAccent: "#6C99BB" // selection-background / regular4 (steel blue) readonly property color cSelected: "#303030" // neutral selected state, aligned with Foot's dark palette readonly property color cSuccess: "#7B9246" // regular2 (olive green) readonly property color cWarn: "#D3A04D" // regular3 (warm amber) readonly property color cDanger: "#A53C23" // regular1 (crimson rust) readonly property color cTeal: "#5EA8A2" // regular6 (teal cyan) // ───────────── SYSTEM STATE ───────────── property bool launcherOpen: false property bool powerMenuOpen: false property bool trayExpanded: false property string powerConfirmAction: "" property string activeApplet: "" // "wifi", "bluetooth", "sound", "calendar", or "" function closeMenus(): void { root.launcherOpen = false root.activeApplet = "" root.powerMenuOpen = false root.powerConfirmAction = "" } function runPowerAction(action: string): void { root.closeMenus() if (action === "logout") Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) else if (action === "reboot") Quickshell.execDetached(["foot", "-e", "doas", "/sbin/reboot"]) else if (action === "poweroff") Quickshell.execDetached(["foot", "-e", "doas", "/sbin/poweroff"]) } function runBluetoothAction(action: string, address: string): void { root.btActionPending = true root.btActionAddress = address root.btActionType = action root.btActionMessage = "" if (root.btScanning) { root.btScanning = false btScanTimer.stop() root.runBluetoothCommand("scan off") } root.runBluetoothCommand(action + " " + address) } function runBluetoothCommand(command: string): void { if (btAgentProc.running && root.btAgentReady) { btAgentProc.write(command + "\n") } else { root.btQueuedCommands.push(command) if (!btAgentProc.running) btAgentProc.running = true } } function startBluetoothScan(): void { if (!root.btOn) { if (Bluetooth.defaultAdapter) Bluetooth.defaultAdapter.enabled = true statusProc.running = true return } root.btScanning = true root.btActionMessage = "" root.runBluetoothCommand("scan on") btScanTimer.restart() } function finishBluetoothAction(message: string): void { root.btActionMessage = message root.btActionPending = false root.btActionAddress = "" root.btActionType = "" statusProc.running = true } function readBluetoothOutput(rawLine: string): void { const line = rawLine.trim() if (!line) return console.info("bluetoothctl:", line) if (!root.btAgentReady && line.includes("Agent registered")) { root.btAgentReady = true btAgentProc.write("default-agent\n") for (const command of root.btQueuedCommands) btAgentProc.write(command + "\n") root.btQueuedCommands = [] } const failed = /failed|not available|no default controller|no agent|not authorized/i.test(line) if (failed && root.btActionPending) { root.finishBluetoothAction(line.replace(/^.*?bluetooth\]#\s*/, "")) return } if (root.btActionPending && line.includes(root.btActionAddress)) { if (root.btActionType === "pair" && /Paired: yes|Pairing successful/i.test(line)) root.finishBluetoothAction("Pairing complete") else if (root.btActionType === "connect" && /Connected: yes|Connection successful/i.test(line)) root.finishBluetoothAction("Connected") else if (root.btActionType === "disconnect" && /Connected: no|Successful disconnected/i.test(line)) root.finishBluetoothAction("Disconnected") } } function showVolumeOsd(): void { const audio = Pipewire.defaultAudioSink?.audio root.osdKind = "volume" root.osdVolume = audio?.volume ?? 0 root.osdMuted = audio?.muted ?? false root.volumeOsdVisible = true volumeOsdTimer.restart() } function showBrightnessOsd(): void { root.osdKind = "brightness" root.volumeOsdVisible = true volumeOsdTimer.restart() } function setScreenBrightness(value: real): void { root.brightnessLevel = Math.max(0, Math.min(100, Math.round(value))) root.showBrightnessOsd() brightnessSetTimer.restart() } function changeScreenBrightness(delta: int): void { Quickshell.execDetached(["brightnessctl", "-e4", "-n2", "set", delta > 0 ? "5%+" : "5%-"]) if (root.brightnessLevel >= 0) root.brightnessLevel = Math.max(0, Math.min(100, root.brightnessLevel + (delta > 0 ? 5 : -5))) root.showBrightnessOsd() brightnessReadTimer.restart() } function changeOutputVolume(delta: real): void { const audio = Pipewire.defaultAudioSink?.audio if (!audio) return audio.volume = Math.max(0, Math.min(1, audio.volume + delta)) if (audio.muted) audio.muted = false root.showVolumeOsd() } function toggleOutputMute(): void { const audio = Pipewire.defaultAudioSink?.audio if (!audio) return audio.muted = !audio.muted root.showVolumeOsd() } function setOutputVolume(value: real): void { const audio = Pipewire.defaultAudioSink?.audio if (!audio) return audio.volume = Math.max(0, Math.min(1, value)) if (audio.muted) audio.muted = false root.showVolumeOsd() } function formatBatteryTime(seconds: int): string { if (seconds < 0) return "Unavailable" const totalMinutes = Math.max(1, Math.round(seconds / 60)) const hours = Math.floor(totalMinutes / 60) const minutes = totalMinutes % 60 return hours > 0 ? (hours + " hr " + minutes + " min") : (minutes + " min") } function batteryChargeColor(): color { if (root.batteryPercent < 0) return root.cSubtle if (root.batteryPercent >= 80) return root.cSuccess if (root.batteryPercent >= 30) return root.cWarn return root.cDanger } function sliderValueFromPointer(slider: var, x: real): real { const span = Math.max(1, slider.availableWidth) const fraction = Math.max(0, Math.min(1, (x - slider.leftPadding) / span)) return slider.from + fraction * (slider.to - slider.from) } function isPlaybackNode(node: var): bool { return node.type === "Stream/Output/Audio" || (node.isStream && node.audio && !node.isSink) } property bool netUp: false property bool wifiEnabled: true property string activeSsid: "" property string ipAddress: "" property var wifiNetworks: [] property bool btOn: false property bool btConnected: false property bool btScanning: false property bool btActionPending: false property string btActionAddress: "" property string btActionType: "" property string btActionMessage: "" property bool btAgentReady: false property var btQueuedCommands: [] property var btDevices: [] property bool volumeOsdVisible: false property string osdKind: "volume" property real osdVolume: 0 property bool osdMuted: false property int batteryPercent: -1 property string batteryStatus: "Unknown" property int batteryTimeSeconds: -1 property bool onExternalPower: false property int brightnessLevel: -1 // Writable volume/mute values are only available after each PipeWire node // is bound. Track the complete live node model, including app streams. PwObjectTracker { objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource] } IpcHandler { target: "launcher" function toggle(): void { root.launcherOpen = !root.launcherOpen } function open(): void { root.launcherOpen = true } function close(): void { root.launcherOpen = false } } IpcHandler { target: "power" function toggle(): void { root.launcherOpen = false root.activeApplet = "" root.powerConfirmAction = "" root.powerMenuOpen = !root.powerMenuOpen } function open(): void { root.launcherOpen = false root.activeApplet = "" root.powerConfirmAction = "" root.powerMenuOpen = true } function close(): void { root.closeMenus() } } IpcHandler { target: "volume" function up(): void { root.changeOutputVolume(0.05) } function down(): void { root.changeOutputVolume(-0.05) } function mute(): void { root.toggleOutputMute() } } IpcHandler { target: "brightness" function up(): void { root.changeScreenBrightness(1) } function down(): void { root.changeScreenBrightness(-1) } } SystemClock { id: clock; precision: SystemClock.Seconds } // Periodic system status collector (Network + Bluetooth) Process { id: statusProc command: ["python3", "-c", ` import subprocess, json data = { 'net_up': False, 'wifi_enabled': False, 'active_ssid': '', 'ip': '', 'bt_on': False, 'bt_connected': False, 'bt_devices': [] } # Network check try: state = subprocess.run(['nmcli', '-t', '-f', 'STATE', 'general'], capture_output=True, text=True, timeout=2).stdout.strip() data['net_up'] = state.startswith('connected') wifi_rad = subprocess.run(['nmcli', '-t', '-f', 'WIFI', 'general'], capture_output=True, text=True, timeout=2).stdout.strip() data['wifi_enabled'] = 'enabled' in wifi_rad conns = subprocess.run(['nmcli', '-t', '-f', 'NAME,TYPE,DEVICE,STATE', 'con', 'show', '--active'], capture_output=True, text=True, timeout=2).stdout.strip() for line in conns.splitlines(): if line: parts = line.split(':') if len(parts) >= 2 and ('wireless' in parts[1] or 'ethernet' in parts[1]): data['active_ssid'] = parts[0] break ip_out = subprocess.run(['sh', '-c', "ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7}'"], capture_output=True, text=True, timeout=2).stdout.strip() data['ip'] = ip_out except Exception: pass # Bluetooth check try: show = subprocess.run(['bluetoothctl', 'show'], capture_output=True, text=True, timeout=2).stdout if 'Powered: yes' in show: data['bt_on'] = True devs = subprocess.run(['bluetoothctl', 'devices'], capture_output=True, text=True, timeout=2).stdout for line in devs.splitlines(): if line.startswith('Device '): parts = line.split(' ', 2) if len(parts) >= 3: mac, name = parts[1], parts[2] info = subprocess.run(['bluetoothctl', 'info', mac], capture_output=True, text=True, timeout=1).stdout conn = 'Connected: yes' in info if conn: data['bt_connected'] = True paired = 'Paired: yes' in info trusted = 'Trusted: yes' in info data['bt_devices'].append({'mac': mac, 'name': name, 'connected': conn, 'paired': paired, 'trusted': trusted}) data['bt_devices'].sort(key=lambda d: (not d['connected'], not d['paired'], d['name'].lower())) except Exception: pass print(json.dumps(data)) `] stdout: StdioCollector { onStreamFinished: { try { const res = JSON.parse(text) root.netUp = res.net_up root.wifiEnabled = res.wifi_enabled root.activeSsid = res.active_ssid root.ipAddress = res.ip root.btOn = res.bt_on root.btConnected = res.bt_connected root.btDevices = res.bt_devices if (root.btActionPending) { const device = res.bt_devices.find(d => d.mac === root.btActionAddress) if (device && root.btActionType === "pair" && device.paired) root.finishBluetoothAction("Pairing complete") else if (device && root.btActionType === "connect" && device.connected) root.finishBluetoothAction("Connected") else if (device && root.btActionType === "disconnect" && !device.connected) root.finishBluetoothAction("Disconnected") } } catch (e) {} } } } // Read Linux power_supply sysfs directly so the indicator also works on // minimal Gentoo installs that do not run the optional UPower daemon. Process { id: batteryProc command: ["python3", "-c", ` import glob, json, os result = {'percent': -1, 'status': 'Unknown', 'seconds': -1, 'external': False} for path in glob.glob('/sys/class/power_supply/*'): if os.path.basename(path).startswith(('AC', 'ADP')) or os.path.basename(path) in ('USB', 'USB_C'): try: with open(path + '/online') as f: result['external'] = result['external'] or f.read().strip() == '1' except OSError: pass for path in glob.glob('/sys/class/power_supply/*'): try: with open(path + '/type') as f: if f.read().strip() != 'Battery': continue with open(path + '/capacity') as f: result['percent'] = int(f.read().strip()) with open(path + '/status') as f: result['status'] = f.read().strip() if result['status'] not in ('Full', 'Not charging'): try: with open(path + '/energy_now') as f: remaining = int(f.read().strip()) with open(path + '/energy_full') as f: full = int(f.read().strip()) with open(path + '/power_now') as f: rate = int(f.read().strip()) except OSError: try: with open(path + '/charge_now') as f: remaining = int(f.read().strip()) with open(path + '/charge_full') as f: full = int(f.read().strip()) with open(path + '/current_now') as f: rate = int(f.read().strip()) except OSError: remaining = full = rate = 0 if rate > 0: amount = max(0, full - remaining) if result['status'] == 'Charging' else remaining result['seconds'] = int(amount / rate * 3600) break except (OSError, ValueError): continue print(json.dumps(result)) `] stdout: StdioCollector { onStreamFinished: { try { const status = JSON.parse(text) root.batteryPercent = status.percent root.batteryStatus = status.status root.batteryTimeSeconds = status.seconds root.onExternalPower = status.external } catch (e) { root.batteryPercent = -1 root.batteryStatus = "Unknown" root.batteryTimeSeconds = -1 root.onExternalPower = false } } } } Timer { interval: 30000 running: true repeat: true triggeredOnStart: true onTriggered: if (!batteryProc.running) batteryProc.running = true } Process { id: brightnessReadProc command: ["brightnessctl", "-m"] stdout: StdioCollector { onStreamFinished: { const fields = text.trim().split(",") const current = Number(fields[2]) const maximum = Number(fields[4]) root.brightnessLevel = maximum > 0 && Number.isFinite(current) && Number.isFinite(maximum) ? Math.round(current * 100 / maximum) : -1 } } } Process { id: brightnessSetProc command: ["brightnessctl", "set", root.brightnessLevel + "%"] onExited: if (!brightnessReadProc.running) brightnessReadProc.running = true } Timer { id: brightnessSetTimer interval: 100 repeat: false onTriggered: { if (brightnessSetProc.running) { restart() } else if (root.brightnessLevel >= 0) { brightnessSetProc.running = true } } } Timer { id: brightnessReadTimer interval: 160 repeat: false onTriggered: if (!brightnessReadProc.running && !brightnessSetProc.running) brightnessReadProc.running = true } Timer { interval: 5000 running: root.activeApplet === "battery" repeat: true triggeredOnStart: true onTriggered: if (!brightnessReadProc.running && !brightnessSetProc.running && !brightnessSlider.pressed) brightnessReadProc.running = true } onActiveAppletChanged: { if (activeApplet === "battery" && !brightnessReadProc.running) brightnessReadProc.running = true } // Keep one BlueZ CLI session alive so the selected pairing agent remains // registered while scan/pair/connect requests complete. Process { id: btAgentProc command: ["bluetoothctl", "--agent", "NoInputNoOutput"] stdinEnabled: true stdout: SplitParser { onRead: data => root.readBluetoothOutput(data) } stderr: SplitParser { onRead: data => root.readBluetoothOutput(data) } onExited: { root.btAgentReady = false root.btQueuedCommands = [] root.btScanning = false root.btActionPending = false root.btActionAddress = "" root.btActionType = "" statusProc.running = true } } Timer { id: btScanTimer interval: 15000 repeat: false onTriggered: { root.runBluetoothCommand("scan off") root.btScanning = false statusProc.running = true } } Timer { id: volumeOsdTimer interval: 1600 repeat: false onTriggered: root.volumeOsdVisible = false } Timer { interval: 4000 running: true repeat: true triggeredOnStart: true onTriggered: statusProc.running = true } // Wi-Fi network scanner Process { id: wifiScanProc command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi"] stdout: StdioCollector { onStreamFinished: { const lines = text.trim().split("\n") let results = [] for (let l of lines) { if (!l) continue const parts = l.split(":") if (parts[0] && !results.some(r => r.ssid === parts[0])) { results.push({ ssid: parts[0], signal: parseInt(parts[1]) || 0, security: parts[2] || "Open", active: parts[3] === "*" }) } } results.sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0) || b.signal - a.signal) root.wifiNetworks = results } } } // ───────────── VERTICAL BAR ───────────── Variants { model: Quickshell.screens PanelWindow { id: bar required property var modelData screen: modelData anchors { top: true; bottom: true; left: true } implicitWidth: root.barWidth color: root.cBar Rectangle { anchors { top: parent.top; bottom: parent.bottom; right: parent.right } width: 1 color: root.cBorder } // Top: Workspaces ColumnLayout { anchors { top: parent.top topMargin: 12 horizontalCenter: parent.horizontalCenter horizontalCenterOffset: -0.5 } spacing: 6 // Workspaces Repeater { model: root.wsCount delegate: Rectangle { required property int index readonly property int wsId: index + 1 readonly property bool active: Hyprland.focusedWorkspace?.id === wsId readonly property bool occupied: Hyprland.workspaces.values.some(w => w.id === wsId) Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 radius: 0 color: active ? root.cSelected : (wsHover.containsMouse ? root.cSurface : "transparent") border.color: active ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: parent.wsId font.family: root.mono font.pixelSize: 12 font.bold: parent.active color: parent.active ? root.cBright : (parent.occupied ? root.cText : root.cDim) } MouseArea { id: wsHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: Hyprland.dispatch('hl.dsp.focus({ workspace = "' + parent.wsId + '" })') } } } } // Center: Clock (Clickable, opens Calendar) Rectangle { anchors.centerIn: parent anchors.horizontalCenterOffset: -0.5 implicitWidth: 34 implicitHeight: 52 radius: 0 color: root.activeApplet === "calendar" ? root.cSelected : (clockHover.containsMouse ? root.cSurface : "transparent") border.color: root.activeApplet === "calendar" ? root.cBorderFocus : "transparent" ColumnLayout { anchors.centerIn: parent spacing: 1 Text { Layout.alignment: Qt.AlignHCenter text: Qt.formatDateTime(clock.date, "HH") font.family: root.mono font.pixelSize: 13 font.bold: true color: root.activeApplet === "calendar" ? root.cBright : (clockHover.containsMouse ? root.cBright : root.cText) } Rectangle { Layout.alignment: Qt.AlignHCenter width: 4; height: 1 color: root.activeApplet === "calendar" ? root.cBorderFocus : root.cBorder } Text { Layout.alignment: Qt.AlignHCenter text: Qt.formatDateTime(clock.date, "mm") font.family: root.mono font.pixelSize: 13 font.bold: true color: root.activeApplet === "calendar" ? root.cBright : (clockHover.containsMouse ? root.cBright : root.cDim) } } MouseArea { id: clockHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.activeApplet = (root.activeApplet === "calendar" ? "" : "calendar") } } // Bottom: Battery, Network, Bluetooth, Audio Triggers ColumnLayout { anchors { bottom: parent.bottom bottomMargin: 12 horizontalCenter: parent.horizontalCenter horizontalCenterOffset: -0.5 } spacing: 6 // Expandable system tray. The arrow stays visible while the // registered application icons are tucked away. Repeater { model: root.trayExpanded ? SystemTray.items : [] delegate: Item { id: trayItem required property var modelData Layout.alignment: Qt.AlignHCenter implicitWidth: 32 implicitHeight: 28 Image { anchors.centerIn: parent width: 18 height: 18 source: trayItem.modelData.icon sourceSize.width: 36 sourceSize.height: 36 fillMode: Image.PreserveAspectFit smooth: true } QsMenuAnchor { id: trayMenuAnchor menu: trayItem.modelData.menu anchor.item: trayItem } MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: mouse => { const item = trayItem.modelData if (mouse.button === Qt.LeftButton) { if (item.onlyMenu) openMenu() else item.activate() } else if (mouse.button === Qt.MiddleButton) { item.secondaryActivate() } else if (mouse.button === Qt.RightButton) { openMenu() } } onWheel: wheel => { const horizontal = wheel.angleDelta.x !== 0 trayItem.modelData.scroll(horizontal ? wheel.angleDelta.x : wheel.angleDelta.y, horizontal) } function openMenu(): void { const item = trayItem.modelData if (!item.hasMenu) return trayMenuAnchor.open() } } } } Rectangle { Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 visible: SystemTray.items.values.length > 0 color: root.trayExpanded ? root.cSelected : (trayHover.containsMouse ? root.cSurface : "transparent") border.color: root.trayExpanded ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: root.trayExpanded ? "󰅀" : "󰅃" font.family: root.mono font.pixelSize: 14 color: root.trayExpanded ? root.cBright : (trayHover.containsMouse ? root.cBright : root.cText) } MouseArea { id: trayHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.trayExpanded = !root.trayExpanded } } // Charge glyph with a distinct external-power glyph. The bar // stays compact and deliberately omits a numeric percentage. Rectangle { Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 color: root.activeApplet === "battery" ? root.cSelected : (batteryHover.containsMouse ? root.cSurface : "transparent") border.color: root.activeApplet === "battery" ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent anchors.verticalCenterOffset: root.onExternalPower ? -3 : 0 text: { if (root.batteryPercent < 0) return "󰂑" if (root.batteryPercent >= 95) return "󰁹" if (root.batteryPercent >= 85) return "󰂂" if (root.batteryPercent >= 75) return "󰂁" if (root.batteryPercent >= 65) return "󰂀" if (root.batteryPercent >= 55) return "󰁿" if (root.batteryPercent >= 45) return "󰁾" if (root.batteryPercent >= 35) return "󰁽" if (root.batteryPercent >= 25) return "󰁼" if (root.batteryPercent >= 15) return "󰁻" return "󰂃" } font.family: root.mono font.pixelSize: 14 color: root.batteryPercent >= 0 && root.batteryPercent <= 15 ? root.cDanger : root.onExternalPower ? root.cSuccess : root.cText } Text { visible: root.onExternalPower anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom text: "󰚥" font.family: root.mono; font.pixelSize: 9 color: root.cSuccess } MouseArea { id: batteryHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.activeApplet = root.activeApplet === "battery" ? "" : "battery" } } Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder } // Network Button Rectangle { Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 radius: 0 color: root.activeApplet === "wifi" ? root.cSelected : (netHover.containsMouse ? root.cSurface : "transparent") border.color: root.activeApplet === "wifi" ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: root.netUp ? (root.activeSsid ? "" : "󰈀") : "󰤮" font.family: root.mono; font.pixelSize: 14 color: root.activeApplet === "wifi" ? root.cBright : (root.netUp ? root.cText : root.cDim) } MouseArea { id: netHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (root.activeApplet === "wifi") { root.activeApplet = "" } else { root.activeApplet = "wifi" wifiScanProc.running = true } } } } Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder } // Bluetooth Button Rectangle { Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 radius: 0 color: root.activeApplet === "bluetooth" ? root.cSelected : (btHover.containsMouse ? root.cSurface : "transparent") border.color: root.activeApplet === "bluetooth" ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: root.btConnected ? "󰂱" : (root.btOn ? "" : "󰂲") font.family: root.mono; font.pixelSize: 14 color: root.activeApplet === "bluetooth" ? root.cBright : (root.btOn ? root.cText : root.cDim) } MouseArea { id: btHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.activeApplet = (root.activeApplet === "bluetooth" ? "" : "bluetooth") } } Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder } // Audio Button Rectangle { Layout.alignment: Qt.AlignHCenter implicitWidth: 32; implicitHeight: 32 radius: 0 color: root.activeApplet === "sound" ? root.cSelected : (soundHover.containsMouse ? root.cSurface : "transparent") border.color: root.activeApplet === "sound" ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: { if (Pipewire.defaultAudioSink?.audio?.muted) return "󰝟" const vol = Pipewire.defaultAudioSink?.audio?.volume ?? 0 if (vol > 0.5) return "" if (vol > 0) return "" return "" } font.family: root.mono; font.pixelSize: 14 color: { if (Pipewire.defaultAudioSink?.audio?.muted) return root.cDanger if (root.activeApplet === "sound") return root.cBright return soundHover.containsMouse ? root.cBright : root.cText } } MouseArea { id: soundHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.activeApplet = (root.activeApplet === "sound" ? "" : "sound") } } } } } // ───────────── MENU BACKDROP BLUR ───────────── // Blur the active monitor behind open shell surfaces while preserving the bar. Variants { model: Quickshell.screens PanelWindow { id: menuBackdrop required property var modelData screen: modelData visible: (root.activeApplet !== "" || root.launcherOpen || root.powerMenuOpen) && modelData.name === (Hyprland.focusedMonitor?.name ?? Quickshell.screens[0]?.name) anchors { top: true; bottom: true; left: true; right: true } color: "transparent" mask: Region {} WlrLayershell.layer: WlrLayer.Top WlrLayershell.namespace: "menu-backdrop" WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore BackgroundEffect.blurRegion: Region { item: menuBackdrop.contentItem // The vertical bar occupies the leftmost 42 logical pixels. Region { x: 0 y: 0 width: root.barWidth height: menuBackdrop.height intersection: Intersection.Subtract } } } } // ───────────── APP LAUNCHER OVERLAY ───────────── PanelWindow { id: launcherWindow visible: root.launcherOpen screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0] // Full-screen overlay surface; the launcher itself stays centered. anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "launcher" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore MouseArea { anchors.fill: parent onClicked: root.launcherOpen = false } Rectangle { anchors.centerIn: parent width: 340 height: 480 color: root.cPanel border.color: root.cBorder border.width: 1 radius: 0 MouseArea { anchors.fill: parent } FocusScope { id: launcherScope anchors.fill: parent focus: launcherWindow.visible Keys.onEscapePressed: root.closeMenus() ColumnLayout { anchors.fill: parent anchors.margins: 12 spacing: 10 // Search Input Box Rectangle { Layout.fillWidth: true implicitHeight: 36 color: root.cSurface border.color: root.cBorderFocus border.width: 1 radius: 0 RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 8 Text { text: "" font.family: root.mono font.pixelSize: 13 color: root.cSubtle } TextInput { id: searchInput Layout.fillWidth: true font.family: root.mono font.pixelSize: 12 color: root.cBright focus: true selectByMouse: true Text { text: "Search applications..." font.family: root.mono font.pixelSize: 12 color: root.cDim visible: searchInput.text === "" } Keys.onDownPressed: appListView.incrementCurrentIndex() Keys.onUpPressed: appListView.decrementCurrentIndex() Keys.onReturnPressed: { if (appListView.currentItem) { appListView.currentItem.launch() } } } } } // Application List ListView { id: appListView Layout.fillWidth: true Layout.fillHeight: true clip: true spacing: 3 currentIndex: 0 model: appListProc.apps delegate: Rectangle { id: appItem required property var modelData required property int index width: appListView.width height: 38 radius: 0 color: ListView.isCurrentItem ? root.cSelected : (itemArea.containsMouse ? root.cSurfaceHover : "transparent") border.color: ListView.isCurrentItem ? root.cBorderFocus : "transparent" function launch() { Quickshell.execDetached(["sh", "-c", modelData.exec]) root.launcherOpen = false searchInput.text = "" } RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 10 Text { text: "" font.family: root.mono font.pixelSize: 13 color: ListView.isCurrentItem ? root.cAccent : root.cSubtle } Text { text: modelData.name font.family: root.mono font.pixelSize: 12 color: ListView.isCurrentItem ? root.cBright : root.cText Layout.fillWidth: true elide: Text.ElideRight } } MouseArea { id: itemArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: appItem.launch() } } // Empty search state Item { anchors.centerIn: parent visible: appListView.count === 0 ColumnLayout { anchors.centerIn: parent spacing: 4 Text { Layout.alignment: Qt.AlignHCenter; text: "No applications found"; font.family: root.mono; font.pixelSize: 12; color: root.cDim } } } } } } // Desktop applications scanner with flatpak and standard fallbacks Process { id: appListProc property var rawApps: [] property var apps: { const query = searchInput.text.toLowerCase().trim() if (!query) return rawApps return rawApps.filter(a => a.name.toLowerCase().includes(query)) } command: ["python3", "-c", ` import os, glob dirs = [ '/usr/share/applications', '/usr/local/share/applications', os.path.expanduser('~/.local/share/applications'), os.path.expanduser('~/.local/share/flatpak/exports/share/applications'), '/var/lib/flatpak/exports/share/applications' ] xdg = os.environ.get('XDG_DATA_DIRS', '') if xdg: for p in xdg.split(':'): app_dir = os.path.join(p, 'applications') if app_dir not in dirs: dirs.append(app_dir) apps = [] seen = set() for d in dirs: if not os.path.isdir(d): continue for path in glob.glob(os.path.join(d, '*.desktop')): try: with open(path, 'r', encoding='utf-8', errors='ignore') as f: name, exec_cmd, nodisplay = '', '', False for line in f: line = line.strip() if line.startswith('Name=') and not name: name = line.split('=', 1)[1].strip() elif line.startswith('Exec=') and not exec_cmd: exec_cmd = line.split('=', 1)[1].strip().split('%')[0].strip() elif line.startswith('NoDisplay=true'): nodisplay = True if name and exec_cmd and not nodisplay: if name.lower() not in seen: seen.add(name.lower()) apps.append((name, exec_cmd)) except: pass # Fallbacks for essential desktop tools fallbacks = [ ('Terminal (Foot)', 'foot'), ('Fish Shell', 'foot -e fish'), ] for name, cmd in fallbacks: if name.lower() not in seen: apps.append((name, cmd)) apps.sort(key=lambda x: x[0].lower()) for name, cmd in apps: print(f"{name}|||{cmd}") `] stdout: StdioCollector { onStreamFinished: { const lines = text.trim().split("\n") let parsed = [] for (let line of lines) { const parts = line.split("|||") if (parts.length === 2 && parts[0]) { parsed.push({ name: parts[0], exec: parts[1] }) } } appListProc.rawApps = parsed } } } Component.onCompleted: appListProc.running = true onVisibleChanged: { if (visible) { searchInput.text = "" searchInput.forceActiveFocus() appListProc.running = true } } } } // ───────────── CALENDAR POPUP WINDOW ───────────── PanelWindow { id: calendarWindow visible: root.activeApplet === "calendar" screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0] // Vertically centered beside the bar; this overlay reserves no space from app windows. anchors { left: true; top: true } margins.left: root.barWidth + 14 margins.top: Math.max(8, (screen.height - implicitHeight) / 2) implicitWidth: 320 implicitHeight: 390 color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "calendar" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive aboveWindows: true exclusionMode: ExclusionMode.Ignore Rectangle { anchors.fill: parent color: root.cPanel border.color: root.cBorder border.width: 1 radius: 0 FocusScope { id: calendarScope anchors.fill: parent focus: calendarWindow.visible Keys.onEscapePressed: root.closeMenus() property date viewDate: new Date() function getCellData(index) { const y = calendarScope.viewDate.getFullYear() const m = calendarScope.viewDate.getMonth() const firstDay = new Date(y, m, 1) // Monday as 0, Sunday as 6 const startDay = (firstDay.getDay() + 6) % 7 const date = new Date(y, m, 1 - startDay + index) const now = clock.date const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const offset = Math.round((date.getTime() - today.getTime()) / 86400000) return { day: date.getDate(), isCurrentMonth: date.getMonth() === m, isToday: offset === 0, isWeekend: index % 7 >= 5, dayOffset: offset } } ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 // Header: Full Date & Clock RowLayout { Layout.fillWidth: true ColumnLayout { spacing: 2 Text { text: Qt.formatDateTime(clock.date, "dddd, MMMM d, yyyy") font.family: root.mono; font.pixelSize: 12; font.bold: true; color: root.cBright } Text { text: Qt.formatDateTime(clock.date, "HH:mm:ss") font.family: root.mono; font.pixelSize: 11; color: root.cAccent } } Item { Layout.fillWidth: true } } Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder } // Month Navigation Row RowLayout { Layout.fillWidth: true Text { text: Qt.formatDate(calendarScope.viewDate, "MMMM yyyy") font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright Layout.fillWidth: true } // Prev Month Button Rectangle { width: 26; height: 26; radius: 0 color: prevHover.containsMouse ? root.cSurfaceHover : root.cSurface border.color: root.cBorder Text { anchors.centerIn: parent; text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cText } MouseArea { id: prevHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { const v = calendarScope.viewDate calendarScope.viewDate = new Date(v.getFullYear(), v.getMonth() - 1, 1) } } } // Today Button Rectangle { implicitWidth: 46; implicitHeight: 26; radius: 0 color: todayHover.containsMouse ? root.cSurfaceHover : root.cSurface border.color: root.cBorder Text { anchors.centerIn: parent; text: "Today"; font.family: root.mono; font.pixelSize: 10; color: root.cAccent } MouseArea { id: todayHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: calendarScope.viewDate = new Date() } } // Next Month Button Rectangle { width: 26; height: 26; radius: 0 color: nextHover.containsMouse ? root.cSurfaceHover : root.cSurface border.color: root.cBorder Text { anchors.centerIn: parent; text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cText } MouseArea { id: nextHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { const v = calendarScope.viewDate calendarScope.viewDate = new Date(v.getFullYear(), v.getMonth() + 1, 1) } } } } // Weekdays Header (Mon..Sun) RowLayout { Layout.fillWidth: true spacing: 2 Repeater { model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] delegate: Item { Layout.fillWidth: true height: 20 Text { anchors.centerIn: parent text: modelData font.family: root.mono; font.pixelSize: 11; font.bold: true color: (index >= 5) ? root.cDanger : root.cSubtle } } } } // 42-day Calendar Grid GridLayout { id: calendarGrid Layout.fillWidth: true Layout.fillHeight: true columns: 7 rows: 6 rowSpacing: 3 columnSpacing: 2 Repeater { model: 42 delegate: Rectangle { required property int index readonly property var cell: calendarScope.getCellData(index) readonly property string dayOffsetText: { if (cell.dayOffset > 0) return cell.dayOffset === 1 ? "In 1 day" : "In " + cell.dayOffset + " days" const daysAgo = Math.abs(cell.dayOffset) return daysAgo === 1 ? "1 day ago" : daysAgo + " days ago" } Layout.fillWidth: true Layout.fillHeight: true radius: 0 z: dayHover.containsMouse ? 2 : 0 color: cell.isToday ? root.cAccent : (dayHover.containsMouse ? root.cSurfaceHover : "transparent") border.color: (cell.isToday) ? root.cAccent : "transparent" Text { anchors.centerIn: parent text: parent.cell.day font.family: root.mono font.pixelSize: 11 font.bold: parent.cell.isToday color: { if (parent.cell.isToday) return root.cBar if (parent.cell.isWeekend) return root.cDanger if (parent.cell.isCurrentMonth) return root.cText return root.cDim } } Rectangle { visible: dayHover.containsMouse && !cell.isToday z: 3 anchors.bottom: parent.top anchors.bottomMargin: 4 anchors.horizontalCenter: parent.horizontalCenter implicitWidth: dayOffsetLabel.implicitWidth + 14 implicitHeight: 24 radius: 0 color: root.cSurface border.color: root.cBorder Text { id: dayOffsetLabel anchors.centerIn: parent text: parent.parent.dayOffsetText font.family: root.mono font.pixelSize: 10 color: root.cText } } MouseArea { id: dayHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor } } } } } } } } // ───────────── APPLETS POPUP WINDOW (NETWORK / BLUETOOTH / AUDIO) ───────────── PanelWindow { id: appletWindow visible: root.activeApplet !== "" && root.activeApplet !== "calendar" screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0] // Keep a clear 14px gutter from the vertical bar, anchored to bottom anchors { left: true; bottom: true } margins.left: root.barWidth + 14 margins.bottom: 8 implicitWidth: 360 implicitHeight: 460 color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "applet" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive aboveWindows: true exclusionMode: ExclusionMode.Ignore Rectangle { anchors.fill: parent color: root.cPanel border.color: root.cBorder border.width: 1 radius: 0 FocusScope { id: appletScope anchors.fill: parent focus: appletWindow.visible Keys.onEscapePressed: root.closeMenus() ColumnLayout { anchors.fill: parent anchors.margins: 16 spacing: 12 // Each bar control opens its own view; keep this header informational. RowLayout { Layout.fillWidth: true Text { text: root.activeApplet === "wifi" ? "NETWORK" : root.activeApplet === "bluetooth" ? "BLUETOOTH" : root.activeApplet === "battery" ? "BATTERY" : "AUDIO" font.family: root.mono font.pixelSize: 12 font.bold: true color: root.cBright } Item { Layout.fillWidth: true } } Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder } // Container for the 3 Applet Views Item { Layout.fillWidth: true Layout.fillHeight: true // ══════════════════ BATTERY VIEW ══════════════════ ColumnLayout { anchors.fill: parent visible: root.activeApplet === "battery" spacing: 12 Rectangle { Layout.fillWidth: true implicitHeight: 126 color: root.cSurface border.color: root.cBorder ColumnLayout { anchors.fill: parent anchors.margins: 14 spacing: 12 RowLayout { Layout.fillWidth: true spacing: 12 Text { text: root.batteryPercent < 0 ? "󰂑" : root.batteryPercent >= 95 ? "󰁹" : root.batteryPercent >= 85 ? "󰂂" : root.batteryPercent >= 75 ? "󰂁" : root.batteryPercent >= 65 ? "󰂀" : root.batteryPercent >= 55 ? "󰁿" : root.batteryPercent >= 45 ? "󰁾" : root.batteryPercent >= 35 ? "󰁽" : root.batteryPercent >= 25 ? "󰁼" : root.batteryPercent >= 15 ? "󰁻" : "󰂃" font.family: root.mono; font.pixelSize: 30 color: root.batteryChargeColor() } ColumnLayout { Layout.fillWidth: true spacing: 3 Text { text: root.batteryPercent >= 0 ? root.batteryPercent + "%" : "Unavailable" font.family: root.mono; font.pixelSize: 20; font.bold: true color: root.batteryChargeColor() } Text { text: root.onExternalPower ? "Connected to external power" : root.batteryStatus === "Discharging" ? "Discharging" : root.batteryStatus === "Full" ? "Fully charged" : root.batteryStatus font.family: root.mono; font.pixelSize: 10 color: root.batteryChargeColor() } } } Rectangle { Layout.fillWidth: true height: 4 color: root.cBorder Rectangle { width: parent.width * Math.max(0, Math.min(100, root.batteryPercent)) / 100 height: parent.height color: root.batteryChargeColor() } } } } Rectangle { Layout.fillWidth: true implicitHeight: 74 color: root.cSurface border.color: root.cBorder RowLayout { anchors.fill: parent anchors.margins: 14 spacing: 10 Text { text: root.onExternalPower ? "󰚥" : "󰔚" font.family: root.mono; font.pixelSize: 18 color: root.cSubtle } ColumnLayout { Layout.fillWidth: true spacing: 3 Text { text: root.batteryStatus === "Charging" ? "TIME TO FULL" : "TIME REMAINING" font.family: root.mono; font.pixelSize: 10; font.bold: true color: root.cSubtle } Text { text: root.batteryStatus === "Full" ? "Fully charged" : root.batteryTimeSeconds >= 0 ? root.formatBatteryTime(root.batteryTimeSeconds) : "Time estimate unavailable" font.family: root.mono; font.pixelSize: 13 color: root.cBright } } } } Rectangle { Layout.fillWidth: true implicitHeight: 92 color: root.cSurface border.color: root.cBorder ColumnLayout { anchors.fill: parent anchors.margins: 14 spacing: 8 RowLayout { Layout.fillWidth: true Text { text: "DISPLAY BRIGHTNESS" font.family: root.mono; font.pixelSize: 10; font.bold: true color: root.cSubtle } Item { Layout.fillWidth: true } Text { text: root.brightnessLevel >= 0 ? root.brightnessLevel + "%" : "Unavailable" font.family: root.mono; font.pixelSize: 10 color: root.cText } } Slider { id: brightnessSlider Layout.fillWidth: true enabled: root.brightnessLevel >= 0 from: 0; to: 100; stepSize: 1 value: Math.max(0, root.brightnessLevel) live: true onValueChanged: if (pressed) root.setScreenBrightness(value) background: Rectangle { x: brightnessSlider.leftPadding y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2 width: brightnessSlider.availableWidth; height: 3 color: root.cBorder Rectangle { width: brightnessSlider.visualPosition * parent.width height: parent.height color: brightnessSlider.enabled ? root.cAccent : root.cDim } } handle: Rectangle { x: brightnessSlider.leftPadding + brightnessSlider.visualPosition * (brightnessSlider.availableWidth - width) y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2 width: 10; height: 10 color: brightnessSlider.enabled ? root.cBright : root.cDim border.color: root.cBorderFocus; border.width: 1 } MouseArea { parent: brightnessSlider anchors.fill: brightnessSlider z: 99 preventStealing: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor function applyAt(x: real): void { const next = root.sliderValueFromPointer(brightnessSlider, x) root.setScreenBrightness(next) } onPressed: { brightnessSlider.forceActiveFocus() applyAt(mouse.x) } onPositionChanged: if (pressed) applyAt(mouse.x) } } } } Text { Layout.fillWidth: true text: "Time is estimated from the battery’s current charge and power rate." wrapMode: Text.WordWrap font.family: root.mono; font.pixelSize: 9 color: root.cDim } } // ══════════════════ NETWORK VIEW ══════════════════ ColumnLayout { anchors.fill: parent visible: root.activeApplet === "wifi" spacing: 12 // Status & Toggle Card Rectangle { Layout.fillWidth: true implicitHeight: 64 color: root.cSurface border.color: root.cBorder radius: 0 RowLayout { anchors.fill: parent anchors.margins: 12 spacing: 10 Text { text: root.netUp ? (root.activeSsid ? "" : "󰈀") : "󰤮" font.family: root.mono; font.pixelSize: 22 color: root.netUp ? root.cSuccess : root.cWarn } ColumnLayout { spacing: 2 Text { text: root.netUp ? (root.activeSsid ? root.activeSsid : "Wired Connection") : "Disconnected" font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright elide: Text.ElideRight } Text { text: root.ipAddress ? ("IP: " + root.ipAddress) : (root.wifiEnabled ? "Wi-Fi Enabled" : "Wi-Fi Disabled") font.family: root.mono; font.pixelSize: 10; color: root.cSubtle } } Item { Layout.fillWidth: true } // Wi-Fi Toggle Pill Rectangle { implicitWidth: 36; implicitHeight: 20; radius: 0 color: root.wifiEnabled ? root.cAccent : root.cSurfaceHover border.color: root.wifiEnabled ? root.cAccent : root.cBorder border.width: 1 Rectangle { x: root.wifiEnabled ? 18 : 2; y: 2; width: 16; height: 16; radius: 0 color: root.wifiEnabled ? root.cBar : root.cDim Behavior on x { NumberAnimation { duration: 150 } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { const newState = !root.wifiEnabled root.wifiEnabled = newState Quickshell.execDetached(["nmcli", "radio", "wifi", newState ? "on" : "off"]) if (newState) wifiScanProc.running = true } } } } } // Keep the network action in the panel; no terminal launcher. RowLayout { Layout.fillWidth: true spacing: 0 Rectangle { Layout.fillWidth: true; implicitHeight: 28; radius: 0 color: rescanHover.containsMouse ? root.cSurfaceHover : root.cSurface border.color: root.cBorder RowLayout { anchors.centerIn: parent; spacing: 6 Text { text: ""; font.family: root.mono; font.pixelSize: 11; color: root.cAccent } Text { text: "Rescan"; font.family: root.mono; font.pixelSize: 11; color: root.cText } } MouseArea { id: rescanHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: wifiScanProc.running = true } } } // Available Networks Header Text { text: "AVAILABLE NETWORKS" font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle Layout.topMargin: 2 } // Wi-Fi Networks List ListView { Layout.fillWidth: true Layout.fillHeight: true clip: true spacing: 4 model: root.wifiNetworks delegate: Rectangle { width: ListView.view.width height: 36 radius: 0 color: modelData.active ? root.cSelected : (wifiItemHover.containsMouse ? root.cSurfaceHover : root.cSurface) border.color: modelData.active ? root.cAccent : "transparent" RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 10 Text { text: "" font.family: root.mono; font.pixelSize: 12 color: modelData.active ? root.cAccent : (modelData.signal > 50 ? root.cText : root.cDim) } Text { text: modelData.ssid font.family: root.mono; font.pixelSize: 12 color: modelData.active ? root.cBright : root.cText Layout.fillWidth: true elide: Text.ElideRight } // Security lock Text { visible: modelData.security && !modelData.security.includes("Open") text: "" font.family: root.mono; font.pixelSize: 10; color: root.cDim } // Signal percentage Text { text: modelData.signal + "%" font.family: root.mono; font.pixelSize: 10; color: root.cSubtle } // Active badge Rectangle { visible: modelData.active width: 8; height: 8; radius: 0 color: root.cSuccess } } MouseArea { id: wifiItemHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (modelData.active) { Quickshell.execDetached(["nmcli", "connection", "down", "id", modelData.ssid]) } else { Quickshell.execDetached(["foot", "-e", "nmtui-connect", modelData.ssid]) } root.activeApplet = "" } } } // Empty state Item { anchors.centerIn: parent visible: root.wifiNetworks.length === 0 ColumnLayout { anchors.centerIn: parent spacing: 4 Text { Layout.alignment: Qt.AlignHCenter; text: root.wifiEnabled ? "Scanning for networks..." : "Wi-Fi is currently disabled"; font.family: root.mono; font.pixelSize: 12; color: root.cDim } } } } } // ══════════════════ BLUETOOTH VIEW ══════════════════ ColumnLayout { anchors.fill: parent visible: root.activeApplet === "bluetooth" spacing: 12 // Status & Toggle Card Rectangle { Layout.fillWidth: true implicitHeight: 64 color: root.cSurface border.color: root.cBorder radius: 0 RowLayout { anchors.fill: parent anchors.margins: 12 spacing: 10 Text { text: root.btConnected ? "󰂱" : (root.btOn ? "" : "󰂲") font.family: root.mono; font.pixelSize: 22 color: root.btOn ? (root.btConnected ? root.cSuccess : root.cAccent) : root.cDim } ColumnLayout { spacing: 2 Text { text: root.btOn ? (root.btConnected ? "Device Connected" : "Bluetooth Enabled") : "Bluetooth Disabled" font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright } Text { text: root.btActionMessage || (root.btDevices.length > 0 ? (root.btDevices.length + " known devices") : "No devices") font.family: root.mono; font.pixelSize: 10; color: root.cSubtle elide: Text.ElideRight Layout.maximumWidth: 190 } } Item { Layout.fillWidth: true } // Bluetooth Power Toggle Pill Rectangle { implicitWidth: 36; implicitHeight: 20; radius: 0 color: root.btOn ? root.cAccent : root.cSurfaceHover border.color: root.btOn ? root.cAccent : root.cBorder border.width: 1 Rectangle { x: root.btOn ? 18 : 2; y: 2; width: 16; height: 16; radius: 0 color: root.btOn ? root.cBar : root.cDim Behavior on x { NumberAnimation { duration: 150 } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { const newState = !root.btOn root.btOn = newState if (!newState && root.btScanning) { btScanTimer.stop() root.btScanning = false root.runBluetoothCommand("scan off") } if (Bluetooth.defaultAdapter) Bluetooth.defaultAdapter.enabled = newState statusProc.running = true } } } } } // Start a bounded discovery from this panel. Pairing and // connect actions remain here, without opening a terminal. Rectangle { Layout.fillWidth: true implicitHeight: 30 radius: 0 color: btScanHover.containsMouse ? root.cSurfaceHover : root.cSurface border.color: root.cBorder RowLayout { anchors.centerIn: parent spacing: 7 Text { text: root.btScanning ? "◌" : (root.btOn ? "" : "") font.family: root.mono; font.pixelSize: 11 color: root.cAccent } Text { text: root.btScanning ? "Scanning nearby devices…" : (root.btOn ? "Scan nearby devices" : "Enable Bluetooth") font.family: root.mono; font.pixelSize: 11 color: root.cText } } MouseArea { id: btScanHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: !root.btScanning onClicked: root.startBluetoothScan() } } // Paired Devices Header Text { text: "NEARBY & PAIRED DEVICES" font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle Layout.topMargin: 2 } // Bluetooth Devices List ListView { Layout.fillWidth: true Layout.fillHeight: true clip: true spacing: 4 model: root.btDevices delegate: Rectangle { width: ListView.view.width height: 50 radius: 0 color: modelData.connected ? root.cSelected : root.cSurface border.color: modelData.connected ? root.cBorderFocus : "transparent" RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 10 Text { text: "" font.family: root.mono; font.pixelSize: 14 color: modelData.connected ? root.cSuccess : root.cSubtle } ColumnLayout { Layout.fillWidth: true; spacing: 1 Text { text: modelData.name font.family: root.mono; font.pixelSize: 12 color: modelData.connected ? root.cBright : root.cText elide: Text.ElideRight } Text { text: modelData.mac font.family: root.mono; font.pixelSize: 9 color: root.cDim } } // Connect / Disconnect Action Button Rectangle { implicitWidth: modelData.connected ? 78 : (modelData.paired ? 62 : 48) implicitHeight: 22 radius: 0 color: modelData.connected ? root.cDanger : (btActionHover.containsMouse ? root.cSelected : root.cSurfaceHover) border.color: root.cBorder Text { anchors.centerIn: parent text: root.btActionPending && root.btActionAddress === modelData.mac ? "Working…" : (modelData.connected ? "Disconnect" : (modelData.paired ? "Connect" : "Pair")) font.family: root.mono; font.pixelSize: 9 color: modelData.connected ? root.cBright : root.cText } MouseArea { id: btActionHover anchors.fill: parent hoverEnabled: true enabled: !root.btActionPending cursorShape: Qt.PointingHandCursor onClicked: { const action = modelData.connected ? "disconnect" : (modelData.paired ? "connect" : "pair") root.runBluetoothAction(action, modelData.mac) } } } } } // Empty state Item { anchors.centerIn: parent visible: root.btDevices.length === 0 ColumnLayout { anchors.centerIn: parent spacing: 4 Text { Layout.alignment: Qt.AlignHCenter text: root.btOn ? (root.btScanning ? "Looking for nearby devices…" : "No devices found yet") : "Bluetooth is powered off" font.family: root.mono; font.pixelSize: 12; color: root.cDim } Text { Layout.alignment: Qt.AlignHCenter text: "Scan nearby devices, then choose Pair or Connect" font.family: root.mono; font.pixelSize: 10; color: root.cSubtle visible: root.btOn && !root.btScanning } } } } } // ══════════════════ AUDIO VIEW ══════════════════ ScrollView { anchors.fill: parent contentWidth: availableWidth clip: true visible: root.activeApplet === "sound" ColumnLayout { width: parent.width spacing: 10 RowLayout { Layout.fillWidth: true Text { text: "OUTPUT" font.family: root.mono; font.pixelSize: 10; font.bold: true color: root.cSubtle } Item { Layout.fillWidth: true } Text { text: Pipewire.defaultAudioSink?.description || Pipewire.defaultAudioSink?.name || "No output device" font.family: root.mono; font.pixelSize: 10 color: root.cDim elide: Text.ElideRight Layout.maximumWidth: 190 } } RowLayout { Layout.fillWidth: true spacing: 10 Text { text: Pipewire.defaultAudioSink?.audio?.muted ? "󰝟" : "" font.family: root.mono; font.pixelSize: 15 color: Pipewire.defaultAudioSink?.audio?.muted ? root.cDanger : root.cText } Slider { id: masterVolSlider Layout.fillWidth: true from: 0; to: 1.0; stepSize: 0.01 value: Pipewire.defaultAudioSink?.audio?.volume ?? 0 live: true onValueChanged: if (pressed) root.setOutputVolume(value) background: Rectangle { x: masterVolSlider.leftPadding y: masterVolSlider.topPadding + masterVolSlider.availableHeight / 2 - height / 2 width: masterVolSlider.availableWidth; height: 3 color: root.cBorder Rectangle { width: masterVolSlider.visualPosition * parent.width; height: parent.height color: Pipewire.defaultAudioSink?.audio?.muted ? root.cDim : root.cAccent } } handle: Rectangle { x: masterVolSlider.leftPadding + masterVolSlider.visualPosition * (masterVolSlider.availableWidth - width) y: masterVolSlider.topPadding + masterVolSlider.availableHeight / 2 - height / 2 width: 10; height: 10; color: root.cBright border.color: root.cBorderFocus; border.width: 1 } MouseArea { parent: masterVolSlider anchors.fill: masterVolSlider z: 99 preventStealing: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor function applyAt(x: real): void { root.setOutputVolume(root.sliderValueFromPointer(masterVolSlider, x)) } onPressed: { masterVolSlider.forceActiveFocus() applyAt(mouse.x) } onPositionChanged: if (pressed) applyAt(mouse.x) } } Text { text: Pipewire.defaultAudioSink?.audio?.muted ? "Muted" : Math.round((Pipewire.defaultAudioSink?.audio?.volume ?? 0) * 100) + "%" font.family: root.mono; font.pixelSize: 10; color: root.cText horizontalAlignment: Text.AlignRight Layout.preferredWidth: 38 } Rectangle { implicitWidth: 30; implicitHeight: 28 color: muteHover.containsMouse ? root.cSelected : "transparent" border.color: muteHover.containsMouse ? root.cBorderFocus : root.cBorder Text { anchors.centerIn: parent text: Pipewire.defaultAudioSink?.audio?.muted ? "󰝟" : "󰕾" font.family: root.mono; font.pixelSize: 12; color: root.cText } MouseArea { id: muteHover anchors.fill: parent; hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (Pipewire.defaultAudioSink?.audio) Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted root.showVolumeOsd() } } } } Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder } Text { text: "PLAYING NOW" font.family: root.mono; font.pixelSize: 10; font.bold: true color: root.cSubtle } Repeater { model: Pipewire.nodes.values.filter(n => root.isPlaybackNode(n)) delegate: RowLayout { required property var modelData PwObjectTracker { objects: [modelData] } Layout.fillWidth: true spacing: 8 ColumnLayout { Layout.fillWidth: true spacing: 2 Text { text: modelData.properties["application.name"] || modelData.description || modelData.name || "Audio stream" font.family: root.mono; font.pixelSize: 10 color: root.cText elide: Text.ElideRight Layout.fillWidth: true } Text { text: modelData.properties["media.title"] ? modelData.properties["media.title"] : (modelData.properties["media.name"] || "Active playback") font.family: root.mono; font.pixelSize: 9 color: root.cDim elide: Text.ElideRight Layout.fillWidth: true } } Slider { id: playbackSlider Layout.preferredWidth: 82 from: 0; to: 1.0; stepSize: 0.01 value: modelData.audio?.volume ?? 0 live: true onValueChanged: if (pressed && modelData.audio) modelData.audio.volume = value background: Rectangle { x: playbackSlider.leftPadding y: playbackSlider.topPadding + playbackSlider.availableHeight / 2 - height / 2 width: playbackSlider.availableWidth; height: 2; color: root.cBorder Rectangle { width: playbackSlider.visualPosition * parent.width height: parent.height color: root.cAccent } } handle: Rectangle { x: playbackSlider.leftPadding + playbackSlider.visualPosition * (playbackSlider.availableWidth - width) y: playbackSlider.topPadding + playbackSlider.availableHeight / 2 - height / 2 width: 8; height: 8; color: root.cText } MouseArea { parent: playbackSlider anchors.fill: playbackSlider z: 99 preventStealing: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor function applyAt(x: real): void { if (!modelData.audio) return modelData.audio.volume = root.sliderValueFromPointer(playbackSlider, x) } onPressed: { playbackSlider.forceActiveFocus() applyAt(mouse.x) } onPositionChanged: if (pressed) applyAt(mouse.x) } } Text { text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%" font.family: root.mono; font.pixelSize: 9 color: root.cSubtle Layout.preferredWidth: 30 horizontalAlignment: Text.AlignRight } Rectangle { implicitWidth: 26; implicitHeight: 24 color: streamMuteHover.containsMouse ? root.cSelected : "transparent" border.color: streamMuteHover.containsMouse ? root.cBorderFocus : "transparent" Text { anchors.centerIn: parent text: modelData.audio?.muted ? "󰝟" : "󰕾" font.family: root.mono; font.pixelSize: 11 color: modelData.audio?.muted ? root.cDanger : root.cText } MouseArea { id: streamMuteHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: if (modelData.audio) modelData.audio.muted = !modelData.audio.muted } } } } Text { text: "No active playback" visible: Pipewire.nodes.values.filter(n => root.isPlaybackNode(n)).length === 0 font.family: root.mono; font.pixelSize: 10 color: root.cDim } Text { text: "AVAILABLE OUTPUTS" font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle } Repeater { model: Pipewire.nodes.values.filter(n => n.type === "Audio/Sink") delegate: ColumnLayout { required property var modelData PwObjectTracker { objects: [modelData] } readonly property bool isDefault: Pipewire.defaultAudioSink?.id === modelData.id Layout.fillWidth: true spacing: 3 RowLayout { Layout.fillWidth: true Text { text: isDefault ? "●" : "○" font.family: root.mono; font.pixelSize: 10 color: isDefault ? root.cAccent : root.cDim } Text { text: modelData.description || modelData.name || "Output device" font.family: root.mono; font.pixelSize: 10 color: isDefault ? root.cBright : root.cText Layout.fillWidth: true; elide: Text.ElideRight } Text { text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%" font.family: root.mono; font.pixelSize: 9; color: root.cSubtle } MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor onClicked: Pipewire.preferredDefaultAudioSink = modelData } } Slider { id: sinkSlider Layout.fillWidth: true from: 0; to: 1.0; stepSize: 0.01 value: modelData.audio?.volume ?? 0 live: true onValueChanged: if (pressed && modelData.audio) modelData.audio.volume = value background: Rectangle { x: sinkSlider.leftPadding y: sinkSlider.topPadding + sinkSlider.availableHeight / 2 - height / 2 width: sinkSlider.availableWidth; height: 2; color: root.cBorder Rectangle { width: sinkSlider.visualPosition * parent.width; height: parent.height; color: isDefault ? root.cAccent : root.cDim } } handle: Rectangle { x: sinkSlider.leftPadding + sinkSlider.visualPosition * (sinkSlider.availableWidth - width) y: sinkSlider.topPadding + sinkSlider.availableHeight / 2 - height / 2 width: 8; height: 8; color: root.cText } MouseArea { parent: sinkSlider anchors.fill: sinkSlider z: 99 preventStealing: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor function applyAt(x: real): void { if (!modelData.audio) return modelData.audio.volume = root.sliderValueFromPointer(sinkSlider, x) } onPressed: { sinkSlider.forceActiveFocus() applyAt(mouse.x) } onPositionChanged: if (pressed) applyAt(mouse.x) } } } } Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder; Layout.topMargin: 2 } Text { text: "INPUTS" font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle } Repeater { model: Pipewire.nodes.values.filter(n => n.type === "Audio/Source" && !n.description.includes("Monitor")) delegate: ColumnLayout { required property var modelData PwObjectTracker { objects: [modelData] } Layout.fillWidth: true spacing: 3 RowLayout { Layout.fillWidth: true Text { text: ""; font.family: root.mono; font.pixelSize: 10; color: root.cSubtle } Text { text: modelData.description || modelData.name || "Microphone" font.family: root.mono; font.pixelSize: 10; color: root.cText Layout.fillWidth: true; elide: Text.ElideRight } Text { text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%" font.family: root.mono; font.pixelSize: 9; color: root.cSubtle } } Slider { id: srcSlider Layout.fillWidth: true from: 0; to: 1.0; stepSize: 0.01 value: modelData.audio?.volume ?? 0 live: true onValueChanged: if (pressed && modelData.audio) modelData.audio.volume = value background: Rectangle { x: srcSlider.leftPadding y: srcSlider.topPadding + srcSlider.availableHeight / 2 - height / 2 width: srcSlider.availableWidth; height: 2; color: root.cBorder Rectangle { width: srcSlider.visualPosition * parent.width; height: parent.height; color: root.cTeal } } handle: Rectangle { x: srcSlider.leftPadding + srcSlider.visualPosition * (srcSlider.availableWidth - width) y: srcSlider.topPadding + srcSlider.availableHeight / 2 - height / 2 width: 8; height: 8; color: root.cText } MouseArea { parent: srcSlider anchors.fill: srcSlider z: 99 preventStealing: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor function applyAt(x: real): void { if (!modelData.audio) return modelData.audio.volume = root.sliderValueFromPointer(srcSlider, x) } onPressed: { srcSlider.forceActiveFocus() applyAt(mouse.x) } onPositionChanged: if (pressed) applyAt(mouse.x) } } } } } } } } } } } // ───────────── VOLUME OSD ───────────── PanelWindow { id: volumeOsdWindow visible: root.volumeOsdVisible screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0] anchors { left: true; right: true; bottom: true } implicitHeight: 78 color: "transparent" mask: Region {} WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "volume-osd" WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore aboveWindows: true Rectangle { id: volumeOsdCard width: 300 height: 58 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 18 color: root.cPanel border.color: root.cBorder border.width: 1 radius: 0 ColumnLayout { anchors.fill: parent anchors.leftMargin: 14 anchors.rightMargin: 14 anchors.topMargin: 9 anchors.bottomMargin: 9 spacing: 7 RowLayout { Layout.fillWidth: true Text { text: root.osdKind === "brightness" ? "󰃠" : root.osdMuted ? "󰝟" : (root.osdVolume > 0.5 ? "" : (root.osdVolume > 0 ? "" : "")) font.family: root.mono font.pixelSize: 13 color: root.osdKind === "brightness" ? root.cBright : (root.osdMuted ? root.cSubtle : root.cBright) } Text { text: root.osdKind === "brightness" ? "Brightness" : (root.osdMuted ? "Muted" : "Volume") font.family: root.mono font.pixelSize: 10 color: root.cText } Item { Layout.fillWidth: true } Text { text: root.osdKind === "brightness" ? (root.brightnessLevel >= 0 ? root.brightnessLevel + "%" : "--") : root.osdMuted ? "—" : Math.round(root.osdVolume * 100) + "%" font.family: root.mono font.pixelSize: 10 color: root.cSubtle } } Rectangle { Layout.fillWidth: true height: 3 color: root.cBorder Rectangle { width: root.osdKind === "brightness" ? parent.width * Math.max(0, root.brightnessLevel) / 100 : root.osdMuted ? 0 : parent.width * root.osdVolume height: parent.height color: root.osdKind === "brightness" ? root.cBright : (root.osdMuted ? root.cDim : root.cBright) Behavior on width { NumberAnimation { duration: 100 } } } } } } } // ───────────── POWER MENU ───────────── PanelWindow { id: powerWindow visible: root.powerMenuOpen screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0] anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "power-menu" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore MouseArea { anchors.fill: parent onClicked: root.closeMenus() } Rectangle { anchors.centerIn: parent width: 330 height: root.powerConfirmAction === "" ? 310 : 190 color: root.cPanel border.color: root.cBorder border.width: 1 radius: 0 MouseArea { anchors.fill: parent } FocusScope { anchors.fill: parent focus: powerWindow.visible Keys.onEscapePressed: { if (root.powerConfirmAction !== "") root.powerConfirmAction = "" else root.closeMenus() } ColumnLayout { anchors.fill: parent anchors.margins: 18 spacing: 8 Text { text: root.powerConfirmAction === "" ? "POWER" : "CONFIRM ACTION" font.family: root.mono font.pixelSize: 11 font.bold: true color: root.cBright } Text { Layout.fillWidth: true text: root.powerConfirmAction === "" ? "Choose what to do with this session." : (root.powerConfirmAction === "reboot" ? "Restart the computer now?" : "Shut down the computer now?") font.family: root.mono font.pixelSize: 10 color: root.cSubtle wrapMode: Text.WordWrap } Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 4 visible: root.powerConfirmAction === "" Repeater { model: [ { label: "Log out", icon: "󰍃", action: "logout", confirm: false }, { label: "Restart…", icon: "󰜉", action: "reboot", confirm: true }, { label: "Shut down…", icon: "", action: "poweroff", confirm: true }, ] delegate: Rectangle { required property var modelData Layout.fillWidth: true implicitHeight: 42 color: powerActionHover.containsMouse ? root.cSurfaceHover : "transparent" border.color: powerActionHover.containsMouse ? root.cBorderFocus : "transparent" border.width: 1 RowLayout { anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 spacing: 12 Text { text: modelData.icon font.family: root.mono font.pixelSize: 14 color: root.cSubtle } Text { Layout.fillWidth: true text: modelData.label font.family: root.mono font.pixelSize: 11 color: root.cText } Text { text: modelData.confirm ? "›" : "" font.family: root.mono font.pixelSize: 14 color: root.cSubtle } } MouseArea { id: powerActionHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (modelData.confirm) root.powerConfirmAction = modelData.action else root.runPowerAction(modelData.action) } } } } } RowLayout { Layout.fillWidth: true visible: root.powerConfirmAction !== "" Item { Layout.fillWidth: true } Rectangle { implicitWidth: 88 implicitHeight: 34 color: cancelPowerHover.containsMouse ? root.cSurfaceHover : "transparent" border.color: root.cBorder border.width: 1 Text { anchors.centerIn: parent; text: "Cancel"; font.family: root.mono; font.pixelSize: 10; color: root.cText } MouseArea { id: cancelPowerHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.powerConfirmAction = "" } } Rectangle { implicitWidth: 104 implicitHeight: 34 color: confirmPowerHover.containsMouse ? root.cSurfaceHover : root.cSelected border.color: root.cBorderFocus border.width: 1 Text { anchors.centerIn: parent text: root.powerConfirmAction === "reboot" ? "Restart" : "Shut down" font.family: root.mono font.pixelSize: 10 color: root.cBright } MouseArea { id: confirmPowerHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.runPowerAction(root.powerConfirmAction) } } } } } } } }