// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. pragma Singleton import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick // Desktop modes as state. One file per mode under $XDG_RUNTIME_DIR, holding // "0" or "1"; a missing file means off. The runtime directory is tmpfs, so a // reboot resets every mode with no cleanup code here. // // The files are the interface, not this singleton: statusctl reads and writes // them directly so it works while the shell is down, and the FileView watch // means an external write repaints the drawer with no polling. It also means // a mode set from outside still fires its effects, because the watch reaches // the same handler a tile click would. Singleton { id: root readonly property string dir: Quickshell.env("XDG_RUNTIME_DIR") || "/tmp" readonly property bool dnd: dndFile.value readonly property bool presentation: presFile.value readonly property int activeCount: (root.dnd ? 1 : 0) + (root.presentation ? 1 : 0) // Set once by shell.qml. IdleInhibitor does nothing with a null window, // and the singleton has no window of its own to offer. property var inhibitWindow: null // What dnd was before presentation mode turned it on, so turning // presentation mode off restores it rather than clearing it. Held here // rather than in a file: it is meaningful only while presentation mode is // on, and presentation mode does not survive a reboot. // // Known limit: on a shell restart while presentation is already on, // onPresentationChanged can run before the dnd FileView has loaded, so the // recorded prior value depends on which file loads first. property bool dndBeforePresentation: false function setMode(name, on) { if (name === "dnd") { dndFile.write(on); } else if (name === "presentation") { presFile.write(on); } } function toggleMode(name) { if (name === "dnd") root.setMode("dnd", !root.dnd); else if (name === "presentation") root.setMode("presentation", !root.presentation); } // Effects follow the mode rather than the setter, so a mode set by // statusctl while the drawer is closed asserts them too. onPresentationChanged: { if (root.presentation) { root.dndBeforePresentation = root.dnd; root.setMode("dnd", true); root.runBreaktimer("pause"); } else { root.setMode("dnd", root.dndBeforePresentation); root.runBreaktimer("resume"); } } function runBreaktimer(verb) { breakProc.command = ["breaktimer.sh", verb]; breakProc.running = false; breakProc.running = true; } // breaktimer owns its own state file; this only calls its verbs. Two // writers on that file would race with its daemon loop, which rewrites it // on every phase change. Process { id: breakProc onExited: code => { if (code !== 0) console.warn("status: breaktimer.sh " + breakProc.command[1] + " exited " + code); } } // Wayland idle inhibit. The compositor advertises // zwp_idle_inhibit_manager_v1 and hypridle honours it, so no D-Bus path // is needed even though elogind runs here. IdleInhibitor { window: root.inhibitWindow enabled: root.presentation && root.inhibitWindow !== null } component ModeFile: FileView { id: mf property bool value: false // FileView is documented to fire fileChanged on its own setText, so a // write would re-enter this handler. Comparing before assigning makes // that harmless: the reparse yields the value just written and the // binding does not change. function reparse() { const t = mf.text().trim(); const v = (t === "1"); if (v !== mf.value) mf.value = v; } function write(on) { const s = on ? "1\n" : "0\n"; mf.value = on; mf.setText(s); } // Both are the documented defaults in 0.3.1, set explicitly because // the CLI depends on them: statusctl watches close_write,moved_to // precisely because an atomic write lands as a rename, so a future // release flipping this default would break the watcher silently. atomicWrites: true watchChanges: true printErrors: false onFileChanged: mf.reload() onLoaded: mf.reparse() // A missing file is the off state, not an error worth logging. onLoadFailed: mf.value = false } ModeFile { id: dndFile; path: root.dir + "/status.dnd" } ModeFile { id: presFile; path: root.dir + "/status.presentation" } }