Thymis Documentation

Thymis Module

A Thymis Module is a reusable component that defines configurations, settings, and behaviors for devices managed by Thymis. Modules form the building blocks of device configurations, allowing you to package software, define system settings, and manage deployments in a declarative manner.

Module Structure

All Thymis modules inherit from the base Module class and define a set of settings and configuration options. Here’s the basic structure:

from thymis_controller.modules.modules import Module, Setting, LocalizedString

class MyModule(Module):
    display_name: LocalizedString = "My Module"

    # Define settings
    my_setting = Setting(
        display_name=LocalizedString(en="My Setting", de="Meine Einstellung"),
        type="string",
        default="default_value",
        description=LocalizedString(en="A description of the setting", de="Eine Beschreibung der Einstellung"),
    )

Module Properties

  • display_name: Name of the module that appears in the UI. Can be a string or a LocalizedString for internationalization.
  • icon: Optional base64 encoded icon for light theme.
  • icon_dark: Optional base64 encoded icon for dark theme.

Setting Types

Modules define settings using the Setting class, which supports various types:

# Simple setting types
Setting(type="string")      # Text input
Setting(type="url")         # http(s):// or file:// (absolute path) URL, validated before saving
Setting(type="int")         # Number input
Setting(type="bool")        # Checkbox
Setting(type="textarea")    # Multi-line text input

# Complex types
Setting(type=SelectOneType(...))    # Dropdown selection
Setting(type=ListType(...))         # List of items
Setting(type=SecretType(...))       # For sensitive data
Setting(type=ArtifactType())        # For file artifacts

How Settings Reach the Device

The controller writes the settings of a module instance into the nix configuration of the device configuration or tag it belongs to, and the NixOS module system merges them:

  • a setting that declares nix_attr_name (built-in modules use thymis.config.<namespace>.<setting>) is written as one lib.mkOverride <priority> definition per value leaf, so fields of one setting coming from different sources (device configuration, tags, custom nix) merge individually and a field defined by several sources is resolved by priority (lower wins);
  • a list setting whose elements have an identity (an interface, a container, an artifact path) declares it with ListType(element_key="<setting name of the identity>"); its elements are then written as separate definitions keyed by that identity, so elements and their fields coming from different sources merge as well;
  • a setting a source does not configure is written with the module default and a lib.mkOptionDefault priority, so it never overrides a value another source configured explicitly;
  • a setting a source configures is written even when it is empty: setting a list or an attribute-set setting to empty in a device configuration clears the elements another source (e.g. a tag) contributes;
  • modules that set settings_namespace additionally publish the priority of every setting of the instance as thymis.config._priority.<namespace>.<setting>, which the nix side uses for the configuration it derives (see the helpers below).

NixOS configuration should be derived in nix, not in python: a module that sets settings_namespace declares its derivation with nix_derivation (built-in modules name a file of the controller) or nix_derivation_source (a module that ships the derivation itself), and the controller copies it into the modules directory of the project. The derivation reads the merged settings and writes the resulting configuration with the priority of the setting it comes from:

{ config, lib, ... }:
let
  settings = import ../module-settings.nix { inherit config lib; };
in
{
  systemd.services.my-service = {
    environment.URL = settings.apply "my-module" "url" "https://fallback";
    description = settings.override "my-module" "name" "My service";
  };
}

The helpers (settings, value, isSet, priority, priorityOf, used, lowestPriority, apply, override, overrideOf) are in the module-settings.nix the controller writes into every project, so a derivation imports them relative to itself. See Thymis Modules for how an external module ships its nix code.

List-valued options that other modules also contribute to (like systemd.tmpfiles.rules) must be written without mkOverride, because a priority on a list option drops the definitions of all other sources instead of merging with them; use settings.value for those.

write_nix_settings is only needed for settings whose nix representation is not a settings value (for example the device type, which selects the modules to import). When it writes NixOS configuration itself, it must apply lib.mkOverride <priority> to every definition it writes, so that the configuration of a tag and of a device configuration still merge by priority.

Overridable Functions

Modules can override several functions to customize their behavior:

write_nix_settings

Writes Nix configuration for the settings of a module instance. Modules only need this when their settings cannot be expressed as settings values (see above), otherwise the value of each setting is written by the base implementation from its nix_attr_name:

