Привет, Хабр! Меня зовут Aloncie. Пока в моем окружении часто спорят о том, какой язык программирования учить первым, я решил не выбирать легких путей и закопаться в «кишки» системного программирования.

Мой проект Rwal — это CLI-утилита (с перспективой перехода на GUI) для управления обоями, которая должна одинаково хорошо чувствовать себя в разных окружениях: от KDE и GNOME до Windows. В этой статье я подробно разберу архитектуру проекта, работу с D-Bus, интеграцию со стандартами C++20 и то, как я организовал сборку.

Архитектура: Абстракция над рабочим столом

Главная проблема при написании менеджера обоев — фрагментация сред рабочего стола (DE) в Linux. В KDE это делается через скрипты Plasma, в GNOME — через GSettings. Чтобы код не превратился в нагромождение #ifdef, я использовал паттерн Адаптер.

Основой стал базовый интерфейс IWallpaperSetter. Это позволяет остальной части приложения не знать, в какой среде оно запущено.

// src/wallpaper/IWallpaperSetter.hpp
class IWallpaperSetter {
public:    
  virtual ~IWallpaperSetter() = default;    
  virtual bool setWallpaper(const std::string& path) = 0;
};

Реализация под KDE (D-Bus и JS-инъекции)

Для KDE Plasma простого вызова системной команды недостаточно. Приходится общаться с org.kde.plasmashell через D-Bus. Я реализовал это в KdeSetter.cpp. Особенность в том, что мы посылаем Plasma-скрипт на языке JavaScript, который находит все рабочие столы и меняет им фон.

// Упрощенный фрагмент из src/wallpaper/KdeSetter.cpp
QDBusInterface remoteApp("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell");
QString script = QString(
    "var allDesktops = desktops();"
    "for (var i = 0; i < allDesktops.length; i++) {"
    "    var d = allDesktops[i];"
    "    d.wallpaperPlugin = 'org.kde.image';"
    "    d.currentConfigGroup = Array('Wallpapers', 'org.kde.image', 'General');"
    "    d.writeConfig('Image', 'file://%1');"
    "}"
).arg(QString::fromStdString(path));

remoteApp.call("evaluateScript", script);

Реализация под GNOME (GSettings)

В GNOME всё прозрачнее: мы используем QProcess для вызова утилиты gsettings. Однако здесь важно учитывать, что настройки разделены на светлую и темную темы.

// Из src/wallpaper/GnomeSetter.cpp
QProcess::execute("gsettings", {    "set", "org.gnome.desktop.background", "picture-uri-dark",     QString("file://%1").arg(QString::fromStdString(path))
});

Сетевой слой и RAII: Обертка над libcurl

Для загрузки высококачественных изображений я выбрал libcurl. Чтобы избежать утечек памяти и типичных проблем C-style библиотек, я реализовал обертку CurlWrapper с использованием принципов RAII.

Особое внимание уделил управлению ресурсами через std::unique_ptr с кастомным делейтером. Это гарантирует очистку дескриптора CURL даже при возникновении исключений.

// src/net/CurlWrapper.hpp
using CurlPtr = std::unique_ptr<CURL, void(*)(CURL*)>;
// Реализация
CurlWrapper::CurlWrapper() : curl_(curl_easy_init(), curl_easy_cleanup) {    if (!curl_) throw std::runtime_error("Failed to initialize CURL");
}

Это решение позволило мне инкапсулировать логику настройки запросов (User-Agent, таймауты) внутри одного класса, предоставляя приложению чистый интерфейс для скачивания файлов.

Система сборки: От Docker к модульному CMake

Изначально я пробовал использовать Docker для изоляции окружения, но для системной утилиты, которой нужен доступ к D-Bus хоста, это создавало лишние накладные расходы. В итоге я перешел на «чистый» CMake.

В моем CMakeLists.txt я жестко задал стандарт C++20. Это критично, так как проект использует современные фичи вроде std::format (в планах) и асинхронные потоки.

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 REQUIRED COMPONENTS Core DBus Widgets)
find_package(CURL REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
add_subdirectory(src/wallpaper)
target_link_libraries(${PROJECT_NAME} PRIVATE wallpaper_lib Qt5::DBus CURL::libcurl nlohmann_json::nlohmann_json)

Технические сложности и C++20

Одной из самых раздражающих проблем стали фризы интерфейса при загрузке 4K-изображений. Сейчас я работаю над внедрением std::jthread из стандарта C++20.

Почему именно jthread?

  1. Авто-join: поток сам завершится корректно при выходе из области видимости.

  2. Stop Tokens: это позволяет элегантно прервать загрузку, если пользователь передумал или закрыл программу, не дожидаясь таймаута сокета.

Также я столкнулся с тем, что разные версии GCC и Clang имеют разную степень поддержки заголовка <format> и jthread. Это заставило меня глубже разобраться в настройках компилятора и линковке libstdc++.

Чему я научился как разработчик

Этот проект стал для меня тренажером по проектированию систем. Основные выводы:

  • Интерфейсы — это сила. Разделение на IWallpaperSetter позволило добавить поддержку нового DE за 15 минут.

  • Статический анализ. Использование clang-tidy помогло найти несколько потенциальных use-after-free при работе с Qt-сигналами.

  • Документирование решений (ADR). Даже если ты единственный разработчик, полезно записывать, почему ты выбрал D-Bus вместо прямого редактирования конфигов Plasma.

Что дальше?

Rwal находится в активной разработке. В планах:

  1. Полноценный адаптер для Windows (через WinAPI SystemParametersInfo).

  2. Переход на асинхронные запросы через QtNetwork или boost::asio для лучшей интеграции с event loop.

  3. Оптимизация потребления памяти при парсинге больших JSON-ответов от API фотостоков.

Для меня, как для ученика 10 класса, работа над системным инструментом — это лучший способ понять, как устроена ОС. Системное программирование — это не страшно, если уметь декомпозировать задачи.

Буду рад конструктивной критике архитектуры и советами по работе с потоками в C++!


GitHub проекта