Merge pull request #1244 from alebastr/swaybar-ipc

Yet another swaybar ipc implementation
This commit is contained in:
Alex 2021-12-01 11:48:49 +01:00 committed by GitHub
commit d41a60d2d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 623 additions and 43 deletions

View File

@ -13,6 +13,7 @@ jobs:
- name: Test in FreeBSD VM - name: Test in FreeBSD VM
uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0 uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0
with: with:
mem: 2048
usesh: true usesh: true
prepare: | prepare: |
export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio

View File

@ -8,6 +8,9 @@
#include <gtkmm/window.h> #include <gtkmm/window.h>
#include <json/json.h> #include <json/json.h>
#include <memory>
#include <vector>
#include "AModule.hpp" #include "AModule.hpp"
#include "xdg-output-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h"
@ -36,6 +39,19 @@ struct bar_margins {
int left = 0; int left = 0;
}; };
struct bar_mode {
bar_layer layer;
bool exclusive;
bool passthrough;
bool visible;
};
#ifdef HAVE_SWAY
namespace modules::sway {
class BarIpcClient;
}
#endif // HAVE_SWAY
class BarSurface { class BarSurface {
protected: protected:
BarSurface() = default; BarSurface() = default;
@ -54,31 +70,44 @@ class BarSurface {
class Bar { class Bar {
public: public:
using bar_mode_map = std::map<std::string_view, struct bar_mode>;
static const bar_mode_map PRESET_MODES;
static const std::string_view MODE_DEFAULT;
static const std::string_view MODE_INVISIBLE;
Bar(struct waybar_output *w_output, const Json::Value &); Bar(struct waybar_output *w_output, const Json::Value &);
Bar(const Bar &) = delete; Bar(const Bar &) = delete;
~Bar() = default; ~Bar();
void setMode(const std::string_view &);
void setVisible(bool visible); void setVisible(bool visible);
void toggle(); void toggle();
void handleSignal(int); void handleSignal(int);
struct waybar_output *output; struct waybar_output *output;
Json::Value config; Json::Value config;
struct wl_surface * surface; struct wl_surface *surface;
bool exclusive = true;
bool visible = true; bool visible = true;
bool vertical = false; bool vertical = false;
Gtk::Window window; Gtk::Window window;
#ifdef HAVE_SWAY
std::string bar_id;
#endif
private: private:
void onMap(GdkEventAny *); void onMap(GdkEventAny *);
auto setupWidgets() -> void; auto setupWidgets() -> void;
void getModules(const Factory &, const std::string &, Gtk::Box*); void getModules(const Factory &, const std::string &, Gtk::Box*);
void setupAltFormatKeyForModule(const std::string &module_name); void setupAltFormatKeyForModule(const std::string &module_name);
void setupAltFormatKeyForModuleList(const char *module_list_name); void setupAltFormatKeyForModuleList(const char *module_list_name);
void setMode(const bar_mode &);
/* Copy initial set of modes to allow customization */
bar_mode_map configured_modes = PRESET_MODES;
std::string last_mode_{MODE_DEFAULT};
std::unique_ptr<BarSurface> surface_impl_; std::unique_ptr<BarSurface> surface_impl_;
bar_layer layer_;
Gtk::Box left_; Gtk::Box left_;
Gtk::Box center_; Gtk::Box center_;
Gtk::Box right_; Gtk::Box right_;
@ -86,6 +115,10 @@ class Bar {
std::vector<std::unique_ptr<waybar::AModule>> modules_left_; std::vector<std::unique_ptr<waybar::AModule>> modules_left_;
std::vector<std::unique_ptr<waybar::AModule>> modules_center_; std::vector<std::unique_ptr<waybar::AModule>> modules_center_;
std::vector<std::unique_ptr<waybar::AModule>> modules_right_; std::vector<std::unique_ptr<waybar::AModule>> modules_right_;
#ifdef HAVE_SWAY
using BarIpcClient = modules::sway::BarIpcClient;
std::unique_ptr<BarIpcClient> _ipc_client;
#endif
std::vector<std::unique_ptr<waybar::AModule>> modules_all_; std::vector<std::unique_ptr<waybar::AModule>> modules_all_;
}; };

View File

@ -29,6 +29,7 @@ class Client {
struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr;
std::vector<std::unique_ptr<Bar>> bars; std::vector<std::unique_ptr<Bar>> bars;
Config config; Config config;
std::string bar_id;
private: private:
Client() = default; Client() = default;

View File

@ -0,0 +1,49 @@
#pragma once
#include <string>
#include "modules/sway/ipc/client.hpp"
#include "util/SafeSignal.hpp"
#include "util/json.hpp"
namespace waybar {
class Bar;
namespace modules::sway {
/*
* Supported subset of i3/sway IPC barconfig object
*/
struct swaybar_config {
std::string id;
std::string mode;
std::string hidden_state;
};
/**
* swaybar IPC client
*/
class BarIpcClient {
public:
BarIpcClient(waybar::Bar& bar);
private:
void onInitialConfig(const struct Ipc::ipc_response& res);
void onIpcEvent(const struct Ipc::ipc_response&);
void onConfigUpdate(const swaybar_config& config);
void onVisibilityUpdate(bool visible_by_modifier);
void update();
Bar& bar_;
util::JsonParser parser_;
Ipc ipc_;
swaybar_config bar_config_;
bool visible_by_modifier_ = false;
SafeSignal<bool> signal_visible_;
SafeSignal<swaybar_config> signal_config_;
};
} // namespace modules::sway
} // namespace waybar

View File

@ -0,0 +1,75 @@
#pragma once
#include <glibmm/dispatcher.h>
#include <sigc++/signal.h>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
namespace waybar {
/**
* Thread-safe signal wrapper.
* Uses Glib::Dispatcher to pass events to another thread and locked queue to pass the arguments.
*/
template <typename... Args>
struct SafeSignal : sigc::signal<void(std::decay_t<Args>...)> {
public:
SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); }
template <typename... EmitArgs>
void emit(EmitArgs&&... args) {
if (main_tid_ == std::this_thread::get_id()) {
/*
* Bypass the queue if the method is called the main thread.
* Ensures that events emitted from the main thread are processed synchronously and saves a
* few CPU cycles on locking/queuing.
* As a downside, this makes main thread events prioritized over the other threads and
* disrupts chronological order.
*/
signal_t::emit(std::forward<EmitArgs>(args)...);
} else {
{
std::unique_lock lock(mutex_);
queue_.emplace(std::forward<EmitArgs>(args)...);
}
dp_.emit();
}
}
template <typename... EmitArgs>
inline void operator()(EmitArgs&&... args) {
emit(std::forward<EmitArgs>(args)...);
}
protected:
using signal_t = sigc::signal<void(std::decay_t<Args>...)>;
using slot_t = decltype(std::declval<signal_t>().make_slot());
using arg_tuple_t = std::tuple<std::decay_t<Args>...>;
// ensure that unwrapped methods are not accessible
using signal_t::emit_reverse;
using signal_t::make_slot;
void handle_event() {
for (std::unique_lock lock(mutex_); !queue_.empty(); lock.lock()) {
auto args = queue_.front();
queue_.pop();
lock.unlock();
std::apply(cached_fn_, args);
}
}
Glib::Dispatcher dp_;
std::mutex mutex_;
std::queue<arg_tuple_t> queue_;
const std::thread::id main_tid_ = std::this_thread::get_id();
// cache functor for signal emission to avoid recreating it on each event
const slot_t cached_fn_ = make_slot();
};
} // namespace waybar

View File

@ -72,14 +72,19 @@ Also a minimal example configuration can be found on the at the bottom of this m
typeof: string ++ typeof: string ++
Optional name added as a CSS class, for styling multiple waybars. Optional name added as a CSS class, for styling multiple waybars.
*mode* ++
typeof: string ++
Selects one of the preconfigured display modes. This is an equivalent of the sway-bar(5) *mode* command and supports the same values: *dock*, *hide*, *invisible*, *overlay*. ++
Note: *hide* and *invisible* modes may be not as useful without Sway IPC.
*exclusive* ++ *exclusive* ++
typeof: bool ++ typeof: bool ++
default: *true* unless the layer is set to *overlay* ++ default: *true* ++
Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar. Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar.
*passthrough* ++ *passthrough* ++
typeof: bool ++ typeof: bool ++
default: *false* unless the layer is set to *overlay* ++ default: *false* ++
Option to pass any pointer events to the window under the bar. Option to pass any pointer events to the window under the bar.
Intended to be used with either *top* or *overlay* layers and without exclusive zone. Intended to be used with either *top* or *overlay* layers and without exclusive zone.
@ -89,6 +94,16 @@ Also a minimal example configuration can be found on the at the bottom of this m
Option to disable the use of gtk-layer-shell for popups. Option to disable the use of gtk-layer-shell for popups.
Only functional if compiled with gtk-layer-shell support. Only functional if compiled with gtk-layer-shell support.
*ipc* ++
typeof: bool ++
default: false ++
Option to subscribe to the Sway IPC bar configuration and visibility events and control waybar with *swaymsg bar* commands. ++
Requires *bar_id* value from sway configuration to be either passed with the *-b* commandline argument or specified with the *id* option.
*id* ++
typeof: string ++
*bar_id* for the Sway IPC. Use this if you need to override the value passed with the *-b bar_id* commandline argument for the specific bar instance.
*include* ++ *include* ++
typeof: string|array ++ typeof: string|array ++
Paths to additional configuration files. Paths to additional configuration files.

View File

@ -178,6 +178,7 @@ endif
add_project_arguments('-DHAVE_SWAY', language: 'cpp') add_project_arguments('-DHAVE_SWAY', language: 'cpp')
src_files += [ src_files += [
'src/modules/sway/ipc/client.cpp', 'src/modules/sway/ipc/client.cpp',
'src/modules/sway/bar.cpp',
'src/modules/sway/mode.cpp', 'src/modules/sway/mode.cpp',
'src/modules/sway/language.cpp', 'src/modules/sway/language.cpp',
'src/modules/sway/window.cpp', 'src/modules/sway/window.cpp',

View File

@ -12,6 +12,10 @@
#include "group.hpp" #include "group.hpp"
#include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h"
#ifdef HAVE_SWAY
#include "modules/sway/bar.hpp"
#endif
namespace waybar { namespace waybar {
static constexpr const char* MIN_HEIGHT_MSG = static constexpr const char* MIN_HEIGHT_MSG =
"Requested height: {} is less than the minimum height: {} required by the modules"; "Requested height: {} is less than the minimum height: {} required by the modules";
@ -24,6 +28,84 @@ static constexpr const char* BAR_SIZE_MSG = "Bar configured (width: {}, height:
static constexpr const char* SIZE_DEFINED = static constexpr const char* SIZE_DEFINED =
"{} size is defined in the config file so it will stay like that"; "{} size is defined in the config file so it will stay like that";
const Bar::bar_mode_map Bar::PRESET_MODES = { //
{"default",
{// Special mode to hold the global bar configuration
.layer = bar_layer::BOTTOM,
.exclusive = true,
.passthrough = false,
.visible = true}},
{"dock",
{// Modes supported by the sway config; see man sway-bar(5)
.layer = bar_layer::BOTTOM,
.exclusive = true,
.passthrough = false,
.visible = true}},
{"hide",
{//
.layer = bar_layer::TOP,
.exclusive = false,
.passthrough = false,
.visible = true}},
{"invisible",
{//
.layer = bar_layer::BOTTOM,
.exclusive = false,
.passthrough = true,
.visible = false}},
{"overlay",
{//
.layer = bar_layer::TOP,
.exclusive = false,
.passthrough = true,
.visible = true}}};
const std::string_view Bar::MODE_DEFAULT = "default";
const std::string_view Bar::MODE_INVISIBLE = "invisible";
const std::string_view DEFAULT_BAR_ID = "bar-0";
/* Deserializer for enum bar_layer */
void from_json(const Json::Value& j, bar_layer& l) {
if (j == "bottom") {
l = bar_layer::BOTTOM;
} else if (j == "top") {
l = bar_layer::TOP;
} else if (j == "overlay") {
l = bar_layer::OVERLAY;
}
}
/* Deserializer for struct bar_mode */
void from_json(const Json::Value& j, bar_mode& m) {
if (j.isObject()) {
if (auto v = j["layer"]; v.isString()) {
from_json(v, m.layer);
}
if (auto v = j["exclusive"]; v.isBool()) {
m.exclusive = v.asBool();
}
if (auto v = j["passthrough"]; v.isBool()) {
m.passthrough = v.asBool();
}
if (auto v = j["visible"]; v.isBool()) {
m.visible = v.asBool();
}
}
}
/* Deserializer for JSON Object -> map<string compatible type, Value>
* Assumes that all the values in the object are deserializable to the same type.
*/
template <typename Key, typename Value,
typename = std::enable_if_t<std::is_convertible<std::string_view, Key>::value>>
void from_json(const Json::Value& j, std::map<Key, Value>& m) {
if (j.isObject()) {
for (auto it = j.begin(); it != j.end(); ++it) {
from_json(*it, m[it.key().asString()]);
}
}
}
#ifdef HAVE_GTK_LAYER_SHELL #ifdef HAVE_GTK_LAYER_SHELL
struct GLSSurfaceImpl : public BarSurface, public sigc::trackable { struct GLSSurfaceImpl : public BarSurface, public sigc::trackable {
GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} {
@ -392,7 +474,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
: output(w_output), : output(w_output),
config(w_config), config(w_config),
window{Gtk::WindowType::WINDOW_TOPLEVEL}, window{Gtk::WindowType::WINDOW_TOPLEVEL},
layer_{bar_layer::BOTTOM},
left_(Gtk::ORIENTATION_HORIZONTAL, 0), left_(Gtk::ORIENTATION_HORIZONTAL, 0),
center_(Gtk::ORIENTATION_HORIZONTAL, 0), center_(Gtk::ORIENTATION_HORIZONTAL, 0),
right_(Gtk::ORIENTATION_HORIZONTAL, 0), right_(Gtk::ORIENTATION_HORIZONTAL, 0),
@ -404,27 +485,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.get_style_context()->add_class(config["name"].asString()); window.get_style_context()->add_class(config["name"].asString());
window.get_style_context()->add_class(config["position"].asString()); window.get_style_context()->add_class(config["position"].asString());
if (config["layer"] == "top") {
layer_ = bar_layer::TOP;
} else if (config["layer"] == "overlay") {
layer_ = bar_layer::OVERLAY;
}
if (config["exclusive"].isBool()) {
exclusive = config["exclusive"].asBool();
} else if (layer_ == bar_layer::OVERLAY) {
// swaybar defaults: overlay mode does not reserve an exclusive zone
exclusive = false;
}
bool passthrough = false;
if (config["passthrough"].isBool()) {
passthrough = config["passthrough"].asBool();
} else if (layer_ == bar_layer::OVERLAY) {
// swaybar defaults: overlay mode does not accept pointer events.
passthrough = true;
}
auto position = config["position"].asString(); auto position = config["position"].asString();
if (position == "right" || position == "left") { if (position == "right" || position == "left") {
@ -506,15 +566,43 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
surface_impl_ = std::make_unique<RawSurfaceImpl>(window, *output); surface_impl_ = std::make_unique<RawSurfaceImpl>(window, *output);
} }
surface_impl_->setLayer(layer_);
surface_impl_->setExclusiveZone(exclusive);
surface_impl_->setMargins(margins_); surface_impl_->setMargins(margins_);
surface_impl_->setPassThrough(passthrough);
surface_impl_->setPosition(position); surface_impl_->setPosition(position);
surface_impl_->setSize(width, height); surface_impl_->setSize(width, height);
/* Read custom modes if available */
if (auto modes = config.get("modes", {}); modes.isObject()) {
from_json(modes, configured_modes);
}
/* Update "default" mode with the global bar options */
from_json(config, configured_modes[MODE_DEFAULT]);
if (auto mode = config.get("mode", {}); mode.isString()) {
setMode(config["mode"].asString());
} else {
setMode(MODE_DEFAULT);
}
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
#if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id;
if (auto id = config["id"]; id.isString()) {
bar_id = id.asString();
}
if (bar_id.empty()) {
bar_id = DEFAULT_BAR_ID;
}
try {
_ipc_client = std::make_unique<BarIpcClient>(*this);
} catch (const std::exception& exc) {
spdlog::warn("Failed to open bar ipc connection: {}", exc.what());
}
}
#endif
setupWidgets(); setupWidgets();
window.show_all(); window.show_all();
@ -529,6 +617,44 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
} }
} }
/* Need to define it here because of forward declared members */
waybar::Bar::~Bar() = default;
void waybar::Bar::setMode(const std::string_view& mode) {
using namespace std::literals::string_literals;
auto style = window.get_style_context();
/* remove styles added by previous setMode calls */
style->remove_class("mode-"s + last_mode_);
auto it = configured_modes.find(mode);
if (it != configured_modes.end()) {
last_mode_ = mode;
style->add_class("mode-"s + last_mode_);
setMode(it->second);
} else {
spdlog::warn("Unknown mode \"{}\" requested", mode);
last_mode_ = MODE_DEFAULT;
style->add_class("mode-"s + last_mode_);
setMode(configured_modes.at(MODE_DEFAULT));
}
}
void waybar::Bar::setMode(const struct bar_mode& mode) {
surface_impl_->setLayer(mode.layer);
surface_impl_->setExclusiveZone(mode.exclusive);
surface_impl_->setPassThrough(mode.passthrough);
if (mode.visible) {
window.get_style_context()->remove_class("hidden");
window.set_opacity(1);
} else {
window.get_style_context()->add_class("hidden");
window.set_opacity(0);
}
surface_impl_->commit();
}
void waybar::Bar::onMap(GdkEventAny*) { void waybar::Bar::onMap(GdkEventAny*) {
/* /*
* Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor).
@ -539,17 +665,7 @@ void waybar::Bar::onMap(GdkEventAny*) {
void waybar::Bar::setVisible(bool value) { void waybar::Bar::setVisible(bool value) {
visible = value; visible = value;
if (!visible) { setMode(visible ? MODE_DEFAULT : MODE_INVISIBLE);
window.get_style_context()->add_class("hidden");
window.set_opacity(0);
surface_impl_->setLayer(bar_layer::BOTTOM);
} else {
window.get_style_context()->remove_class("hidden");
window.set_opacity(1);
surface_impl_->setLayer(layer_);
}
surface_impl_->setExclusiveZone(exclusive && visible);
surface_impl_->commit();
} }
void waybar::Bar::toggle() { setVisible(!visible); } void waybar::Bar::toggle() { setVisible(!visible); }

View File

@ -199,7 +199,6 @@ int waybar::Client::main(int argc, char *argv[]) {
bool show_version = false; bool show_version = false;
std::string config_opt; std::string config_opt;
std::string style_opt; std::string style_opt;
std::string bar_id;
std::string log_level; std::string log_level;
auto cli = clara::detail::Help(show_help) | auto cli = clara::detail::Help(show_help) |
clara::detail::Opt(show_version)["-v"]["--version"]("Show version") | clara::detail::Opt(show_version)["-v"]["--version"]("Show version") |

107
src/modules/sway/bar.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "modules/sway/bar.hpp"
#include <fmt/ostream.h>
#include <spdlog/spdlog.h>
#include <stdexcept>
#include "bar.hpp"
#include "modules/sway/ipc/ipc.hpp"
namespace waybar::modules::sway {
BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} {
{
sigc::connection handle =
ipc_.signal_cmd.connect(sigc::mem_fun(*this, &BarIpcClient::onInitialConfig));
ipc_.sendCmd(IPC_GET_BAR_CONFIG, bar_.bar_id);
handle.disconnect();
}
signal_config_.connect(sigc::mem_fun(*this, &BarIpcClient::onConfigUpdate));
signal_visible_.connect(sigc::mem_fun(*this, &BarIpcClient::onVisibilityUpdate));
ipc_.subscribe(R"(["bar_state_update", "barconfig_update"])");
ipc_.signal_event.connect(sigc::mem_fun(*this, &BarIpcClient::onIpcEvent));
// Launch worker
ipc_.setWorker([this] {
try {
ipc_.handleEvent();
} catch (const std::exception& e) {
spdlog::error("BarIpcClient::handleEvent {}", e.what());
}
});
}
struct swaybar_config parseConfig(const Json::Value& payload) {
swaybar_config conf;
if (auto id = payload["id"]; id.isString()) {
conf.id = id.asString();
}
if (auto mode = payload["mode"]; mode.isString()) {
conf.mode = mode.asString();
}
if (auto hs = payload["hidden_state"]; hs.isString()) {
conf.hidden_state = hs.asString();
}
return conf;
}
void BarIpcClient::onInitialConfig(const struct Ipc::ipc_response& res) {
auto payload = parser_.parse(res.payload);
if (auto success = payload.get("success", true); !success.asBool()) {
auto err = payload.get("error", "Unknown error");
throw std::runtime_error(err.asString());
}
auto config = parseConfig(payload);
onConfigUpdate(config);
}
void BarIpcClient::onIpcEvent(const struct Ipc::ipc_response& res) {
try {
auto payload = parser_.parse(res.payload);
if (auto id = payload["id"]; id.isString() && id.asString() != bar_.bar_id) {
spdlog::trace("swaybar ipc: ignore event for {}", id.asString());
return;
}
if (payload.isMember("visible_by_modifier")) {
// visibility change for hidden bar
signal_visible_(payload["visible_by_modifier"].asBool());
} else {
// configuration update
auto config = parseConfig(payload);
signal_config_(std::move(config));
}
} catch (const std::exception& e) {
spdlog::error("BarIpcClient::onEvent {}", e.what());
}
}
void BarIpcClient::onConfigUpdate(const swaybar_config& config) {
spdlog::info("config update for {}: id {}, mode {}, hidden_state {}",
bar_.bar_id,
config.id,
config.mode,
config.hidden_state);
bar_config_ = config;
update();
}
void BarIpcClient::onVisibilityUpdate(bool visible_by_modifier) {
spdlog::debug("visiblity update for {}: {}", bar_.bar_id, visible_by_modifier);
visible_by_modifier_ = visible_by_modifier;
update();
}
void BarIpcClient::update() {
bool visible = visible_by_modifier_;
if (bar_config_.mode == "invisible") {
visible = false;
} else if (bar_config_.mode != "hide" || bar_config_.hidden_state != "hide") {
visible = true;
}
bar_.setMode(visible ? bar_config_.mode : Bar::MODE_INVISIBLE);
}
} // namespace waybar::modules::sway

24
test/GlibTestsFixture.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <glibmm/main.h>
/**
* Minimal Glib application to be used for tests that require Glib main loop
*/
class GlibTestsFixture : public sigc::trackable {
public:
GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {}
void setTimeout(int timeout) {
Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); },
timeout);
}
void run(std::function<void()> fn) {
Glib::signal_idle().connect_once(fn);
main_loop_->run();
}
void quit() { main_loop_->quit(); }
protected:
Glib::RefPtr<Glib::MainLoop> main_loop_;
};

145
test/SafeSignal.cpp Normal file
View File

@ -0,0 +1,145 @@
#define CATCH_CONFIG_RUNNER
#include "util/SafeSignal.hpp"
#include <glibmm.h>
#include <catch2/catch.hpp>
#include <thread>
#include <type_traits>
#include "GlibTestsFixture.hpp"
using namespace waybar;
template <typename T>
using remove_cvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
/**
* Basic sanity test for SafeSignal:
* check that type deduction works, events are delivered and the order is right
* Running this with -fsanitize=thread should not fail
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal basic functionality", "[signal][thread][util]") {
const int NUM_EVENTS = 100;
int count = 0;
int last_value = 0;
SafeSignal<int, std::string> test_signal;
const auto main_tid = std::this_thread::get_id();
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto val, auto str) {
static_assert(std::is_same<int, decltype(val)>::value);
static_assert(std::is_same<std::string, decltype(str)>::value);
// check that we're in the same thread as the main loop
REQUIRE(std::this_thread::get_id() == main_tid);
// check event order
REQUIRE(val == last_value + 1);
last_value = val;
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
// check that events from the same thread are delivered and processed synchronously
test_signal.emit(1, "test");
REQUIRE(count == 1);
// start another thread and generate events
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
test_signal.emit(i, "test");
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}
template <typename T>
struct TestObject {
T value;
unsigned copied = 0;
unsigned moved = 0;
TestObject(const T& v) : value(v){};
~TestObject() = default;
TestObject(const TestObject& other)
: value(other.value), copied(other.copied + 1), moved(other.moved) {}
TestObject(TestObject&& other) noexcept
: value(std::move(other.value)),
copied(std::exchange(other.copied, 0)),
moved(std::exchange(other.moved, 0) + 1) {}
TestObject& operator=(const TestObject& other) {
value = other.value;
copied = other.copied + 1;
moved = other.moved;
return *this;
}
TestObject& operator=(TestObject&& other) noexcept {
value = std::move(other.value);
copied = std::exchange(other.copied, 0);
moved = std::exchange(other.moved, 0) + 1;
return *this;
}
bool operator==(T other) const { return value == other; }
operator T() const { return value; }
};
/*
* Check the number of copies/moves performed on the object passed through SafeSignal
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thread][util]") {
const int NUM_EVENTS = 3;
int count = 0;
SafeSignal<TestObject<int>> test_signal;
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto& val) {
static_assert(std::is_same<TestObject<int>, remove_cvref_t<decltype(val)>>::value);
/* explicit move in the producer thread */
REQUIRE(val.moved <= 1);
/* copy within the SafeSignal queuing code */
REQUIRE(val.copied <= 1);
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
test_signal.emit(1);
REQUIRE(count == 1);
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
TestObject<int> t{i};
// check that signal.emit accepts moved objects
test_signal.emit(std::move(t));
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}
int main(int argc, char* argv[]) {
Glib::init();
return Catch::Session().run(argc, argv);
}

View File

@ -2,6 +2,7 @@ test_inc = include_directories('../include')
test_dep = [ test_dep = [
catch2, catch2,
fmt, fmt,
gtkmm,
jsoncpp, jsoncpp,
spdlog, spdlog,
] ]
@ -14,8 +15,21 @@ config_test = executable(
include_directories: test_inc, include_directories: test_inc,
) )
safesignal_test = executable(
'safesignal_test',
'SafeSignal.cpp',
dependencies: test_dep,
include_directories: test_inc,
)
test( test(
'Configuration test', 'Configuration test',
config_test, config_test,
workdir: meson.source_root(), workdir: meson.source_root(),
) )
test(
'SafeSignal test',
safesignal_test,
workdir: meson.source_root(),
)