# ABOUTME: User detection — parses /etc/passwd for login users, finds avatars and GTK themes. # ABOUTME: Provides User dataclass and helper functions for the greeter UI. import configparser from dataclasses import dataclass from pathlib import Path NOLOGIN_SHELLS = {"/usr/sbin/nologin", "/sbin/nologin", "/bin/false", "/usr/bin/nologin"} MIN_UID = 1000 MAX_UID = 65533 DEFAULT_PASSWD = Path("/etc/passwd") DEFAULT_ACCOUNTSSERVICE_DIR = Path("/var/lib/AccountsService/icons") @dataclass class User: """Represents a system user suitable for login.""" username: str uid: int gecos: str home: Path shell: str @property def display_name(self) -> str: """Return gecos if available, otherwise username.""" return self.gecos if self.gecos else self.username def get_users(passwd_path: Path = DEFAULT_PASSWD) -> list[User]: """Parse /etc/passwd and return users with UID in the login range.""" users: list[User] = [] if not passwd_path.exists(): return users for line in passwd_path.read_text().splitlines(): parts = line.split(":") if len(parts) < 7: continue username, _, uid_str, _, gecos, home, shell = parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] try: uid = int(uid_str) except ValueError: continue if uid < MIN_UID or uid > MAX_UID: continue if shell in NOLOGIN_SHELLS: continue if "/" in username or username.startswith("."): continue users.append(User( username=username, uid=uid, gecos=gecos, home=Path(home), shell=shell, )) return users def get_avatar_path( username: str, accountsservice_dir: Path = DEFAULT_ACCOUNTSSERVICE_DIR, home_dir: Path | None = None, ) -> Path | None: """Find avatar for a user: AccountsService icon → ~/.face → None.""" # AccountsService icon icon = accountsservice_dir / username if icon.exists() and not icon.is_symlink(): return icon # ~/.face fallback if home_dir is not None: face = home_dir / ".face" if face.exists() and not face.is_symlink(): return face return None def get_user_gtk_theme(config_dir: Path | None = None) -> str | None: """Read the GTK theme name from a user's gtk-4.0/settings.ini.""" if config_dir is None: return None settings_file = config_dir / "settings.ini" if not settings_file.exists(): return None config = configparser.ConfigParser() config.read(settings_file) if config.has_option("Settings", "gtk-theme-name"): return config.get("Settings", "gtk-theme-name") return None