Привет, Хабр! Меня зовут 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?
Авто-join: поток сам завершится корректно при выходе из области видимости.
Stop Tokens: это позволяет элегантно прервать загрузку, если пользователь передумал или закрыл программу, не дожидаясь таймаута сокета.
Также я столкнулся с тем, что разные версии GCC и Clang имеют разную степень поддержки заголовка <format> и jthread. Это заставило меня глубже разобраться в настройках компилятора и линковке libstdc++.
Чему я научился как разработчик
Этот проект стал для меня тренажером по проектированию систем. Основные выводы:
Интерфейсы — это сила. Разделение на
IWallpaperSetterпозволило добавить поддержку нового DE за 15 минут.Статический анализ. Использование
clang-tidyпомогло найти несколько потенциальныхuse-after-freeпри работе с Qt-сигналами.Документирование решений (ADR). Даже если ты единственный разработчик, полезно записывать, почему ты выбрал D-Bus вместо прямого редактирования конфигов Plasma.
Что дальше?
Rwal находится в активной разработке. В планах:
Полноценный адаптер для Windows (через WinAPI
SystemParametersInfo).Переход на асинхронные запросы через
QtNetworkилиboost::asioдля лучшей интеграции с event loop.Оптимизация потребления памяти при парсинге больших JSON-ответов от API фотостоков.
Для меня, как для ученика 10 класса, работа над системным инструментом — это лучший способ понять, как устроена ОС. Системное программирование — это не страшно, если уметь декомпозировать задачи.
Буду рад конструктивной критике архитектуры и советами по работе с потоками в C++!
