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) —ConfigFilecalls your loader functor with an open stream of the base file, andIniFileparses it as INI. Use this when you want INI parsing but no drop-in overrides.Base + drop-in overrides (
ConfigFile::OverrideLoader+live_config::IniFileWithOverrides) —ConfigFilecollects the base file plus any<name>.d/*<ext>overrides across XDG roots and delivers them through anOverridesList;IniFileWithOverridesparses 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:
$XDG_CONFIG_HOME(defaults to$HOME/.config)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¶
Configuration overrides in Mir — merge semantics, XDG resolution, and reload model