Merge branch 'master' into fix/mpd-markup-truncation

This commit is contained in:
Alexis Rouillard
2026-07-03 20:45:15 +02:00
committed by GitHub
21 changed files with 137 additions and 70 deletions
+7
View File
@@ -25,6 +25,10 @@ class AModule : public IModule {
bool expandEnabled() const; bool expandEnabled() const;
virtual void suspend() {};
virtual void resume() {};
bool shouldSuspend() const { return disable_on_sleep_; }
protected: protected:
// Don't need to make an object directly // Don't need to make an object directly
// Derived classes are able to use it // Derived classes are able to use it
@@ -48,6 +52,8 @@ class AModule : public IModule {
virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleMouseLeave(GdkEventCrossing* const& ev);
virtual bool handleScroll(GdkEventScroll*); virtual bool handleScroll(GdkEventScroll*);
virtual bool handleRelease(GdkEventButton* const& ev); virtual bool handleRelease(GdkEventButton* const& ev);
bool disable_on_sleep_{false};
GObject* menu_ = nullptr; GObject* menu_ = nullptr;
private: private:
@@ -57,6 +63,7 @@ class AModule : public IModule {
bool hasUserEvents_; bool hasUserEvents_;
gdouble distance_scrolled_y_; gdouble distance_scrolled_y_;
gdouble distance_scrolled_x_; gdouble distance_scrolled_x_;
sigc::connection cursor_timeout_conn_;
std::map<std::string, std::string> eventActionMap_; std::map<std::string, std::string> eventActionMap_;
static const inline std::map<std::pair<uint, GdkEventType>, std::string> eventMap_{ static const inline std::map<std::pair<uint, GdkEventType>, std::string> eventMap_{
{std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"},
+2
View File
@@ -75,6 +75,8 @@ class Bar : public sigc::trackable {
util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr1Action();
util::KillSignalAction getOnSigusr2Action(); util::KillSignalAction getOnSigusr2Action();
void toggleSuspend(bool suspend);
struct waybar_output* output; struct waybar_output* output;
Json::Value config; Json::Value config;
struct wl_surface* surface; struct wl_surface* surface;
-2
View File
@@ -19,8 +19,6 @@ class Memory : public ALabel {
private: private:
void parseMeminfo(); void parseMeminfo();
static float calc_divisor(const std::string& divisor);
std::unordered_map<std::string, unsigned long> meminfo_; std::unordered_map<std::string, unsigned long> meminfo_;
util::SleeperThread thread_; util::SleeperThread thread_;
-1
View File
@@ -59,7 +59,6 @@ class MPD : public ALabel {
void tryConnect(); void tryConnect();
void checkErrors(mpd_connection* conn); void checkErrors(mpd_connection* conn);
void fetchState(); void fetchState();
void queryMPD();
inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; }
inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; }
-1
View File
@@ -197,7 +197,6 @@ class Context {
void tryConnect() const; void tryConnect() const;
void checkErrors(mpd_connection*) const; void checkErrors(mpd_connection*) const;
void do_update(); void do_update();
void queryMPD() const;
void fetchState() const; void fetchState() const;
constexpr mpd_state state() const; constexpr mpd_state state() const;
void emit() const; void emit() const;
-1
View File
@@ -15,7 +15,6 @@ constexpr inline mpd_state Context::state() const { return mpd_module_->state_;
inline void Context::do_update() { mpd_module_->setLabel(); } inline void Context::do_update() { mpd_module_->setLabel(); }
inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); }
inline void Context::queryMPD() const { mpd_module_->queryMPD(); }
inline void Context::fetchState() const { mpd_module_->fetchState(); } inline void Context::fetchState() const { mpd_module_->fetchState(); }
inline void Context::emit() const { mpd_module_->emit(); } inline void Context::emit() const { mpd_module_->emit(); }
+2
View File
@@ -14,6 +14,8 @@ class Temperature : public ALabel {
Temperature(const std::string&, const Json::Value&); Temperature(const std::string&, const Json::Value&);
virtual ~Temperature() = default; virtual ~Temperature() = default;
auto update() -> void override; auto update() -> void override;
void suspend() override;
void resume() override;
private: private:
float getTemperature(); float getTemperature();
+9 -3
View File
@@ -7,6 +7,7 @@
#include <codecvt> #include <codecvt>
#include <iostream> #include <iostream>
#include <locale> #include <locale>
#include <memory>
#include <regex> #include <regex>
#if (FMT_VERSION >= 90000) #if (FMT_VERSION >= 90000)
@@ -26,14 +27,19 @@ class JsonParser {
Json::Value root; Json::Value root;
// replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences
std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); std::string modifiedJsonStr;
const std::string* json = &jsonStr;
if (jsonStr.find("\\x") != std::string::npos) {
modifiedJsonStr = replaceHexadecimalEscape(jsonStr);
json = &modifiedJsonStr;
}
std::istringstream jsonStream(modifiedJsonStr);
std::string errs; std::string errs;
// Use local CharReaderBuilder for thread safety - the IPC singleton's // Use local CharReaderBuilder for thread safety - the IPC singleton's
// parser can be called concurrently from multiple module threads // parser can be called concurrently from multiple module threads
Json::CharReaderBuilder readerBuilder; Json::CharReaderBuilder readerBuilder;
if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { auto reader = std::unique_ptr<Json::CharReader>(readerBuilder.newCharReader());
if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) {
throw std::runtime_error("Error parsing JSON: " + errs); throw std::runtime_error("Error parsing JSON: " + errs);
} }
return root; return root;
+24
View File
@@ -79,6 +79,12 @@ class SleeperThread {
auto sleep_for(std::chrono::system_clock::duration dur) { auto sleep_for(std::chrono::system_clock::duration dur) {
std::unique_lock lk(mutex_); std::unique_lock lk(mutex_);
CancellationGuard cancel_lock; CancellationGuard cancel_lock;
condvar_.wait(lk, [this] {
return !is_paused_ || signal_.load(std::memory_order_relaxed) ||
!do_run_.load(std::memory_order_relaxed);
});
constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); constexpr auto max_time_point = std::chrono::steady_clock::time_point::max();
auto wait_end = max_time_point; auto wait_end = max_time_point;
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
@@ -95,6 +101,12 @@ class SleeperThread {
time_point) { time_point) {
std::unique_lock lk(mutex_); std::unique_lock lk(mutex_);
CancellationGuard cancel_lock; CancellationGuard cancel_lock;
condvar_.wait(lk, [this] {
return !is_paused_ || signal_.load(std::memory_order_relaxed) ||
!do_run_.load(std::memory_order_relaxed);
});
return condvar_.wait_until(lk, time_point, [this] { return condvar_.wait_until(lk, time_point, [this] {
return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed);
}); });
@@ -122,6 +134,17 @@ class SleeperThread {
} }
} }
void pause() {
std::lock_guard<std::mutex> lock(mutex_);
is_paused_ = true;
}
void resume() {
std::lock_guard<std::mutex> lock(mutex_);
is_paused_ = false;
condvar_.notify_all();
}
~SleeperThread() { ~SleeperThread() {
connection_.disconnect(); connection_.disconnect();
stop(); stop();
@@ -137,6 +160,7 @@ class SleeperThread {
std::atomic<bool> do_run_ = true; std::atomic<bool> do_run_ = true;
std::atomic<bool> signal_ = false; std::atomic<bool> signal_ = false;
sigc::connection connection_; sigc::connection connection_;
bool is_paused_{false};
}; };
} // namespace waybar::util } // namespace waybar::util
+9 -2
View File
@@ -17,10 +17,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true},
isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false},
distance_scrolled_y_(0.0), distance_scrolled_y_(0.0),
distance_scrolled_x_(0.0) { distance_scrolled_x_(0.0),
cursor_timeout_conn_() {
// Configure module action Map // Configure module action Map
const Json::Value actions{config_["actions"]}; const Json::Value actions{config_["actions"]};
disable_on_sleep_ =
config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false;
for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) {
if (it.key().isString() && it->isString()) if (it.key().isString() && it->isString())
if (!eventActionMap_.contains(it.key().asString())) { if (!eventActionMap_.contains(it.key().asString())) {
@@ -83,6 +87,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
} }
AModule::~AModule() { AModule::~AModule() {
if (cursor_timeout_conn_.connected()) {
cursor_timeout_conn_.disconnect();
}
for (const auto& pid : pid_children_) { for (const auto& pid : pid_children_) {
if (pid != -1) { if (pid != -1) {
killpg(pid, SIGTERM); killpg(pid, SIGTERM);
@@ -118,7 +125,7 @@ void AModule::setCursor(Gdk::CursorType const& c) {
} else { } else {
// window may not be accessible yet, in this case, // window may not be accessible yet, in this case,
// schedule another call for setting the cursor in 1 sec // schedule another call for setting the cursor in 1 sec
Glib::signal_timeout().connect_seconds( cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds(
[this, c]() { [this, c]() {
setCursor(c); setCursor(c);
return false; return false;
+31 -2
View File
@@ -263,6 +263,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
window.signal_unmap().connect([this]() {
spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name);
toggleSuspend(true);
});
window.signal_map().connect([this]() {
spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name);
toggleSuspend(false);
});
#if HAVE_SWAY #if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id; bar_id = Client::inst()->bar_id;
@@ -545,8 +555,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos,
if (group_config["modules"].isNull()) { if (group_config["modules"].isNull()) {
spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref);
} }
auto group_module = std::make_unique<waybar::Group>( auto group_module =
id_name, class_name, group_config, vertical); std::make_unique<waybar::Group>(id_name, class_name, group_config, vertical);
getModules(factory, ref, group_module.get()); getModules(factory, ref, group_module.get());
module = group_module.release(); module = group_module.release();
@@ -696,3 +706,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) {
void waybar::Bar::onOutputGeometryChanged() { void waybar::Bar::onOutputGeometryChanged() {
configureGlobalOffset(window.get_width(), window.get_height()); configureGlobalOffset(window.get_width(), window.get_height());
} }
void waybar::Bar::toggleSuspend(bool suspend) {
auto process_modules = [suspend](Gtk::Box& module_box) {
for (auto* widget : module_box.get_children()) {
auto* module = dynamic_cast<waybar::AModule*>(widget);
if (module && module->shouldSuspend()) {
if (suspend) {
module->suspend();
} else {
module->resume();
}
}
}
};
process_modules(left_);
process_modules(center_);
process_modules(right_);
}
+1 -1
View File
@@ -153,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() {
} }
} }
} catch (fs::filesystem_error& e) { } catch (fs::filesystem_error& e) {
throw std::runtime_error(e.what()); spdlog::warn("Battery directory tracking failed: {}", e.what());
} }
if (warnFirstTime_ && batteries_.empty()) { if (warnFirstTime_ && batteries_.empty()) {
if (config_["bat"].isString()) { if (config_["bat"].isString()) {
+5
View File
@@ -39,6 +39,11 @@ waybar::modules::Custom::~Custom() {
} }
void waybar::modules::Custom::delayWorker() { void waybar::modules::Custom::delayWorker() {
if (!config_["exec"].isString() && !config_["exec-if"].isString()) {
dp.emit();
return;
}
thread_ = [this] { thread_ = [this] {
for (int i : this->pid_children_) { for (int i : this->pid_children_) {
int status; int status;
+2 -2
View File
@@ -320,7 +320,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri
// New format: /dispatch hl.dsp.focus({ workspace = "1" }) // New format: /dispatch hl.dsp.focus({ workspace = "1" })
// //
// Old format: dispatch focusworkspaceoncurrentmonitor 2 // Old format: dispatch focusworkspaceoncurrentmonitor 2
// New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true })
// //
// Old format: dispatch togglespecialworkspace name // Old format: dispatch togglespecialworkspace name
// New format: /dispatch hl.dsp.workspace.toggle_special("name") // New format: /dispatch hl.dsp.workspace.toggle_special("name")
@@ -329,7 +329,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })";
} }
if (dispatcher == "focusworkspaceoncurrentmonitor") { if (dispatcher == "focusworkspaceoncurrentmonitor") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })";
} }
if (dispatcher == "togglespecialworkspace") { if (dispatcher == "togglespecialworkspace") {
if (arg.empty()) { if (arg.empty()) {
+20 -24
View File
@@ -1,5 +1,18 @@
#include "modules/memory.hpp" #include "modules/memory.hpp"
namespace {
const std::unordered_map<std::string, float> kUnits = {
{"kB", 1.000},
{"kiB", 1.024},
{"MB", 1.000 * 1000.0},
{"MiB", 1.024 * 1024.0},
{"GB", 1.000 * 1000.0 * 1000.0},
{"GiB", 1.024 * 1024.0 * 1024.0},
{"TB", 1.000 * 1000.0 * 1000.0 * 1000.0},
{"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}
};
}
waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config)
: ALabel(config, "memory", id, "{}%", 30) { : ALabel(config, "memory", id, "{}%", 30) {
thread_ = [this] { thread_ = [this] {
@@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config
}; };
if (config["unit"].isString()) { if (config["unit"].isString()) {
unit_ = config["unit"].asString(); unit_ = config["unit"].asString();
if (!kUnits.contains(unit_)) {
unit_ = "GiB";
}
} else {
unit_ = "GiB";
} }
} }
@@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void {
used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal;
} }
float divisor = calc_divisor(unit_); float divisor = kUnits.at(unit_);
float total_ram = memtotal / divisor; float total_ram = memtotal / divisor;
float total_swap = swaptotal / divisor; float total_swap = swaptotal / divisor;
float used_ram = (memtotal - memfree) / divisor; float used_ram = (memtotal - memfree) / divisor;
@@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void {
fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram),
fmt::arg("swapAvail", available_swap))); fmt::arg("swapAvail", available_swap)));
} else { } else {
label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_));
} }
} }
} else { } else {
@@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void {
// Call parent update // Call parent update
ALabel::update(); ALabel::update();
} }
float waybar::modules::Memory::calc_divisor(const std::string& divisor) {
if (divisor == "kB") {
return 1.0;
} else if (divisor == "kiB") {
return 1.024;
} else if (divisor == "MB") {
return 1.000 * 1000.0;
} else if (divisor == "MiB") {
return 1.024 * 1024.0;
} else if (divisor == "GB") {
return 1.000 * 1000.0 * 1000.0;
} else if (divisor == "GiB") {
return 1.024 * 1024.0 * 1024.0;
} else if (divisor == "TB") {
return 1.000 * 1000.0 * 1000.0 * 1000.0;
} else if (divisor == "TiB") {
return 1.024 * 1024.0 * 1024.0 * 1024.0;
} else { // default to GiB if it is anything that we don't recongnise
return 1.024 * 1024.0 * 1024.0;
}
}
+3 -17
View File
@@ -53,21 +53,6 @@ auto waybar::modules::MPD::update() -> void {
ALabel::update(); ALabel::update();
} }
void waybar::modules::MPD::queryMPD() {
if (connection_ != nullptr) {
spdlog::trace("{}: fetching state information", module_name_);
try {
fetchState();
spdlog::trace("{}: fetch complete", module_name_);
} catch (std::exception const& e) {
spdlog::error("{}: {}", module_name_, e.what());
state_ = MPD_STATE_UNKNOWN;
}
dp.emit();
}
}
std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const {
std::string result = std::string result =
config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A";
@@ -125,8 +110,9 @@ void waybar::modules::MPD::setLabel() {
std::string stateIcon = ""; std::string stateIcon = "";
bool no_song = song_.get() == nullptr; bool no_song = song_.get() == nullptr;
if (stopped() || no_song) { bool is_stopped = stopped();
if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); if (is_stopped || no_song) {
if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped");
format = format =
config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped";
label_.get_style_context()->add_class("stopped"); label_.get_style_context()->add_class("stopped");
-1
View File
@@ -152,7 +152,6 @@ bool Playing::on_timer() {
return false; return false;
} }
ctx_->queryMPD();
ctx_->emit(); ctx_->emit();
} catch (std::exception const& e) { } catch (std::exception const& e) {
spdlog::warn("mpd: Playing: error: {}", e.what()); spdlog::warn("mpd: Playing: error: {}", e.what());
+4
View File
@@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) {
return config_["critical-threshold"].isInt() && return config_["critical-threshold"].isInt() &&
temperature_c >= config_["critical-threshold"].asInt(); temperature_c >= config_["critical-threshold"].asInt();
} }
void waybar::modules::Temperature::suspend() { thread_.pause(); }
void waybar::modules::Temperature::resume() { thread_.resume(); }
+7 -4
View File
@@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data
if (event == "output-wset-changed") { if (event == "output-wset-changed") {
// data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output }
auto& output = state.outputs.at(data["output-data"]["name"].asString()); try {
auto wset_idx = data["new-wset-data"]["index"].asUInt(); auto& output = state.outputs.at(data["output-data"]["name"].asString());
state.wsets.at(wset_idx).output = output; auto wset_idx = data["new-wset-data"]["index"].asUInt();
output.wset_idx = wset_idx; state.wsets.at(wset_idx).output = output;
output.wset_idx = wset_idx;
} catch (const std::exception&) {
}
return; return;
} }
+10 -8
View File
@@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) {
nullptr, nullptr); nullptr, nullptr);
break; break;
case PA_CONTEXT_FAILED: case PA_CONTEXT_FAILED:
// When pulseaudio server restarts, the connection is "failed". Try to reconnect. if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) {
// pa_threaded_mainloop_lock is already acquired in callback threads. // When pulseaudio server restarts, the connection is "failed". Try to reconnect.
// So there is no need to lock it again. // pa_threaded_mainloop_lock is already acquired in callback threads.
if (backend->context_ != nullptr) { // So there is no need to lock it again.
pa_context_disconnect(backend->context_); if (backend->context_ != nullptr) {
pa_context_unref(backend->context_); pa_context_disconnect(backend->context_);
backend->context_ = nullptr; pa_context_unref(backend->context_);
backend->context_ = nullptr;
}
backend->connectContext();
} }
backend->connectContext();
break; break;
case PA_CONTEXT_CONNECTING: case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_AUTHORIZING:
+1 -1
View File
@@ -160,7 +160,7 @@ TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]
IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3");
REQUIRE( REQUIRE(
result == result ==
"/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })");
} }
TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") {