How to add configuration overrides in Mir

This guide shows how to load a base configuration file and drop-in override files, with automatic reload on change, using miral::ConfigFile and miral::live_config::IniFileWithOverrides.

Requires MirAL 5.9 for the override-aware ConfigFile constructor; IniFileWithOverrides is available since MirAL 5.8.

Note: Write Your First Wayland Compositor is a prerequisite for this how-to.


Choosing an approach

ConfigFile provides two constructors, each paired with a live_config::Store implementation that knows how to parse the stream(s) it is handed:

  • Single file (ConfigFile::Loader + live_config::IniFile)ConfigFile calls your loader functor with an open stream of the base file, and IniFile parses it as INI. Use this when you want INI parsing but no drop-in overrides.

  • Base + drop-in overrides (ConfigFile::OverrideLoader + live_config::IniFileWithOverrides)ConfigFile collects the base file plus any <name>.d/*<ext> overrides across XDG roots and delivers them through an OverridesList; IniFileWithOverrides parses and merges them in one transaction. Use this for layered, distributor-friendly configuration.

For non-INI formats, pair the Loader or OverrideLoader constructors with your own parser:

miral::ConfigFile config_file{
    runner,
    "myapp.conf",
    miral::ConfigFile::Mode::reload_on_change,
    [](std::istream& stream, std::filesystem::path const& path)
    {
        // parse stream...
    }};

// or

miral::ConfigFile config_file{
    runner,
    "myapp.conf",
    miral::ConfigFile::Mode::reload_on_change,
    [](miral::live_config::OverridesList const& changes)
    {
        // ...
    },
    ".conf"};

The rest of this guide uses the OverrideLoader + IniFileWithOverrides approach.

Set up the configuration store

First, add the required headers:

#include <miral/config_file.h>
#include <miral/live_config_ini_file_with_overrides.h>
#include <miral/live_config_overrides_list.h>

Create an IniFileWithOverrides instance, register typed attributes, and attach an on_done callback that fires once per load:

mlc::IniFileWithOverrides config;

config.add_string_attribute(
    {"display", "background"}, "Background colour", "black",
    [](mlc::Key const& key, std::optional<std::string_view> value)
    {
        std::cout << "config: " << key.to_string() << "=" << value.value_or("") << std::endl;
    });

config.add_strings_attribute(
    {"display", "workspaces"}, "Workspace names",
    [](mlc::Key const& key, std::optional<std::span<std::string const>> values)
    {
        std::string joined;
        if (values)
            for (auto const& v : *values)
                joined += (joined.empty() ? "" : ",") + v;
        std::cout << "config: " << key.to_string() << "=" << joined << std::endl;
    });

config.on_done([] { std::cout << "config: loaded" << std::endl; });

Each attribute handler receives the registered miral::live_config::Key and an std::optional value. The value is std::nullopt when the key is absent from all files; when adding attributes, it’s possible to provide a preset default (here "black").

Attach ConfigFile

Construct ConfigFile with the override-aware constructor, using the load method in the callback and a file extension to filter override files:

ConfigFile config_file{
    runner,
    "demo-compositor.conf",
    ConfigFile::Mode::reload_on_change,
    [&config](mlc::OverridesList const& changes) { config.load(changes); },
    ".conf"};

Where files live

Given "demo-compositor.conf" as the base filename, ConfigFile searches these directories in priority order:

  1. $XDG_CONFIG_HOME (defaults to $HOME/.config)

  2. Entries in $XDG_CONFIG_DIRS (defaults to /etc/xdg)

The base file is loaded from the highest-priority root that contains it. Override files are collected from demo-compositor.conf.d/ under every root, filtered by the extension passed to ConfigFile (.conf). Override files in higher-priority roots shadow same-named files from lower-priority roots.

Example layout:

~/.config/demo-compositor.conf           # base (user)
~/.config/demo-compositor.conf.d/
    90-user.conf                         # user override
/etc/xdg/demo-compositor.conf            # base (system, used if user base absent)
/etc/xdg/demo-compositor.conf.d/
    10-site.conf                         # system override

Override files are applied in lexicographic order of their basename. With the layout above, 10-site.conf is applied before 90-user.conf. Whole number prefixes are taken into account, so 5-foo.conf < 10-bar.conf.

A sample demo-compositor.conf:

display_background=black
display_workspaces=home
display_workspaces=work

A sample demo-compositor.conf.d/90-user.conf that overrides the background and adds a workspace:

display_background=blue
display_workspaces=gaming

After merging: display_background is blue (last-writer-wins); display_workspaces is home,work,gaming (array values accumulate across files). See Configuration overrides in Mir for the full merge rules.

Build and try it out

Create a new directory for the project and add these files:

CMakeLists.txt:

cmake_minimum_required(VERSION 3.25)

project(demo-config-override)

set(CMAKE_CXX_STANDARD 26)

include(FindPkgConfig)
pkg_check_modules(MIRAL miral REQUIRED IMPORTED_TARGET)

add_executable(demo-config-override main.cpp)

target_link_libraries(demo-config-override PkgConfig::MIRAL)

main.cpp:

/*
 * Copyright © Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 or 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <miral/runner.h>
#include <miral/floating_window_manager.h>
#include <miral/set_window_management_policy.h>

#include <miral/config_file.h>
#include <miral/live_config_ini_file_with_overrides.h>
#include <miral/live_config_overrides_list.h>

#include <iostream>

using namespace miral;
namespace mlc = miral::live_config;

int main(int argc, char const* argv[])
{
    MirRunner runner{argc, argv};

    mlc::IniFileWithOverrides config;

    config.add_string_attribute(
        {"display", "background"}, "Background colour", "black",
        [](mlc::Key const& key, std::optional<std::string_view> value)
        {
            std::cout << "config: " << key.to_string() << "=" << value.value_or("") << std::endl;
        });

    config.add_strings_attribute(
        {"display", "workspaces"}, "Workspace names",
        [](mlc::Key const& key, std::optional<std::span<std::string const>> values)
        {
            std::string joined;
            if (values)
                for (auto const& v : *values)
                    joined += (joined.empty() ? "" : ",") + v;
            std::cout << "config: " << key.to_string() << "=" << joined << std::endl;
        });

    config.on_done([] { std::cout << "config: loaded" << std::endl; });

    ConfigFile config_file{
        runner,
        "demo-compositor.conf",
        ConfigFile::Mode::reload_on_change,
        [&config](mlc::OverridesList const& changes) { config.load(changes); },
        ".conf"};

    return runner.run_with({set_window_management_policy<FloatingWindowManager>()});
}

Build:

cmake -B build .
cmake --build build

Set up config files:

mkdir -p ~/.config/demo-compositor.conf.d

cat > ~/.config/demo-compositor.conf <<'EOF'
display_background=black
display_workspaces=home
display_workspaces=work
EOF

cat > ~/.config/demo-compositor.conf.d/90-user.conf <<'EOF'
display_background=blue
display_workspaces=gaming
EOF

Run the compositor:

WAYLAND_DISPLAY=wayland-99 ./build/demo-config-override &

You should see lines like:

config: display_background=blue
config: display_workspaces=home,work,gaming
config: loaded

To test reload: edit ~/.config/demo-compositor.conf.d/90-user.conf and the compositor will re-read and re-merge all files automatically.

See also