Skip to content
Waxed Display Server
← Back to Docs

Property System

Property System

Overview

The Waxed Property System enables runtime configuration of plugins through a centralized registry. Plugins expose configurable properties that can be modified at runtime via waxedctl or IPC commands, without requiring a restart.

The system follows a callback-based pattern:

  • Plugins register properties during initialization with a setter callback
  • External tools (waxedctl) request property changes via IPC
  • The core dispatches the change to the plugin’s callback
  • The plugin applies the new value immediately

Architecture

Property Key Format

Properties are identified by a hierarchical key with three components:

plugin

namespace

key

Examples:

desktop:yaml:background

desktop:yaml:mode

transitioner:initial:plugin

  • plugin: The plugin name (e.g., “desktop”, “loginscreen”)
  • namespace: A grouping category (e.g., “yaml”, “transition”)
  • key: The specific property name (e.g., “background”, “mode”)

Examples:

  • desktop:yaml:background - Background image path for desktop plugin
  • desktop:yaml:mode - Render mode for desktop plugin
  • transitioner:initial:plugin - Initial plugin for transitioner

Data Types

Properties support the following types (defined in PropertyType enum):

TypeDescriptionExample Values
StringText string"/path/to/image.jpg"
IntegerWhole number42
BooleanTrue/false"true", "false"
FloatFloating-point3.14

All property values are passed as strings. The plugin’s setter callback is responsible for parsing the string value into the appropriate type.

Core Components

PropertySetterFn Callback Type

The callback signature for property setters (defined in core_api.h):

extern "C" {
    using PropertySetterFn = auto (*)(const char* value, void* user_data) -> void;
}

Parameters:

  • value: The new property value as a C string
  • user_data: Opaque pointer provided during registration

Return: void (no error reporting via return value)

The callback is invoked synchronously by the core when a property change request is received. The callback should:

  1. Validate the value
  2. Parse the string into the appropriate type
  3. Update the plugin’s internal state
  4. Apply any side effects (e.g., reload resources, re-render)

PropertyCallback Storage

Stored in PluginManager (plugin_manager.h):

struct PropertyCallback {
    std::string plugin_name;       // Plugin name
    std::string property_namespace;// Property namespace
    std::string property_key;      // Property key
    PropertySetterFn setter;       // Callback function pointer
    void* user_data;               // Opaque user data pointer
};

Callbacks are stored in a map indexed by the full key:

std::unordered_map<std::string, PropertyCallback> property_callbacks_;

register_property() API

Core API function (from core_api.h):

auto (*register_property)(CoreAPI* core,
                         const char* namespace_,
                         const char* key,
                         const char* description,
                         PropertySetterFn setter,
                         void* user_data) -> int;

Parameters:

  • core: Core API handle
  • namespace_: Property namespace (e.g., “yaml”)
  • key: Property key (e.g., “background”)
  • description: Human-readable description
  • setter: Callback function when property is set
  • user_data: User data passed to callback

Return: 0 on success, non-zero on error

Implementation (core_api.cpp):

auto core_register_property(CoreAPI* core, const char* namespace_,
                             const char* key, const char* description,
                             PropertySetterFn setter, void* user_data) -> int {
    if (!core || !core->internal_handle || !namespace_ || !key || !setter) {
        return -1;
    }

    auto* manager = static_cast<PluginManager*>(core->internal_handle);

    // Build the full property key: plugin_name:namespace:key
    std::string full_key = std::string(core->own_name) + ":" + namespace_ + ":" + key;

    LOGC_INFO("Registering property: {}", full_key);
    LOGC_INFO("  Description: {}", description);

    // Store the property registration in PluginManager
    manager->register_property_callback(core->own_name, namespace_, key, setter, user_data);

    LOGC_INFO("Property callback registered");
    return 0;
}

invoke_property_callback() Dispatch

PluginManager method (plugin_manager.cpp):

