Skip to content

Commit cb225ec

Browse files
committed
Implement windows formating in sway/workspaces
This implementation mimics to some extend the implementation of hyprland Signed-off-by: Jo De Boeck <deboeck.jo@gmail.com>
1 parent 197bc6a commit cb225ec

File tree

3 files changed

+133
-16
lines changed

3 files changed

+133
-16
lines changed

Diff for: include/modules/sway/workspaces.hpp

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "client.hpp"
1313
#include "modules/sway/ipc/client.hpp"
1414
#include "util/json.hpp"
15+
#include "util/regex_collection.hpp"
1516

1617
namespace waybar::modules::sway {
1718

@@ -27,10 +28,13 @@ class Workspaces : public AModule, public sigc::trackable {
2728
R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")";
2829

2930
static int convertWorkspaceNameToNum(std::string name);
31+
static int windowRewritePriorityFunction(std::string const& window_rule);
3032

3133
void onCmd(const struct Ipc::ipc_response&);
3234
void onEvent(const struct Ipc::ipc_response&);
3335
bool filterButtons();
36+
static bool hasFlag(const Json::Value&, const std::string&);
37+
void updateWindows(const Json::Value&, std::string&);
3438
Gtk::Button& addButton(const Json::Value&);
3539
void onButtonReady(const Json::Value&, Gtk::Button&);
3640
std::string getIcon(const std::string&, const Json::Value&);
@@ -44,6 +48,9 @@ class Workspaces : public AModule, public sigc::trackable {
4448
std::vector<std::string> high_priority_named_;
4549
std::vector<std::string> workspaces_order_;
4650
Gtk::Box box_;
51+
std::string m_formatWindowSeperator;
52+
std::string m_windowRewriteDefault;
53+
util::RegexCollection m_windowRewriteRules;
4754
util::JsonParser parser_;
4855
std::unordered_map<std::string, Gtk::Button> buttons_;
4956
std::mutex mutex_;

Diff for: man/waybar-sway-workspaces.5.scd

+32
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ warp-on-scroll: ++
8282
default: true ++
8383
If set to false, you can scroll to cycle through workspaces without mouse warping being enabled. If set to true this behaviour is disabled.
8484

85+
*window-rewrite*: ++
86+
typeof: object ++
87+
Regex rules to map window class to an icon or preferred method of representation for a workspace's window.
88+
Keys are the rules, while the values are the methods of representation.
89+
Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching.
90+
91+
*window-rewrite-default*:
92+
typeof: string ++
93+
default: "?" ++
94+
The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*.
95+
96+
*format-window-separator*: ++
97+
typeof: string ++
98+
default: " " ++
99+
The separator to be used between windows in a workspace.
100+
101+
85102
# FORMAT REPLACEMENTS
86103

87104
*{value}*: Name of the workspace, as defined by sway.
@@ -94,6 +111,8 @@ warp-on-scroll: ++
94111

95112
*{output}*: Output where the workspace is located.
96113

114+
*{windows}*: Result from window-rewrite
115+
97116
# ICONS
98117

99118
Additional to workspace name matching, the following *format-icons* can be set.
@@ -143,6 +162,19 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge
143162
}
144163
```
145164

165+
```
166+
"sway/workspaces": {
167+
"format": "<span size='larger'>{name}</span> {windows}",
168+
"format-window-separator": " | ",
169+
"window-rewrite-default": "{name}",
170+
"window-format": "<span color='#e0e0e0'>{name}</span>",
171+
"window-rewrite": {
172+
"class<firefox>": "",
173+
"class<kitty>": "k",
174+
}
175+
}
176+
```
177+
146178
# Style
147179

148180
- *#workspaces button*

Diff for: src/modules/sway/workspaces.cpp

+94-16
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ int Workspaces::convertWorkspaceNameToNum(std::string name) {
2424
return -1;
2525
}
2626

27+
int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
28+
// Rules that match against title are prioritized
29+
// Rules that don't specify if they're matching against either title or class are deprioritized
30+
bool const hasTitle = window_rule.find("title") != std::string::npos;
31+
bool const hasClass = window_rule.find("class") != std::string::npos;
32+
33+
if (hasTitle && hasClass) {
34+
return 3;
35+
}
36+
if (hasTitle) {
37+
return 2;
38+
}
39+
if (hasClass) {
40+
return 1;
41+
}
42+
return 0;
43+
}
44+
2745
Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config)
2846
: AModule(config, "workspaces", id, false, !config["disable-scroll"].asBool()),
2947
bar_(bar),
@@ -39,10 +57,25 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value
3957
}
4058
box_.get_style_context()->add_class(MODULE_CLASS);
4159
event_box_.add(box_);
60+
if (config_["format-window-separator"].isString()) {
61+
m_formatWindowSeperator = config_["format-window-separator"].asString();
62+
} else {
63+
m_formatWindowSeperator = " ";
64+
}
65+
const Json::Value &windowRewrite = config["window-rewrite"];
66+
67+
const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"];
68+
m_windowRewriteDefault =
69+
windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?";
70+
71+
m_windowRewriteRules = waybar::util::RegexCollection(
72+
windowRewrite, m_windowRewriteDefault,
73+
[this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); });
4274
ipc_.subscribe(R"(["workspace"])");
75+
ipc_.subscribe(R"(["window"])");
4376
ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent));
4477
ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Workspaces::onCmd));
45-
ipc_.sendCmd(IPC_GET_WORKSPACES);
78+
ipc_.sendCmd(IPC_GET_TREE);
4679
if (config["enable-bar-scroll"].asBool()) {
4780
auto &window = const_cast<Bar &>(bar_).window;
4881
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
@@ -60,26 +93,33 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value
6093

6194
void Workspaces::onEvent(const struct Ipc::ipc_response &res) {
6295
try {
63-
ipc_.sendCmd(IPC_GET_WORKSPACES);
96+
ipc_.sendCmd(IPC_GET_TREE);
6497
} catch (const std::exception &e) {
6598
spdlog::error("Workspaces: {}", e.what());
6699
}
67100
}
68101

69102
void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
70-
if (res.type == IPC_GET_WORKSPACES) {
103+
if (res.type == IPC_GET_TREE) {
71104
try {
72105
{
73106
std::lock_guard<std::mutex> lock(mutex_);
74107
auto payload = parser_.parse(res.payload);
75108
workspaces_.clear();
76-
std::copy_if(payload.begin(), payload.end(), std::back_inserter(workspaces_),
109+
std::vector<Json::Value> outputs;
110+
std::copy_if(payload["nodes"].begin(), payload["nodes"].end(), std::back_inserter(outputs),
77111
[&](const auto &workspace) {
78112
return !config_["all-outputs"].asBool()
79-
? workspace["output"].asString() == bar_.output->name
113+
? workspace["name"].asString() == bar_.output->name
80114
: true;
81115
});
82116

117+
for (auto &output : outputs) {
118+
std::copy(output["nodes"].begin(), output["nodes"].end(),
119+
std::back_inserter(workspaces_));
120+
std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(),
121+
std::back_inserter(workspaces_));
122+
}
83123
if (config_["persistent_workspaces"].isObject()) {
84124
spdlog::warn(
85125
"persistent_workspaces is deprecated. Please change config to use "
@@ -204,6 +244,38 @@ bool Workspaces::filterButtons() {
204244
return needReorder;
205245
}
206246

247+
bool Workspaces::hasFlag(const Json::Value &node, const std::string &flag) {
248+
if (node[flag].asBool()) {
249+
return true;
250+
}
251+
252+
if (std::any_of(node["nodes"].begin(), node["nodes"].end(), [&](auto const &e) { return hasFlag(e, flag); })) {
253+
return true;
254+
}
255+
return false;
256+
}
257+
258+
void Workspaces::updateWindows(const Json::Value &node, std::string &windows) {
259+
auto format = config_["window-format"].asString();
260+
if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && node["name"].isString()) {
261+
std::string title = g_markup_escape_text(node["name"].asString().c_str(), -1);
262+
std::string windowClass = node["app_id"].asString();
263+
std::string windowReprKey = fmt::format("class<{}> title<{}>", windowClass, title);
264+
std::string window = m_windowRewriteRules.get(windowReprKey);
265+
// allow result to have formatting
266+
window =
267+
fmt::format(fmt::runtime(window), fmt::arg("name", title), fmt::arg("class", windowClass));
268+
windows.append(window);
269+
windows.append(m_formatWindowSeperator);
270+
}
271+
for (const Json::Value &child : node["nodes"]) {
272+
updateWindows(child, windows);
273+
}
274+
for (const Json::Value &child : node["floating_nodes"]) {
275+
updateWindows(child, windows);
276+
}
277+
}
278+
207279
auto Workspaces::update() -> void {
208280
std::lock_guard<std::mutex> lock(mutex_);
209281
bool needReorder = filterButtons();
@@ -213,22 +285,25 @@ auto Workspaces::update() -> void {
213285
needReorder = true;
214286
}
215287
auto &button = bit == buttons_.end() ? addButton(*it) : bit->second;
216-
if ((*it)["focused"].asBool()) {
288+
if (needReorder) {
289+
box_.reorder_child(button, it - workspaces_.begin());
290+
}
291+
if (hasFlag((*it), "focused")) {
217292
button.get_style_context()->add_class("focused");
218293
} else {
219294
button.get_style_context()->remove_class("focused");
220295
}
221-
if ((*it)["visible"].asBool()) {
296+
if (hasFlag((*it), "visible")) {
222297
button.get_style_context()->add_class("visible");
223298
} else {
224299
button.get_style_context()->remove_class("visible");
225300
}
226-
if ((*it)["urgent"].asBool()) {
301+
if (hasFlag((*it), "urgent")) {
227302
button.get_style_context()->add_class("urgent");
228303
} else {
229304
button.get_style_context()->remove_class("urgent");
230305
}
231-
if ((*it)["target_output"].isString()) {
306+
if (hasFlag((*it), "target_output")) {
232307
button.get_style_context()->add_class("persistent");
233308
} else {
234309
button.get_style_context()->remove_class("persistent");
@@ -242,16 +317,19 @@ auto Workspaces::update() -> void {
242317
} else {
243318
button.get_style_context()->remove_class("current_output");
244319
}
245-
if (needReorder) {
246-
box_.reorder_child(button, it - workspaces_.begin());
247-
}
248320
std::string output = (*it)["name"].asString();
321+
std::string windows = "";
322+
if (config_["window-format"].isString()) {
323+
updateWindows((*it), windows);
324+
}
249325
if (config_["format"].isString()) {
250326
auto format = config_["format"].asString();
251-
output = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)),
252-
fmt::arg("value", output), fmt::arg("name", trimWorkspaceName(output)),
253-
fmt::arg("index", (*it)["num"].asString()),
254-
fmt::arg("output", (*it)["output"].asString()));
327+
output = fmt::format(
328+
fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output),
329+
fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()),
330+
fmt::arg("windows",
331+
windows.substr(0, windows.length() - m_formatWindowSeperator.length())),
332+
fmt::arg("output", (*it)["output"].asString()));
255333
}
256334
if (!config_["disable-markup"].asBool()) {
257335
static_cast<Gtk::Label *>(button.get_children()[0])->set_markup(output);

0 commit comments

Comments
 (0)