pragma Singleton // A single focus session bound to a Hyprland workspace. The deadline and the // few user decisions are persisted; the once-per-second display value is not, // so an active session does not turn into a constant disk writer. import Quickshell import Quickshell.Hyprland import Quickshell.Io import QtQuick import qs.config Singleton { id: root readonly property alias active: state.active readonly property alias paused: state.paused readonly property alias capsuleVisible: state.capsuleVisible readonly property alias workspaceId: state.workspaceId readonly property alias workspaceLabel: state.workspaceLabel readonly property alias monitorName: state.monitorName // Kept in memory only. The persisted deadline is enough to reconstruct it // after a reload, and paused sessions persist their fixed remainder. property double nowMs: Date.now() readonly property int remainingSeconds: { if (!state.active) return 0; if (state.paused) return Math.max(0, state.pausedRemainingSeconds); return Math.max(0, Math.ceil((state.deadlineMs - root.nowMs) / 1000)); } readonly property string remainingText: { const total = root.remainingSeconds; const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const seconds = total % 60; const pad = value => String(value).padStart(2, "0"); return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`; } readonly property string statusText: state.paused ? "Paused" : "Do Not Disturb ยท Caffeine"; FileView { id: stateFile path: Quickshell.stateDir + "/focus-session.json" blockLoading: true printErrors: false atomicWrites: true onLoaded: restoreTimer.restart() JsonAdapter { id: state property bool active: false property bool paused: false property bool capsuleVisible: false property int workspaceId: 0 property string workspaceLabel: "" property string monitorName: "" property double deadlineMs: 0 property int pausedRemainingSeconds: 0 property bool previousDnd: false property bool previousCaffeine: false } } Timer { interval: 1000 repeat: true running: state.active && !state.paused triggeredOnStart: true onTriggered: { root.nowMs = Date.now(); if (state.deadlineMs > 0 && root.nowMs >= state.deadlineMs) root.end(true); } } // Defer restoration until the other service singletons have completed // construction. An active focus session owns these states across reloads. Timer { id: restoreTimer interval: 0 onTriggered: root.restore() } Component.onCompleted: { // `blockLoading` usually makes this true immediately. Keep the loaded // signal above as the authoritative path for slower storage. if (stateFile.loaded) restoreTimer.restart(); } function start(minutes: int): void { const duration = Math.max(1, minutes); const workspace = root.currentPositiveWorkspace(); // Replacing an active session first releases exactly the state it owns. if (state.active) root.end(false); state.previousDnd = Notifs.doNotDisturb; state.previousCaffeine = Caffeine.enabled; Notifs.doNotDisturb = true; Caffeine.enabled = true; state.workspaceId = workspace ? workspace.id : 1; state.workspaceLabel = root.displayWorkspaceName(workspace); state.monitorName = Hyprland.focusedMonitor?.name ?? ""; state.pausedRemainingSeconds = duration * 60; state.deadlineMs = Date.now() + duration * 60 * 1000; state.paused = false; state.capsuleVisible = true; state.active = true; root.nowMs = Date.now(); stateFile.writeAdapter(); } function startDefault(): void { root.start(Settings.focusDurationMinutes); } // The discoverable "start or show" behavior used by quick settings and // Super+Shift+F. A second invocation never destroys work accidentally. function reveal(): void { if (!state.active) { root.startDefault(); return; } state.capsuleVisible = true; stateFile.writeAdapter(); } function dismiss(): void { state.capsuleVisible = false; stateFile.writeAdapter(); } function pauseOrResume(): void { if (!state.active) return; root.nowMs = Date.now(); if (state.paused) { state.deadlineMs = root.nowMs + state.pausedRemainingSeconds * 1000; state.paused = false; } else { state.pausedRemainingSeconds = root.remainingSeconds; state.paused = true; } stateFile.writeAdapter(); } function activateWorkspace(): void { if (!state.active || state.workspaceId <= 0) return; const all = Hyprland.workspaces?.values ?? []; for (const workspace of all) { if (workspace && workspace.id === state.workspaceId) { workspace.activate(); return; } } // Dynamic workspaces can disappear while the session is running. // Focusing the stored positive id recreates the destination cleanly. Hyprland.dispatch(`hl.dsp.focus({ workspace = ${state.workspaceId} })`); } function end(completed: bool): void { if (!state.active) return; const previousDnd = state.previousDnd; const previousCaffeine = state.previousCaffeine; const finishedWorkspaceId = state.workspaceId; const finishedWorkspace = state.workspaceLabel; state.active = false; state.paused = false; state.capsuleVisible = false; state.workspaceId = 0; state.workspaceLabel = ""; state.monitorName = ""; state.deadlineMs = 0; state.pausedRemainingSeconds = 0; stateFile.writeAdapter(); Notifs.doNotDisturb = previousDnd; Caffeine.enabled = previousCaffeine; if (completed) { StatusEvents.publish({ key: "focus-complete", glyph: "\u{F051F}", title: "Focus complete", detail: finishedWorkspace ? `${finishedWorkspace} session finished` : "Your focus session finished", tone: "ok", priority: StatusEvents.importantPriority, actionId: "open-workspace", actionData: String(finishedWorkspaceId) }); Quickshell.execDetached([ "notify-send", "-a", "Panama", "-i", "appointment-soon-symbolic", "Focus complete", finishedWorkspace ? `${finishedWorkspace} session finished` : "Your focus session finished" ]); } } function restore(): void { root.nowMs = Date.now(); if (!state.active) return; if (!state.paused && state.deadlineMs <= root.nowMs) { root.end(true); return; } // Caffeine and the in-process notification server restart on a shell // reload. The persisted session therefore has to reclaim both states. Notifs.doNotDisturb = true; Caffeine.enabled = true; } function currentPositiveWorkspace(): var { const focused = Hyprland.focusedWorkspace; if (focused && focused.id > 0) return focused; const all = Hyprland.workspaces?.values ?? []; for (const workspace of all) { if (workspace && workspace.id > 0) return workspace; } return null; } function displayWorkspaceName(workspace: var): string { if (!workspace) return "Workspace 1"; const name = workspace.name || String(workspace.id); return /^\d+$/.test(name) ? `Workspace ${name}` : name; } }