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:
nevaforget 2026-03-26 10:51:30 +01:00
parent 87c2e7d9c8
commit 806a76e44f
10 changed files with 186 additions and 38 deletions

View File

@ -2,7 +2,7 @@
# ABOUTME: Background image and other visual settings.
[appearance]
# background = "/usr/share/backgrounds/moonarch.jpg"
background = "../data/wallpaper.jpg"
[behavior]
# show_user_list = true

8
data/CREDITS.md Normal file
View File

@ -0,0 +1,8 @@
# Asset Credits
## wallpaper.jpg
- **Title**: Canyon Mountains on Night Sky
- **Author**: eberhard grossgasteiger
- **Source**: https://www.pexels.com/photo/canion-mountains-on-night-sky-2098428/
- **License**: Pexels License (free for personal and commercial use)

View File

@ -1,6 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="96" height="96">
<!-- Default user avatar: silhouette circle -->
<circle cx="60" cy="60" r="60" fill="#3a3a5c"/>
<circle cx="60" cy="45" r="20" fill="#6c6c8a"/>
<ellipse cx="60" cy="95" rx="35" ry="25" fill="#6c6c8a"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 512 512"><path fill="#PLACEHOLDER" d="M256 23c-16.076 0-32.375 3.73-48.178 10.24c-2.126 6.525-3.877 14.76-4.877 23.754c-1.31 11.79-1.73 24.706-1.87 36.819c33.864-3.704 75.986-3.704 109.85 0c-.14-12.113-.56-25.03-1.87-36.82c-1-8.992-2.75-17.228-4.877-23.753C288.375 26.73 272.076 23 256 23m100.564 19.332c9.315 7.054 18.107 14.878 26.282 23.234c1.53-6.65 4.69-12.696 9.03-17.695zm-170.03 1.49c-34.675 20.22-65.047 52.714-82.552 86.334c-33.08 63.536-39.69 156.956-.53 214.8C132.786 388.278 200.276 405 256 405s123.215-16.72 152.547-60.045c39.162-57.843 32.55-151.263-.53-214.8c-17.504-33.62-47.876-66.112-82.55-86.333c.578 3.65 1.057 7.388 1.478 11.184c1.522 13.694 1.912 28.197 2.014 41.267C347.664 99.427 362 104 368 110c32 32 75.537 134.695 16 224c-37.654 56.48-218.346 56.48-256 0c-59.537-89.305-16-192 16-224c6-6 20.335-10.573 39.04-13.727c.103-13.07.493-27.573 2.015-41.267c.42-3.796.9-7.534 1.478-11.184zM64 48c-8.837 0-16 7.163-16 16a16 16 0 0 0 7 13.227V145.5L73 132V77.21A16 16 0 0 0 80 64c0-8.837-7.163-16-16-16m358.81 3.68c-12.81 0-23 10.19-23 23s10.19 23 23 23s23-10.19 23-23s-10.19-23-23-23m25.272 55.205c-6.98 5.497-15.758 8.795-25.27 8.795c-.745 0-1.48-.027-2.214-.067a217 217 0 0 1 2.38 4.37l29.852 22.39zm-238.822 2.5c-17.257.09-37.256 3.757-53.233 16.12c-26.634 20.608-43.034 114.763-33.49 146.763c16.584-61.767 31.993-124.02 107.92-161.274a133.5 133.5 0 0 0-21.197-1.61zm-135.055 44.21L40.15 179.138l-14.48 72.408l38.18 45.814c-10.947-46.523-5.776-98.723 10.355-143.764zm363.59 0c16.13 45.042 21.302 97.242 10.355 143.764l38.18-45.815l-14.48-72.408zM106.645 375.93c-3.583 1.17-7.252 3.406-10.282 6.435c-4.136 4.136-6.68 9.43-7.164 14.104c.21.364.603 1.157 1.73 2.162c2.453 2.188 6.693 5.17 12.127 8.358c10.867 6.38 26.55 13.757 44.205 20.623c21.177 8.237 45.35 15.704 67.738 20.38v-27.61c-39.47-5.12-79.897-18.325-108.355-44.452zm298.71 0C376.897 402.055 336.47 415.26 297 420.38v27.61c22.387-4.676 46.56-12.143 67.738-20.38c17.655-6.865 33.338-14.243 44.205-20.622c5.434-3.19 9.674-6.17 12.127-8.36c1.127-1.004 1.52-1.797 1.73-2.16c-.482-4.675-3.027-9.97-7.163-14.105c-3.03-3.03-6.7-5.264-10.282-6.435zM77.322 410.602L18 450.15V494h37v-18h18v18h366v-18h18v18h37v-43.85l-59.322-39.548c-.537.488-1.08.97-1.623 1.457c-3.922 3.497-8.932 6.89-14.998 10.452c-12.133 7.12-28.45 14.743-46.795 21.877C334.572 458.656 290.25 471 256 471s-78.572-12.343-115.262-26.61c-18.345-7.135-34.662-14.757-46.795-21.878c-6.066-3.56-11.076-6.955-14.998-10.453c-.543-.487-1.086-.97-1.623-1.458zM233 422.184v28.992c8.236 1.162 16.012 1.824 23 1.824s14.764-.662 23-1.824v-28.992a325 325 0 0 1-46 0"></path></svg>

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 512 512"><path fill="#222222" d="M256 23c-16.076 0-32.375 3.73-48.178 10.24c-2.126 6.525-3.877 14.76-4.877 23.754c-1.31 11.79-1.73 24.706-1.87 36.819c33.864-3.704 75.986-3.704 109.85 0c-.14-12.113-.56-25.03-1.87-36.82c-1-8.992-2.75-17.228-4.877-23.753C288.375 26.73 272.076 23 256 23m100.564 19.332c9.315 7.054 18.107 14.878 26.282 23.234c1.53-6.65 4.69-12.696 9.03-17.695zm-170.03 1.49c-34.675 20.22-65.047 52.714-82.552 86.334c-33.08 63.536-39.69 156.956-.53 214.8C132.786 388.278 200.276 405 256 405s123.215-16.72 152.547-60.045c39.162-57.843 32.55-151.263-.53-214.8c-17.504-33.62-47.876-66.112-82.55-86.333c.578 3.65 1.057 7.388 1.478 11.184c1.522 13.694 1.912 28.197 2.014 41.267C347.664 99.427 362 104 368 110c32 32 75.537 134.695 16 224c-37.654 56.48-218.346 56.48-256 0c-59.537-89.305-16-192 16-224c6-6 20.335-10.573 39.04-13.727c.103-13.07.493-27.573 2.015-41.267c.42-3.796.9-7.534 1.478-11.184zM64 48c-8.837 0-16 7.163-16 16a16 16 0 0 0 7 13.227V145.5L73 132V77.21A16 16 0 0 0 80 64c0-8.837-7.163-16-16-16m358.81 3.68c-12.81 0-23 10.19-23 23s10.19 23 23 23s23-10.19 23-23s-10.19-23-23-23m25.272 55.205c-6.98 5.497-15.758 8.795-25.27 8.795c-.745 0-1.48-.027-2.214-.067a217 217 0 0 1 2.38 4.37l29.852 22.39zm-238.822 2.5c-17.257.09-37.256 3.757-53.233 16.12c-26.634 20.608-43.034 114.763-33.49 146.763c16.584-61.767 31.993-124.02 107.92-161.274a133.5 133.5 0 0 0-21.197-1.61zm-135.055 44.21L40.15 179.138l-14.48 72.408l38.18 45.814c-10.947-46.523-5.776-98.723 10.355-143.764zm363.59 0c16.13 45.042 21.302 97.242 10.355 143.764l38.18-45.815l-14.48-72.408zM106.645 375.93c-3.583 1.17-7.252 3.406-10.282 6.435c-4.136 4.136-6.68 9.43-7.164 14.104c.21.364.603 1.157 1.73 2.162c2.453 2.188 6.693 5.17 12.127 8.358c10.867 6.38 26.55 13.757 44.205 20.623c21.177 8.237 45.35 15.704 67.738 20.38v-27.61c-39.47-5.12-79.897-18.325-108.355-44.452zm298.71 0C376.897 402.055 336.47 415.26 297 420.38v27.61c22.387-4.676 46.56-12.143 67.738-20.38c17.655-6.865 33.338-14.243 44.205-20.622c5.434-3.19 9.674-6.17 12.127-8.36c1.127-1.004 1.52-1.797 1.73-2.16c-.482-4.675-3.027-9.97-7.163-14.105c-3.03-3.03-6.7-5.264-10.282-6.435zM77.322 410.602L18 450.15V494h37v-18h18v18h366v-18h18v18h37v-43.85l-59.322-39.548c-.537.488-1.08.97-1.623 1.457c-3.922 3.497-8.932 6.89-14.998 10.452c-12.133 7.12-28.45 14.743-46.795 21.877C334.572 458.656 290.25 471 256 471s-78.572-12.343-115.262-26.61c-18.345-7.135-34.662-14.757-46.795-21.878c-6.066-3.56-11.076-6.955-14.998-10.453c-.543-.487-1.086-.97-1.623-1.458zM233 422.184v28.992c8.236 1.162 16.012 1.824 23 1.824s14.764-.662 23-1.824v-28.992a325 325 0 0 1-46 0"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
data/wallpaper.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

