How to Implement the PowertoyModuleIface Interface for a New PowerToys Utility

To implement the PowertoyModuleIface interface for a new PowerToys utility, create a DLL that exports powertoy_create(), derive a class from the abstract interface defined in src/modules/interface/powertoy_module_interface.h, and implement the mandatory life-cycle methods including enable(), disable(), and get_config().

PowerToys utilities are dynamically loaded as DLLs by the Runner process at startup. Each module must expose a C-linkage factory function that instantiates an object implementing the PowertoyModuleIface abstract base class, enabling the Runner to manage the utility's lifecycle, settings, and hotkey registration through a standardized API.

Understanding the PowertoyModuleIface Interface

Core Interface Location and Purpose

The PowertoyModuleIface interface is defined in src/modules/interface/powertoy_module_interface.h. This header establishes the contract between the PowerToys Runner and individual utilities. According to the Microsoft PowerToys source code, the Runner interacts with modules exclusively through this interface, treating each utility as a black box that responds to standard enable, disable, and configuration commands.

Mandatory Methods to Implement

Every class deriving from PowertoyModuleIface must override these pure-virtual methods:

  • get_name() – Returns the human-readable module name displayed in the Settings UI
  • get_key() – Returns a unique identifier used as the JSON key for settings storage
  • get_config() – Serializes the module's configuration schema to JSON for the Settings UI
  • set_config() – Deserializes JSON from the Settings UI and persists values to disk
  • enable() – Called when the user activates the utility; start background threads or hooks here
  • disable() – Called when the user deactivates the utility; release resources here
  • is_enabled() – Returns the current runtime enabled state
  • destroy() – Self-deletion hook called by the Runner during shutdown

Step-by-Step Implementation Guide

Create the DLL Project Structure

Microsoft provides a ModuleTemplate in tools/project_template/ModuleTemplate/ that preconfigures the correct project settings, export definitions, and folder layout. Use this template to ensure your DLL compiles with the proper runtime settings and linker options compatible with the PowerToys Runner.

The template includes boilerplate for registering an ETW trace provider in DllMain(), which allows your module to emit structured logs that PowerToys aggregates:

BOOL APIENTRY DllMain(HMODULE, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_ATTACH)   Trace::RegisterProvider();
    if (reason == DLL_PROCESS_DETACH)   Trace::UnregisterProvider();
    return TRUE;
}

Implement the Factory Function

The Runner locates your module by looking up the powertoy_create symbol via GetProcAddress. You must export this function with C linkage to prevent name mangling:

extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() {
    return new MyNewUtility();   // MyNewUtility derives from PowertoyModuleIface
}

This factory is the sole entry point. The Runner calls it once during startup to obtain the module instance, then manages the object through the PowertoyModuleIface pointer.

Handle Settings Persistence

Use the PowerToysSettings helper classes from src/common/SettingsAPI/settings_objects.h to guarantee a JSON-based schema that the Settings UI can render. Implement get_config() to describe your configurable properties:

virtual bool get_config(wchar_t* buffer, int* buffer_size) override {
    HINSTANCE hinst = reinterpret_cast<HINSTANCE>(&__ImageBase);
    PowerToysSettings::Settings settings(hinst, get_name());
    
    settings.add_bool_toggle(L"enabled", L"Enable feature", g_settings.enabled);
    settings.add_int_spinner(L"interval", L"Refresh interval", g_settings.interval, 1, 60, 1);
    
    return settings.serialize_to_buffer(buffer, buffer_size);
}

Implement set_config() to parse incoming JSON and persist values:

virtual void set_config(const wchar_t* config) override {
    try {
        auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
        if (auto v = values.get_bool_value(L"enabled"))   g_settings.enabled = *v;
        if (auto v = values.get_int_value(L"interval"))   g_settings.interval = *v;
        values.save_to_settings_file();   // Writes to %LOCALAPPDATA%\PowerToys\
    } catch (...) { /* ignore malformed JSON */ }
}

Add Hotkey Support (Optional)

To expose system-wide hotkeys that the Runner registers on your behalf, override get_hotkeys() and on_hotkey():

virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_sz) override {
    if (buffer && buffer_sz > 0) {
        buffer[0] = { .win = true, .ctrl = false, .shift = true, .alt = false,
                      .key = 'U', .id = 0, .isShown = true };  // Win+Shift+U
    }
    return 1; // Number of hotkeys defined
}

