# Exo Modules ## Structure Exo features live in `ignis/modules//`. Each module must have a `module.py` manifest exporting `MODULE`. - `ignis/core/` contains the registry, module types and shared code. - `ignis/modules/` contains Exo Modules. - `m3components` and `shared_modules` are shared code, not Exo Modules. - `ignis/config.py` loads the registry and attaches it to `app.registry`. Import `core.*`, not `ignis.core.*`. The latter is not the Exo core package. ## Manifest A module directory must match its ID (`[a-z][a-z0-9_]*`). Directories without a manifest are ignored. ```python from core.module import Module def setup(ctx): from .impl import create_service ctx.provide("my_service", create_service()) MODULE = Module( id="my_module", requires=(), provides=("my_service",), needs=(), settings=None, setup=setup, settings_tabs=(), bar_widgets=(), styles=None, nix_options=None, enabled_by_default=True, ) ``` `Module` fields: - `requires`: other module IDs - `provides`: services provided by this module - `needs`: required services - `settings`: optional JSON file path - `setup`: module setup function receiving a `Context` - `settings_tabs`: `SettingsTab` definitions - `bar_widgets`: `BarWidget` definitions - `styles`: optional styles - `nix_options`: optional Nix options - `enabled_by_default`: defaults to `True` `SettingsTab(id, title, icon, order, group, factory)` defines a settings tab. Its factory is called when needed. Groups are ordered as `Shell`, `Connections`, `System`. Unknown groups are sorted alphabetically after them. `BarWidget(id, factory, default_location, default_bar_id, default_visible, name, description)` defines a bar widget. Bar widgets must stay inside `bar` for now. Keep manifests import-light. Do not import `gi`, `ignis`, `user_settings` or anything that imports them at the top level. Use lazy imports inside `setup()` and factories instead. This also applies to disabled modules because all manifests are read before startup. `tests/test_manifest_imports.py` checks this. ## Registry `core.registry` scans `modules/*/module.py` and loads `MODULE`. Module enablement follows this priority: 1. `EXO_MODULE_` environment variable (`1`, `true`, `yes`, `on`, `0`, `false`, `no`, `off`). 2. `modules.json`, generated by Nix from `exo.modules..enable`. 3. `enabled_by_default` in the manifest. Invalid environment values produce a warning. Module states are decided at startup. `requires` uses module IDs. `provides` and `needs` use service names. Missing dependencies or required services cause the affected module to be skipped. Modules are loaded in dependency order, with alphabetical order used for ties. The registry catches discovery and setup errors, logs the module ID and traceback, and skips the affected module. Modules depending on a failed module are skipped too. Failed setup side effects are not rolled back. ## Context and services Every `setup()` receives a `Context`. Modules should communicate through services and signals instead of importing each other directly. ```python ctx.provide("name", service) # Register a service ctx.get("name") # Optional dependency, returns None if missing ctx.require("name") # Required dependency, raises if missing ``` `core` must not depend on specific Exo Modules. ## Settings and UI Settings are stored in shared JSON files: - `ignis/core/settings.json` - `ignis/modules//settings.json` Python uses them to build `ExoGroup` and `OptionsManager`. Nix reads the same files with `builtins.fromJSON` and `readDir`. No IFD is needed. Settings tabs and bar widgets are declared in the module manifest. The settings UI combines tabs using `modules/settings/tab_layout.py`. Keep the existing settings JSON structure, `exo.settings.*` paths and public Nix options unchanged. Bar widgets stay inside `bar`. The `interface.modules.*` namespace and "Bar Modules" label are kept for compatibility. Use "bar widget" in new code and documentation. `BarWidget` is not an Exo Module. If two settings tabs have the same ID, the later one wins and a warning is logged. ## Styles Modules can provide styles through `Module.styles`. They are merged at runtime into a writable overlay at: `state_path("exo", "styles-overlay", "ignis")` Styles are loaded in this order: `bar, dock, corners, settings, network, bluetooth, sidebar, launcher, powermenu, quickcenter, notifications, osd, m3, previews` Unknown styles produce a warning and are placed last. No manual `@use` statements are needed. ## Nix Use `exo.modules..enable` to enable or disable modules. Nix writes the result to `modules.json` next to the seed file. Settings are exposed through `exo.settings.*` and read from the shared JSON files. The old hand-written `toJSON` in `settings.nix` is no longer used. Other Nix options belong in `nix/options/*`. ## Notes - Do not use `utils.Poll` or blindly resample values. Use GObject/GLib signals and cancellable `GLib.timeout*` calls. - Keep compatibility with existing settings, Nix paths and lock behaviour. - Compatibility wrappers in `scripts/` may remain during migration. - `ignis/modules/_template/` contains a minimal manifest template. Copy it into a properly named module directory before using it. The registry currently includes `corners`, `launcher`, `notifications`, `osd`, `m3test`, `powermenu`, `quickcenter`, `bluetooth`, `network`, `settings`, `bar`, `wallpaper` and `recorder`. Other services include `bar_styles` and `auto_dark`.