nexoniarz-config-nixos/dotfiles/quickshell/RightFlyout.qml
Nexoniarz aa89dd729c Redesign Display Settings, Wallpapers, Booru; add System Monitor, Weather
Display Settings: drag-to-arrange multi-monitor canvas with edge
snapping (locked when only one screen is connected), 8/10-bit depth,
color management presets (sRGB/Wide/EDID/HDR+EDID), SDR
brightness/saturation, ICC profile path, VRR toggle. display-set now
takes a single pre-built descriptor instead of a fixed positional arg
list, since Hyprland's monitor keyword grew well past that.

Wallpapers: fixed the current-wallpaper highlight never loading on
open, added real first-frame thumbnails for animated wallpapers
(wallpaper-thumbs + ffmpeg), bigger 2-column grid, filename filter.

Booru: separate downloads folder from Wallpapers so browsing doesn't
clutter the wallpaper picker, bigger grid, Safe/Moderate/Explicit
rating tiers replacing the old binary toggle, sort order (Newest/
Score/Random). Fixed pagination being completely broken (a Flow
anchor violation was silently disabling the whole layout) and
Danbooru's Random sort (order:random tag times out server-side --
switched to their random=true endpoint for that case).

Wi-Fi panel: added a "Scanning.../No networks found" state and a
manual rescan button -- previously showed nothing either way, making
"still scanning" indistinguishable from a real failure.

Bluetooth: auto-scans on open like Wi-Fi now, instead of requiring a
manual click every time.

New: System Monitor panel (CPU/RAM/GPU/VRAM/disk usage, NVIDIA or AMD
detected automatically) and Weather panel (IP-geolocated or a typed
city, current + hourly forecast, Open-Meteo, no API key) with a
compact taskbar badge.
2026-08-30 20:55:06 +02:00