virtual bool on_hotkey(size_t hotkeyId) override {
    if (hotkeyId == 0) {
        // Execute action
        return true; // Swallow the keypress
    }
    return false;
}

Alternatively, implement GetHotkeyEx() and OnHotkeyEx() for extended registration options.

Complete Code Example

The following trimmed implementation from tools/project_template/ModuleTemplate/dllmain.cpp demonstrates the essential structure:

#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>

const static wchar_t* MODULE_NAME = L"MyNewUtility";

struct ModuleSettings {
    bool enabled = true;
    int refresh_interval = 5;
} g_settings;

class MyNewUtility : public PowertoyModuleIface {
    bool m_enabled = false;

    void init_settings() {
        try {
            auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
            if (auto v = values.get_bool_value(L"enabled"))           g_settings.enabled = *v;
            if (auto v = values.get_int_value(L"refresh_interval"))   g_settings.refresh_interval = *v;
        } catch (...) {}
    }

public:
    MyNewUtility() { init_settings(); }

    virtual const wchar_t* get_name() override { return MODULE_NAME; }
    virtual const wchar_t* get_key() override { return MODULE_NAME; }
    virtual void destroy() override { delete this; }

    virtual bool get_config(wchar_t* buffer, int* buffer_size) override {
        PowerToysSettings::Settings settings(reinterpret_cast<HINSTANCE>(&__ImageBase), get_name());
        settings.add_bool_toggle(L"enabled", L"Enable", g_settings.enabled);
        settings.add_int_spinner(L"refresh_interval", L"Interval (sec)", g_settings.refresh_interval, 1, 60, 1);
        return settings.serialize_to_buffer(buffer, buffer_size);
    }

    virtual void set_config(const wchar_t* config) override {
        auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
        if (auto v = values.get_bool_value(L"enabled"))           g_settings.enabled = *v;
        if (auto v = values.get_int_value(L"refresh_interval"))   g_settings.refresh_interval = *v;
        values.save_to_settings_file();
    }

    virtual void enable() override { m_enabled = true; }
    virtual void disable() override { m_enabled = false; }
    virtual bool is_enabled() const override { return m_enabled; }
};

extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() {
    return new MyNewUtility();
}

Registering Your Module with the Runner

After building your DLL, add the filename to the Runner's loading list in src/runner/main.cpp. The Runner iterates through this list during startup, calling LoadLibrary and powertoy_create() for each entry. Without this registration step, your module will not be instantiated even if the DLL is present in the installation directory.

Summary

  • Implement PowertoyModuleIface by deriving from the abstract base class in src/modules/interface/powertoy_module_interface.h
  • Export powertoy_create() as a C-linkage factory function returning your implementation
  • Override mandatory methods: get_name(), get_key(), get_config(), set_config(), enable(), disable(), is_enabled(), and destroy()
  • Use PowerToysSettings helpers from src/common/SettingsAPI/settings_objects.h for JSON schema generation and persistence
  • Optionally implement hotkey methods (get_hotkeys(), on_hotkey()) to receive global shortcut events
  • Register your DLL filename in src/runner/main.cpp so the Runner loads it at startup

Frequently Asked Questions

What is the minimum set of methods required to implement PowertoyModuleIface?

You must implement seven pure-virtual methods: get_name(), get_key(), get_config(), set_config(), enable(), disable(), and destroy(). Additionally, you must implement is_enabled() to track runtime state. These methods allow the Runner to identify your module, render its settings UI, and control its lifecycle.

How does the PowerToys Runner load custom modules?

The Runner loads modules by searching for the powertoy_create symbol in DLLs listed in its internal registry within src/runner/main.cpp. It dynamically loads each DLL using LoadLibrary, retrieves the factory function via GetProcAddress, and calls it to obtain a PowertoyModuleIface instance. The Runner then manages this instance throughout the application lifecycle.

Where are module settings stored on disk?

Settings are persisted as JSON files in the user's local application data folder (%LOCALAPPDATA%\PowerToys\). The PowerToyValues::save_to_settings_file() method writes to a file named {module_key}-settings.json, where {module_key} is the string returned by your get_key() implementation.

Can a PowerToys module implement multiple hotkeys?

Yes. Return a count greater than one from get_hotkeys() and populate the buffer with multiple Hotkey structures, each with a unique id field. The on_hotkey() method receives the id of the triggered hotkey, allowing you to distinguish between different shortcuts within the same module.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →