From 09873f0ed9b45b2be72b7083c13cdd1604b4e274 Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Wed, 6 Sep 2023 15:19:56 +0000 Subject: [PATCH] search for dark or light mode stylesheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit summary: ------- This commit adds xdg-desktop-portal support to waybar. If a portal supporting `org.freedesktop.portal.Settings` exists, then it will be queried for the current colorscheme. This colorscheme will then be used to prefer a `style-light.css` or `style-dark.css` over the basic `style.css`. technical details: ----------------- Appearance is provided by several libraries, such as libhandy (mobile) and libadwaita. However, waybar links to neither of these libraries. As the amount of code required to communicate with xdg-desktop portal as a client is rather minimal, I believe doing so is better than linking to an additional library. The Gio library for communicating with dbus is rather messy, Instead of the `Portal` class containing a `Gio::Dbus::Proxy`, it extends it which simplifies signal handling. `Portal` then exposes its own signal, which can be listened to by waybar to update CSS. For a reference implementation, please see another one of my projects: https://github.com/4e554c4c/darkman.nvim/blob/main/portal.go test plan: --------- If no desktop portal which provides `Settings` exists, then waybar continues with the log line ``` [2023-09-06 14:14:37.754] [info] Unable to receive desktop appearance: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such interface “org.freedesktop.portal.Settings” on object at path /org/freedesktop/portal/desktop ``` Furthermore, if `style-light.css` or `style-dark.css` do not exist, then `style.css` will still be searched for. Waybar has been tested with both light and dark startup. E.g. if the appearance is dark on startup the log lines ``` [2023-09-06 14:27:45.379] [info] Discovered appearance 'dark' [2023-09-06 14:27:45.379] [debug] Try expanding: $XDG_CONFIG_HOME/waybar/style-dark.css [2023-09-06 14:27:45.379] [debug] Found config file: $XDG_CONFIG_HOME/waybar/style-dark.css [2023-09-06 14:27:45.379] [info] Using CSS file /home/pounce/.config/waybar/style-dark.css ``` will be observed. If the color then changes to light during the operation of waybar, it will change css files: ``` [2023-09-06 14:28:17.173] [info] Received new appearance 'dark' [2023-09-06 14:28:17.173] [debug] Try expanding: $XDG_CONFIG_HOME/waybar/style-light.css [2023-09-06 14:28:17.173] [debug] Found config file: $XDG_CONFIG_HOME/waybar/style-light.css [2023-09-06 14:28:17.173] [info] Using CSS file /home/pounce/.config/waybar/style-light.css ``` Finally, tested resetting waybar and toggling style (works, and style is only changed once). fixes: Alexays/Waybar#1973 --- include/client.hpp | 5 +- include/util/portal.hpp | 38 ++++++++++++++ meson.build | 1 + src/client.cpp | 35 +++++++++++-- src/util/portal.cpp | 106 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 include/util/portal.hpp create mode 100644 src/util/portal.cpp diff --git a/include/client.hpp b/include/client.hpp index aaba3b6b..8faa7198 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -7,6 +7,7 @@ #include "bar.hpp" #include "config.hpp" +#include "util/portal.hpp" struct zwlr_layer_shell_v1; struct zwp_idle_inhibitor_v1; @@ -33,7 +34,7 @@ class Client { private: Client() = default; - const std::string getStyle(const std::string &style); + const std::string getStyle(const std::string &style, std::optional appearance); void bindInterfaces(); void handleOutput(struct waybar_output &output); auto setupCss(const std::string &css_file) -> void; @@ -46,12 +47,14 @@ class Client { static void handleOutputDone(void *, struct zxdg_output_v1 *); static void handleOutputName(void *, struct zxdg_output_v1 *, const char *); static void handleOutputDescription(void *, struct zxdg_output_v1 *, const char *); + void handleAppearanceChanged(waybar::Appearance appearance); void handleMonitorAdded(Glib::RefPtr monitor); void handleMonitorRemoved(Glib::RefPtr monitor); void handleDeferredMonitorRemoval(Glib::RefPtr monitor); Glib::RefPtr style_context_; Glib::RefPtr css_provider_; + std::unique_ptr portal; std::list outputs_; }; diff --git a/include/util/portal.hpp b/include/util/portal.hpp new file mode 100644 index 00000000..23619169 --- /dev/null +++ b/include/util/portal.hpp @@ -0,0 +1,38 @@ +#include + +#include + +#include "fmt/format.h" + +namespace waybar { + +using namespace Gio; + +enum class Appearance { + UNKNOWN = 0, + DARK = 1, + LIGHT = 2, +}; +class Portal : private DBus::Proxy { + public: + Portal(); + void refreshAppearance(); + Appearance getAppearance(); + + typedef sigc::signal type_signal_appearance_changed; + type_signal_appearance_changed signal_appearance_changed() { return m_signal_appearance_changed; } + + private: + type_signal_appearance_changed m_signal_appearance_changed; + Appearance currentMode; + void on_signal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, + const Glib::VariantContainerBase& parameters); +}; + +} // namespace waybar + +template <> +struct fmt::formatter : formatter { + // parse is inherited from formatter. + auto format(waybar::Appearance c, format_context& ctx) const; +}; diff --git a/meson.build b/meson.build index e71807ec..296cbf56 100644 --- a/meson.build +++ b/meson.build @@ -171,6 +171,7 @@ src_files = files( 'src/client.cpp', 'src/config.cpp', 'src/group.cpp', + 'src/util/portal.cpp', 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', diff --git a/src/client.cpp b/src/client.cpp index a815e2fe..10073df0 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -151,8 +151,26 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; }); } -const std::string waybar::Client::getStyle(const std::string &style) { - auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style; +const std::string waybar::Client::getStyle(const std::string &style, + std::optional appearance = std::nullopt) { + std::optional css_file; + if (!style.empty()) { + css_file = style; + } else { + std::vector search_files; + switch (appearance.value_or(portal->getAppearance())) { + case waybar::Appearance::LIGHT: + search_files.push_back("style-light.css"); + break; + case waybar::Appearance::DARK: + search_files.push_back("style-dark.css"); + break; + case waybar::Appearance::UNKNOWN: + break; + } + search_files.push_back("style.css"); + css_file = Config::findConfigPath(search_files); + } if (!css_file) { throw std::runtime_error("Missing required resource files"); } @@ -235,8 +253,15 @@ int waybar::Client::main(int argc, char *argv[]) { } wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); config.load(config_opt); + if (!portal) { + portal = std::make_unique(); + } auto css_file = getStyle(style_opt); setupCss(css_file); + portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { + auto css_file = getStyle(style_opt, appearance); + setupCss(css_file); + }); bindInterfaces(); gtk_app->hold(); gtk_app->run(); @@ -244,4 +269,8 @@ int waybar::Client::main(int argc, char *argv[]) { return 0; } -void waybar::Client::reset() { gtk_app->quit(); } +void waybar::Client::reset() { + gtk_app->quit(); + // delete signal handler for css changes + portal->signal_appearance_changed().clear(); +} diff --git a/src/util/portal.cpp b/src/util/portal.cpp new file mode 100644 index 00000000..50c646c5 --- /dev/null +++ b/src/util/portal.cpp @@ -0,0 +1,106 @@ +#include "util/portal.hpp" + +#include +#include +#include + +#include +#include + +#include "fmt/format.h" + +namespace waybar { +static constexpr const char* PORTAL_BUS_NAME = "org.freedesktop.portal.Desktop"; +static constexpr const char* PORTAL_OBJ_PATH = "/org/freedesktop/portal/desktop"; +static constexpr const char* PORTAL_INTERFACE = "org.freedesktop.portal.Settings"; +static constexpr const char* PORTAL_NAMESPACE = "org.freedesktop.appearance"; +static constexpr const char* PORTAL_KEY = "color-scheme"; +} // namespace waybar + +using namespace Gio; + +auto fmt::formatter::format(waybar::Appearance c, format_context& ctx) const { + string_view name; + switch (c) { + case waybar::Appearance::LIGHT: + name = "light"; + break; + case waybar::Appearance::DARK: + name = "dark"; + break; + default: + name = "unknown"; + break; + } + return formatter::format(name, ctx); +} + +waybar::Portal::Portal() + : DBus::Proxy(DBus::Connection::get_sync(DBus::BusType::BUS_TYPE_SESSION), PORTAL_BUS_NAME, + PORTAL_OBJ_PATH, PORTAL_INTERFACE), + currentMode(Appearance::UNKNOWN) { + refreshAppearance(); +}; + +void waybar::Portal::refreshAppearance() { + auto params = Glib::Variant>::create( + {PORTAL_NAMESPACE, PORTAL_KEY}); + Glib::VariantBase response; + try { + response = call_sync(std::string(PORTAL_INTERFACE) + ".Read", params); + } catch (const Glib::Error& e) { + spdlog::info("Unable to receive desktop appearance: {}", std::string(e.what())); + return; + } + + // unfortunately, the response is triple-nested, with type (v>), + // so we have cast thrice. This is a variation from the freedesktop standard + // (it should only be doubly nested) but all implementations appear to do so. + // + // xdg-desktop-portal 1.17 will fix this issue with a new `ReadOne` method, + // but this version is not yet released. + // TODO(xdg-desktop-portal v1.17): switch to ReadOne + auto container = Glib::VariantBase::cast_dynamic(response); + Glib::VariantBase modev; + container.get_child(modev, 0); + auto mode = + Glib::VariantBase::cast_dynamic>>>(modev) + .get() + .get() + .get(); + auto newMode = Appearance(mode); + if (newMode == currentMode) { + return; + } + spdlog::info("Discovered appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); +} + +waybar::Appearance waybar::Portal::getAppearance() { return currentMode; }; + +void waybar::Portal::on_signal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, + const Glib::VariantContainerBase& parameters) { + spdlog::debug("Received signal {}", (std::string)signal_name); + if (signal_name != "SettingChanged" || parameters.get_n_children() != 3) { + return; + } + Glib::VariantBase nspcv, keyv, valuev; + parameters.get_child(nspcv, 0); + parameters.get_child(keyv, 1); + parameters.get_child(valuev, 2); + auto nspc = Glib::VariantBase::cast_dynamic>(nspcv).get(); + auto key = Glib::VariantBase::cast_dynamic>(keyv).get(); + if (nspc != PORTAL_NAMESPACE || key != PORTAL_KEY) { + return; + } + auto value = + Glib::VariantBase::cast_dynamic>>(valuev).get().get(); + auto newMode = Appearance(value); + if (newMode == currentMode) { + return; + } + spdlog::info("Received new appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); +}