Привет, Хабр! Меня зовут 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 проекта

Комментарии (8)


  1. Cheater
    05.04.2026 14:00

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

    Каким образом jthread может решить вашу проблему, явно происходящую от того, что UI и загрузка не распараллелены?

    так как проект использует современные фичи вроде std::format (в планах) и асинхронные потоки

    ни того ни другого в коде нет

    я реализовал обертку CurlWrapper с использованием принципов RAII.

    не изобретайте велосипед, используйте библиотеку

    стандарт C++20. Это критично, так как проект использует современные фичи вроде std::format

    Вы удивитесь насколько слабая поддержка std::format в STL C++20. Используйте libfmt лучше сразу

    Системное программирование — это не страшно

    У вас не системная а прикладная утилита, несмотря на то что C++


    1. Aloncie Автор
      05.04.2026 14:00

      Я понял. Учту в следующих статья. Спасибо за критику.


  1. PyXiion
    05.04.2026 14:00

    должна одинаково хорошо чувствовать себя в разных окружениях: от KDE и GNOME до Windows

    В коде есть GnomeSetter и KdeSetter, упоминается несуществующий LinuxFallbackSetter. Поддержки Windows нет.

    WallpaperFactory::create
    /* soon
    
    	#ifdef _WIN32
    		return std::make_unique<WindowsSetter>();	
    	#elif defined(__APPLE__)		
    		return std::make_unique<MacosSetter>();
    */
    
    	#ifdef RWAL_USE_KDE
            return std::make_unique<KdeSetter>();
        #elif defined(RWAL_USE_GNOME)
            return std::make_unique<GnomeSetter>();
        #elif defined(RWAL_USE_FALLBACK)
            return std::make_unique<LinuxFallbackSetter>();
        #else
            return nullptr; 
        #endif

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

    Как связаны Docker и CMake?

    И статья выглядит очень похожей на ИИ-генерацию, учитвая, что био профиля, README (и прочие .md файлы в репозитории) явно сгенерированы нейронкой


  1. Aloncie Автор
    05.04.2026 14:00

    Про LinuxFallbackSetter и Windows - мой косяк. В коде пока только заготовки и планы, в статье я имел ввиду только плановую поддержку Windows.

    Связь Docker и CMake была в попытке сделать контейнерную среду сборки, но из-за проблем с пробросом я в итоге выбрал обычный CMake.

    Для оформления README и профилей действительно использую ИИ. Но архитектуру и статью пишу сам. Ошибки в репозитории это как раз подтверждают)


    1. PyXiion
      05.04.2026 14:00

      Скрытый текст

      Я думал, что у меня шиза, но чекеры типа gptzero и gigacheck действительно показывают, что писал ИИ.


  1. Aloncie Автор
    05.04.2026 14:00

    Детекторы на технические тексты всегда так реагируют, там слишком много структуры. Я не отрицал, что использую ИИ как инструмент, странно ожидать, что я буду вручную вылизывать все, что делаю. Но за логику и код в статье отвечаю я, и ошибки там вполне человеческие. Если считаешь, что статью написал бот - твое право. Я пришел за фидбеком по коду, а не за охотой за ИИ.


    1. PyXiion
      05.04.2026 14:00

      Проблема не в самом ИИ, а в том, что за статьёй по факту ничего не стоит. Вместо того, чтобы просить нейронку "напиши статью, вот код", можно было написать личный опыт при разработке, аля на какие грабли наступил. Я не проф писатель статей, но именно это я примерно и ожидал, когда увидел статью в ленте, как читатель.

      И современные детекторы очень даже неплохо различают рукописные технические статьи от сгенерированных (сам Хабр пользуется GigaCheck). В самом тексте есть различные нестыковки, которые написал, скорее всего, ИИ, так как ему неизвестен полный контекст создания программы.

      У тебя крутой возраст и проект для старта в C++, но не стоит это тратить на какой-то промпт-инжиниринг для Хабра


  1. Aloncie Автор
    05.04.2026 14:00

    Да, ты прав.