Skip to content

visualdynamics.theme

theme

Light/dark theme colors shared by the 3D view, 2D plots, and the GUI — and the one answer to "which is it right now".

The colors are for the things visualdynamics draws itself (the VTK scene and the pyqtgraph plots); Qt's own widgets take theirs from the application palette. Which scheme applies is decided here too, in system_scheme: a statement for this launch (VISUALDYNAMICS_THEME, which the --theme flag sets), else what Qt reports, else — on Linux, where a packaged build without a platform-theme plugin made Qt answer Unknown on a dark desktop (2026-09-14) — the desktop asked directly through desktop_scheme (the settings portal, then GNOME's key, then KDE's file), else the palette's own lightness. The user's remembered choice, and telling Qt to wear a scheme rather than follow the OS, live in gui/preferences.py, which is Qt's side of this.

Functions:

Name Description
theme

Resolve a theme name (or a colors dict) to a colors dict.

colormap

values in 0..1 as an (N, 3) array of RGB, on VIRIDIS.

system_scheme

'dark' or 'light': the environment's say, else the OS appearance

desktop_scheme

The Linux desktop's own answer, or None where there is none.

Functions:

theme

theme(name: str | Mapping[str, str] | None = None) -> dict[str, str]

Resolve a theme name (or a colors dict) to a colors dict.

Source code in src/visualdynamics/theme.py
def theme(name: str | Mapping[str, str] | None = None) -> dict[str, str]:
    """Resolve a theme name (or a colors dict) to a colors dict."""
    if isinstance(name, dict):
        return name
    if name is None:
        return THEMES[DEFAULT]
    try:
        return THEMES[name]
    except KeyError:
        raise ValueError(f"Unknown theme {name!r}; choose from {sorted(THEMES)}")

colormap

colormap(values: Any) -> Any

values in 0..1 as an (N, 3) array of RGB, on VIRIDIS.

Source code in src/visualdynamics/theme.py
def colormap(values: Any) -> Any:
    """`values` in 0..1 as an (N, 3) array of RGB, on `VIRIDIS`."""
    import numpy as np

    stops = np.asarray(VIRIDIS, dtype=float)
    t = np.clip(np.asarray(values, dtype=float), 0.0, 1.0) * (len(stops) - 1)
    low = np.clip(t.astype(int), 0, len(stops) - 2)
    f = (t - low)[..., None]
    return stops[low] * (1.0 - f) + stops[low + 1] * f

system_scheme

system_scheme(app: Any = None) -> str

'dark' or 'light': the environment's say, else the OS appearance via Qt, else the desktop asked directly, else the palette.

Falls back to the default theme when Qt or an application instance is unavailable (e.g. plain scripting use).

Source code in src/visualdynamics/theme.py
def system_scheme(app: Any = None) -> str:
    """'dark' or 'light': the environment's say, else the OS appearance
    via Qt, else the desktop asked directly, else the palette.

    Falls back to the default theme when Qt or an application instance is
    unavailable (e.g. plain scripting use).
    """
    import os

    said = os.environ.get(OVERRIDE, '').strip().lower()
    if said in THEMES:
        return said
    try:
        from PySide6.QtCore import Qt
        from PySide6.QtWidgets import QApplication
    except ImportError:
        return DEFAULT
    app = app or QApplication.instance()
    if app is None:
        return DEFAULT
    scheme = app.styleHints().colorScheme()
    if scheme == Qt.ColorScheme.Dark:
        return 'dark'
    if scheme == Qt.ColorScheme.Light:
        return 'light'
    # Qt could not tell. On Linux that is the usual case: Qt learns the
    # scheme through a platform-theme plugin, and a packaged build
    # shipped none until 2026-09-14 — a friend of Brandon's on a dark
    # desktop got a light window. The desktop is asked directly before
    # the palette is judged, since the palette is Qt's own light one
    # whenever the plugin is missing.
    asked = desktop_scheme()
    if asked is not None:
        return asked
    palette = app.palette()  # judge by palette lightness
    return ('dark' if palette.color(palette.ColorRole.Window).lightness() < 128
            else 'light')

desktop_scheme

desktop_scheme(platform: str | None = None, run: Any = None, home: str | None = None) -> str | None

The Linux desktop's own answer, or None where there is none.

Three doors, in the order a modern desktop answers them: the XDG settings portal's color-scheme (GNOME, KDE and the rest, over D-Bus through gdbus, which ships with GLib), GNOME's gsettings key of the same name, and KDE's kdeglobals. Each is a subprocess or a file read with a short timeout, and any failure is "no answer" — never an exception in the way of a window. platform, run and home are for the tests.

Source code in src/visualdynamics/theme.py
def desktop_scheme(platform: str | None = None,
                   run: Any = None, home: str | None = None) -> str | None:
    """The Linux desktop's own answer, or None where there is none.

    Three doors, in the order a modern desktop answers them: the XDG
    settings portal's `color-scheme` (GNOME, KDE and the rest, over
    D-Bus through `gdbus`, which ships with GLib), GNOME's
    `gsettings` key of the same name, and KDE's `kdeglobals`. Each is
    a subprocess or a file read with a short timeout, and any failure
    is "no answer" — never an exception in the way of a window.
    `platform`, `run` and `home` are for the tests.
    """
    import os
    import subprocess
    import sys

    platform = platform or sys.platform
    if not platform.startswith('linux'):
        return None
    run = run or (lambda cmd: subprocess.run(
        cmd, capture_output=True, text=True, timeout=2, check=False).stdout)
    for command, dark, light in (
            (['gdbus', 'call', '--session',
              '--dest', 'org.freedesktop.portal.Desktop',
              '--object-path', '/org/freedesktop/portal/desktop',
              '--method', 'org.freedesktop.portal.Settings.ReadOne',
              'org.freedesktop.appearance', 'color-scheme'],
             'uint32 1', 'uint32 2'),
            (['gsettings', 'get', 'org.gnome.desktop.interface',
              'color-scheme'],
             'prefer-dark', 'prefer-light')):
        try:
            out = run(command) or ''
        except Exception:  # noqa: BLE001, S112 — absent tool, timeout: no answer from this door
            continue
        if dark in out:
            return 'dark'
        if light in out:
            return 'light'
    try:
        kdeglobals = os.path.join(home or os.path.expanduser('~'),
                                  '.config', 'kdeglobals')
        with open(kdeglobals, encoding='utf-8', errors='replace') as f:
            for line in f:
                if line.strip().lower().startswith('colorscheme='):
                    return ('dark' if 'dark' in line.lower() else 'light')
    except OSError:
        pass
    return None