fix login?
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@ ColumnLayout {
|
|||||||
id: pfp
|
id: pfp
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
path: `${Paths.home}/.face`
|
path: root.greeter.userFace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-11
@@ -30,17 +30,17 @@ RowLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
// CustomRect {
|
||||||
Layout.fillWidth: true
|
// Layout.fillWidth: true
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
// color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: resources.implicitHeight
|
// implicitHeight: resources.implicitHeight
|
||||||
radius: Appearance.rounding.small
|
// radius: Appearance.rounding.small
|
||||||
|
//
|
||||||
Resources {
|
// Resources {
|
||||||
id: resources
|
// id: resources
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -48,6 +48,10 @@ RowLayout {
|
|||||||
bottomLeftRadius: Appearance.rounding.large
|
bottomLeftRadius: Appearance.rounding.large
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
|
UserDock {
|
||||||
|
greeter: root.greeter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Quickshell
|
|||||||
import Quickshell.Services.Greetd
|
import Quickshell.Services.Greetd
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,7 +18,25 @@ Scope {
|
|||||||
readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null
|
readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null
|
||||||
property int sessionIndex: sessions.length > 0 ? 0 : -1
|
property int sessionIndex: sessions.length > 0 ? 0 : -1
|
||||||
property var sessions: []
|
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
|
signal flashMsg
|
||||||
|
|
||||||
@@ -55,6 +74,9 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the current user as the default for next login
|
||||||
|
Users.saveDefaultUser();
|
||||||
|
|
||||||
launching = true;
|
launching = true;
|
||||||
Greetd.launch(selectedSession.command, [], 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 {
|
GreeterState {
|
||||||
id: greeter
|
id: greeter
|
||||||
|
|
||||||
username: Config.general.username
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterSurface {
|
GreeterSurface {
|
||||||
|
|||||||
Reference in New Issue
Block a user