2060 lines
88 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Bluetooth
import Quickshell.Networking
Rectangle {
id: root
property string activePanel: ""
signal requestClose()
width: 320
implicitHeight: header.height + loader.item.implicitHeight + 16
color: Theme.bg
border.width: 2
border.color: Theme.border
property var titles: ({
network: "Network",
messages: "Messages",
bluetooth: "Bluetooth",
screenshot: "Screenshot",
volume: "Volume",
brightness: "Brightness",
devices: "Devices",
system: "System Monitor",
weather: "Weather",
power: "Power"
})
// Lives here, not inside messagesPanel's own Column — that Component
// gets destroyed every time the panel closes (the Loader tears it
// down), which was silently resetting this to [] on every reopen and
// making "cleared" messages reappear. `root` itself persists for the
// life of the popup, so this survives closing/reopening the panel
// (though not a full Quickshell restart — mako's own history is just
// as ephemeral, so there'd be nothing to hide-filter after one anyway).
property var hiddenMessageIds: []
function hideMessageId(id) {
var ids = root.hiddenMessageIds.slice();
if (ids.indexOf(id) === -1) ids.push(id);
root.hiddenMessageIds = ids;
}
function clearAllMessages(currentIds) {
var ids = root.hiddenMessageIds.slice();
for (var i = 0; i < currentIds.length; i++) {
if (ids.indexOf(currentIds[i]) === -1) ids.push(currentIds[i]);
}
root.hiddenMessageIds = ids;
}
Item {
id: header
width: parent.width
height: 40
Text {
anchors.left: parent.left
anchors.leftMargin: 14
anchors.verticalCenter: parent.verticalCenter
text: root.titles[root.activePanel] || ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 14
font.bold: true
}
CloseButton {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
onClicked: root.requestClose()
}
}
Rectangle { anchors.top: header.bottom; width: parent.width; height: 1; color: Theme.border }
Loader {
id: loader
y: header.height + 8
x: 12
width: parent.width - 24
sourceComponent: {
switch (root.activePanel) {
case "network": return networkPanel;
case "messages": return messagesPanel;
case "bluetooth": return bluetoothPanel;
case "screenshot": return screenshotPanel;
case "volume": return volumePanel;
case "brightness": return brightnessPanel;
case "devices": return devicesPanel;
case "system": return systemPanel;
case "weather": return weatherPanel;
case "power": return powerPanel;
default: return emptyPanel;
}
}
}
Component {
id: emptyPanel
Item { implicitHeight: 1 }
}
// --- Network -------------------------------------------------------
Component {
id: networkPanel
Column {
id: netCol
spacing: 10
width: loader.width
property var selected: null // a Network, or a NetworkDevice (wired fallback)
property bool selectedIsDevice: netCol.selected !== null && netCol.selected.networks !== undefined
// --- List view ---------------------------------------------
Column {
visible: netCol.selected === null
width: parent.width
spacing: 10
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Wi-Fi"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 13
font.bold: true
}
ToggleSwitch {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: Networking.wifiEnabled
onToggled: Networking.wifiEnabled = !Networking.wifiEnabled
}
}
Text {
visible: Networking.devices.values.length === 0
text: "No network devices"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
// Hardware/rfkill state isn't reflected by wifiEnabled at all — without
// this, a soft- or hardware-killed adapter just looks identical to "no
// networks in range" with no way to tell the two apart.
Text {
visible: !Networking.wifiHardwareEnabled
text: "Wi-Fi is disabled in hardware (check airplane mode / rfkill switch)"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
wrapMode: Text.WordWrap
width: parent.width
}
Repeater {
model: Networking.devices
delegate: Column {
width: loader.width
spacing: 4
property var device: modelData
// Wired devices report a single phantom "network" entry with no
// signal strength, so detect Wi-Fi by scannerEnabled (only WifiDevice
// has that property) instead of by networks-list emptiness.
property bool isWifi: device.scannerEnabled !== undefined
// The plugin exposes no "scan in progress" signal, just the
// scannerEnabled bool — so this is a grace-period guess, not a
// real status readout. Good enough to stop "no networks yet"
// from looking identical to "scan came back empty", which is
// what made this look like a silent bug before.
property bool scanning: false
Timer {
id: scanGrace
interval: 4000
onTriggered: scanning = false
}
// Turn on active scanning while this panel is open so
// the network list shows more than just whatever we're
// already connected to.
Component.onCompleted: if (isWifi) { device.scannerEnabled = true; scanning = true; scanGrace.restart(); }
Component.onDestruction: if (isWifi) device.scannerEnabled = false;
// NetworkManager throttles RequestScan, so a bare re-toggle right
// after the last one can be silently ignored — this is still just
// a best-effort nudge, not a guaranteed fresh scan.
function rescan() {
device.scannerEnabled = false;
scanning = true;
scanGrace.restart();
rescanRestart.start();
}
Timer {
id: rescanRestart
interval: 150
onTriggered: device.scannerEnabled = true
}
Text {
text: device.name
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Rectangle {
visible: isWifi
width: loader.width
height: 26
color: rescanMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: Theme.borderDim
Text {
anchors.centerIn: parent
text: scanning ? "Scanning…" : "Rescan"
color: scanning ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
MouseArea {
id: rescanMa
anchors.fill: parent
hoverEnabled: true
onClicked: rescan()
}
}
// Non-Wi-Fi devices — show link state directly instead of a list.
Rectangle {
visible: !isWifi
width: loader.width
height: 32
color: wiredMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: device.connected ? Theme.accent : Theme.borderDim
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: device.connected ? "Connected" : "No link"
color: device.connected ? Theme.accent : Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
MouseArea {
id: wiredMa
anchors.fill: parent
hoverEnabled: true
onClicked: netCol.selected = device
}
}
Text {
visible: isWifi && (!device.networks || device.networks.values.length === 0)
text: scanning ? "Scanning…" : "No networks found"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Repeater {
model: (isWifi && device.networks) ? device.networks : []
delegate: Rectangle {
width: loader.width
height: 40
color: netMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: net.connected ? Theme.accent : Theme.borderDim
property var net: modelData
Column {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
text: net.name
color: net.connected ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Text {
text: (net.connected ? "Connected" : (net.known ? "Saved" : "Available"))
+ " · " + net.signalStrength + "%"
+ " · " + WifiSecurityType.toString(net.security)
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
}
MouseArea {
id: netMa
anchors.fill: parent
hoverEnabled: true
onClicked: netCol.selected = net
}
}
}
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Column {
id: hotspotCol
width: loader.width
spacing: 8
property bool hotspotActive: false
property bool canStart: ssidInput.text.length > 0 && pskInput.text.length >= 8
// Persists SSID/password/security/band across Quickshell
// restarts, blank/default otherwise.
PersistentProperties {
id: hotspotState
property string ssid: ""
property string password: ""
property string security: "wpa2"
property string band: "auto"
}
Text {
text: "Hotspot"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 13
font.bold: true
}
Process { id: hotspotProc; command: [] }
Process {
id: hotspotStatusProc
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"]
stdout: StdioCollector {
onStreamFinished: hotspotCol.hotspotActive = text.indexOf("Hotspot") !== -1
}
}
Component.onCompleted: hotspotStatusProc.running = true
Rectangle {
width: loader.width
height: 30
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
TextInput {
id: ssidInput
anchors.fill: parent
anchors.margins: 8
text: hotspotState.ssid
onTextEdited: hotspotState.ssid = text
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
clip: true
Text {
visible: ssidInput.text.length === 0
text: "SSID"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
}
Rectangle {
width: loader.width
height: 30
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
TextInput {
id: pskInput
anchors.fill: parent
anchors.margins: 8
text: hotspotState.password
onTextEdited: hotspotState.password = text
echoMode: TextInput.Password
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
clip: true
Text {
visible: pskInput.text.length === 0
text: "Password"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
}
Text {
text: "Security"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Row {
spacing: 6
Repeater {
model: [
{ key: "wpa2", label: "WPA2" },
{ key: "wpa3", label: "WPA3" }
]
delegate: Rectangle {
width: (loader.width - 6) / 2
height: 28
color: modelData.key === hotspotState.security ? Theme.bgAlt : (secMa.containsMouse ? Theme.bgAlt : "transparent")
border.width: modelData.key === hotspotState.security ? 2 : 1
border.color: modelData.key === hotspotState.security ? Theme.accent : Theme.borderDim
Text {
anchors.centerIn: parent
text: modelData.label
color: modelData.key === hotspotState.security ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
MouseArea {
id: secMa
anchors.fill: parent
hoverEnabled: true
onClicked: hotspotState.security = modelData.key
}
}
}
}
Text {
text: "Band"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Row {
spacing: 6
Repeater {
model: [
{ key: "auto", label: "Auto" },
{ key: "bg", label: "2.4GHz" },
{ key: "a", label: "5GHz" }
]
delegate: Rectangle {
width: (loader.width - 12) / 3
height: 28
color: modelData.key === hotspotState.band ? Theme.bgAlt : (bandMa.containsMouse ? Theme.bgAlt : "transparent")
border.width: modelData.key === hotspotState.band ? 2 : 1
border.color: modelData.key === hotspotState.band ? Theme.accent : Theme.borderDim
Text {
anchors.centerIn: parent
text: modelData.label
color: modelData.key === hotspotState.band ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
MouseArea {
id: bandMa
anchors.fill: parent
hoverEnabled: true
onClicked: hotspotState.band = modelData.key
}
}
}
}
Rectangle {
id: hsButton
width: loader.width
height: 32
opacity: (hotspotCol.hotspotActive || hotspotCol.canStart) ? 1 : 0.4
color: hsMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: hotspotCol.hotspotActive ? Theme.danger : Theme.accent
Text {
anchors.centerIn: parent
text: hotspotCol.hotspotActive ? "Stop Hotspot" : "Start Hotspot"
color: hotspotCol.hotspotActive ? Theme.danger : Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: hsMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (hotspotCol.hotspotActive) {
hotspotProc.command = ["nmcli", "connection", "down", "Hotspot"];
hotspotProc.running = true;
hotspotCol.hotspotActive = false;
} else if (hotspotCol.canStart) {
hotspotProc.command = ["hotspot-start", ssidInput.text, pskInput.text, hotspotState.security, hotspotState.band];
hotspotProc.running = true;
hotspotCol.hotspotActive = true;
}
}
}
}
Text {
text: "Password needs at least 8 characters."
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
wrapMode: Text.WordWrap
width: loader.width
}
}
}
// --- Detail view ---------------------------------------------
Column {
id: netDetail
visible: netCol.selected !== null
width: parent.width
spacing: 10
property var sel: netCol.selected
property bool needsPassword: !!(sel && !netCol.selectedIsDevice && !sel.known && !sel.connected)
// NetworkDevice.address is actually the MAC address, not an IP —
// the real IP has to come from nmcli.
property string deviceName: sel ? (netCol.selectedIsDevice ? sel.name : (sel.device ? sel.device.name : "")) : ""
property string ip: ""
Process {
id: ipProc
command: netDetail.deviceName ? ["nmcli", "-g", "IP4.ADDRESS", "device", "show", netDetail.deviceName] : []
stdout: StdioCollector {
onStreamFinished: netDetail.ip = text.split("/")[0].split("\n")[0].trim()
}
}
onDeviceNameChanged: { ip = ""; if (deviceName) ipProc.running = true; }
Component.onCompleted: if (deviceName) ipProc.running = true
Process { id: netActionProc; command: [] }
Rectangle {
width: parent.width
height: 26
color: "transparent"
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "< Back"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea { anchors.fill: parent; onClicked: netCol.selected = null }
}
Text {
text: netCol.selected ? netCol.selected.name : ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 15
font.bold: true
}
Column {
width: parent.width
spacing: 4
property var rows: {
if (!netCol.selected) return [];
if (netCol.selectedIsDevice) {
return [
{ label: "Status", value: netCol.selected.connected ? "Connected" : "No link" },
{ label: "IP Address", value: netDetail.ip || "—" },
{ label: "MAC Address", value: netCol.selected.address || "—" }
];
}
var r = [
{ label: "Status", value: netCol.selected.connected ? "Connected" : (netCol.selected.known ? "Saved" : "Available") },
{ label: "Signal", value: netCol.selected.signalStrength + "%" },
{ label: "Security", value: WifiSecurityType.toString(netCol.selected.security) }
];
if (netCol.selected.connected)
r.push({ label: "IP Address", value: netDetail.ip || "—" });
if (netCol.selected.device && netCol.selected.device.address)
r.push({ label: "MAC Address", value: netCol.selected.device.address });
return r;
}
Repeater {
model: parent.rows
delegate: Item {
width: loader.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.value
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Column {
visible: !netCol.selectedIsDevice && netDetail.needsPassword
width: parent.width
spacing: 8
Rectangle {
width: parent.width
height: 30
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
TextInput {
id: detailPskField
anchors.fill: parent
anchors.margins: 8
echoMode: TextInput.Password
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
clip: true
Text {
visible: detailPskField.text.length === 0
text: "Password"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
}
}
Rectangle {
visible: !netCol.selectedIsDevice
width: parent.width
height: 34
property bool canSubmit: !netDetail.needsPassword || detailPskField.text.length >= 8
opacity: canSubmit ? 1 : 0.4
color: connMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 2
border.color: (netCol.selected && netCol.selected.connected) ? Theme.danger : Theme.accent
Text {
anchors.centerIn: parent
text: (netCol.selected && netCol.selected.connected) ? "Disconnect"
: (netDetail.needsPassword ? "Connect with password" : "Connect")
color: (netCol.selected && netCol.selected.connected) ? Theme.danger : Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: connMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!netCol.selected) return;
if (netCol.selected.connected) netCol.selected.disconnect();
else if (netDetail.needsPassword) {
if (detailPskField.text.length >= 8) netCol.selected.connectWithPsk(detailPskField.text);
} else netCol.selected.connect();
}
}
}
Text {
visible: !netCol.selectedIsDevice && netCol.selected && netCol.selected.known
text: "Forget this network"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea {
anchors.fill: parent
onClicked: { netCol.selected.forget(); netCol.selected = null; }
}
}
}
}
}
// --- Bluetooth -------------------------------------------------------
Component {
id: bluetoothPanel
Column {
id: btCol
spacing: 10
width: loader.width
property var selected: null
// Same reasoning as the Wi-Fi panel's per-device scannerEnabled
// toggle — auto-start scanning while the panel is open instead
// of requiring a manual click every time, then stop when it
// closes so the adapter isn't left discovering in the background.
// Qt.callLater, not a direct write: writing `discovering` in the
// same tick this Column is constructed was silently dropped
// (verified against BlueZ directly over D-Bus — Discovering
// stayed false) even though the identical write from the
// manual "Scan for devices" button works fine; deferring one
// event-loop tick gives Quickshell's Bluetooth adapter wrapper
// time to finish attaching before the write goes out.
Component.onCompleted: Qt.callLater(function () {
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) Bluetooth.defaultAdapter.discovering = true;
});
Component.onDestruction: if (Bluetooth.defaultAdapter) Bluetooth.defaultAdapter.discovering = false;
// --- List view ---------------------------------------------
Column {
visible: btCol.selected === null
width: parent.width
spacing: 10
Item {
width: parent.width
height: 22
visible: Bluetooth.defaultAdapter !== null
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Bluetooth"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 13
font.bold: true
}
ToggleSwitch {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: !!(Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled)
onToggled: if (Bluetooth.defaultAdapter) Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
}
}
Text {
visible: Bluetooth.defaultAdapter === null
text: "No Bluetooth adapter found"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Rectangle {
visible: !!(Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled)
width: loader.width
height: 30
color: scanMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: Theme.borderDim
Text {
anchors.centerIn: parent
text: (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) ? "Scanning…" : "Scan for devices"
color: (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering) ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea {
id: scanMa
anchors.fill: parent
hoverEnabled: true
onClicked: if (Bluetooth.defaultAdapter) Bluetooth.defaultAdapter.discovering = !Bluetooth.defaultAdapter.discovering
}
}
Text {
text: "Paired & nearby devices"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Text {
visible: Bluetooth.defaultAdapter !== null && Bluetooth.devices.values.length === 0
text: "No devices"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Repeater {
model: Bluetooth.devices
delegate: Rectangle {
width: loader.width
height: 40
color: btMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: dev.connected ? Theme.accent : Theme.borderDim
property var dev: modelData
Column {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 1
Text {
text: dev.name
color: dev.connected ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Text {
text: (dev.connected ? "Connected" : (dev.paired ? "Paired" : "Available"))
+ (dev.batteryAvailable ? " · " + Math.round(dev.battery <= 1 ? dev.battery * 100 : dev.battery) + "%" : "")
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
}
MouseArea {
id: btMa
anchors.fill: parent
hoverEnabled: true
onClicked: btCol.selected = dev
}
}
}
}
// --- Detail view ---------------------------------------------
Column {
visible: btCol.selected !== null
width: parent.width
spacing: 10
Process { id: btActionProc; command: [] }
Rectangle {
width: parent.width
height: 26
color: "transparent"
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "< Back"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea { anchors.fill: parent; onClicked: btCol.selected = null }
}
Text {
text: btCol.selected ? btCol.selected.name : ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 15
font.bold: true
}
Column {
width: parent.width
spacing: 4
property var rows: {
if (!btCol.selected) return [];
var d = btCol.selected;
var r = [
{ label: "Status", value: d.connected ? "Connected" : (d.paired ? "Paired" : "Available") },
{ label: "MAC Address", value: d.address || "—" },
{ label: "Type", value: d.icon || "unknown" },
{ label: "Trusted", value: d.trusted ? "Yes" : "No" }
];
if (d.batteryAvailable)
r.push({ label: "Battery", value: Math.round(d.battery <= 1 ? d.battery * 100 : d.battery) + "%" });
return r;
}
Repeater {
model: parent.rows
delegate: Item {
width: loader.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.value
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Rectangle {
width: parent.width
height: 34
color: btConnMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 2
border.color: (btCol.selected && btCol.selected.connected) ? Theme.danger : Theme.accent
Text {
anchors.centerIn: parent
text: (btCol.selected && btCol.selected.connected) ? "Disconnect"
: (btCol.selected && btCol.selected.paired) ? "Connect" : "Pair"
color: (btCol.selected && btCol.selected.connected) ? Theme.danger : Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: btConnMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!btCol.selected) return;
if (btCol.selected.connected) btCol.selected.disconnect();
else if (btCol.selected.paired) btCol.selected.connect();
else btCol.selected.pair();
}
}
}
Text {
visible: !!(btCol.selected && btCol.selected.paired)
text: "Forget this device"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea {
anchors.fill: parent
onClicked: { btCol.selected.forget(); btCol.selected = null; }
}
}
}
}
}
// --- Messages (notification history, via mako) -----------------------
// mako's IPC has no way to actually delete a history entry (only
// `dismiss` for currently-displayed notifications and a read-only
// `history` list) — so "clearing" is done client-side by remembering
// hidden ids across restarts and filtering them out of the list. The
// hidden ids naturally become irrelevant once mako's own history ring
// buffer rolls them out.
Component {
id: messagesPanel
Column {
id: msgCol
spacing: 6
width: loader.width
property var history: []
// The hidden-id list itself lives on `root` now (see top of
// file) — this Column gets destroyed every time the panel
// closes, so anything stored here alone would silently reset.
property var visibleHistory: {
var hidden = root.hiddenMessageIds;
var out = [];
for (var i = 0; i < msgCol.history.length; i++) {
if (hidden.indexOf(msgCol.history[i].id) === -1) out.push(msgCol.history[i]);
}
return out;
}
function refresh() { historyProc.running = true; }
function hideId(id) { root.hideMessageId(id); }
function clearAll() {
var ids = [];
for (var i = 0; i < msgCol.history.length; i++) ids.push(msgCol.history[i].id);
root.clearAllMessages(ids);
}
Process {
id: historyProc
command: ["makoctl", "history", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
msgCol.history = JSON.parse(text);
} catch (e) { msgCol.history = []; }
}
}
}
Component.onCompleted: msgCol.refresh()
Process { id: invokeProc; command: [] }
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Recent notifications"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 12
Text {
visible: msgCol.visibleHistory.length > 0
text: "Clear all"
color: Theme.danger
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea { anchors.fill: parent; onClicked: msgCol.clearAll() }
}
Text {
text: "Refresh"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea { anchors.fill: parent; onClicked: msgCol.refresh() }
}
}
}
Text {
visible: msgCol.visibleHistory.length === 0
text: "No notifications yet"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Repeater {
model: msgCol.visibleHistory
delegate: Rectangle {
id: msgRow
width: loader.width
height: bodyText.implicitHeight + summaryText.implicitHeight + 20
color: rowHover.hovered ? Theme.bgAlt : "transparent"
border.width: 1
border.color: Theme.borderDim
property var entry: modelData
HoverHandler { id: rowHover }
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 10
anchors.rightMargin: 30
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Row {
width: parent.width
Text {
text: entry.app_name || "unknown"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
}
Text {
id: summaryText
width: parent.width
text: entry.summary || ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
}
Text {
id: bodyText
width: parent.width
text: entry.body || ""
color: Theme.fgDim2
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
}
}
MouseArea {
id: msgMa
anchors.fill: parent
onClicked: {
invokeProc.command = ["makoctl", "invoke", "-n", String(entry.id)];
invokeProc.running = true;
}
}
Rectangle {
id: closeBtn
z: 1
visible: rowHover.hovered
width: 18
height: 18
anchors.right: parent.right
anchors.rightMargin: 6
anchors.top: parent.top
anchors.topMargin: 6
color: closeMa.containsMouse ? Theme.danger : "transparent"
border.width: 1
border.color: Theme.danger
Text {
anchors.centerIn: parent
text: "×"
color: closeMa.containsMouse ? Theme.bg : Theme.danger
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: closeMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: msgCol.hideId(entry.id)
}
}
}
}
}
}
// --- Screenshot -------------------------------------------------------
Component {
id: screenshotPanel
Column {
id: ssCol
spacing: 14
width: loader.width
property string mode: "region"
property string action: "both"
property var modes: [
{ key: "region", label: "Region (drag to select)" },
{ key: "full", label: "Full screen" },
{ key: "window", label: "Active window" }
]
property var actions: [
{ key: "both", label: "Save file + copy" },
{ key: "file", label: "Save file only" },
{ key: "clipboard", label: "Copy only" }
]
Text {
text: "Capture"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Column {
width: parent.width
spacing: 4
Repeater {
model: ssCol.modes
delegate: Rectangle {
width: loader.width
height: 32
color: modelData.key === ssCol.mode ? Theme.bgAlt : (modeMa.containsMouse ? Theme.bgAlt : "transparent")
border.width: modelData.key === ssCol.mode ? 2 : 1
border.color: modelData.key === ssCol.mode ? Theme.accent : Theme.borderDim
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
color: modelData.key === ssCol.mode ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea {
id: modeMa
anchors.fill: parent
hoverEnabled: true
onClicked: ssCol.mode = modelData.key
}
}
}
}
Text {
text: "Save"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Column {
width: parent.width
spacing: 4
Repeater {
model: ssCol.actions
delegate: Rectangle {
width: loader.width
height: 32
color: modelData.key === ssCol.action ? Theme.bgAlt : (actionMa.containsMouse ? Theme.bgAlt : "transparent")
border.width: modelData.key === ssCol.action ? 2 : 1
border.color: modelData.key === ssCol.action ? Theme.accent : Theme.borderDim
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
color: modelData.key === ssCol.action ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea {
id: actionMa
anchors.fill: parent
hoverEnabled: true
onClicked: ssCol.action = modelData.key
}
}
}
}
Rectangle {
width: parent.width
height: 36
color: captureMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 2
border.color: Theme.accent
Text {
anchors.centerIn: parent
text: "Capture"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 13
font.bold: true
}
MouseArea {
id: captureMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
// execDetached, not a local Process: closing the panel
// right after this swaps the Loader's sourceComponent,
// which destroys this whole Component (and any Process
// owned by it) before it would ever get to actually run.
Quickshell.execDetached(["screenshot", ssCol.mode, ssCol.action]);
root.requestClose();
}
}
}
Text {
text: "Saves to ~/Screenshots"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
}
}
}
// --- Volume -------------------------------------------------------
Component {
id: volumePanel
Column {
spacing: 12
width: loader.width
Item {
width: parent.width
height: 18
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.right: pct.left
anchors.rightMargin: 8
text: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.description : "No output"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
elide: Text.ElideRight
}
Text {
id: pct
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio)
? Math.round(Pipewire.defaultAudioSink.audio.volume * 100) + "%" : "--"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
}
SquareSlider {
width: parent.width
value: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio) ? Pipewire.defaultAudioSink.audio.volume : 0
onMoved: (v) => {
if (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio)
Pipewire.defaultAudioSink.audio.volume = v;
}
}
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Mute"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
ToggleSwitch {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onLabel: "Muted"
offLabel: "Unmuted"
checked: !!(Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio && Pipewire.defaultAudioSink.audio.muted)
onToggled: if (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio)
Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Text {
text: "Output device"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Repeater {
model: Pipewire.nodes
delegate: Rectangle {
property var node: modelData
visible: node.isSink && !node.isStream
width: loader.width
height: 32
color: outMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === node.id) ? Theme.accent : Theme.borderDim
Text {
anchors.left: parent.left
anchors.leftMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: node.description
color: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.id === node.id) ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
elide: Text.ElideRight
}
MouseArea {
id: outMa
anchors.fill: parent
hoverEnabled: true
onClicked: Pipewire.preferredDefaultAudioSink = node
}
}
}
}
}
// --- Brightness -------------------------------------------------------
Component {
id: brightnessPanel
Column {
spacing: 10
width: loader.width
property int pct: 50
Process {
id: getProc
command: ["brightness-ctl", "get"]
stdout: StdioCollector {
onStreamFinished: {
var n = parseInt(text.trim(), 10);
if (!isNaN(n)) slider.value = n / 100;
}
}
}
Process {
id: setProc
command: []
}
Component.onCompleted: getProc.running = true
Text {
text: "Monitor brightness"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
SquareSlider {
id: slider
width: parent.width
onMoved: (v) => {
setProc.command = ["brightness-ctl", "set", String(Math.round(v * 100))];
setProc.running = true;
}
}
}
}
// --- Devices (removable drives) -------------------------------------
Component {
id: devicesPanel
Column {
id: devCol
spacing: 6
width: loader.width
property var drives: []
function refresh() { lsblkProc.running = true; }
Process {
id: lsblkProc
command: ["lsblk", "-J", "-o", "NAME,LABEL,SIZE,RM,TYPE,MOUNTPOINT,PATH,FSTYPE"]
stdout: StdioCollector {
onStreamFinished: {
try {
var data = JSON.parse(text);
var out = [];
function walk(devs) {
for (var i = 0; i < devs.length; i++) {
var d = devs[i];
// Any data partition that isn't the running system
// itself — not just literally-removable (RM) ones,
// since internal secondary drives count too.
if (d.type === "part" && d.fstype !== "swap"
&& d.mountpoint !== "/" && d.mountpoint !== "/boot") {
out.push(d);
}
if (d.children) walk(d.children);
}
}
walk(data.blockdevices || []);
devCol.drives = out;
} catch (e) { devCol.drives = []; }
}
}
}
Component.onCompleted: devCol.refresh()
Text {
visible: devCol.drives.length === 0
text: "No extra drives found"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Repeater {
model: devCol.drives
delegate: Rectangle {
width: loader.width
height: 44
color: "transparent"
property var drive: modelData
Process {
id: mountProc
command: []
onExited: devCol.refresh()
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: (drive.label || drive.name) + " " + drive.size
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Row {
spacing: 10
Text {
text: drive.mountpoint ? "Unmount" : "Mount"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea {
anchors.fill: parent
onClicked: {
mountProc.command = drive.mountpoint
? ["udisksctl", "unmount", "-b", drive.path]
: ["udisksctl", "mount", "-b", drive.path];
mountProc.running = true;
}
}
}
Text {
visible: !!drive.mountpoint
text: "Open"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
MouseArea {
anchors.fill: parent
onClicked: {
mountProc.command = ["thunar", drive.mountpoint];
mountProc.running = true;
}
}
}
}
}
}
}
}
}
// --- System Monitor -------------------------------------------------------
// One process invocation per poll (system-telemetry, modules/scripts/
// system-telemetry.sh) instead of separate Process objects per metric —
// it reads /proc/stat twice 200ms apart internally to compute CPU%,
// /proc/meminfo for RAM, nvidia-smi or the AMD sysfs counters for GPU/
// VRAM (whichever's actually present), and `df -h` for / and /home —
// and returns it all as one JSON line.
Component {
id: systemPanel
Column {
id: sysCol
width: loader.width
spacing: 14
property var stats: ({})
property bool haveData: Object.keys(sysCol.stats).length > 0
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: if (!telemetryProc.running) telemetryProc.running = true
}
Process {
id: telemetryProc
command: ["system-telemetry"]
stdout: StdioCollector {
onStreamFinished: {
try { sysCol.stats = JSON.parse(text); } catch (e) { /* keep last good reading */ }
}
}
}
Text {
visible: !sysCol.haveData
text: "Reading system stats..."
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
// -- CPU --
Column {
visible: sysCol.haveData
width: sysCol.width
spacing: 4
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
text: "CPU"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
Text {
anchors.right: parent.right
text: (sysCol.stats.cpuPct !== undefined ? sysCol.stats.cpuPct : 0) + "%"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
Meter { width: parent.width; value: (sysCol.stats.cpuPct || 0) / 100 }
}
// -- RAM --
Column {
visible: sysCol.haveData
width: sysCol.width
spacing: 4
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
text: "Memory"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
Text {
anchors.right: parent.right
text: ((sysCol.stats.memUsedKb || 0) / 1048576).toFixed(1) + " / "
+ ((sysCol.stats.memTotalKb || 0) / 1048576).toFixed(1) + " GB"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
Meter {
width: parent.width
value: sysCol.stats.memTotalKb > 0 ? sysCol.stats.memUsedKb / sysCol.stats.memTotalKb : 0
}
}
// -- GPU / VRAM --
Text {
visible: sysCol.haveData && (sysCol.stats.gpuPct === undefined || sysCol.stats.gpuPct < 0)
text: "No GPU metrics available"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
Column {
visible: sysCol.haveData && sysCol.stats.gpuPct >= 0
width: sysCol.width
spacing: 4
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
text: "GPU"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
Text {
anchors.right: parent.right
text: (sysCol.stats.gpuPct !== undefined ? sysCol.stats.gpuPct : 0) + "%"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
Meter { width: parent.width; value: (sysCol.stats.gpuPct || 0) / 100 }
}
Column {
visible: sysCol.haveData && sysCol.stats.vramTotalMb > 0
width: sysCol.width
spacing: 4
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
text: "VRAM"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
Text {
anchors.right: parent.right
text: (sysCol.stats.vramUsedMb || 0) + " / " + (sysCol.stats.vramTotalMb || 0) + " MB"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
Meter {
width: parent.width
value: sysCol.stats.vramTotalMb > 0 ? sysCol.stats.vramUsedMb / sysCol.stats.vramTotalMb : 0
}
}
// -- Disks --
Column {
visible: sysCol.haveData && sysCol.stats.disks && sysCol.stats.disks.length > 0
width: sysCol.width
spacing: 8
Text {
text: "STORAGE"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
Repeater {
model: sysCol.stats.disks || []
delegate: Column {
width: parent.width
spacing: 4
property var disk: modelData
Item {
width: parent.width
height: 16
Text {
anchors.left: parent.left
text: disk.mount
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Text {
anchors.right: parent.right
text: disk.used + " / " + disk.size
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
Meter { width: parent.width; value: (parseFloat(disk.pct) || 0) / 100 }
}
}
}
}
}
// --- Weather ---------------------------------------------------------
// weather-fetch (modules/scripts/weather-fetch.sh) does the actual
// network work — IP-geolocates via ip-api.com by default (or geocodes
// an explicitly typed city via Open-Meteo's own geocoding endpoint),
// then pulls current + hourly conditions from Open-Meteo. Both are
// free and keyless, matching curl everywhere else in this file rather
// than QML's own networking (Image loads already had to route around
// 403s from a CDN that didn't like Qt's request fingerprint — curl
// has been the reliable path for every other external fetch here).
Component {
id: weatherPanel
Column {
id: wxCol
width: loader.width
spacing: 12
property var forecast: ({})
property bool haveData: !!wxCol.forecast.current
property bool hasError: !!wxCol.forecast.error
Item {
id: wxSettings
property string city: ""
property string stateFile: Quickshell.stateDir + "/weather-settings.json"
function save() {
wxSaveProc.command = ["sh", "-c",
"mkdir -p \"$(dirname \"$2\")\" && printf '%s' \"$1\" > \"$2\"",
"--", JSON.stringify({ city: wxSettings.city }), wxSettings.stateFile];
wxSaveProc.running = true;
}
Process { id: wxSaveProc }
Process {
id: wxLoadProc
command: ["cat", wxSettings.stateFile]
stdout: StdioCollector {
onStreamFinished: {
try {
var d = JSON.parse(text);
if (d.city !== undefined) wxSettings.city = d.city;
} catch (e) { /* no state file yet — defaults stand */ }
wxCol.refresh();
}
}
}
Component.onCompleted: wxLoadProc.running = true
}
function refresh() {
fetchProc.command = wxSettings.city.length > 0
? ["weather-fetch", wxSettings.city] : ["weather-fetch"];
fetchProc.running = true;
}
Process {
id: fetchProc
stdout: StdioCollector {
onStreamFinished: {
try { wxCol.forecast = JSON.parse(text); } catch (e) { wxCol.forecast = { error: "bad response" }; }
}
}
}
// Open-Meteo's current reading updates roughly every 15
// minutes on their end — polling faster would just repeat the
// same numbers.
Timer { interval: 900000; running: true; repeat: true; onTriggered: wxCol.refresh() }
// WMO weather codes -> a short label + a plain Unicode symbol
// (not a Nerd Font glyph — those need exact codepoints from
// the specific icon set and guessing wrong renders as a
// tofu box; these are plain emoji, safe under any font with
// emoji fallback, which every font on this system has).
function describeCode(code) {
if (code === 0) return { label: "Clear sky", icon: "☀" };
if (code === 1) return { label: "Mostly clear", icon: "🌤" };
if (code === 2) return { label: "Partly cloudy", icon: "⛅" };
if (code === 3) return { label: "Overcast", icon: "☁" };
if (code === 45 || code === 48) return { label: "Fog", icon: "🌫" };
if (code >= 51 && code <= 57) return { label: "Drizzle", icon: "🌦" };
if (code >= 61 && code <= 67) return { label: "Rain", icon: "🌧" };
if (code >= 71 && code <= 77) return { label: "Snow", icon: "❄" };
if (code >= 80 && code <= 82) return { label: "Showers", icon: "🌧" };
if (code === 85 || code === 86) return { label: "Snow showers", icon: "🌨" };
if (code >= 95) return { label: "Thunderstorm", icon: "⛈" };
return { label: "Unknown", icon: "?" };
}
// -- City search (blank = auto IP-geolocate) --
Row {
width: wxCol.width
spacing: 6
Rectangle {
width: parent.width - 66
height: 30
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
TextInput {
id: cityInput
anchors.fill: parent
anchors.margins: 8
text: wxSettings.city
onTextEdited: wxSettings.city = text
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
clip: true
Keys.onReturnPressed: { wxSettings.save(); wxCol.forecast = {}; wxCol.refresh(); }
Text {
visible: cityInput.text.length === 0
text: "City (blank = auto-detect)"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
}
}
Rectangle {
width: 60
height: 30
color: wxSearchMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: Theme.accent
Text {
anchors.centerIn: parent
text: "Set"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
MouseArea {
id: wxSearchMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: { wxSettings.save(); wxCol.forecast = {}; wxCol.refresh(); }
}
}
}
Text {
visible: !wxCol.haveData && !wxCol.hasError
text: "Loading weather..."
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
Text {
visible: wxCol.hasError
text: "Couldn't load weather — check the city name or try again later."
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
wrapMode: Text.WordWrap
width: wxCol.width
}
// -- Current conditions --
Column {
visible: wxCol.haveData
width: wxCol.width
spacing: 4
Text {
text: [wxCol.forecast.place, wxCol.forecast.country].filter((s) => s && s.length > 0).join(", ")
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Row {
spacing: 12
Text {
text: wxCol.haveData ? wxCol.describeCode(wxCol.forecast.current.weather_code).icon : ""
font.pixelSize: 40
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: wxCol.haveData ? Math.round(wxCol.forecast.current.temperature_2m) + "°C" : ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 24
font.bold: true
}
Text {
text: wxCol.haveData ? wxCol.describeCode(wxCol.forecast.current.weather_code).label : ""
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
}
Row {
spacing: 16
Text {
text: wxCol.haveData ? ("Wind: " + wxCol.forecast.current.wind_speed_10m + " km/h") : ""
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
Text {
text: wxCol.haveData ? ("Precip: " + wxCol.forecast.current.precipitation + " mm") : ""
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
}
}
Rectangle { visible: wxCol.haveData; width: wxCol.width; height: 1; color: Theme.borderDim }
// -- Hourly forecast timeline --
Column {
visible: wxCol.haveData && !!wxCol.forecast.hourly
width: wxCol.width
spacing: 8
Text {
text: "NEXT HOURS"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
Flow {
width: wxCol.width
spacing: 6
Repeater {
// Only future hours from "now" — Open-Meteo's hourly
// array starts at local midnight, not the current
// hour, so without this the timeline would open
// showing most of a day that's already passed.
model: {
if (!wxCol.haveData || !wxCol.forecast.hourly) return [];
var nowIso = wxCol.forecast.current.time.substring(0, 13);
var times = wxCol.forecast.hourly.time;
var startIdx = 0;
for (var i = 0; i < times.length; i++) {
if (times[i].substring(0, 13) >= nowIso) { startIdx = i; break; }
}
var out = [];
for (var j = startIdx; j < Math.min(startIdx + 8, times.length); j++) out.push(j);
return out;
}
delegate: Rectangle {
property int idx: modelData
width: (wxCol.width - 30) / 4
height: 64
color: Theme.bgAlt
border.width: 1
border.color: Theme.borderDim
Column {
anchors.centerIn: parent
spacing: 2
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: wxCol.forecast.hourly.time[idx].substring(11, 16)
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: wxCol.describeCode(wxCol.forecast.hourly.weather_code[idx]).icon
font.pixelSize: 16
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Math.round(wxCol.forecast.hourly.temperature_2m[idx]) + "°"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
font.bold: true
}
}
}
}
}
}
}
}
// --- Power -------------------------------------------------------
Component {
id: powerPanel
Column {
spacing: 4
width: loader.width
Process { id: powerProc; command: [] }
property var actions: [
{ label: " Lock", cmd: ["hyprlock"] },
{ label: " Suspend", cmd: ["systemctl", "suspend"] },
{ label: " Reboot", cmd: ["systemctl", "reboot"] },
{ label: " Shutdown", cmd: ["systemctl", "poweroff"] },
{ label: " Log out", cmd: ["hyprctl", "dispatch", "exit"] }
]
Repeater {
model: parent.actions
delegate: Rectangle {
width: loader.width
height: 32
color: pma.containsMouse ? Theme.bgAlt : "transparent"
Text {
anchors.verticalCenter: parent.verticalCenter
x: 8
text: modelData.label
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 13
}
MouseArea {
id: pma
anchors.fill: parent
hoverEnabled: true
onClicked: { powerProc.command = modelData.cmd; powerProc.running = true; }
}
}
}
}
}
}