Files
2026-10-15 21:17:12 +00:00

4223 lines
213 KiB
QML
Raw Permalink 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 wallpaperPickerOpen: false
property string activeWallpaper: "/home/wfalkowski/Pictures/Wallpapers/wallhaven-0j258m.jpg"
property string previousWallpaper: ""
property var wallpaperCacheKeys: ({})
property var wallpapers: []
property bool powerMenuOpen: false
property bool trayExpanded: false
property bool trayMenuOpen: false
property var trayMenuItem: null
property var trayMenuHandle: null
property var trayMenuStack: []
property var trayMenuScreen: null
property real trayMenuY: 0
property string powerConfirmAction: ""
property string activeApplet: "" // "wifi", "bluetooth", "sound", "calendar", or ""
function closeMenus(): void {
root.launcherOpen = false
root.wallpaperPickerOpen = false
root.activeApplet = ""
root.powerMenuOpen = false
root.powerConfirmAction = ""
root.trayMenuOpen = false
root.trayMenuItem = null
root.trayMenuHandle = null
root.trayMenuStack = []
}
function setWallpaper(path: string): void {
if (path === root.activeWallpaper) {
root.wallpaperPickerOpen = false
return
}
root.previousWallpaper = root.activeWallpaper
root.activeWallpaper = path
wallpaperProc.command = ["/home/wfalkowski/.local/bin/wallpaper-set", path]
wallpaperProc.running = true
root.wallpaperPickerOpen = false
}
function refreshWallpaperCache(): void {
if (wallpaperListProc.running) return
wallpaperListProc.running = true
}
function wallpaperLabel(path: string): string {
const fileName = path.split("/").pop()
const labels = {
"car-blue-ridge.jpg": "Blue Ridge",
"car-night-drive.jpg": "Night Drive",
"car-sequoia-sunset.jpg": "Sequoia Sunset"
}
if (labels[fileName]) return labels[fileName]
return fileName.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ").replace(/\b\w/g, letter => letter.toUpperCase())
}
function openTrayMenu(item: var, targetScreen: var, y: real): void {
root.closeMenus()
root.trayMenuItem = item
root.trayMenuHandle = item.menu
root.trayMenuScreen = targetScreen
root.trayMenuY = y
root.trayMenuOpen = true
}
function enterTraySubmenu(entry: var): void {
if (!entry.hasChildren) return
root.trayMenuStack = root.trayMenuStack.concat([root.trayMenuHandle])
root.trayMenuHandle = entry
}
function returnFromTraySubmenu(): void {
if (root.trayMenuStack.length === 0) return
root.trayMenuHandle = root.trayMenuStack[root.trayMenuStack.length - 1]
root.trayMenuStack = root.trayMenuStack.slice(0, -1)
}
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"
volumeOsdTimer.interval = 1600
root.osdVolume = audio?.volume ?? 0
root.osdMuted = audio?.muted ?? false
root.volumeOsdVisible = true
volumeOsdTimer.restart()
}
function showBrightnessOsd(): void {
root.osdKind = "brightness"
volumeOsdTimer.interval = 1600
root.volumeOsdVisible = true
volumeOsdTimer.restart()
}
function showAppleMusicOsd(player: var): void {
root.osdKind = "track"
root.osdTrackTitle = player.title || "Unknown track"
root.osdTrackArtist = player.artist || "Apple Music"
root.osdTrackArtUrl = player.artUrl || ""
root.volumeOsdVisible = true
volumeOsdTimer.interval = 3500
volumeOsdTimer.restart()
}
function setScreenBrightness(value: real): void {
root.brightnessLevel = Math.max(0, Math.min(100, Math.round(value)))
root.showBrightnessOsd()
brightnessSetTimer.restart()
brightnessReadTimer.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 setInputVolume(value: real): void {
const audio = Pipewire.defaultAudioSource?.audio
if (!audio) return
audio.volume = Math.max(0, Math.min(1, value))
if (audio.muted) audio.muted = false
}
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 bool ethernetConnected: false
property string ipAddress: ""
property var wifiNetworks: []
property bool wireguardAvailable: false
property bool wireguardEnabled: false
property string wireguardProfile: ""
property string wireguardInterface: ""
property string wireguardAutoTarget: ""
property bool simPresent: false
property bool simEnabled: true
property string simOperator: ""
property string simAccessMode: ""
property string simIpAddress: ""
property int simSignalPercent: -1
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 string osdTrackTitle: ""
property string osdTrackArtist: ""
property string osdTrackArtUrl: ""
property string appleMusicTrackKey: ""
property var pulsePlaybackStreams: []
property var mprisPlayers: []
property var pulseOutputs: []
property var pulseInputs: []
property bool outputDevicesExpanded: false
property bool inputDevicesExpanded: false
property int batteryPercent: -1
property string batteryStatus: "Unknown"
property int batteryTimeSeconds: -1
property string systemLoad: "—"
property bool nightLightEnabled: false
property bool onExternalPower: false
property int brightnessLevel: -1
// Bind every node before inspecting its media.class. Application streams
// such as Firefox may not expose their type and properties until bound.
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
.concat(Pipewire.nodes.values)
}
function refreshPlaybackStreams(): void {
if (!playbackStreamsProc.running)
playbackStreamsProc.running = true
if (!mprisPlayersProc.running)
mprisPlayersProc.running = true
if (!pulseOutputsProc.running)
pulseOutputsProc.running = true
if (!pulseInputsProc.running)
pulseInputsProc.running = true
}
function updatePlaybackMetadata(): void {
const firefoxStreams = root.pulsePlaybackStreams.filter(stream =>
(stream.application || "").toLowerCase().includes("firefox"))
root.pulsePlaybackStreams = root.pulsePlaybackStreams.map(stream => {
const app = (stream.application || "").toLowerCase()
let candidates = root.mprisPlayers.filter(player =>
player.name.toLowerCase().includes(app.includes("firefox") ? "firefox" : app))
let matched = null
if (app.includes("firefox")) {
const pid = String(stream.pid || "")
const pidMatches = pid ? candidates.filter(player => player.name.endsWith("_" + pid)) : []
if (pidMatches.length === 1)
matched = pidMatches[0]
else if (candidates.length === 1 && firefoxStreams.length === 1)
matched = candidates[0]
} else if (candidates.length === 1) {
matched = candidates[0]
}
const appleMusic = matched && matched.url.toLowerCase().includes("music.apple.com")
return Object.assign({}, stream, {
player: matched ? matched.name : "",
applicationLabel: appleMusic ? "Apple Music" : stream.application,
title: matched && matched.title ? matched.title : stream.media,
artist: matched ? matched.artist : "",
artUrl: matched ? matched.artUrl : ""
})
})
}
function setPulseDeviceVolume(kind: string, name: string, value: real): void {
Quickshell.execDetached(["pactl", "set-" + kind + "-volume", name, Math.round(value * 100) + "%"])
}
function togglePulseDeviceMute(kind: string, name: string, muted: bool): void {
Quickshell.execDetached(["pactl", "set-" + kind + "-mute", name, muted ? "1" : "0"])
}
Timer {
interval: 2500
repeat: true
running: root.activeApplet === "sound"
onTriggered: root.refreshPlaybackStreams()
onRunningChanged: if (running) root.refreshPlaybackStreams()
}
Process {
id: playbackStreamsProc
command: ["pactl", "-f", "json", "list", "sink-inputs"]
stdout: StdioCollector {
onStreamFinished: {
try {
const inputs = JSON.parse(text)
root.pulsePlaybackStreams = inputs
.filter(input => !input.corked)
.map(input => {
const properties = input.properties || {}
const media = properties["media.title"] || properties["media.name"]
return {
application: properties["application.name"] || properties["node.name"] || "Audio stream",
pid: properties["application.process.id"] || "",
media: media && media !== "(null)" && media !== "null" ? media : ""
}
})
root.updatePlaybackMetadata()
} catch (error) {
console.warn("Unable to read active audio streams:", error)
}
}
}
}
Process {
id: mprisPlayersProc
command: ["playerctl", "--all-players", "metadata", "--format", "{{playerName}}|{{xesam:artist}}|{{xesam:title}}|{{xesam:url}}|{{mpris:artUrl}}"]
stdout: StdioCollector {
onStreamFinished: {
const entries = []
for (const line of text.split("\n")) {
const fields = line.split("|")
if (fields.length < 5) continue
entries.push({ name: fields[0], artist: fields[1], title: fields[2], url: fields[3], artUrl: fields.slice(4).join("|") })
}
const appleMusic = entries.find(player => player.url.toLowerCase().includes("music.apple.com") && player.title)
if (appleMusic) {
const key = [appleMusic.name, appleMusic.url, appleMusic.artist, appleMusic.title].join("|")
if (root.appleMusicTrackKey && root.appleMusicTrackKey !== key)
root.showAppleMusicOsd(appleMusic)
root.appleMusicTrackKey = key
} else {
root.appleMusicTrackKey = ""
}
root.mprisPlayers = entries
root.updatePlaybackMetadata()
}
}
}
Process {
id: pulseOutputsProc
command: ["pactl", "-f", "json", "list", "sinks"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.pulseOutputs = JSON.parse(text).map(device => {
const channels = Object.values(device.volume || {})
return {
name: device.name,
description: device.description || device.name,
volume: channels.length ? Math.max(0, Math.min(1, parseInt(channels[0].value_percent) / 100)) : 0,
muted: device.mute
}
})
} catch (error) {
console.warn("Unable to read audio outputs:", error)
}
}
}
}
Process {
id: pulseInputsProc
command: ["pactl", "-f", "json", "list", "sources"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.pulseInputs = JSON.parse(text)
.filter(device => !(device.name || "").endsWith(".monitor") && (device.properties || {})["device.class"] !== "monitor")
.map(device => {
const channels = Object.values(device.volume || {})
return {
name: device.name,
description: device.description || device.name,
volume: channels.length ? Math.max(0, Math.min(1, parseInt(channels[0].value_percent) / 100)) : 0,
muted: device.mute
}
})
} catch (error) {
console.warn("Unable to read audio inputs:", error)
}
}
}
}
IpcHandler {
target: "launcher"
function toggle(): void { root.launcherOpen = !root.launcherOpen }
function open(): void { root.launcherOpen = true }
function close(): void { root.launcherOpen = false }
}
IpcHandler {
target: "wallpapers"
function toggle(): void { root.wallpaperPickerOpen = !root.wallpaperPickerOpen }
function open(): void { root.wallpaperPickerOpen = true }
function close(): void { root.wallpaperPickerOpen = false }
function previous(): void {
if (root.previousWallpaper !== "") root.setWallpaper(root.previousWallpaper)
}
}
Process { id: wallpaperProc; command: ["true"] }
Process {
id: wallpaperListProc
command: ["find", "/home/wfalkowski/Pictures/Wallpapers", "-maxdepth", "1", "-type", "f"]
stdout: StdioCollector {
onStreamFinished: {
const paths = text.trim().split("\n").filter(path => /\.(jpe?g|png|webp|avif|bmp|tiff?)$/i.test(path)).sort()
root.wallpapers = paths.map(path => ({ name: root.wallpaperLabel(path), path: path }))
const keys = {}
for (const path of paths) {
if (Object.prototype.hasOwnProperty.call(root.wallpaperCacheKeys, path))
keys[path] = root.wallpaperCacheKeys[path]
}
root.wallpaperCacheKeys = keys
if (paths.length === 0) return
wallpaperStatProc.command = ["stat", "-c", "%n|%y|%s", ...paths]
wallpaperStatProc.running = true
}
}
}
Process {
id: wallpaperStatProc
command: ["true"]
stdout: StdioCollector {
onStreamFinished: {
const keys = Object.assign({}, root.wallpaperCacheKeys)
for (const line of text.trim().split("\n")) {
const separator = line.indexOf("|")
if (separator < 0) continue
const path = line.slice(0, separator)
keys[path] = line.slice(separator + 1)
}
root.wallpaperCacheKeys = keys
}
}
}
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': '',
'ethernet_connected': False,
'ip': '',
'wireguard_available': False,
'wireguard_enabled': False,
'wireguard_profile': '',
'wireguard_interface': '',
'sim_present': False,
'sim_enabled': False,
'sim_operator': '',
'sim_mode': '',
'sim_ip': '',
'sim_signal': -1,
'bt_on': False,
'bt_connected': False,
'bt_devices': []
}
def nm_fields(line):
fields = []
value = ''
escaped = False
for char in line:
if escaped:
value += char
escaped = False
elif char == '\\\\':
escaped = True
elif char == ':':
fields.append(value)
value = ''
else:
value += char
if escaped:
value += '\\\\'
fields.append(value)
return fields
# 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 = nm_fields(line)
if len(parts) >= 2:
connection_type = parts[1].lower()
if 'wireless' in connection_type and not data['active_ssid']:
data['active_ssid'] = parts[0]
ssid = subprocess.run(['nmcli', '-g', '802-11-wireless.ssid', 'connection', 'show', 'id', parts[0]], capture_output=True, text=True, timeout=2).stdout.strip()
if ssid:
data['active_ssid'] = ssid
elif 'ethernet' in connection_type:
data['ethernet_connected'] = True
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
# Select the WireGuard config for this network and read active wg-quick interfaces.
data['wireguard_available'] = True
data['wireguard_profile'] = 'int' if data['active_ssid'] in ('TCH5298221-5G', 'TCH5298221') else 'home'
try:
interfaces = subprocess.run(['wg', 'show', 'interfaces'], capture_output=True, text=True, timeout=2).stdout.strip().split()
active_interface = next((name for name in ('int', 'home') if name in interfaces), '')
data['wireguard_interface'] = active_interface
data['wireguard_enabled'] = bool(active_interface)
except Exception:
pass
# Cellular status from ModemManager. Parse the stable section labels emitted by
# mmcli's human-readable output; keep the SIM card hidden when no SIM is present.
try:
active_profiles = subprocess.run(['nmcli', '-t', '-f', 'NAME', 'connection', 'show', '--active'], capture_output=True, text=True, timeout=2).stdout.strip().splitlines()
data['sim_enabled'] = any(profile.split(':', 1)[0] == 'mobile' for profile in active_profiles)
listing = subprocess.run(['mmcli', '-L'], capture_output=True, text=True, timeout=3).stdout
match = __import__('re').search(r'/Modem/(\\d+)', listing)
if match:
modem_id = match.group(1)
modem = subprocess.run(['mmcli', '-m', modem_id], capture_output=True, text=True, timeout=3).stdout
sim_path = __import__('re').search(r'primary sim path:\\s*(\\S+)', modem)
if sim_path and sim_path.group(1) not in ('none', '/'):
data['sim_present'] = True
operator = __import__('re').search(r'operator name:\\s*(.*?)\\s*$', modem, __import__('re').M)
mode = __import__('re').search(r'access tech:\\s*(.*?)\\s*$', modem, __import__('re').M)
signal = __import__('re').search(r'signal quality:\\s*(\\d+)%', modem)
net_port = __import__('re').search(r'(wwp\\S+)\\s*\\(net\\)', modem)
data['sim_operator'] = operator.group(1).strip() if operator else 'Operator unavailable'
raw_mode = mode.group(1).strip().lower() if mode else ''
if any(tech in raw_mode for tech in ('nr5g', '5g')):
data['sim_mode'] = '5G'
elif 'lte' in raw_mode:
data['sim_mode'] = '4G'
elif any(tech in raw_mode for tech in ('umts', 'hspa', 'hsdpa', 'hsupa', 'wcdma')):
data['sim_mode'] = '3G'
elif any(tech in raw_mode for tech in ('gprs', 'edge', 'gsm')):
data['sim_mode'] = '2G'
else:
data['sim_mode'] = '—'
data['sim_signal'] = int(signal.group(1)) if signal else -1
if net_port:
addresses = subprocess.run(['ip', '-4', '-o', 'addr', 'show', 'dev', net_port.group(1), 'scope', 'global'], capture_output=True, text=True, timeout=2).stdout.strip().splitlines()
if addresses:
fields = addresses[0].split()
if len(fields) >= 4 and '/' in fields[3]:
data['sim_ip'] = fields[3].split('/')[0]
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.ethernetConnected = res.ethernet_connected
root.ipAddress = res.ip
root.wireguardAvailable = res.wireguard_available
root.wireguardEnabled = res.wireguard_enabled
root.wireguardProfile = res.wireguard_profile
root.wireguardInterface = res.wireguard_interface
if (root.wireguardInterface === root.wireguardProfile) {
root.wireguardAutoTarget = root.wireguardProfile
} else if (root.wireguardAutoTarget !== root.wireguardProfile) {
const target = root.wireguardProfile
const active = root.wireguardInterface
root.wireguardAutoTarget = target
const command = active
? "doas /usr/bin/wg-quick down " + active + " && doas /usr/bin/wg-quick up " + target
: "doas /usr/bin/wg-quick up " + target
Quickshell.execDetached(["sh", "-c", command])
}
root.simPresent = res.sim_present
root.simEnabled = res.sim_enabled
root.simOperator = res.sim_operator
root.simAccessMode = res.sim_mode
root.simIpAddress = res.sim_ip
root.simSignalPercent = res.sim_signal
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: nightLightService
command: ["hyprsunset"]
running: true
}
Process {
id: loadReadProc
command: ["cat", "/proc/loadavg"]
stdout: StdioCollector {
onStreamFinished: {
const fields = text.trim().split(/\s+/)
root.systemLoad = fields.length >= 3 ? fields.slice(0, 3).join(" ") : "Unavailable"
}
}
}
Timer {
interval: 5000
running: root.activeApplet === "battery"
repeat: true
triggeredOnStart: true
onTriggered: if (!loadReadProc.running) loadReadProc.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
}
}
}
Timer {
id: brightnessSetTimer
interval: 100
repeat: false
onTriggered: if (root.brightnessLevel >= 0)
Quickshell.execDetached(["brightnessctl", "-e4", "-n2", "set", root.brightnessLevel + "%"])
}
Timer {
id: brightnessReadTimer
interval: 160
repeat: false
onTriggered: if (!brightnessReadProc.running && !brightnessSetTimer.running) brightnessReadProc.running = true
}
Timer {
interval: 5000
running: root.activeApplet === "battery"
repeat: true
triggeredOnStart: true
onTriggered: if (!brightnessReadProc.running && !brightnessSetTimer.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: 1000
repeat: true
running: true
onTriggered: if (!mprisPlayersProc.running) mprisPlayersProc.running = true
}
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
// A restrained edge shadow separates the bar from the desktop.
Rectangle {
anchors { top: parent.top; bottom: parent.bottom; right: parent.right }
width: 4
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#00000000" }
GradientStop { position: 1.0; color: "#28000000" }
}
}
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
}
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
const position = trayItem.mapToItem(bar.contentItem, 0, trayItem.height / 2)
root.openTrayMenu(item, bar.screen, position.y)
}
}
}
}
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.activeSsid ? "" : (root.ethernetConnected ? "󰈀" : (root.simEnabled ? "󰈂" : "󰤮"))
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 || root.trayMenuOpen)
&& modelData.name === (root.trayMenuOpen
? (root.trayMenuScreen?.name ?? Quickshell.screens[0]?.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
}
}
}
}
// Application-provided DBusMenu rendered inside a layer-shell panel.
// Native Qt platform menus cannot be parented directly to the bar surface.
PanelWindow {
id: trayMenuWindow
visible: root.trayMenuOpen
screen: root.trayMenuScreen ?? Quickshell.screens[0]
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "tray-menu"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
aboveWindows: true
QsMenuOpener {
id: trayMenuOpener
menu: root.trayMenuHandle
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: root.closeMenus()
}
Item {
anchors.fill: parent
focus: root.trayMenuOpen
Keys.onEscapePressed: root.closeMenus()
Rectangle {
id: trayMenuCard
x: root.barWidth + 14
y: Math.max(8, Math.min(root.trayMenuY - height / 2, trayMenuWindow.height - height - 8))
width: 300
height: Math.min(trayMenuWindow.height - 16, trayMenuContent.implicitHeight)
color: root.cPanel
border.color: root.cBorder
border.width: 1
ColumnLayout {
id: trayMenuContent
anchors.fill: parent
anchors.margins: 16
spacing: 10
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 30
spacing: 8
Rectangle {
visible: root.trayMenuStack.length > 0
Layout.preferredWidth: visible ? 28 : 0
Layout.preferredHeight: 26
color: backHover.containsMouse ? root.cSurfaceHover : "transparent"
Text {
anchors.centerIn: parent
text: "‹"
font.family: root.mono
font.pixelSize: 18
color: root.cText
}
MouseArea {
id: backHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.returnFromTraySubmenu()
}
}
Text {
Layout.fillWidth: true
text: (root.trayMenuStack.length > 0
? (root.trayMenuHandle?.text ?? "Menu")
: (root.trayMenuItem?.tooltipTitle || root.trayMenuItem?.title || root.trayMenuItem?.id || "Tray")).toUpperCase()
font.family: root.mono
font.pixelSize: 13
font.bold: true
color: root.cBright
elide: Text.ElideRight
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.cBorder
}
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: Math.min(trayMenuEntries.implicitHeight, 480)
contentHeight: trayMenuEntries.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: trayMenuEntries
width: parent.width
spacing: 0
Repeater {
model: trayMenuOpener.children
delegate: Item {
id: trayMenuEntry
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: modelData.isSeparator ? 9 : 32
Rectangle {
visible: trayMenuEntry.modelData.isSeparator
anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter }
height: 1
color: root.cBorder
}
Rectangle {
visible: !trayMenuEntry.modelData.isSeparator
anchors.fill: parent
color: entryHover.containsMouse && trayMenuEntry.modelData.enabled ? root.cSurfaceHover : "transparent"
RowLayout {
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
spacing: 8
Text {
Layout.preferredWidth: 16
horizontalAlignment: Text.AlignHCenter
text: {
const entry = trayMenuEntry.modelData
if (entry.buttonType === QsMenuButtonType.CheckBox)
return entry.checkState === Qt.Checked ? "✓" : ""
if (entry.buttonType === QsMenuButtonType.RadioButton)
return entry.checkState === Qt.Checked ? "●" : "○"
return ""
}
font.family: root.mono
font.pixelSize: 12
color: root.cAccent
}
Image {
visible: trayMenuEntry.modelData.icon !== ""
Layout.preferredWidth: visible ? 16 : 0
Layout.preferredHeight: 16
source: trayMenuEntry.modelData.icon
sourceSize.width: 32
sourceSize.height: 32
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
text: trayMenuEntry.modelData.text
font.family: root.mono
font.pixelSize: 11
color: trayMenuEntry.modelData.enabled ? root.cText : root.cDim
elide: Text.ElideRight
}
Text {
visible: trayMenuEntry.modelData.hasChildren
text: "›"
font.family: root.mono
font.pixelSize: 16
color: root.cSubtle
}
}
}
MouseArea {
id: entryHover
anchors.fill: parent
visible: !trayMenuEntry.modelData.isSeparator
enabled: trayMenuEntry.modelData.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const entry = trayMenuEntry.modelData
if (entry.hasChildren)
root.enterTraySubmenu(entry)
else {
entry.triggered()
root.closeMenus()
}
}
}
}
}
}
}
}
}
}
}
// ───────────── 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: 520
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()
onVisibleChanged: if (visible) forceActiveFocus()
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 ══════════════════
ScrollView {
anchors.fill: parent
visible: root.activeApplet === "battery"
clip: true
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: 12
Rectangle {
Layout.fillWidth: true
implicitHeight: 112
color: root.cSurface
border.color: root.cBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 9
RowLayout {
Layout.fillWidth: true
spacing: 10
Item {
Layout.preferredWidth: 32; Layout.minimumWidth: 32; Layout.maximumWidth: 32
Layout.preferredHeight: 32
Text {
anchors.centerIn: parent
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: 26
color: root.batteryChargeColor()
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text {
text: root.batteryPercent >= 0 ? root.batteryPercent + "%" : "Unavailable"
font.family: root.mono; font.pixelSize: 18; 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: 12
spacing: 10
Item {
Layout.preferredWidth: 32; Layout.minimumWidth: 32; Layout.maximumWidth: 32
Layout.preferredHeight: 32; Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: root.onExternalPower ? "󰚥" : "󰔚"
font.family: root.mono; font.pixelSize: 18
color: root.cSubtle
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
text: root.batteryStatus === "Charging" ? "TIME TO FULL" : "TIME REMAINING"
font.family: root.mono; font.pixelSize: 10; font.bold: true
color: root.cSubtle
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
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
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.preferredWidth: 32; Layout.minimumWidth: 32; Layout.maximumWidth: 32
Layout.preferredHeight: 32; Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "󰃠"
font.family: root.mono; font.pixelSize: 20
color: root.cAccent
}
}
ColumnLayout {
Layout.fillWidth: true
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
}
}
Item {
Layout.fillWidth: true
implicitHeight: brightnessSlider.implicitHeight
Slider {
id: brightnessSlider
HoverHandler { cursorShape: Qt.PointingHandCursor }
anchors.fill: parent
enabled: root.brightnessLevel >= 0
from: 0; to: 100; stepSize: 1
value: Math.max(0, root.brightnessLevel)
live: true
onMoved: 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
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 58
color: root.cSurface
border.color: root.cBorder
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.preferredWidth: 32; Layout.minimumWidth: 32; Layout.maximumWidth: 32
Layout.preferredHeight: 32; Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "󰖔"
font.family: root.mono; font.pixelSize: 18
color: root.nightLightEnabled ? root.cAccent : root.cSubtle
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text { text: "NIGHT LIGHT"; font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cText }
Text {
Layout.fillWidth: true
text: root.nightLightEnabled ? "Warm color filter · 4500 K" : "Blue light filter"
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
elide: Text.ElideRight
}
}
Rectangle {
implicitWidth: 36; implicitHeight: 20; radius: 0
Layout.minimumWidth: 36; Layout.preferredWidth: 36; Layout.maximumWidth: 36
color: root.nightLightEnabled ? root.cAccent : root.cSurfaceHover
border.color: nightLightToggle.activeFocus ? root.cBorderFocus : (root.nightLightEnabled ? root.cAccent : root.cBorder)
border.width: 1
Rectangle {
x: root.nightLightEnabled ? 18 : 2; y: 2; width: 16; height: 16; radius: 0
color: root.nightLightEnabled ? root.cBar : root.cDim
Behavior on x { NumberAnimation { duration: 150 } }
}
MouseArea {
id: nightLightToggle
anchors.fill: parent
hoverEnabled: true
activeFocusOnTab: true
cursorShape: Qt.PointingHandCursor
Keys.onReturnPressed: toggleNightLight()
Keys.onSpacePressed: toggleNightLight()
function toggleNightLight() {
root.nightLightEnabled = !root.nightLightEnabled
if (root.nightLightEnabled)
Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", "4500"])
else
Quickshell.execDetached(["hyprctl", "hyprsunset", "identity"])
}
onClicked: toggleNightLight()
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 58
color: root.cSurface
border.color: root.cBorder
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.preferredWidth: 32; Layout.minimumWidth: 32; Layout.maximumWidth: 32
Layout.preferredHeight: 32; Layout.alignment: Qt.AlignVCenter
Text { anchors.centerIn: parent; text: "󰓅"; font.family: root.mono; font.pixelSize: 18; color: root.cAccent }
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text { text: "SYSTEM LOAD"; font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cText }
RowLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: ["1 min", "5 min", "15 min"]
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: {
const values = root.systemLoad.trim().split(/\s+/)
return values[index] ?? "—"
}
font.family: root.mono; font.pixelSize: 10
color: root.cBright
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: modelData
font.family: root.mono; font.pixelSize: 8
color: root.cSubtle
}
}
}
}
}
}
}
}
}
// ══════════════════ NETWORK VIEW ══════════════════
ColumnLayout {
anchors.fill: parent
visible: root.activeApplet === "wifi"
spacing: 12
// WireGuard profile and connection switch.
Rectangle {
visible: root.wireguardAvailable
Layout.fillWidth: true
implicitHeight: 64
color: root.cSurface
border.color: root.cBorder
radius: 0
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.preferredWidth: 32
Layout.minimumWidth: 32
Layout.maximumWidth: 32
Layout.preferredHeight: 32
Layout.minimumHeight: 32
Layout.maximumHeight: 32
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "󰖂"
font.family: root.mono; font.pixelSize: 22
color: root.wireguardEnabled ? root.cSuccess : root.cDim
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
Layout.fillWidth: true
text: "Wireguard (" + (root.wireguardProfile || "connection name") + ")"
font.family: root.mono; font.pixelSize: 13; font.bold: true
color: root.cBright
elide: Text.ElideRight
}
Text {
text: root.wireguardInterface === root.wireguardProfile ? "Connected"
: root.wireguardInterface ? "Switching from " + root.wireguardInterface
: "Disconnected"
font.family: root.mono; font.pixelSize: 10
color: root.wireguardInterface === root.wireguardProfile ? root.cSuccess : root.cSubtle
}
}
Rectangle {
implicitWidth: 36; implicitHeight: 20; radius: 0
color: root.wireguardEnabled ? root.cAccent : root.cSurfaceHover
border.color: root.wireguardEnabled ? root.cAccent : root.cBorder
border.width: 1
Rectangle {
x: root.wireguardEnabled ? 18 : 2; y: 2; width: 16; height: 16; radius: 0
color: root.wireguardEnabled ? root.cBar : root.cDim
Behavior on x { NumberAnimation { duration: 150 } }
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const newState = !root.wireguardEnabled
root.wireguardEnabled = newState
root.wireguardAutoTarget = root.wireguardProfile
if (newState) {
Quickshell.execDetached(["doas", "/usr/bin/wg-quick", "up", root.wireguardProfile])
} else {
Quickshell.execDetached(["doas", "/usr/bin/wg-quick", "down", root.wireguardInterface || root.wireguardProfile])
}
}
}
}
}
}
Rectangle {
visible: root.wireguardAvailable
Layout.fillWidth: true
height: 1
color: root.cBorder
opacity: 0.7
}
// SIM card status and WWAN switch (shown only with a detected SIM).
Rectangle {
visible: root.simPresent
Layout.fillWidth: true
implicitHeight: 64
color: root.cSurface
border.color: root.cBorder
radius: 0
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.preferredWidth: 32
Layout.minimumWidth: 32
Layout.maximumWidth: 32
Layout.preferredHeight: 32
Layout.minimumHeight: 32
Layout.maximumHeight: 32
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
text: "󰒧"
font.family: root.mono; font.pixelSize: 22
color: root.simEnabled ? root.cSuccess : root.cDim
}
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
Layout.maximumWidth: 180
text: root.simOperator || "Cellular"
font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright
elide: Text.ElideRight
}
Text {
text: root.simEnabled ? (root.simAccessMode || "—") + " " + (root.simSignalPercent >= 0 ? root.simSignalPercent + "%" : "—") : "OFF"
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
}
}
Text {
Layout.fillWidth: true
text: "IP: " + (root.simEnabled && root.simIpAddress ? root.simIpAddress : "—")
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
elide: Text.ElideRight
}
}
// Cellular connection toggle.
Rectangle {
implicitWidth: 36; implicitHeight: 20; radius: 0
color: root.simEnabled ? root.cAccent : root.cSurfaceHover
border.color: root.simEnabled ? root.cAccent : root.cBorder
border.width: 1
Rectangle {
x: root.simEnabled ? 18 : 2; y: 2; width: 16; height: 16; radius: 0
color: root.simEnabled ? root.cBar : root.cDim
Behavior on x { NumberAnimation { duration: 150 } }
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const newState = !root.simEnabled
root.simEnabled = newState
Quickshell.execDetached(["nmcli", "connection", newState ? "up" : "down", "mobile"])
}
}
}
}
}
Rectangle {
visible: root.simPresent
Layout.fillWidth: true
height: 1
color: root.cBorder
opacity: 0.7
}
// Wi-Fi status and 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
Item {
Layout.preferredWidth: 32
Layout.minimumWidth: 32
Layout.maximumWidth: 32
Layout.preferredHeight: 32
Layout.minimumHeight: 32
Layout.maximumHeight: 32
Layout.alignment: Qt.AlignVCenter
Text {
anchors.centerIn: parent
anchors.horizontalCenterOffset: -2
text: root.activeSsid ? "" : (root.ethernetConnected ? "󰈀" : (root.simEnabled ? "󰈂" : "󰤮"))
font.family: root.mono; font.pixelSize: 22
color: root.netUp ? root.cSuccess : root.cWarn
}
}
ColumnLayout {
spacing: 2
Text {
text: root.activeSsid ? root.activeSsid
: root.ethernetConnected ? "Wired Connection"
: root.simEnabled ? "Mobile Connection"
: root.netUp ? "Connected" : "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: wifiToggleMouse.activeFocus ? root.cBorderFocus : (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 {
id: wifiToggleMouse
anchors.fill: parent
activeFocusOnTab: true
Keys.onReturnPressed: toggleWifi()
Keys.onSpacePressed: toggleWifi()
function toggleWifi() {
const newState = !root.wifiEnabled
root.wifiEnabled = newState
Quickshell.execDetached(["nmcli", "radio", "wifi", newState ? "on" : "off"])
if (newState) wifiScanProc.running = true
}
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
activeFocusOnTab: true
Keys.onReturnPressed: wifiScanProc.running = true
Keys.onSpacePressed: wifiScanProc.running = true
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 {
id: wifiNetworkList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 4
model: root.wifiNetworks
delegate: Rectangle {
width: ListView.view.width
height: 36
activeFocusOnTab: true
radius: 0
color: modelData.active ? root.cSelected : (wifiItemHover.containsMouse ? root.cSurfaceHover : root.cSurface)
border.color: modelData.active ? root.cAccent : (activeFocus ? root.cBorderFocus : "transparent")
Keys.onUpPressed: if (ListView.view.currentIndex > 0) ListView.view.decrementCurrentIndex()
Keys.onDownPressed: if (ListView.view.currentIndex < ListView.view.count - 1) ListView.view.incrementCurrentIndex()
Keys.onPressed: event => {
if (event.key === Qt.Key_J && ListView.view.currentIndex < ListView.view.count - 1) { ListView.view.incrementCurrentIndex(); event.accepted = true }
else if (event.key === Qt.Key_K && ListView.view.currentIndex > 0) { ListView.view.decrementCurrentIndex(); event.accepted = true }
else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
if (modelData.active) Quickshell.execDetached(["nmcli", "connection", "down", "id", modelData.ssid])
else Quickshell.execDetached(["foot", "-e", "nmtui-connect", modelData.ssid])
root.activeApplet = ""; event.accepted = true
}
}
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: btToggleMouse.activeFocus ? root.cBorderFocus : (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 {
id: btToggleMouse
anchors.fill: parent
activeFocusOnTab: true
Keys.onReturnPressed: toggleBluetooth()
Keys.onSpacePressed: toggleBluetooth()
function toggleBluetooth() {
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
}
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
activeFocusOnTab: true
Keys.onReturnPressed: root.startBluetoothScan()
Keys.onSpacePressed: root.startBluetoothScan()
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 {
id: bluetoothDeviceList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 4
model: root.btDevices
delegate: Rectangle {
width: ListView.view.width
height: 50
activeFocusOnTab: true
radius: 0
color: modelData.connected ? root.cSelected : root.cSurface
border.color: modelData.connected || activeFocus ? root.cBorderFocus : "transparent"
Keys.onUpPressed: if (ListView.view.currentIndex > 0) ListView.view.decrementCurrentIndex()
Keys.onDownPressed: if (ListView.view.currentIndex < ListView.view.count - 1) ListView.view.incrementCurrentIndex()
Keys.onPressed: event => {
if (event.key === Qt.Key_J && ListView.view.currentIndex < ListView.view.count - 1) { ListView.view.incrementCurrentIndex(); event.accepted = true }
else if (event.key === Qt.Key_K && ListView.view.currentIndex > 0) { ListView.view.decrementCurrentIndex(); event.accepted = true }
else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) { root.runBluetoothAction(modelData.connected ? "disconnect" : (modelData.paired ? "connect" : "pair"), modelData.mac); event.accepted = true }
}
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
Layout.minimumWidth: 0
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 {
Layout.preferredWidth: 84
Layout.minimumWidth: 84
Layout.maximumWidth: 84
Layout.alignment: Qt.AlignRight
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
activeFocusOnTab: true
Keys.onReturnPressed: activateBtAction()
Keys.onSpacePressed: activateBtAction()
function activateBtAction() {
const action = modelData.connected ? "disconnect" : (modelData.paired ? "connect" : "pair")
root.runBluetoothAction(action, modelData.mac)
}
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: 14
Text {
text: "PLAYING NOW"
font.family: root.mono; font.pixelSize: 10; font.bold: true
color: root.cSubtle
}
Repeater {
model: root.pulsePlaybackStreams
delegate: RowLayout {
id: playbackRow
required property var modelData
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
Layout.topMargin: 4
Layout.bottomMargin: 4
spacing: 10
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: modelData.applicationLabel || modelData.application
font.family: root.mono; font.pixelSize: 10
color: modelData.applicationLabel === "Apple Music" ? root.cAccent : root.cText
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: modelData.artist && modelData.title
? modelData.artist + " — " + modelData.title
: (modelData.title || modelData.media || "")
font.family: root.mono; font.pixelSize: 9
color: root.cDim
elide: Text.ElideRight
Layout.fillWidth: true
visible: text.length > 0
}
}
Image {
source: modelData.artUrl || ""
visible: source.toString().length > 0
Layout.preferredWidth: 36
Layout.preferredHeight: 36
fillMode: Image.PreserveAspectCrop
asynchronous: true
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: root.cBorder
}
}
RowLayout {
spacing: 2
Repeater {
model: [
{ action: "previous", icon: "", label: "Previous track" },
{ action: "play-pause", icon: "", label: "Play or pause" },
{ action: "next", icon: "", label: "Next track" }
]
delegate: Rectangle {
required property var modelData
width: 28; height: 26
color: playerControlHover.containsMouse ? root.cSelected : "transparent"
border.color: playerControlHover.containsMouse ? root.cBorderFocus : root.cBorder
Accessible.name: modelData.label
Text {
anchors.centerIn: parent
text: modelData.icon
font.family: root.mono
font.pixelSize: 12
color: playerControlHover.containsMouse ? root.cBright : root.cSubtle
}
MouseArea {
id: playerControlHover
anchors.fill: parent
activeFocusOnTab: true
Keys.onReturnPressed: activatePlayerControl()
Keys.onSpacePressed: activatePlayerControl()
function activatePlayerControl() {
if (playbackRow.modelData.player)
Quickshell.execDetached(["playerctl", "--player=" + playbackRow.modelData.player, modelData.action])
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (playbackRow.modelData.player)
Quickshell.execDetached(["playerctl", "--player=" + playbackRow.modelData.player, modelData.action])
}
}
}
}
}
}
}
Text {
text: "No active playback"
visible: root.pulsePlaybackStreams.length === 0
font.family: root.mono; font.pixelSize: 10
color: root.cDim
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
RowLayout {
Layout.fillWidth: true
Text {
text: "OUTPUT VOLUME"
font.family: root.mono; font.pixelSize: 11; font.bold: true
color: root.cAccent
}
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
Layout.topMargin: 2
Layout.bottomMargin: 2
spacing: 10
Item {
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
implicitHeight: 20
Text {
anchors.centerIn: parent
text: Pipewire.defaultAudioSink?.audio?.muted ? "󰝟" : ""
font.family: root.mono; font.pixelSize: 15
color: Pipewire.defaultAudioSink?.audio?.muted ? root.cDanger : root.cText
}
}
Item {
Layout.fillWidth: true
implicitHeight: 24
Slider {
id: masterVolSlider
HoverHandler { cursorShape: Qt.PointingHandCursor }
activeFocusOnTab: true
anchors.fill: parent
implicitHeight: 24
from: 0; to: 1.0; stepSize: 0.01
value: Pipewire.defaultAudioSink?.audio?.volume ?? 0
live: true
Keys.onLeftPressed: root.setOutputVolume(Math.max(0, value - stepSize))
Keys.onRightPressed: root.setOutputVolume(Math.min(1, value + stepSize))
Keys.onPressed: event => {
if (event.key === Qt.Key_H) { root.setOutputVolume(Math.max(0, value - stepSize)); event.accepted = true }
else if (event.key === Qt.Key_L) { root.setOutputVolume(Math.min(1, value + stepSize)); event.accepted = true }
}
onMoved: 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
}
}
}
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.activeFocus ? root.cBorderFocus : (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
activeFocusOnTab: true
Keys.onReturnPressed: toggleOutputMute()
Keys.onSpacePressed: toggleOutputMute()
function toggleOutputMute() {
if (Pipewire.defaultAudioSink?.audio) Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted
root.showVolumeOsd()
}
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Pipewire.defaultAudioSink?.audio)
Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted
root.showVolumeOsd()
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 2
Text {
text: "INPUT VOLUME"
font.family: root.mono; font.pixelSize: 11; font.bold: true
color: root.cAccent
}
Item { Layout.fillWidth: true }
Text {
text: Pipewire.defaultAudioSource?.description || Pipewire.defaultAudioSource?.name || "No input device"
font.family: root.mono; font.pixelSize: 10
color: root.cDim
elide: Text.ElideRight
Layout.maximumWidth: 190
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 2
Layout.bottomMargin: 2
spacing: 10
Item {
Layout.preferredWidth: 20
Layout.minimumWidth: 20
Layout.maximumWidth: 20
implicitHeight: 20
Text {
anchors.centerIn: parent
text: Pipewire.defaultAudioSource?.audio?.muted ? "󰍭" : ""
font.family: root.mono; font.pixelSize: 15
color: Pipewire.defaultAudioSource?.audio?.muted ? root.cDanger : root.cText
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24
implicitHeight: 24
Slider {
id: masterInputSlider
HoverHandler { cursorShape: Qt.PointingHandCursor }
activeFocusOnTab: true
anchors.fill: parent
implicitHeight: 24
from: 0; to: 1.0; stepSize: 0.01
value: Pipewire.defaultAudioSource?.audio?.volume ?? 0
live: true
Keys.onLeftPressed: root.setInputVolume(Math.max(0, value - stepSize))
Keys.onRightPressed: root.setInputVolume(Math.min(1, value + stepSize))
Keys.onPressed: event => {
if (event.key === Qt.Key_H) { root.setInputVolume(Math.max(0, value - stepSize)); event.accepted = true }
else if (event.key === Qt.Key_L) { root.setInputVolume(Math.min(1, value + stepSize)); event.accepted = true }
}
onMoved: root.setInputVolume(value)
background: Rectangle {
x: masterInputSlider.leftPadding
y: masterInputSlider.topPadding + masterInputSlider.availableHeight / 2 - height / 2
width: masterInputSlider.availableWidth; height: 3
color: root.cBorder
Rectangle {
width: masterInputSlider.visualPosition * parent.width; height: parent.height
color: Pipewire.defaultAudioSource?.audio?.muted ? root.cDim : root.cAccent
}
}
handle: Rectangle {
implicitWidth: 10; implicitHeight: 10
x: masterInputSlider.leftPadding + masterInputSlider.visualPosition * (masterInputSlider.availableWidth - width)
y: masterInputSlider.topPadding + masterInputSlider.availableHeight / 2 - height / 2
width: 10; height: 10; color: root.cBright
border.color: root.cBorderFocus; border.width: 1
}
}
}
Text {
text: Pipewire.defaultAudioSource?.audio?.muted ? "Muted" : Math.round((Pipewire.defaultAudioSource?.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: inputMuteHover.containsMouse ? root.cSelected : "transparent"
border.color: inputMuteHover.activeFocus ? root.cBorderFocus : (inputMuteHover.containsMouse ? root.cBorderFocus : root.cBorder)
Text {
anchors.centerIn: parent
text: Pipewire.defaultAudioSource?.audio?.muted ? "󰍭" : ""
font.family: root.mono; font.pixelSize: 12; color: root.cText
}
MouseArea {
id: inputMuteHover
anchors.fill: parent; hoverEnabled: true
activeFocusOnTab: true
Keys.onReturnPressed: toggleInputMute()
Keys.onSpacePressed: toggleInputMute()
function toggleInputMute() {
if (Pipewire.defaultAudioSource?.audio) Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted
}
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Pipewire.defaultAudioSource?.audio)
Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder; Layout.topMargin: 2 }
Rectangle {
id: outputDevicesHeader
Layout.fillWidth: true
height: 32
color: outputHeaderHover.containsMouse ? root.cSurfaceHover : root.cSurface
border.color: outputHeaderHover.activeFocus || outputHeaderHover.containsMouse ? root.cBorderFocus : root.cBorder
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10; anchors.rightMargin: 8
spacing: 8
Text {
text: ""
font.family: root.mono; font.pixelSize: 12; color: root.cAccent
}
Text {
text: "OUTPUT DEVICES"
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cText
Layout.fillWidth: true
}
Text {
text: root.pulseOutputs.length
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
}
Text {
text: root.outputDevicesExpanded ? "" : ""
font.family: root.mono; font.pixelSize: 10; color: root.cAccent
}
}
MouseArea {
id: outputHeaderHover
anchors.fill: parent
activeFocusOnTab: true
Keys.onReturnPressed: toggleOutputDevices()
Keys.onSpacePressed: toggleOutputDevices()
function toggleOutputDevices() {
root.outputDevicesExpanded = !root.outputDevicesExpanded
if (!root.outputDevicesExpanded) forceActiveFocus()
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleOutputDevices()
}
}
Repeater {
model: root.outputDevicesExpanded ? root.pulseOutputs : []
delegate: Rectangle {
required property var modelData
property real deviceVolume: modelData.volume
onModelDataChanged: deviceVolume = modelData.volume
readonly property bool isDefault: Pipewire.defaultAudioSink?.name === modelData.name
Layout.fillWidth: true
implicitHeight: 62
color: isDefault ? root.cSelected : (deviceHover.containsMouse ? root.cSurfaceHover : root.cSurface)
border.color: isDefault ? root.cAccent : (deviceHover.containsMouse ? root.cBorderFocus : root.cBorder)
border.width: 1
MouseArea {
id: deviceHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached(["pactl", "set-default-sink", modelData.name])
}
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 10; anchors.rightMargin: 10
anchors.topMargin: 5; anchors.bottomMargin: 3
spacing: 1
z: 1
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: ""
font.family: root.mono; font.pixelSize: 12
color: isDefault ? root.cAccent : root.cSubtle
}
Text {
text: modelData.description
font.family: root.mono; font.pixelSize: 10
color: isDefault ? root.cBright : root.cText
Layout.fillWidth: true; elide: Text.ElideRight
}
Text {
text: isDefault ? "DEFAULT" : ""
font.family: root.mono; font.pixelSize: 8
color: root.cAccent
}
Text {
text: Math.round(modelData.volume * 100) + "%"
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
}
Text {
text: modelData.muted ? "󰝟" : "󰕾"
font.family: root.mono; font.pixelSize: 11
color: modelData.muted ? root.cDanger : root.cText
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.PointingHandCursor
onClicked: root.togglePulseDeviceMute("sink", modelData.name, !modelData.muted)
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24
Layout.leftMargin: 20
Layout.bottomMargin: 4
implicitHeight: 24
Slider {
id: sinkSlider
HoverHandler { cursorShape: Qt.PointingHandCursor }
activeFocusOnTab: true
anchors.fill: parent
implicitHeight: 24
from: 0; to: 1.0; stepSize: 0.01
value: deviceVolume
live: true
Keys.onLeftPressed: { deviceVolume = Math.max(0, value - stepSize); root.setPulseDeviceVolume("sink", modelData.name, deviceVolume) }
Keys.onRightPressed: { deviceVolume = Math.min(1, value + stepSize); root.setPulseDeviceVolume("sink", modelData.name, deviceVolume) }
Keys.onPressed: event => {
if (event.key === Qt.Key_H) { deviceVolume = Math.max(0, value - stepSize); root.setPulseDeviceVolume("sink", modelData.name, deviceVolume); event.accepted = true }
else if (event.key === Qt.Key_L) { deviceVolume = Math.min(1, value + stepSize); root.setPulseDeviceVolume("sink", modelData.name, deviceVolume); event.accepted = true }
}
onMoved: sinkUpdateTimer.restart()
onPressedChanged: if (!pressed) {
sinkUpdateTimer.stop()
root.setPulseDeviceVolume("sink", modelData.name, 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 {
implicitWidth: 10; implicitHeight: 10
x: sinkSlider.leftPadding + sinkSlider.visualPosition * (sinkSlider.availableWidth - width)
y: sinkSlider.topPadding + sinkSlider.availableHeight / 2 - height / 2
width: 10; height: 10; color: sinkSlider.hovered || sinkSlider.pressed ? root.cBright : root.cText
border.color: root.cBorderFocus; border.width: 1
}
Timer {
id: sinkUpdateTimer
interval: 100
onTriggered: root.setPulseDeviceVolume("sink", modelData.name, sinkSlider.value)
}
}
}
}
}
}
Text {
text: "No output devices found"
visible: root.outputDevicesExpanded && root.pulseOutputs.length === 0
font.family: root.mono; font.pixelSize: 10; color: root.cDim
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder; Layout.topMargin: 2 }
Rectangle {
id: inputDevicesHeader
Layout.fillWidth: true
height: 32
color: inputHeaderHover.containsMouse ? root.cSurfaceHover : root.cSurface
border.color: inputHeaderHover.activeFocus || inputHeaderHover.containsMouse ? root.cBorderFocus : root.cBorder
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10; anchors.rightMargin: 8
spacing: 8
Text {
text: ""
font.family: root.mono; font.pixelSize: 12; color: root.cAccent
}
Text {
text: "INPUT DEVICES"
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cText
Layout.fillWidth: true
}
Text {
text: root.pulseInputs.length
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
}
Text {
text: root.inputDevicesExpanded ? "" : ""
font.family: root.mono; font.pixelSize: 10; color: root.cAccent
}
}
MouseArea {
id: inputHeaderHover
anchors.fill: parent
activeFocusOnTab: true
Keys.onReturnPressed: toggleInputDevices()
Keys.onSpacePressed: toggleInputDevices()
function toggleInputDevices() {
root.inputDevicesExpanded = !root.inputDevicesExpanded
if (!root.inputDevicesExpanded) forceActiveFocus()
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: toggleInputDevices()
}
}
Repeater {
model: root.inputDevicesExpanded ? root.pulseInputs : []
delegate: Rectangle {
required property var modelData
property real deviceVolume: modelData.volume
onModelDataChanged: deviceVolume = modelData.volume
readonly property bool isDefault: Pipewire.defaultAudioSource?.name === modelData.name
Layout.fillWidth: true
implicitHeight: 62
color: isDefault ? root.cSelected : (deviceHover.containsMouse ? root.cSurfaceHover : root.cSurface)
border.color: isDefault ? root.cAccent : (deviceHover.containsMouse ? root.cBorderFocus : root.cBorder)
border.width: 1
MouseArea {
id: deviceHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached(["pactl", "set-default-source", modelData.name])
}
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 10; anchors.rightMargin: 10
anchors.topMargin: 5; anchors.bottomMargin: 3
spacing: 1
z: 1
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: ""
font.family: root.mono; font.pixelSize: 12
color: isDefault ? root.cAccent : root.cSubtle
}
Text {
text: modelData.description
font.family: root.mono; font.pixelSize: 10
color: isDefault ? root.cBright : root.cText
Layout.fillWidth: true; elide: Text.ElideRight
}
Text {
text: isDefault ? "DEFAULT" : ""
font.family: root.mono; font.pixelSize: 8
color: root.cAccent
}
Text {
text: Math.round(modelData.volume * 100) + "%"
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
}
Text {
text: modelData.muted ? "󰍭" : ""
font.family: root.mono; font.pixelSize: 11
color: modelData.muted ? root.cDanger : root.cText
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.PointingHandCursor
onClicked: root.togglePulseDeviceMute("source", modelData.name, !modelData.muted)
}
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24
Layout.leftMargin: 20
Layout.bottomMargin: 4
implicitHeight: 24
Slider {
id: srcSlider
HoverHandler { cursorShape: Qt.PointingHandCursor }
activeFocusOnTab: true
anchors.fill: parent
implicitHeight: 24
from: 0; to: 1.0; stepSize: 0.01
value: deviceVolume
live: true
Keys.onLeftPressed: { deviceVolume = Math.max(0, value - stepSize); root.setPulseDeviceVolume("source", modelData.name, deviceVolume) }
Keys.onRightPressed: { deviceVolume = Math.min(1, value + stepSize); root.setPulseDeviceVolume("source", modelData.name, deviceVolume) }
Keys.onPressed: event => {
if (event.key === Qt.Key_H) { deviceVolume = Math.max(0, value - stepSize); root.setPulseDeviceVolume("source", modelData.name, deviceVolume); event.accepted = true }
else if (event.key === Qt.Key_L) { deviceVolume = Math.min(1, value + stepSize); root.setPulseDeviceVolume("source", modelData.name, deviceVolume); event.accepted = true }
}
onMoved: srcUpdateTimer.restart()
onPressedChanged: if (!pressed) {
srcUpdateTimer.stop()
root.setPulseDeviceVolume("source", modelData.name, 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: isDefault ? root.cAccent : root.cDim }
}
handle: Rectangle {
implicitWidth: 10; implicitHeight: 10
x: srcSlider.leftPadding + srcSlider.visualPosition * (srcSlider.availableWidth - width)
y: srcSlider.topPadding + srcSlider.availableHeight / 2 - height / 2
width: 10; height: 10; color: srcSlider.hovered || srcSlider.pressed ? root.cBright : root.cText
border.color: root.cBorderFocus; border.width: 1
}
Timer {
id: srcUpdateTimer
interval: 100
onTriggered: root.setPulseDeviceVolume("source", modelData.name, srcSlider.value)
}
}
}
}
}
}
Text {
text: "No input devices found"
visible: root.inputDevicesExpanded && root.pulseInputs.length === 0
font.family: root.mono; font.pixelSize: 10; color: root.cDim
}
}
}
}
}
}
}
}
// ───────────── 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: root.osdKind === "track" ? 102 : 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: root.osdKind === "track" ? 360 : 300
height: root.osdKind === "track" ? 82 : 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: root.osdKind === "track" ? 10 : 14
anchors.rightMargin: root.osdKind === "track" ? 10 : 14
anchors.topMargin: root.osdKind === "track" ? 10 : 9
anchors.bottomMargin: root.osdKind === "track" ? 10 : 9
spacing: 7
RowLayout {
visible: root.osdKind === "track"
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
Image {
source: root.osdTrackArtUrl
Layout.preferredWidth: 54
Layout.preferredHeight: 54
fillMode: Image.PreserveAspectCrop
asynchronous: true
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: root.cBorder
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Text {
text: "APPLE MUSIC"
font.family: root.mono; font.pixelSize: 9; font.bold: true
color: root.cAccent
}
Text {
text: root.osdTrackTitle
font.family: root.mono; font.pixelSize: 12; font.bold: true
color: root.cBright
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: root.osdTrackArtist
font.family: root.mono; font.pixelSize: 10
color: root.cSubtle
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
RowLayout {
visible: root.osdKind !== "track"
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 {
visible: root.osdKind !== "track"
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)
}
}
}
}
}
}
}
PanelWindow {
id: wallpaperPicker
visible: root.wallpaperPickerOpen
screen: Quickshell.screens[0]
anchors { top: true; bottom: true; left: true; right: true }
color: "#99000000"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "wallpaper-picker"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
aboveWindows: true
onVisibleChanged: if (visible) root.refreshWallpaperCache()
Item {
anchors.fill: parent
focus: wallpaperPicker.visible
Keys.onEscapePressed: root.wallpaperPickerOpen = false
}
MouseArea {
anchors.fill: parent
onClicked: root.wallpaperPickerOpen = false
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 48, 920)
height: Math.min(parent.height - 40, 500)
color: root.cPanel
border.color: root.cBorder
border.width: 1
radius: 3
MouseArea { anchors.fill: parent; acceptedButtons: Qt.AllButtons }
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
RowLayout {
Layout.fillWidth: true
Text {
text: "Wallpaper"
color: root.cBright
font.family: root.mono
font.pixelSize: 17
font.weight: Font.Medium
}
Item { Layout.fillWidth: true }
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
GridLayout {
Layout.fillWidth: true
Layout.fillHeight: true
columns: 3
rowSpacing: 12
columnSpacing: 12
Repeater {
model: root.wallpapers
delegate: Rectangle {
id: wallpaperCard
required property var modelData
property bool hovered: false
Layout.fillWidth: true
Layout.fillHeight: true
color: root.cSurface
border.width: 1
border.color: modelData.path === root.activeWallpaper ? root.cAccent : hovered ? root.cBorderFocus : root.cBorder
clip: true
Image {
anchors.fill: parent
source: "file://" + wallpaperCard.modelData.path + "?v=" + encodeURIComponent(root.wallpaperCacheKeys[wallpaperCard.modelData.path] ?? "0")
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
}
Rectangle {
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
height: 40
color: "#e0151515"
}
Text {
anchors { left: parent.left; bottom: parent.bottom; margins: 10 }
text: wallpaperCard.modelData.name
color: root.cBright
font.family: root.mono
font.pixelSize: 10
elide: Text.ElideRight
}
Rectangle {
visible: wallpaperCard.modelData.path === root.activeWallpaper
anchors { top: parent.top; right: parent.right; margins: 8 }
width: 9; height: 9; radius: 5; color: root.cAccent
}
Rectangle {
anchors.fill: parent
color: "#14ffffff"
visible: wallpaperCard.hovered
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.setWallpaper(wallpaperCard.modelData.path)
onEntered: wallpaperCard.hovered = true
onExited: wallpaperCard.hovered = false
}
}
}
}
}
}
}
}