1
0
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:
purian23
2026-05-25 22:41:23 -04:00
parent d9525908f1
commit 078180fe42
18 changed files with 1577 additions and 127 deletions
+163
View File
@@ -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);
}
}
+15 -1
View File
@@ -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 = "";
}
}
}
+119 -15
View File
@@ -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()
}