mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
feat(sessions): implement local user session switching functionality
- Core user is logged in tty1 while user two is in tty3, you can now seamlessly switch bewteen them New IPC options: - `dms ipc call sessions list` - `dms switch-user [target]` - New Powermenu switch users option
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user