fix login?

This commit is contained in:
2026-03-25 13:17:09 +01:00
parent f4d5f54f9a
commit 2da1d2c8ab
7 changed files with 375 additions and 15 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ ColumnLayout {
id: pfp
anchors.fill: parent
path: `${Paths.home}/.face`
path: root.greeter.userFace
}
}
+15 -11
View File
@@ -30,17 +30,17 @@ RowLayout {
}
}
CustomRect {
Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: resources.implicitHeight
radius: Appearance.rounding.small
Resources {
id: resources
}
}
// CustomRect {
// Layout.fillWidth: true
// color: DynamicColors.tPalette.m3surfaceContainer
// implicitHeight: resources.implicitHeight
// radius: Appearance.rounding.small
//
// Resources {
// id: resources
//
// }
// }
CustomClippingRect {
Layout.fillHeight: true
@@ -48,6 +48,10 @@ RowLayout {
bottomLeftRadius: Appearance.rounding.large
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.small
UserDock {
greeter: root.greeter
}
}
}
+23 -1
View File
@@ -4,6 +4,7 @@ import Quickshell
import Quickshell.Services.Greetd
import Quickshell.Io
import QtQuick
import qs.Helpers
Scope {
id: root
@@ -17,7 +18,25 @@ Scope {
readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null
property int sessionIndex: sessions.length > 0 ? 0 : -1
property var sessions: []
required property string username
// User handling - now uses the Users singleton
readonly property var users: Users.users
readonly property var selectedUser: Users.selectedUser
readonly property string username: Users.selectedUsername
readonly property string userFace: selectedUser ? selectedUser.face : ""
// User selection functions (delegate to Users singleton)
function selectUser(username: string): bool {
return Users.selectUser(username);
}
function selectNextUser(): void {
Users.selectNext();
}
function selectPreviousUser(): void {
Users.selectPrevious();
}
signal flashMsg
@@ -55,6 +74,9 @@ Scope {
return;
}
// Save the current user as the default for next login
Users.saveDefaultUser();
launching = true;
Greetd.launch(selectedSession.command, [], true);
}
+112
View File
@@ -0,0 +1,112 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
// The list of users that can log in graphically
// Each user object has: username, uid, home, shell, gecos (full name), face (avatar path)
property var users: []
// The currently selected user index
property int selectedIndex: 0
// The currently selected user object (or null if none)
readonly property var selectedUser: selectedIndex >= 0 && selectedIndex < users.length ? users[selectedIndex] : null
// Convenience property for the selected username
readonly property string selectedUsername: selectedUser ? selectedUser.username : ""
// Path to store the default user preference
readonly property string defaultUserFile: "/etc/zshell-greeter/default-user"
// Select a user by username
function selectUser(username: string): bool {
for (let i = 0; i < users.length; i++) {
if (users[i].username === username) {
selectedIndex = i;
return true;
}
}
return false;
}
// Select the next user in the list (wraps around)
function selectNext(): void {
if (users.length === 0)
return;
selectedIndex = (selectedIndex + 1) % users.length;
}
// Select the previous user in the list (wraps around)
function selectPrevious(): void {
if (users.length === 0)
return;
selectedIndex = (selectedIndex - 1 + users.length) % users.length;
}
// Save the current user as the default for next login
function saveDefaultUser(): void {
if (selectedUser) {
defaultUserStorage.setText(selectedUser.username);
}
}
// Process to fetch the list of graphical users
Process {
id: userLister
command: ["python3", Quickshell.shellDir + "/scripts/get-users"]
running: true
stdout: StdioCollector {
onStreamFinished: {
try {
root.users = JSON.parse(text);
// If we have users and no selection yet, try to select the default user
if (root.users.length > 0) {
// Try to load the default user
if (defaultUserStorage.loaded) {
const defaultUsername = defaultUserStorage.text().trim();
if (defaultUsername && !root.selectUser(defaultUsername)) {
// Default user not found, select first user
root.selectedIndex = 0;
}
} else {
root.selectedIndex = 0;
}
}
} catch (e) {
console.error("Failed to parse users:", e);
}
}
}
}
// FileView for persisting the default user
FileView {
id: defaultUserStorage
path: root.defaultUserFile
preload: true
onLoaded: {
// If users are already loaded, try to select the default user
if (root.users.length > 0) {
const defaultUsername = text().trim();
if (defaultUsername) {
root.selectUser(defaultUsername);
}
}
}
onLoadFailed: {
// File doesn't exist yet, that's fine - we'll create it on first save
console.log("No default user file found, will use first user");
}
}
}
+136
View File
@@ -0,0 +1,136 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
ColumnLayout {
id: root
required property var greeter
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3outline
elide: Text.ElideRight
font.family: Appearance.font.family.mono
font.weight: 500
text: root.greeter.users.length > 0 ? qsTr("%1 user%2").arg(root.greeter.users.length).arg(root.greeter.users.length === 1 ? "" : "s") : qsTr("Users")
}
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
color: "transparent"
radius: Appearance.rounding.small
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: root.greeter.users.length > 0 ? 0 : 1
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.large
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3outlineVariant
fill: 1
font.pointSize: Appearance.font.size.extraLarge * 2
text: "account_circle"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3outlineVariant
font.family: Appearance.font.family.mono
font.pointSize: Appearance.font.size.large
font.weight: 500
text: qsTr("No Users Found")
}
}
}
ListView {
id: users
anchors.fill: parent
clip: true
highlightFollowsCurrentItem: false
model: root.greeter.users
spacing: Appearance.spacing.small
delegate: CustomRect {
id: user
required property int index
required property var modelData
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: row.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal - Appearance.padding.smaller
StateLayer {
function onClicked(): void {
users.currentIndex = index;
}
}
RowLayout {
id: row
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
MaterialIcon {
color: user.index === users.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurfaceVariant
fill: 1
font.pointSize: Appearance.font.size.extraLarge
text: modelData.kind === "x11" ? "tv" : "account_circle"
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small / 2
CustomText {
Layout.fillWidth: true
color: user.index === users.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
font.weight: 600
text: modelData.username
}
}
}
}
highlight: CustomRect {
color: DynamicColors.palette.m3primary
implicitHeight: users.currentItem?.implicitHeight ?? 0
implicitWidth: users.width
radius: Appearance.rounding.normal - Appearance.padding.smaller
y: users.currentItem?.y ?? 0
Behavior on y {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
}
}
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
Get users that are allowed to log in graphically.
Criteria for a user to be considered a "graphical login" user:
1. UID >= 1000 (regular user, not a system account)
2. Has a valid login shell (not /sbin/nologin, /bin/false, etc.)
3. Has a home directory that exists
Output: JSON array of user objects with username, uid, home, shell, and face (avatar path)
"""
import json
import os
import pwd
# Shells that indicate a user cannot log in
INVALID_SHELLS = {
"/sbin/nologin",
"/usr/sbin/nologin",
"/usr/bin/nologin",
"/bin/false",
"/usr/bin/false",
"/bin/true",
"/usr/bin/true",
"",
}
# Minimum UID for regular users (typically 1000 on most Linux distributions)
MIN_UID = 1000
def get_face_path(home: str, username: str) -> str:
"""Get the path to the user's face/avatar image if it exists."""
# Check common locations for user avatars
candidates = [
os.path.join(home, ".face"),
os.path.join(home, ".face.icon"),
f"/var/lib/AccountsService/icons/{username}",
]
for path in candidates:
if os.path.isfile(path):
return path
return ""
def is_graphical_user(pw: pwd.struct_passwd) -> bool:
"""Check if a user is allowed to log in graphically."""
# Must be a regular user (UID >= 1000)
if pw.pw_uid < MIN_UID:
return False
# Must have a valid login shell
if pw.pw_shell in INVALID_SHELLS:
return False
# Home directory should exist
if not os.path.isdir(pw.pw_dir):
return False
return True
def main():
users = []
for pw in pwd.getpwall():
if not is_graphical_user(pw):
continue
users.append({
"username": pw.pw_name,
"uid": pw.pw_uid,
"home": pw.pw_dir,
"shell": pw.pw_shell,
"gecos": pw.pw_gecos.split(",")[0] if pw.pw_gecos else "", # Full name from GECOS field
"face": get_face_path(pw.pw_dir, pw.pw_name),
})
# Sort by username for consistent ordering
users.sort(key=lambda u: u["username"])
print(json.dumps(users))
if __name__ == "__main__":
main()
-2
View File
@@ -11,8 +11,6 @@ ShellRoot {
GreeterState {
id: greeter
username: Config.general.username
}
GreeterSurface {