[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/andevsr/pythonnative/main/src/pythonnative/appearance.py [Back]  [Original]

"""System color-scheme (light / dark mode) tracking.

The platform screen host publishes the operating system's current
appearance here (Android: ``Configuration.uiMode``; iOS:
``UITraitCollection.userInterfaceStyle``), and components read it back
through [`use_color_scheme`][pythonnative.use_color_scheme] or
[`use_theme`][pythonnative.use_theme], both of which re-render when
the scheme changes.

Apps can also *override* the scheme (e.g. an in-app appearance
setting) with [`set_color_scheme`][pythonnative.appearance.set_color_scheme];
the override wins over the system value until cleared with ``None``.

Example:
    >>> from pythonnative import appearance
    >>> appearance.get_color_scheme()
    'light'
    >>> appearance.set_color_scheme("dark")   # app-level override
    >>> appearance.get_color_scheme()
    'dark'
    >>> appearance.set_color_scheme(None)     # follow the system again
"""

from __future__ import annotations

import threading
from typing import Callable, List, Literal, Optional

ColorScheme = Literal["light", "dark"]
"""The two supported appearance values."""

_system_scheme: str = "light"
_override_scheme: Optional[str] = None

_subscribers: List[Callable[[], None]] = []
_subscribers_lock = threading.Lock()


def _notify_subscribers() -> None:
    """Invoke every registered subscriber, swallowing exceptions."""
    with _subscribers_lock:
        callbacks = list(_subscribers)
    for cb in callbacks:
        try:
            cb()
        except Exception:
            pass


def subscribe(callback: Callable[[], None]) -> Callable[[], None]:
    """Register ``callback`` to fire whenever the effective scheme changes.

    Returns an unsubscribe function. Threadsafe.
    """
    with _subscribers_lock:
        _subscribers.append(callback)

    def _unsub() -> None:
        with _subscribers_lock:
            try:
                _subscribers.remove(callback)
            except ValueError:
                pass

    return _unsub


def _coerce(scheme: object) -> Optional[str]:
    if scheme in ("light", "dark"):
        return str(scheme)
    return None


def set_system_color_scheme(scheme: str) -> None:
    """Publish the operating system's current scheme.

    Called by the platform screen host on create/resume and whenever
    the system reports an appearance change. Invalid values are
    ignored. Subscribers are only notified when the *effective* scheme
    (after any app override) actually changes.
    """
    global _system_scheme
    coerced = _coerce(scheme)
    if coerced is None or coerced == _system_scheme:
        return
    effective_before = get_color_scheme()
    _system_scheme = coerced
    if get_color_scheme() != effective_before:
        _notify_subscribers()


def set_color_scheme(scheme: Optional[str]) -> None:
    """Set (or clear) the app-level scheme override.

    Args:
        scheme: ``"light"`` or ``"dark"`` to force that appearance
            regardless of the system setting, or ``None`` to follow
            the system again.
    """
    global _override_scheme
    coerced = _coerce(scheme) if scheme is not None else None
    if scheme is not None and coerced is None:
        return
    effective_before = get_color_scheme()
    _override_scheme = coerced
    if get_color_scheme() != effective_before:
        _notify_subscribers()


def get_color_scheme() -> str:
    """Return the effective scheme: the app override if set, else the system value."""
    return _override_scheme if _override_scheme is not None else _system_scheme


def get_system_color_scheme() -> str:
    """Return the system-reported scheme, ignoring any app override."""
    return _system_scheme


def reset_color_scheme() -> None:
    """Reset to system ``"light"`` with no override. Intended for tests."""
    global _system_scheme, _override_scheme
    _system_scheme = "light"
    _override_scheme = None

Web Proxy Viewer  |  New URL  |  Original Page