Add wallpaper and default avatar support
Wallpaper with fallback hierarchy: config path > Moonarch system default (/usr/share/moonarch/wallpaper.jpg) > package fallback. Applied to both primary and secondary monitors. Default avatar from Moongreet ecosystem with theme-colored SVG rendering via PixbufLoader and proper clipping frame (Gtk.Box with overflow hidden), matching the Moongreet avatar pattern.
This commit is contained in:
parent
d0d390d0cb
commit
dd9937b020
@ -3,8 +3,12 @@
|
||||
|
||||
import tomllib
|
||||
from dataclasses import dataclass, field
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
|
||||
MOONARCH_WALLPAPER = Path("/usr/share/moonarch/wallpaper.jpg")
|
||||
PACKAGE_WALLPAPER = Path(str(files("moonlock") / "data" / "wallpaper.jpg"))
|
||||
|
||||
DEFAULT_CONFIG_PATHS = [
|
||||
Path("/etc/moonlock/moonlock.toml"),
|
||||
Path.home() / ".config" / "moonlock" / "moonlock.toml",
|
||||
@ -37,3 +41,22 @@ def load_config(
|
||||
background_path=merged.get("background_path"),
|
||||
fingerprint_enabled=merged.get("fingerprint_enabled", True),
|
||||
)
|
||||
|
||||
|
||||
def resolve_background_path(config: Config) -> Path:
|
||||
"""Resolve the wallpaper path using the fallback hierarchy.
|
||||
|
||||
Priority: config background_path > Moonarch system default > package fallback.
|
||||
"""
|
||||
# User-configured path
|
||||
if config.background_path:
|
||||
path = Path(config.background_path)
|
||||
if path.is_file():
|
||||
return path
|
||||
|
||||
# Moonarch ecosystem default
|
||||
if MOONARCH_WALLPAPER.is_file():
|
||||
return MOONARCH_WALLPAPER
|
||||
|
||||
# Package fallback (always present)
|
||||
return PACKAGE_WALLPAPER
|
||||
|
||||
BIN
src/moonlock/data/wallpaper.jpg
Normal file
BIN
src/moonlock/data/wallpaper.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
@ -4,16 +4,19 @@
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from moonlock.auth import authenticate
|
||||
from moonlock.config import Config, load_config
|
||||
from moonlock.config import Config, load_config, resolve_background_path
|
||||
from moonlock.fingerprint import FingerprintListener
|
||||
from moonlock.i18n import Strings, load_strings
|
||||
from moonlock.users import get_current_user, get_avatar_path, User
|
||||
from moonlock.users import get_current_user, get_avatar_path, get_default_avatar_path, User
|
||||
from moonlock import power
|
||||
|
||||
FAILLOCK_MAX_ATTEMPTS = 3
|
||||
AVATAR_SIZE = 128
|
||||
|
||||
|
||||
class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
@ -54,8 +57,10 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
overlay = Gtk.Overlay()
|
||||
self.set_child(overlay)
|
||||
|
||||
# Background
|
||||
background = Gtk.Box()
|
||||
# Background wallpaper
|
||||
wallpaper_path = resolve_background_path(self._config)
|
||||
background = Gtk.Picture.new_for_filename(str(wallpaper_path))
|
||||
background.set_content_fit(Gtk.ContentFit.COVER)
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
overlay.set_child(background)
|
||||
@ -72,18 +77,22 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
self._login_box.add_css_class("login-box")
|
||||
main_box.append(self._login_box)
|
||||
|
||||
# Avatar
|
||||
# Avatar — wrapped in a clipping frame for round shape
|
||||
avatar_frame = Gtk.Box()
|
||||
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.Image()
|
||||
self._avatar_image.set_pixel_size(AVATAR_SIZE)
|
||||
avatar_frame.append(self._avatar_image)
|
||||
self._login_box.append(avatar_frame)
|
||||
|
||||
avatar_path = get_avatar_path(self._user.home, self._user.username)
|
||||
if avatar_path:
|
||||
avatar = Gtk.Picture.new_for_filename(str(avatar_path))
|
||||
avatar.set_content_fit(Gtk.ContentFit.COVER)
|
||||
avatar.set_size_request(128, 128)
|
||||
self._set_avatar_from_file(avatar_path)
|
||||
else:
|
||||
avatar = Gtk.Box()
|
||||
avatar.set_size_request(128, 128)
|
||||
avatar.set_halign(Gtk.Align.CENTER)
|
||||
avatar.add_css_class("avatar")
|
||||
self._login_box.append(avatar)
|
||||
self._set_default_avatar()
|
||||
|
||||
# Username label
|
||||
username_label = Gtk.Label(label=self._user.display_name)
|
||||
@ -213,6 +222,44 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
self._fp_label.remove_css_class("failed")
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
def _get_foreground_color(self) -> str:
|
||||
"""Get the current GTK theme foreground color as a hex string."""
|
||||
rgba = self.get_color()
|
||||
r = int(rgba.red * 255)
|
||||
g = int(rgba.green * 255)
|
||||
b = int(rgba.blue * 255)
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
def _set_default_avatar(self) -> None:
|
||||
"""Load the default avatar SVG, tinted with the GTK foreground color."""
|
||||
try:
|
||||
default_path = get_default_avatar_path()
|
||||
svg_text = default_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)
|
||||
return
|
||||
except (GLib.Error, OSError):
|
||||
pass
|
||||
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._set_default_avatar()
|
||||
|
||||
def _show_error(self, message: str) -> None:
|
||||
"""Display an error message."""
|
||||
self._error_label.set_text(message)
|
||||
|
||||
@ -16,7 +16,7 @@ gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from moonlock.config import load_config
|
||||
from moonlock.config import load_config, resolve_background_path
|
||||
from moonlock.lockscreen import LockscreenWindow
|
||||
|
||||
# ext-session-lock-v1 via gtk4-layer-shell
|
||||
@ -65,9 +65,16 @@ class MoonlockApp(Gtk.Application):
|
||||
config=self._config,
|
||||
)
|
||||
else:
|
||||
# Secondary monitors get a blank lockscreen
|
||||
# Secondary monitors get the wallpaper without login UI
|
||||
window = Gtk.ApplicationWindow(application=self)
|
||||
window.add_css_class("lockscreen")
|
||||
wallpaper = Gtk.Picture.new_for_filename(
|
||||
str(resolve_background_path(self._config))
|
||||
)
|
||||
wallpaper.set_content_fit(Gtk.ContentFit.COVER)
|
||||
wallpaper.set_hexpand(True)
|
||||
wallpaper.set_vexpand(True)
|
||||
window.set_child(wallpaper)
|
||||
|
||||
self._lock_instance.assign_window_to_monitor(window, monitor)
|
||||
window.present()
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import os
|
||||
import pwd
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_ACCOUNTSSERVICE_DIR = Path("/var/lib/AccountsService/icons")
|
||||
@ -56,3 +57,8 @@ def get_avatar_path(
|
||||
return icon
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_default_avatar_path() -> Path:
|
||||
"""Return the path to the package default avatar SVG."""
|
||||
return Path(str(files("moonlock") / "data" / "default-avatar.svg"))
|
||||
|
||||
@ -5,7 +5,7 @@ import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from moonlock.users import get_current_user, get_avatar_path, User
|
||||
from moonlock.users import get_current_user, get_avatar_path, get_default_avatar_path, User
|
||||
|
||||
|
||||
class TestGetCurrentUser:
|
||||
@ -79,3 +79,17 @@ class TestGetAvatarPath:
|
||||
def test_returns_none_when_no_avatar(self, tmp_path: Path):
|
||||
path = get_avatar_path(tmp_path)
|
||||
assert path is None
|
||||
|
||||
|
||||
class TestGetDefaultAvatarPath:
|
||||
"""Tests for default avatar fallback."""
|
||||
|
||||
def test_default_avatar_exists(self):
|
||||
"""The package default avatar must always be present."""
|
||||
path = get_default_avatar_path()
|
||||
assert path.is_file()
|
||||
|
||||
def test_default_avatar_is_svg(self):
|
||||
"""The default avatar should be an SVG file."""
|
||||
path = get_default_avatar_path()
|
||||
assert path.suffix == ".svg"
|
||||
|
||||
53
tests/test_wallpaper.py
Normal file
53
tests/test_wallpaper.py
Normal file
@ -0,0 +1,53 @@
|
||||
# ABOUTME: Tests for wallpaper path resolution.
|
||||
# ABOUTME: Verifies fallback hierarchy: config > Moonarch system default > package fallback.
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from moonlock.config import Config, resolve_background_path, MOONARCH_WALLPAPER, PACKAGE_WALLPAPER
|
||||
|
||||
|
||||
class TestResolveBackgroundPath:
|
||||
"""Tests for the wallpaper fallback hierarchy."""
|
||||
|
||||
def test_config_path_used_when_file_exists(self, tmp_path: Path):
|
||||
"""Config background_path takes priority if the file exists."""
|
||||
wallpaper = tmp_path / "custom.jpg"
|
||||
wallpaper.write_bytes(b"\xff\xd8")
|
||||
config = Config(background_path=str(wallpaper))
|
||||
result = resolve_background_path(config)
|
||||
assert result == wallpaper
|
||||
|
||||
def test_config_path_skipped_when_file_missing(self, tmp_path: Path):
|
||||
"""Config path should be skipped if the file does not exist."""
|
||||
config = Config(background_path="/nonexistent/wallpaper.jpg")
|
||||
with patch("moonlock.config.MOONARCH_WALLPAPER", tmp_path / "nope.jpg"):
|
||||
result = resolve_background_path(config)
|
||||
assert result == PACKAGE_WALLPAPER
|
||||
|
||||
def test_moonarch_default_used_when_no_config(self, tmp_path: Path):
|
||||
"""Moonarch system wallpaper is used when config has no background_path."""
|
||||
moonarch_wp = tmp_path / "wallpaper.jpg"
|
||||
moonarch_wp.write_bytes(b"\xff\xd8")
|
||||
config = Config(background_path=None)
|
||||
with patch("moonlock.config.MOONARCH_WALLPAPER", moonarch_wp):
|
||||
result = resolve_background_path(config)
|
||||
assert result == moonarch_wp
|
||||
|
||||
def test_moonarch_default_skipped_when_missing(self, tmp_path: Path):
|
||||
"""Falls back to package wallpaper when Moonarch default is missing."""
|
||||
config = Config(background_path=None)
|
||||
with patch("moonlock.config.MOONARCH_WALLPAPER", tmp_path / "nope.jpg"):
|
||||
result = resolve_background_path(config)
|
||||
assert result == PACKAGE_WALLPAPER
|
||||
|
||||
def test_package_fallback_always_exists(self):
|
||||
"""The package fallback wallpaper must always be present."""
|
||||
assert PACKAGE_WALLPAPER.is_file()
|
||||
|
||||
def test_full_fallback_chain(self, tmp_path: Path):
|
||||
"""With no config and no Moonarch default, package fallback is returned."""
|
||||
config = Config()
|
||||
with patch("moonlock.config.MOONARCH_WALLPAPER", tmp_path / "nope.jpg"):
|
||||
result = resolve_background_path(config)
|
||||
assert result == PACKAGE_WALLPAPER
|
||||
Loading…
x
Reference in New Issue
Block a user