50
src/moongreet/config.py Normal file
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

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()

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()

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 */

47
tests/test_config.py Normal file
View File

@ -0,0 +1,47 @@
# ABOUTME: Tests for configuration loading from moongreet.toml.
# ABOUTME: Verifies parsing of appearance and behavior settings.
from pathlib import Path
import pytest
from moongreet.config import load_config, Config
class TestLoadConfig:
"""Tests for loading moongreet.toml configuration."""
def test_loads_background_path(self, tmp_path: Path) -> None:
toml_file = tmp_path / "moongreet.toml"
toml_file.write_text(
"[appearance]\n"
'background = "/usr/share/backgrounds/test.jpg"\n'
)
config = load_config(toml_file)
assert config.background == Path("/usr/share/backgrounds/test.jpg")
def test_returns_none_background_when_missing(self, tmp_path: Path) -> None:
toml_file = tmp_path / "moongreet.toml"
toml_file.write_text("[appearance]\n")
config = load_config(toml_file)
assert config.background is None
def test_returns_defaults_for_missing_file(self, tmp_path: Path) -> None:
config = load_config(tmp_path / "nonexistent.toml")
assert config.background is None
def test_resolves_relative_path_against_config_dir(self, tmp_path: Path) -> None:
toml_file = tmp_path / "moongreet.toml"
toml_file.write_text(
"[appearance]\n"
'background = "wallpaper.jpg"\n'
)
config = load_config(toml_file)
assert config.background == tmp_path / "wallpaper.jpg"