fix login?
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@ ColumnLayout {
|
||||
id: pfp
|
||||
|
||||
anchors.fill: parent
|
||||
path: `${Paths.home}/.face`
|
||||
path: root.greeter.userFace
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-11
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+88
@@ -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()
|
||||
@@ -11,8 +11,6 @@ ShellRoot {
|
||||
|
||||
GreeterState {
|
||||
id: greeter
|
||||
|
||||
username: Config.general.username
|
||||
}
|
||||
|
||||
GreeterSurface {
|
||||
|
||||
Reference in New Issue
Block a user