def write_nix_settings(self, f, path, module_settings, priority, project):
    # Access settings
    my_value = module_settings.settings.get("my_setting", self.my_setting.default)

    # Generate Nix configuration
    f.write(f"  my_nix_option = lib.mkOverride {priority} {convert_python_value_to_nix(my_value)};")

register_secret_settings

Registers secrets that need to be managed securely:

def register_secret_settings(self, module_settings, project):
    secret_settings = []
    if "password" in module_settings.settings:
        secret_settings.append((self.password.type, module_settings.settings["password"]))
    return secret_settings

Localization

Module names, settings, and descriptions support localization using LocalizedString:

display_name = LocalizedString(
    en="My Module",
    de="Mein Modul",
    fr="Mon Module"
)

my_setting = Setting(
    display_name=LocalizedString(
        en="My Setting",
        de="Meine Einstellung"
    ),
    type="string",
    description=LocalizedString(
        en="A useful setting",
        de="Eine nützliche Einstellung"
    )
)

Including Icons

Icons can be any image format (SVG, PNG, etc.) and should be embedded as base64 encoded strings:

icon: str = read_into_base64(
    str(pathlib.Path(__file__).parent / "icons" / "module-icon.svg")
)

Example: Simple Kiosk Module

Here’s a complete example module that creates a basic kiosk configuration:

import thymis_controller.modules.modules as modules
from thymis_controller import models
from thymis_controller.lib import read_into_base64
from thymis_controller.project import Project
import pathlib

class KioskModule(modules.Module):
    display_name: str = "Simple Kiosk"

    icon: str = read_into_base64(
        str(pathlib.Path(__file__).parent / "icons" / "display.svg")
    )

    kiosk_url = modules.Setting(
        display_name="Kiosk URL",
        type="url",
        default="https://example.com",
        description="The URL to display in kiosk mode",
    )

    fullscreen = modules.Setting(
        display_name="Fullscreen",
        type="bool",
        default=True,
        description="Launch browser in fullscreen mode",
    )

    def write_nix_settings(
        self,
        f,
        path,
        module_settings: models.ModuleSettings,
        priority: int,
        project: Project,
    ):
        kiosk_url = (
            module_settings.settings["kiosk_url"]
            if "kiosk_url" in module_settings.settings
            else self.kiosk_url.default
        )

        fullscreen = (
            module_settings.settings["fullscreen"]
            if "fullscreen" in module_settings.settings
            else self.fullscreen.default
        )

        fullscreen_arg = "--kiosk" if fullscreen else ""

        f.write(
            f"""
            services.xserver.enable = lib.mkOverride {priority} true;
            services.displayManager.sddm.enable = lib.mkOverride {priority} true;
            services.displayManager.autoLogin.enable = lib.mkOverride {priority} true;
            services.displayManager.autoLogin.user = "thymiskiosk";
            users.users.thymiskiosk = {{
                isNormalUser = true;
                createHome = true;
            }};
            services.xserver.windowManager.i3.enable = lib.mkOverride {priority} true;
            services.xserver.windowManager.i3.configFile = lib.mkOverride {priority} (pkgs.writeText "i3-config" ''
            exec "{pkgs.chromium}/bin/chromium {fullscreen_arg} '{kiosk_url}'"
            '');
            """.strip()
        )

Working with Complex Settings

For more complex configurations like device settings, you might use nested list types:

device_settings = modules.Setting(
    display_name="Device Configuration",
    description="Configure device-specific parameters",
    type=modules.ListType(
        settings={
            "interface": modules.Setting(
                display_name="Network Interface",
                type="string",
            ),
            "ip_address": modules.Setting(
                display_name="IP Address",
                type="string",
            ),
            "subnet": modules.Setting(
                display_name="Subnet",
                type="string",
            ),
        },
        element_name="Network Interface"
    )
)

Source Code

The module system is implemented in the controller/thymis_controller/modules/modules.py file, which defines the base Module class and setting types.

See Also:

Datenschutz-Einstellungen

Wir verwenden technisch erforderliche Cookies für Betrieb und Sicherheit. Optionale Cookies für Messung und Marketing nutzen wir nur mit Ihrer Einwilligung. Zur Datenschutzerklärung

ende