Files
dotfiles/.config/quickshell/backups/shell.qml-right-click-debug-before-20261004_204350.bak
T
2026-10-15 21:17:12 +00:00

2716 lines
130 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}
}
}
}
}
}