some qs fixes

This commit is contained in:
nvrl committed 2026-10-09 04:17:36 +02:00
1 parent 32d5683fad
commit 6addb504e6
26 files changed
+510 -82

No files matched your search

+36 -2
View File
@@ -168,8 +168,41 @@ Singleton {
function logout() { root.run(["uwsm", "stop"]); }
function reboot() { root.run(["systemctl", "reboot"]); }
function shutdown() { root.run(["systemctl", "poweroff"]); }
function suspend() { root.lock(); root.run(["systemctl", "suspend"]); }
function hibernate() { root.lock(); root.run(["systemctl", "hibernate"]); }
function suspend() { root.sleep(["systemctl", "suspend"]); }
function hibernate() { root.sleep(["systemctl", "hibernate"]); }
// Set by lock/Lock.qml once the compositor has the lock surfaces up.
property bool lockSecure: false
// A sleep waiting for the lock. Suspending first would leave the desktop
// on screen for a moment on wake, before the lock had drawn.
property var pendingSleep: null
function sleep(argv: var) {
root.lock();
if (root.lockSecure) {
root.run(argv);
return;
}
root.pendingSleep = argv;
sleepFallback.restart();
}
function _sleepNow() {
if (!root.pendingSleep) return;
const argv = root.pendingSleep;
root.pendingSleep = null;
sleepFallback.stop();
root.run(argv);
}
onLockSecureChanged: if (root.lockSecure) root._sleepNow()
// A lock that never comes up must not keep the machine awake on battery.
Timer {
id: sleepFallback
interval: 3000
onTriggered: root._sleepNow()
}
function reloadShell() { Quickshell.reload(true); }
// ------------------------------------------------------------- helpers
@@ -195,6 +228,7 @@ Singleton {
function toggleDnd(): void { Notifs.toggleDnd(); }
function clearNotifications(): void { Notifs.clearAll(); }
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
function gameMode(on: bool): void { GameMode.active = on; }
function overview(): void { root.toggleOverview(); }
function overviewNext(): void { root.overviewStep(1); }
function overviewPrev(): void { root.overviewStep(-1); }
+16 -2
View File
@@ -107,8 +107,15 @@ Singleton {
root.lastCost = 0;
}
// Set while a run is being killed on purpose, so its exit is not reported
// as a failure.
property bool stopping: false
function stop(): void {
if (proc.running) proc.running = false;
if (proc.running) {
root.stopping = true;
proc.running = false;
}
root.busy = false;
}
@@ -125,7 +132,10 @@ Singleton {
root.exitCode = 0;
root.stderrText = "";
proc.running = false;
if (proc.running) {
root.stopping = true;
proc.running = false;
}
// stdin is closed explicitly. Inherited from the shell it never ends,
// and the CLI waits three seconds for input that is never coming
// before it starts on the question.
@@ -236,6 +246,10 @@ Singleton {
}
onExited: (code, status) => {
if (root.stopping) {
root.stopping = false;
return;
}
root.busy = false;
root.exitCode = code;
if (code === 0 || root.error !== "") return;
+3 -2
View File
@@ -49,9 +49,10 @@ Singleton {
onMicPercentChanged: micBumped()
onMicMutedChanged: micBumped()
// Capped at 100%: past it PipeWire only amplifies in software, and clips.
function setVolume(v: real) {
if (!sink?.audio) return;
sink.audio.volume = Math.max(0, Math.min(1.5, v));
sink.audio.volume = Math.max(0, Math.min(1, v));
}
function changeVolume(delta: real) {
@@ -65,7 +66,7 @@ Singleton {
function setMicVolume(v: real) {
if (!source?.audio) return;
source.audio.volume = Math.max(0, Math.min(1.5, v));
source.audio.volume = Math.max(0, Math.min(1, v));
}
function toggleMicMute() {
+27 -5
View File
@@ -37,6 +37,8 @@ Singleton {
const v = parseInt(text().trim());
if (!isNaN(v) && v !== root.raw) {
root.raw = v;
// Settled, or moved by something else: step from here again.
if (!setter.running && root.pending < 0) root.target = -1;
root.bumped();
}
}
@@ -45,24 +47,44 @@ Singleton {
// sysfs writes do not always fire inotify for backlight, so poll gently too.
Timer {
interval: 1000
running: true
running: !GameMode.active
repeat: true
onTriggered: currentFile.reload()
}
// The level last asked for, until the backlight reports it. Steps build on
// it rather than on `percent`, which only catches up on the next read, so
// a fast scroll adds up instead of repeating one step.
property int target: -1
// A level asked for while a write is still running. Restarting the write
// would kill it half done, so the newest request waits and the rest drop.
property int pending: -1
Process {
id: setter
onExited: {
currentFile.reload();
if (root.pending < 0) return;
const next = root.pending;
root.pending = -1;
root._write(next);
}
}
function _write(p: int) {
setter.command = ["brightnessctl", "-d", root.device, "-q", "set", p + "%"];
setter.running = true;
}
function setPercent(p: real) {
const clamped = Math.max(1, Math.min(100, Math.round(p)));
setter.running = false;
setter.command = ["brightnessctl", "-d", root.device, "-q", "set", clamped + "%"];
setter.running = true;
root.target = clamped;
if (setter.running) root.pending = clamped;
else root._write(clamped);
}
function change(delta: real) {
setPercent(percent + delta);
setPercent((root.target >= 0 ? root.target : percent) + delta);
}
readonly property string icon: {
+24
View File
@@ -0,0 +1,24 @@
pragma Singleton
import QtQuick
import Quickshell
// A fullscreen game is on screen. Hyprland decides (~/.config/hypr/lua/gamemode.lua)
// and says so with `qs ipc call shell gameMode true|false`.
//
// The game covers the hull, but a covered surface still renders: Hyprland keeps
// sending it frame callbacks, so anything that moves keeps redrawing at the
// panel's refresh rate with nobody watching. While a game runs the shell stops
// what it does on its own:
//
// Sys the fluxo stream, so no telemetry wakes the shell
// Brightness the backlight poll
// Media the position tick, and the equaliser on the bar
// Rail the critical battery's pulse
// Workspaces the urgent pip's pulse
// Notifs toasts, except critical ones; everything still reaches the log
//
// Anything you ask for yourself, the OSD included, still shows.
Singleton {
property bool active: false
}
+1 -1
View File
@@ -74,7 +74,7 @@ Singleton {
// MPRIS position is pull-only; tick it so the seek bar moves.
Timer {
interval: 1000
running: root.playing && root.positionSupported
running: root.playing && root.positionSupported && !GameMode.active
repeat: true
onTriggered: if (root.active) root.active.positionChanged()
}
+27 -15
View File
@@ -16,10 +16,13 @@ Singleton {
readonly property var devices: Networking.devices?.values ?? []
readonly property var wifiDevices: devices.filter(d => d.type === DeviceType.Wifi)
readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired)
// Docker's veth pairs show up as wired too; NetworkManager leaves them
// unmanaged, and so does the popout.
readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired && d.nmManaged)
readonly property WifiDevice wifi: wifiDevices.length > 0 ? wifiDevices[0] : null
readonly property WiredDevice wired: wiredDevices.length > 0 ? wiredDevices[0] : null
// The port in use, or failing that the first one there is.
readonly property WiredDevice wired: wiredDevices.find(d => d.connected) ?? wiredDevices[0] ?? null
readonly property bool wiredUp: wired?.connected ?? false
@@ -55,14 +58,8 @@ Singleton {
if (!root.wifiEnabled) return "󰤭";
if (!root.activeWifi) return "󰤯";
const s = root.activeWifi.signalStrength ?? 0;
// Strength is only sampled while a scan is running, which the shell
// does on demand. Connected-but-unsampled should read as full, not dead.
if (s <= 0) return "󰤨";
if (s >= 80) return "󰤨";
if (s >= 60) return "󰤥";
if (s >= 40) return "󰤢";
if (s >= 20) return "󰤟";
return "󰤯";
// Connected but not yet sampled should read as full, not dead.
return s <= 0 ? "󰤨" : root.signalIcon(s);
}
readonly property string label: {
@@ -72,14 +69,29 @@ Singleton {
return "Offline";
}
function signalIcon(strength: int): string {
if (strength >= 80) return "󰤨";
if (strength >= 60) return "󰤥";
if (strength >= 40) return "󰤢";
if (strength >= 20) return "󰤟";
// Strength is a fraction, 0 to 1, not a percentage.
function signalIcon(strength: real): string {
if (strength >= 0.8) return "󰤨";
if (strength >= 0.6) return "󰤥";
if (strength >= 0.4) return "󰤢";
if (strength >= 0.2) return "󰤟";
return "󰤯";
}
// "1 Gb/s" when up, otherwise why not. NetworkManager reports Mb/s.
function wiredDetail(d): string {
if (!d.hasLink) return d.name + " · no cable";
if (!d.connected) return d.name + " · disconnected";
const mbps = d.linkSpeed ?? 0;
if (mbps <= 0) return d.name;
return d.name + " · " + (mbps >= 1000 ? (mbps / 1000) + " Gb/s" : mbps + " Mb/s");
}
function toggleWired(d) {
if (d.connected) d.disconnect();
else if (d.network) d.network.connect();
}
function secured(n): bool {
return n && n.security !== undefined
&& n.security !== WifiSecurityType.Open
+87 -19
View File
@@ -16,7 +16,7 @@ Singleton {
property list<Notification> history: []
property int maxHistory: 60
readonly property int count: history.length
readonly property int count: history.filter(n => n).length
// The history by sender, for the log: one entry per app, newest first
// inside each, the apps ordered by their newest notification.
@@ -24,14 +24,21 @@ Singleton {
const byApp = {};
const order = [];
for (const n of root.history) {
if (!n) continue;
const app = n.appName || "System";
if (!(app in byApp)) { byApp[app] = []; order.push(app); }
byApp[app].push(n);
}
return order.map(app => ({ app: app, items: byApp[app] }));
}
// The server hands out ids in rising order, so everything in the log above
// the newest id seen when the log was last opened is unread. Derived from
// the history rather than counted, so dismissing an entry anywhere takes
// it off the count too.
property int readId: 0
readonly property int unread: history.filter(n => n && n.id > root.readId).length
readonly property bool hasUnread: unread > 0
property int unread: 0
NotificationServer {
id: server
@@ -49,42 +56,103 @@ Singleton {
onNotification: notif => {
// Hold the notification open until we drop it ourselves.
notif.tracked = true;
root.adopt(notif);
root.history = [notif, ...root.history].slice(0, root.maxHistory);
root.unread++;
const next = [notif, ...root.history.filter(n => n)];
for (const n of next.slice(root.maxHistory)) root.release(n);
root.history = next.slice(0, root.maxHistory);
if (!root.dnd) root.popupRequested(notif);
// A game holds every toast but a critical one; the log has them all.
const held = GameMode.active && notif.urgency !== NotificationUrgency.Critical;
if (!root.dnd && !held) root.popupRequested(notif);
}
}
// The server outlives a reload, and keeps what it was tracking; this
// object does not. Take those back into the log, already read.
Component.onCompleted: {
const kept = server.trackedNotifications.values.slice().sort((a, b) => b.id - a.id);
for (const n of kept) root.adopt(n);
root.history = kept.slice(0, root.maxHistory);
for (const n of kept.slice(root.maxHistory)) root.release(n);
root.markRead();
}
// An app may close its own notification at any time, and the server then
// destroys the object, leaving a null in the history. The lock keeps it
// alive, frozen as it was, until the log lets it go; closing it only takes
// it off screen.
function adopt(notif: Notification) {
root.holds[notif.id] = hold.createObject(root, { object: notif });
notif.closed.connect(() => {
root.retracted = Object.assign({}, root.retracted, { [notif.id]: true });
root.notifRetracted(notif);
});
}
Component {
id: hold
RetainableLock { locked: true }
}
// One lock per notification in the history, by id.
property var holds: ({})
// Ids of the notifications whose app has closed them. They stay in the log
// but can no longer be acted on.
property var retracted: ({})
signal popupRequested(Notification notif)
signal notifRetracted(Notification notif)
function isRetracted(notif: Notification): bool {
return !notif || root.retracted[notif.id] === true;
}
// Close a notification if its app has not, and let the object go.
function release(notif: Notification) {
if (!notif) return;
const id = notif.id;
if (!root.isRetracted(notif)) notif.dismiss();
root.holds[id]?.destroy();
delete root.holds[id];
if (id in root.retracted) {
const next = Object.assign({}, root.retracted);
delete next[id];
root.retracted = next;
}
}
function dismiss(notif: Notification) {
if (!notif) return;
const idx = root.history.indexOf(notif);
if (idx !== -1) {
const next = root.history.slice();
next.splice(idx, 1);
root.history = next;
}
notif.dismiss();
root.history = root.history.filter(n => n && n !== notif);
root.release(notif);
}
function dismissApp(app: string) {
const gone = root.history.filter(n => (n.appName || "System") === app);
root.history = root.history.filter(n => (n.appName || "System") !== app);
for (const n of gone) n.dismiss();
const from = n => n.appName || "System";
const gone = root.history.filter(n => n && from(n) === app);
root.history = root.history.filter(n => n && from(n) !== app);
for (const n of gone) root.release(n);
}
function clearAll() {
const all = root.history.slice();
const all = root.history.filter(n => n);
root.history = [];
root.unread = 0;
for (const n of all) n.dismiss();
for (const n of all) root.release(n);
}
function markRead() {
root.unread = 0;
root.readId = Math.max(root.readId, ...root.history.filter(n => n).map(n => n.id));
}
// Run one of a notification's actions and take it out of the log. Done
// here rather than in the card: invoking closes a non-resident
// notification, which takes its toast, and the card with it, off screen
// before the card's handler has finished.
function invoke(notif: Notification, action: NotificationAction) {
if (!notif || root.isRetracted(notif)) return;
action.invoke();
root.dismiss(notif);
}
function toggleDnd() {
+11 -1
View File
@@ -167,7 +167,17 @@ Singleton {
id: retry
interval: 2000
repeat: false
onTriggered: source.running = true
onTriggered: source.running = !GameMode.active
}
// Nothing reads the meters while a game covers the bar. Drop the stream
// and pick it up again afterwards; the first lines back refresh every value.
Connections {
target: GameMode
function onActiveChanged() {
source.running = !GameMode.active;
}
}
// ------------------------------------------------------------ formatting
+3 -1
View File
@@ -19,8 +19,10 @@ Singleton {
// The wallpaper to use when no particular monitor is being asked about.
property string path: ""
// Encoded segment by segment: a `#` or `?` in a file name would otherwise
// be read as the start of a fragment or a query.
readonly property url source: root.path !== ""
? Qt.resolvedUrl("file://" + root.path)
? "file://" + root.path.split("/").map(encodeURIComponent).join("/")
: Theme.fallbackWallpaper
signal changed()