auto PluginManager::invoke_property_callback(
    const std::string& plugin_name,
    const std::string& property_namespace,
    const std::string& property_key,
    const std::string& value
) -> Result<void> {
    std::shared_lock lock(mutex_);

    // Build full key: plugin_name:namespace:key
    std::string full_key = plugin_name + ":" + property_namespace + ":" + property_key;

    auto it = property_callbacks_.find(full_key);
    if (it == property_callbacks_.end()) {
        LOGC_ERROR("Property callback not found: {}", full_key);
        return std::unexpected(ErrorCode::PropertyNotFound);
    }

    const auto& callback = it->second;

    LOGC_INFO("Invoking property callback: {} = {}", full_key, value);

    // Unlock before calling the callback to prevent deadlock
    lock.unlock();

    // Invoke the callback
    callback.setter(value.c_str(), callback.user_data);

    return {};
}

Thread Safety Note: The mutex is unlocked before invoking the callback to prevent deadlock if the callback calls back into PluginManager.

How Plugins Register Properties

Registration Pattern

Plugins typically register properties during their waxed_plugin_init() function:

// Example: Background property setter
auto background_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);

    // Validate
    if (!value) return;

    // Parse value
    std::string path(value);

    // Apply change (e.g., load new background)
    state->load_background(path);

    LOGC_INFO("Background changed to: {}", path);
}

// In init function:
auto waxed_plugin_init(const PluginInitParams* params) -> PluginState* {
    auto* state = new PluginState();
    state->core_api = params->core_api;

    // Register property
    if (state->core_api && state->core_api->funcs) {
        int result = state->core_api->funcs->register_property(
            state->core_api,
            "yaml",                    // namespace
            "background",              // key
            "Background image path",   // description
            background_property_setter,// callback
            state                      // user_data
        );

        if (result == 0) {
            LOGC_INFO("Registered property 'yaml:background'");
        }
    }

    return state;
}

Real-World Example: Desktop Plugin

From plugins/desktop/plugin.cpp:

// Property setter for background image
auto background_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<VulkanState*>(user_data);
    if (state->shutdown_requested_.load(std::memory_order_acquire)) return;

    // Use Core API to enqueue async background load with callback
    const char* mode_str = get_render_mode_name(state->render_mode);
    uint32_t display_width = state->display_config.config.width;
    uint32_t display_height = state->display_config.config.height;

    int result = state->core_api->funcs->set_background_dma_buf(
        state->core_api,
        value,                    // image path
        mode_str,                 // render mode
        display_width,
        display_height,
        background_dma_buf_callback,
        state
    );

    if (result != 0) {
        LOGC_ERROR("Failed to enqueue background load: {}", value);
    }
}

// Property setter for render mode
auto mode_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<VulkanState*>(user_data);

    LOGC_INFO("Property change received: yaml:mode = {}", value);

    // Parse the new mode
    std::string mode_str(value);
    VulkanState::RenderMode new_mode = parse_render_mode(mode_str);

    if (new_mode == state->render_mode) {
        LOGC_INFO("Mode unchanged: {}", value);
        return;
    }

    state->render_mode = new_mode;
    LOGC_INFO("Render mode changed to: {}", value);

    // Trigger background reload with new mode
    // ...
}

// Registration in init()
int prop_result = state->core_api->funcs->register_property(
    state->core_api,
    "yaml",                    // namespace
    "background",              // key
    "Background image path for desktop plugin",
    background_property_setter,
    state
);

if (prop_result == 0) {
    LOGC_INFO("Registered property 'yaml:background'");
}

How waxedctl Sets Properties

IPC Command Flow

The property system integrates with the IPC server to handle property change requests:

waxedctl

IPC Server

PluginManager

Plugin Callback

IPC Command Handler

From src/core/ipc/server.cpp:

// Property set command handler
auto result = plugin_manager_.invoke_property_callback(
    cmd.plugin_name,
    cmd.property_namespace,
    cmd.property_key,
    cmd.property_value
);

if (result) {
    response.status = ResponseStatus::Success;
    response.message = "Property set: " + cmd.plugin_name + ":" +
                      cmd.property_namespace + ":" + cmd.property_key;
} else {
    response.status = ResponseStatus::Error;
    response.message = "Failed to set property: " +
                      to_string(result.error());
}

waxedctl Usage

# Set a property via waxedctl
waxedctl -p desktop:yaml:background "/path/to/image.jpg"

# Set render mode
waxedctl -p desktop:yaml:mode "cover"

# Set transitioner overlay
waxedctl -p transitioner:yaml:overlay "true"

