feat: UI-Polish und Konfiguration

Wallpaper-Support via moongreet.toml, Astronauten-SVG als Default-Avatar
mit dynamischer Theme-Farbe, CSS auf GTK-Theme-Farben umgestellt,
konsistentes Widget-Spacing, Custom-Icon-Registrierung.
This commit is contained in:
2026-03-26 10:51:30 +01:00
parent 87c2e7d9c8
commit 806a76e44f
10 changed files with 186 additions and 38 deletions
+50
View File
@@ -0,0 +1,50 @@
# ABOUTME: Configuration loading from moongreet.toml.
# ABOUTME: Parses appearance and behavior settings for the greeter.
import tomllib
from dataclasses import dataclass
from pathlib import Path
DEFAULT_CONFIG_PATHS = [
Path("/etc/moongreet/moongreet.toml"),
Path(__file__).parent.parent.parent / "config" / "moongreet.toml",
]
@dataclass
class Config:
"""Greeter configuration loaded from moongreet.toml."""
background: Path | None = None
def load_config(config_path: Path | None = None) -> Config:
"""Load configuration from a TOML file.
Relative paths in the config are resolved against the config file's directory.
"""
if config_path is None:
for path in DEFAULT_CONFIG_PATHS:
if path.exists():
config_path = path
break
if config_path is None:
return Config()
if not config_path.exists():
return Config()
with open(config_path, "rb") as f:
data = tomllib.load(f)
config = Config()
appearance = data.get("appearance", {})
bg = appearance.get("background")
if bg:
bg_path = Path(bg)
if not bg_path.is_absolute():
bg_path = config_path.parent / bg_path
config.background = bg_path
return config
+63 -16
View File
@@ -10,6 +10,7 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf
from moongreet.config import load_config
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
from moongreet.sessions import Session, get_sessions
@@ -17,6 +18,7 @@ from moongreet.power import reboot, shutdown
LAST_USER_PATH = Path("/var/cache/moongreet/last-user")
DEFAULT_AVATAR_PATH = Path(__file__).parent.parent.parent / "data" / "default-avatar.svg"
AVATAR_SIZE = 128
class GreeterWindow(Gtk.ApplicationWindow):
@@ -27,6 +29,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
self.add_css_class("greeter")
self.set_default_size(1920, 1080)
self._config = load_config()
self._users = get_users()
self._sessions = get_sessions()
self._selected_user: User | None = None
@@ -42,10 +45,17 @@ class GreeterWindow(Gtk.ApplicationWindow):
overlay = Gtk.Overlay()
self.set_child(overlay)
# Background fills the whole window
background = Gtk.Box()
background.set_hexpand(True)
background.set_vexpand(True)
# Background wallpaper (blurred and darkened)
if self._config.background and self._config.background.exists():
background = Gtk.Picture()
background.set_filename(str(self._config.background))
background.set_content_fit(Gtk.ContentFit.COVER)
background.set_hexpand(True)
background.set_vexpand(True)
else:
background = Gtk.Box()
background.set_hexpand(True)
background.set_vexpand(True)
overlay.set_child(background)
# Main layout: 3 rows (top spacer, center login, bottom bar)
@@ -80,19 +90,16 @@ class GreeterWindow(Gtk.ApplicationWindow):
box.add_css_class("login-box")
box.set_halign(Gtk.Align.CENTER)
box.set_valign(Gtk.Align.CENTER)
box.set_spacing(4)
box.set_spacing(12)
# Avatar — wrapped in a fixed-size frame to constrain the Picture
# Avatar — wrapped in a clipping frame for round shape
avatar_frame = Gtk.Box()
avatar_frame.set_size_request(96, 96)
avatar_frame.set_size_request(AVATAR_SIZE, AVATAR_SIZE)
avatar_frame.set_halign(Gtk.Align.CENTER)
avatar_frame.set_overflow(Gtk.Overflow.HIDDEN)
avatar_frame.add_css_class("avatar")
self._avatar_image = Gtk.Picture()
self._avatar_image.set_size_request(96, 96)
self._avatar_image.set_content_fit(Gtk.ContentFit.COVER)
self._avatar_image.set_hexpand(False)
self._avatar_image.set_vexpand(False)
self._avatar_image = Gtk.Image()
self._avatar_image.set_pixel_size(AVATAR_SIZE)
avatar_frame.append(self._avatar_image)
box.append(avatar_frame)
@@ -104,7 +111,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
# Session dropdown
self._session_dropdown = Gtk.DropDown()
self._session_dropdown.add_css_class("session-dropdown")
self._session_dropdown.set_halign(Gtk.Align.CENTER)
self._session_dropdown.set_hexpand(True)
if self._sessions:
session_names = [s.name for s in self._sessions]
string_list = Gtk.StringList.new(session_names)
@@ -113,10 +120,10 @@ class GreeterWindow(Gtk.ApplicationWindow):
# Password entry
self._password_entry = Gtk.PasswordEntry()
self._password_entry.set_hexpand(True)
self._password_entry.set_property("placeholder-text", "Passwort")
self._password_entry.set_property("show-peek-icon", True)
self._password_entry.add_css_class("password-entry")
self._password_entry.set_halign(Gtk.Align.CENTER)
self._password_entry.connect("activate", self._on_login_activate)
box.append(self._password_entry)
@@ -211,9 +218,11 @@ class GreeterWindow(Gtk.ApplicationWindow):
user.username, home_dir=user.home
)
if avatar_path and avatar_path.exists():
self._avatar_image.set_filename(str(avatar_path))
self._set_avatar_from_file(avatar_path)
elif DEFAULT_AVATAR_PATH.exists():
self._avatar_image.set_filename(str(DEFAULT_AVATAR_PATH))
self._set_default_avatar()
else:
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
# Apply user's GTK theme if available
self._apply_user_theme(user)
@@ -235,6 +244,44 @@ class GreeterWindow(Gtk.ApplicationWindow):
else:
settings.reset_property("gtk-theme-name")
def _get_foreground_color(self) -> str:
"""Get the current GTK theme foreground color as a hex string."""
color = self.get_style_context().lookup_color("theme_fg_color")
if color[0]:
rgba = color[1]
r = int(rgba.red * 255)
g = int(rgba.green * 255)
b = int(rgba.blue * 255)
return f"#{r:02x}{g:02x}{b:02x}"
return "#ffffff"
def _set_default_avatar(self) -> None:
"""Load the default avatar SVG, tinted with the GTK foreground color."""
try:
svg_text = DEFAULT_AVATAR_PATH.read_text()
fg_color = self._get_foreground_color()
svg_text = svg_text.replace("#PLACEHOLDER", fg_color)
svg_bytes = svg_text.encode("utf-8")
loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
loader.set_size(AVATAR_SIZE, AVATAR_SIZE)
loader.write(svg_bytes)
loader.close()
pixbuf = loader.get_pixbuf()
if pixbuf:
self._avatar_image.set_from_pixbuf(pixbuf)
except (GLib.Error, OSError):
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
def _set_avatar_from_file(self, path: Path) -> None:
"""Load an image file and set it as the avatar, scaled to AVATAR_SIZE."""
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
str(path), AVATAR_SIZE, AVATAR_SIZE, True
)
self._avatar_image.set_from_pixbuf(pixbuf)
except GLib.Error:
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
def _setup_keyboard_navigation(self) -> None:
"""Set up keyboard shortcuts."""
controller = Gtk.EventControllerKey()
+7
View File
@@ -28,6 +28,7 @@ class MoongreetApp(Gtk.Application):
def do_activate(self) -> None:
"""Create and present the greeter window."""
self._register_icons()
self._load_css()
window = GreeterWindow(application=self)
@@ -36,6 +37,12 @@ class MoongreetApp(Gtk.Application):
window.present()
def _register_icons(self) -> None:
"""Register custom icons from the data/icons directory."""
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
icon_theme.add_search_path(os.path.realpath(icons_dir))
def _load_css(self) -> None:
"""Load the CSS stylesheet for the greeter."""
css_provider = Gtk.CssProvider()
+8 -15
View File
@@ -12,15 +12,17 @@ window.greeter {
.login-box {
padding: 40px;
border-radius: 12px;
background-color: alpha(@window_bg_color, 0.7);
background-color: alpha(@theme_bg_color, 0.7);
}
/* Round avatar image */
.avatar {
border-radius: 50%;
min-width: 96px;
min-height: 96px;
background-color: #3a3a5c;
min-width: 128px;
min-height: 128px;
max-width: 128px;
max-height: 128px;
background-color: @theme_selected_bg_color;
border: 3px solid alpha(white, 0.3);
}
@@ -30,32 +32,23 @@ window.greeter {
font-weight: bold;
color: white;
margin-top: 12px;
margin-bottom: 8px;
margin-bottom: 40px;
}
/* Session dropdown */
.session-dropdown {
min-width: 200px;
margin-bottom: 12px;
border-radius: 6px;
min-width: 280px;
}
/* Password entry field */
.password-entry {
min-width: 280px;
min-height: 40px;
border-radius: 20px;
padding-left: 16px;
padding-right: 16px;
font-size: 16px;
margin-top: 8px;
}
/* Error message label */
.error-label {
color: #ff6b6b;
font-size: 14px;
margin-top: 8px;
}
/* User list on the bottom left */