pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Common Singleton { id: root readonly property var log: Log.scoped("SessionsService") property var sessions: [] property string currentSessionId: "" property string currentSeat: "" property bool refreshing: false signal switchFailed(string sessionId, string username, string message) signal switchRequested function isCurrent(sessionId) { return sessionId === currentSessionId; } function findByUsername(username) { for (let i = 0; i < sessions.length; i++) { const s = sessions[i]; if (s.username === username && !s.current) return s; } return null; } function findById(sessionId) { for (let i = 0; i < sessions.length; i++) { if (sessions[i].sessionId === sessionId) return sessions[i]; } return null; } function otherSessions() { return sessions.filter(s => !s.current); } function refresh() { if (refreshing) return; refreshing = true; Proc.runCommand("sessionsService-current", ["sh", "-c", "echo \"${XDG_SESSION_ID}:$(loginctl show-session \"${XDG_SESSION_ID}\" -p Seat --value 2>/dev/null)\""], (output, exitCode) => { const trimmed = (output || "").trim(); const parts = trimmed.split(":"); root.currentSessionId = parts[0] || ""; root.currentSeat = parts[1] || ""; _loadSessions(); }, 0); } function _loadSessions() { const script = "loginctl list-sessions --no-legend 2>/dev/null | awk '{print $1}' | while read id; do loginctl show-session \"$id\" -p Id -p User -p Name -p Seat -p TTY -p Type -p Class -p Active -p State -p Remote 2>/dev/null | tr '\\n' '|'; echo; done"; Proc.runCommand("sessionsService-list", ["sh", "-c", script], (output, exitCode) => { const lines = (output || "").trim().split("\n").filter(l => l.length > 0); const list = []; for (let i = 0; i < lines.length; i++) { const fields = {}; const pairs = lines[i].split("|"); for (let j = 0; j < pairs.length; j++) { const eq = pairs[j].indexOf("="); if (eq <= 0) continue; fields[pairs[j].substring(0, eq)] = pairs[j].substring(eq + 1); } if (!fields.Id) continue; if (fields.Class !== "user") continue; if (fields.State === "closing") continue; const sessionId = fields.Id; list.push({ sessionId: sessionId, uid: parseInt(fields.User || "0", 10), username: fields.Name || "", seat: fields.Seat || "", tty: fields.TTY || "", type: fields.Type || "", sessionClass: fields.Class || "", active: fields.Active === "yes", state: fields.State || "", remote: fields.Remote === "yes", current: sessionId === root.currentSessionId }); } list.sort((a, b) => { if (a.current !== b.current) return a.current ? -1 : 1; if (a.username !== b.username) return a.username.localeCompare(b.username); return parseInt(a.sessionId, 10) - parseInt(b.sessionId, 10); }); root.sessions = list; root.refreshing = false; }, 0); } function activate(sessionId, callback) { if (!sessionId) { _fail("", "", I18n.tr("No session selected"), callback); return; } if (sessionId === root.currentSessionId) { _fail(sessionId, "", I18n.tr("Already on that session"), callback); return; } const session = findById(sessionId); const username = session ? session.username : ""; _spawnActivate(sessionId, username, callback); } function switchToUser(target, callback) { if (!target) { _fail("", "", I18n.tr("No user specified"), callback); return; } let session = findById(target); if (!session) session = findByUsername(target); if (!session) { _fail("", target, I18n.tr("No active session found for %1").arg(target), callback); return; } if (session.current) { _fail(session.sessionId, session.username, I18n.tr("Already on that session"), callback); return; } _spawnActivate(session.sessionId, session.username, callback); } function _fail(sessionId, username, message, callback) { log.warn("switch failed:", sessionId, username, message); root.switchFailed(sessionId, username, message); if (typeof callback === "function") { try { callback(false, message); } catch (e) { log.warn("SessionsService callback error:", e); } } } Component { id: activateComp Process { id: activateProc property string targetSession: "" property string targetUsername: "" property var cb: null property string capturedErr: "" running: false stdout: StdioCollector {} stderr: StdioCollector { onStreamFinished: activateProc.capturedErr = text || "" } onExited: exitCode => { const svc = root; const sessionId = activateProc.targetSession; const username = activateProc.targetUsername; const cb = activateProc.cb; const err = (activateProc.capturedErr || "").trim(); Qt.callLater(() => activateProc.destroy()); if (exitCode !== 0) { svc._fail(sessionId, username, err || I18n.tr("loginctl activate failed (exit %1)").arg(exitCode), cb); return; } if (typeof cb === "function") { try { cb(true, ""); } catch (e) { svc.log.warn("activate cb error:", e); } } } } } function _spawnActivate(sessionId, username, callback) { const proc = activateComp.createObject(root, { command: ["loginctl", "activate", sessionId], targetSession: sessionId, targetUsername: username, cb: callback }); proc.running = true; } IpcHandler { target: "sessions" function list(): string { const lines = []; for (let i = 0; i < root.sessions.length; i++) { const s = root.sessions[i]; lines.push([s.sessionId, s.username, s.seat || "-", s.tty || "-", s.type || "-", s.current ? "*current*" : ""].join("\t")); } return lines.join("\n"); } function refresh(): string { root.refresh(); return "ok"; } function open(): string { root.refresh(); root.switchRequested(); return "ok"; } function activate(sessionId: string): string { if (!sessionId) return "ERROR: missing session id"; root.activate(sessionId, null); return "ok"; } function switchTo(target: string): string { if (!target) return "ERROR: missing target (username or session id)"; root.switchToUser(target, null); return "ok"; } } Component.onCompleted: refresh() }