mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
feat(Greeter): improved multi-user UI and per-user theme sync
- Introduce multi-account greeter login with per-user theme previews - Add `dms greeter sync --profile` for secondary users with or without sudo - Add Manage greeter group membership from Settings UI → Users Tab
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("GreeterUsersService")
|
||||
|
||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||
readonly property string usersCacheDir: greetCfgDir + "/users"
|
||||
|
||||
property var users: []
|
||||
property var usernames: []
|
||||
property var profileImageMap: ({})
|
||||
property bool loaded: false
|
||||
property bool refreshing: false
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
function refresh() {
|
||||
if (refreshing)
|
||||
return;
|
||||
refreshing = true;
|
||||
_loadUsers();
|
||||
}
|
||||
|
||||
function displayName(username) {
|
||||
const u = _findUser(username);
|
||||
if (!u)
|
||||
return username || "";
|
||||
const gecos = (u.gecos || "").trim();
|
||||
return gecos.length > 0 ? gecos : username;
|
||||
}
|
||||
|
||||
function optionLabel(username) {
|
||||
const label = displayName(username);
|
||||
return label !== username ? label : username;
|
||||
}
|
||||
|
||||
function usernameFromOptionLabel(label) {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (root.optionLabel(users[i].username) === label)
|
||||
return users[i].username;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
function hasSyncedTheme(username) {
|
||||
if (!username)
|
||||
return false;
|
||||
return syncedThemePaths[username] === true;
|
||||
}
|
||||
|
||||
property var syncedThemePaths: ({})
|
||||
|
||||
function userCacheDir(username) {
|
||||
if (!username)
|
||||
return "";
|
||||
return usersCacheDir + "/" + username;
|
||||
}
|
||||
|
||||
function syncedSettingsPath(username) {
|
||||
const dir = userCacheDir(username);
|
||||
return dir ? dir + "/settings.json" : "";
|
||||
}
|
||||
|
||||
function _findUser(name) {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].username === name)
|
||||
return users[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _loadUsers() {
|
||||
Proc.runCommand("greeterUsersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
|
||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||
const list = [];
|
||||
const names = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split(":");
|
||||
if (parts.length < 5)
|
||||
continue;
|
||||
const username = parts[0];
|
||||
list.push({
|
||||
username,
|
||||
uid: parseInt(parts[1], 10),
|
||||
gecos: (parts[2] || "").split(",")[0],
|
||||
home: parts[3] || "",
|
||||
shell: parts[4] || ""
|
||||
});
|
||||
names.push(username);
|
||||
}
|
||||
list.sort((a, b) => a.username.localeCompare(b.username));
|
||||
names.sort((a, b) => a.localeCompare(b));
|
||||
root.users = list;
|
||||
root.usernames = names;
|
||||
root.loaded = true;
|
||||
root.refreshing = false;
|
||||
_refreshSyncedThemeFlags();
|
||||
_loadProfileIcons();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _refreshSyncedThemeFlags() {
|
||||
if (usernames.length === 0) {
|
||||
syncedThemePaths = ({});
|
||||
return;
|
||||
}
|
||||
const checks = usernames.map(u => `[ -f "${syncedSettingsPath(u)}" ] && echo "${u}:1" || echo "${u}:0"`).join("; ");
|
||||
Proc.runCommand("greeterUsersService-syncedThemes", ["sh", "-c", checks], (output, exitCode) => {
|
||||
const map = {};
|
||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split(":");
|
||||
if (parts.length >= 2)
|
||||
map[parts[0]] = parts[1] === "1";
|
||||
}
|
||||
root.syncedThemePaths = map;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function profileImagePath(username) {
|
||||
if (!username)
|
||||
return "";
|
||||
return profileImageMap[username] || "";
|
||||
}
|
||||
|
||||
function _loadProfileIcons() {
|
||||
if (users.length === 0) {
|
||||
profileImageMap = ({});
|
||||
return;
|
||||
}
|
||||
const script = users.map(u => {
|
||||
const safeUser = u.username.replace(/'/g, "'\\''");
|
||||
const safeHome = (u.home || "").replace(/'/g, "'\\''");
|
||||
const cacheDir = usersCacheDir + "/" + u.username;
|
||||
return `( icon=""; for f in "${cacheDir}/profile.jpg" "${cacheDir}/profile.jpeg" "${cacheDir}/profile.png" "${cacheDir}/profile.webp" "/var/lib/AccountsService/icons/${safeUser}" "${safeHome}/.face" "${safeHome}/.face.icon"; do if [ -f "$f" ] && [ -r "$f" ]; then icon="$f"; break; fi; done; echo "${u.username}:$icon" )`;
|
||||
}).join("; ");
|
||||
Proc.runCommand("greeterUsersService-profileIcons", ["sh", "-c", script], (output, exitCode) => {
|
||||
const map = {};
|
||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const idx = lines[i].indexOf(":");
|
||||
if (idx <= 0)
|
||||
continue;
|
||||
const user = lines[i].substring(0, idx);
|
||||
const icon = lines[i].substring(idx + 1).trim();
|
||||
map[user] = icon && icon.length > 0 ? icon : "";
|
||||
}
|
||||
for (let j = 0; j < users.length; j++) {
|
||||
const u = users[j].username;
|
||||
if (!(u in map))
|
||||
map[u] = "";
|
||||
}
|
||||
root.profileImageMap = map;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,23 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
property string pendingGreeterProfileUser: ""
|
||||
|
||||
function getGreeterUserProfileImage(username) {
|
||||
if (!username) {
|
||||
profileImage = "";
|
||||
pendingGreeterProfileUser = "";
|
||||
return;
|
||||
}
|
||||
if (typeof GreeterUsersService !== "undefined") {
|
||||
const cachedPath = GreeterUsersService.profileImagePath(username);
|
||||
if (cachedPath) {
|
||||
profileImage = cachedPath;
|
||||
pendingGreeterProfileUser = "";
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingGreeterProfileUser = username;
|
||||
userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`];
|
||||
userProfileCheckProcess.running = true;
|
||||
}
|
||||
@@ -261,12 +273,14 @@ Singleton {
|
||||
} else {
|
||||
root.profileImage = "";
|
||||
}
|
||||
root.pendingGreeterProfileUser = "";
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
if (exitCode !== 0 && root.pendingGreeterProfileUser !== "") {
|
||||
root.profileImage = "";
|
||||
root.pendingGreeterProfileUser = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ Singleton {
|
||||
|
||||
property var users: []
|
||||
property string adminGroup: "wheel"
|
||||
property string greeterGroup: "greeter"
|
||||
property var adminMembers: []
|
||||
property var greeterMembers: []
|
||||
property bool refreshing: false
|
||||
|
||||
signal operationCompleted(string op, string username, bool success, string message)
|
||||
@@ -69,6 +71,21 @@ Singleton {
|
||||
Proc.runCommand("usersService-adminMembers", ["sh", "-c", "getent group " + root.adminGroup + " | awk -F: '{print $4}'"], (output, exitCode) => {
|
||||
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||||
root.adminMembers = members;
|
||||
_detectGreeterGroup();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _detectGreeterGroup() {
|
||||
Proc.runCommand("usersService-detectGreeterGroup", ["sh", "-c", "getent group greeter >/dev/null 2>&1 && echo greeter || (getent group greetd >/dev/null 2>&1 && echo greetd || (getent group _greeter >/dev/null 2>&1 && echo _greeter || echo greeter))"], (output, exitCode) => {
|
||||
root.greeterGroup = (output || "").trim() || "greeter";
|
||||
_loadGreeterMembers();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _loadGreeterMembers() {
|
||||
Proc.runCommand("usersService-greeterMembers", ["sh", "-c", "getent group " + root.greeterGroup + " 2>/dev/null | awk -F: '{print $4}'"], (output, exitCode) => {
|
||||
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||||
root.greeterMembers = members;
|
||||
_loadUsers();
|
||||
}, 0);
|
||||
}
|
||||
@@ -78,8 +95,11 @@ Singleton {
|
||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||
const list = [];
|
||||
const adminSet = {};
|
||||
const greeterSet = {};
|
||||
for (let i = 0; i < root.adminMembers.length; i++)
|
||||
adminSet[root.adminMembers[i]] = true;
|
||||
for (let i = 0; i < root.greeterMembers.length; i++)
|
||||
greeterSet[root.greeterMembers[i]] = true;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split(":");
|
||||
@@ -92,7 +112,8 @@ Singleton {
|
||||
gecos: (parts[2] || "").split(",")[0],
|
||||
home: parts[3] || "",
|
||||
shell: parts[4] || "",
|
||||
isAdmin: adminSet[username] === true
|
||||
isAdmin: adminSet[username] === true,
|
||||
isGreeter: greeterSet[username] === true
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.username.localeCompare(b.username));
|
||||
@@ -101,7 +122,7 @@ Singleton {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function createUser(username, password, addToAdmin, callback) {
|
||||
function createUser(username, password, addToAdmin, addToGreeter, callback) {
|
||||
if (!isValidUsername(username)) {
|
||||
_emit("create", username, false, I18n.tr("Invalid username"), callback);
|
||||
return;
|
||||
@@ -114,7 +135,7 @@ Singleton {
|
||||
_emit("create", username, false, I18n.tr("User already exists"), callback);
|
||||
return;
|
||||
}
|
||||
_runUseradd(username, password, addToAdmin === true, callback);
|
||||
_runUseradd(username, password, addToAdmin === true, addToGreeter === true, callback);
|
||||
}
|
||||
|
||||
function setPassword(username, newPassword, callback) {
|
||||
@@ -156,6 +177,55 @@ Singleton {
|
||||
_runAdminToggle(username, makeAdmin === true, callback);
|
||||
}
|
||||
|
||||
function setGreeterAccess(username, enable, callback) {
|
||||
if (!userExists(username)) {
|
||||
_emit("greeter", username, false, I18n.tr("User not found"), callback);
|
||||
return;
|
||||
}
|
||||
_runGreeterToggle(username, enable === true, callback);
|
||||
}
|
||||
|
||||
function _finishCreateUser(targetUser, addAdmin, addGreeter, outerCb) {
|
||||
function finish(success, message) {
|
||||
root._emit("create", targetUser, success, message, outerCb);
|
||||
}
|
||||
|
||||
function maybeGreeter(onDone) {
|
||||
if (addGreeter) {
|
||||
root._runGreeterToggle(targetUser, true, (greeterOk, greeterMsg) => {
|
||||
if (greeterOk)
|
||||
onDone();
|
||||
else
|
||||
finish(false, greeterMsg);
|
||||
});
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}
|
||||
|
||||
function createMessage() {
|
||||
if (addAdmin && addGreeter)
|
||||
return I18n.tr("User created with administrator and greeter login access");
|
||||
if (addAdmin)
|
||||
return I18n.tr("User created with administrator privileges");
|
||||
if (addGreeter)
|
||||
return I18n.tr("User created with greeter login access");
|
||||
return I18n.tr("User created");
|
||||
}
|
||||
|
||||
if (addAdmin) {
|
||||
root._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
|
||||
if (!adminOk) {
|
||||
finish(false, adminMsg);
|
||||
return;
|
||||
}
|
||||
maybeGreeter(() => finish(true, createMessage()));
|
||||
});
|
||||
} else {
|
||||
maybeGreeter(() => finish(true, createMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
function _emit(op, username, success, message, callback) {
|
||||
root.operationCompleted(op, username, success, message);
|
||||
if (typeof callback === "function") {
|
||||
@@ -174,6 +244,7 @@ Singleton {
|
||||
property string targetUser: ""
|
||||
property string targetPassword: ""
|
||||
property bool addAdmin: false
|
||||
property bool addGreeter: false
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
running: false
|
||||
@@ -191,6 +262,7 @@ Singleton {
|
||||
const targetUser = useraddProc.targetUser;
|
||||
const targetPassword = useraddProc.targetPassword;
|
||||
const addAdmin = useraddProc.addAdmin;
|
||||
const addGreeter = useraddProc.addGreeter;
|
||||
const outerCb = useraddProc.cb;
|
||||
Qt.callLater(() => useraddProc.destroy());
|
||||
|
||||
@@ -199,17 +271,7 @@ Singleton {
|
||||
svc._emit("create", targetUser, false, pwMsg, outerCb);
|
||||
return;
|
||||
}
|
||||
if (addAdmin) {
|
||||
svc._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
|
||||
if (adminOk) {
|
||||
svc._emit("create", targetUser, true, I18n.tr("User created with administrator privileges"), outerCb);
|
||||
} else {
|
||||
svc._emit("create", targetUser, false, adminMsg, outerCb);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
svc._emit("create", targetUser, true, I18n.tr("User created"), outerCb);
|
||||
}
|
||||
svc._finishCreateUser(targetUser, addAdmin, addGreeter, outerCb);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -290,6 +352,36 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: greeterToggleComp
|
||||
Process {
|
||||
id: greeterToggleProc
|
||||
property string targetUser: ""
|
||||
property bool enableGreeter: false
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: greeterToggleProc.capturedErr = text || ""
|
||||
}
|
||||
onExited: exitCode => {
|
||||
const targetUser = greeterToggleProc.targetUser;
|
||||
const enableGreeter = greeterToggleProc.enableGreeter;
|
||||
const cb = greeterToggleProc.cb;
|
||||
const err = (greeterToggleProc.capturedErr || "").trim();
|
||||
Qt.callLater(() => greeterToggleProc.destroy());
|
||||
|
||||
if (exitCode !== 0) {
|
||||
root._emit("greeter", targetUser, false, err || I18n.tr("usermod failed (exit %1)").arg(exitCode), cb);
|
||||
} else {
|
||||
root.refresh();
|
||||
root._emit("greeter", targetUser, true, enableGreeter ? I18n.tr("Granted greeter login access") : I18n.tr("Removed greeter login access"), cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: adminToggleComp
|
||||
Process {
|
||||
@@ -320,12 +412,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function _runUseradd(username, password, addToAdmin, callback) {
|
||||
function _runUseradd(username, password, addToAdmin, addToGreeter, callback) {
|
||||
const proc = useraddComp.createObject(root, {
|
||||
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||
targetUser: username,
|
||||
targetPassword: password,
|
||||
addAdmin: addToAdmin,
|
||||
addGreeter: addToGreeter,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
@@ -361,5 +454,16 @@ Singleton {
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function _runGreeterToggle(username, enableGreeter, callback) {
|
||||
const cmd = enableGreeter ? ["pkexec", "usermod", "-aG", root.greeterGroup, username] : ["pkexec", "gpasswd", "-d", username, root.greeterGroup];
|
||||
const proc = greeterToggleComp.createObject(root, {
|
||||
command: cmd,
|
||||
targetUser: username,
|
||||
enableGreeter: enableGreeter,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user