nexoniarz-config-nixos/dotfiles/quickshell/RightFlyout.qml
2026-08-30 01:26:52 +02:00

1485 lines
62 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",
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 "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
}
Repeater {
model: Networking.devices
delegate: Column {
width: loader.width
spacing: 4
property var device: modelData
// 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 (device.scannerEnabled !== undefined) device.scannerEnabled = true;
Component.onDestruction: if (device.scannerEnabled !== undefined) device.scannerEnabled = false;
Text {
text: device.name
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
// 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
// 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
}
}
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
// --- 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;
}
}
}
}
}
}
}
}
}
// --- 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; }
}
}
}
}
}
}