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 aLocalizedStringfor 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 artifactsHow 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 usethymis.config.<namespace>.<setting>) is written as onelib.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.mkOptionDefaultpriority, 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_namespaceadditionally publish the priority of every setting of the instance asthymis.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_settingsLocalization
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.