Property Commands

The IPC server supports the following property commands:

property set <plugin> <namespace> <key> <value>
    Set a plugin property value

property get <plugin> <namespace> <key>
    Get a plugin property value

property list [plugin] [namespace]
    List properties (all plugins or specific plugin/namespace)

Example Property: yaml:background

Full Flow Diagram

PluginIPC ServerwaxedctlPluginIPC Serverwaxedctl2. Parse commandExtract:- plugin: "desktop"- ns: "yaml"- key: "background"- value: "/path..."4. Find callback for key5. Call: background_property_setter(value, state)6. Plugin applies:- Parse path- Enqueue DMA-BUF- Update state1. Send IPC command:"set desktop:yaml:background /path/img.jpg"3. invoke_property_callback()7. Return Result<void>8. Send response:"Property set: desktop:yaml:background"

Thread Safety

The property system is thread-safe:

  1. Registration: Protected by PluginManager::mutex_ (shared mutex)
  2. Invocation: Uses shared lock during lookup, unlocks before callback
  3. Callback execution: Occurs outside the lock to prevent deadlock

Important: Property callbacks may be invoked from any thread (typically the IPC worker thread). Plugins must ensure their internal state updates are thread-safe.

Use Cases and Patterns

1. Configuration Reload

Enable runtime configuration changes without restart:

auto config_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);
    state->config_path = value;
    state->reload_config();
}

2. Resource Swapping

Replace resources at runtime (images, shaders, etc.):

auto background_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);
    state->enqueue_background_load(value);  // Async load
}

3. Debug/Feature Toggles

Enable/disable debug features or modes:

auto debug_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);
    state->debug_enabled = (strcmp(value, "true") == 0);
}

4. Trigger Actions

Invoke actions via property changes:

auto transition_trigger_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<TransitionerState*>(user_data);
    state->target_plugin = value;
    state->start_transition();
}

Best Practices

1. Naming Conventions

  • Use lowercase for namespaces and keys
  • Use descriptive, hierarchical namespaces: yaml, transition, config
  • Use clear key names: background, mode, overlay

2. Error Handling

Property setters cannot return errors. Handle failures gracefully:

auto property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);

    if (!value) {
        LOGC_ERROR("Received null value");
        return;  // Silent failure
    }

    if (!validate(value)) {
        LOGC_ERROR("Invalid value: {}", value);
        return;  // Log and ignore
    }

    // Apply valid value
    state->apply(value);
}

3. Async Operations

For expensive operations (loading images, etc.), use async patterns:

auto property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);

    // Enqueue async load (non-blocking)
    state->enqueue_load(value, [](bool success) {
        if (success) {
            LOGC_INFO("Resource loaded successfully");
        } else {
            LOGC_ERROR("Resource load failed");
        }
    });
}

4. Thread Safety

Assume callbacks can be invoked from any thread:

auto property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);

    // Use mutex or atomics for state updates
    std::lock_guard lock(state->mutex);
    state->value = value;
    state->dirty = true;
}

5. Idempotency

Make setters idempotent when possible:

auto mode_property_setter(const char* value, void* user_data) -> void {
    auto* state = reinterpret_cast<PluginState*>(user_data);

    RenderMode new_mode = parse_mode(value);

    // Check if actually changed
    if (new_mode == state->mode) {
        LOGC_INFO("Mode unchanged: {}", value);
        return;  // Skip redundant work
    }

    state->mode = new_mode;
    state->apply_mode();
}

Property Registry (Alternative)

The codebase also contains PropertyRegistry and PropertyDefinition classes (include/waxed/ipc/property_registry.h) which provide a more structured property system with:

  • Property type metadata (String, Integer, Boolean, Float)
  • Default values
  • Runtime-settable flags
  • Property descriptions

This registry is designed for future enhancement of the property system but is not currently used by the plugin property callback mechanism.

Summary

The Property System provides:

  1. Runtime Configuration: Change plugin behavior without restart
  2. Decoupled Architecture: Plugins register callbacks, core dispatches changes
  3. Thread Safety: Safe concurrent access to property registry
  4. Simple API: Single register_property() function for plugins
  5. IPC Integration: Works seamlessly with waxedctl and IPC commands