Skip to content

Commit 7f06852

Browse files
sway-window, Issue 1399: new style classes
Provides CSS classes empty, floating, tabbed, tiled, solo, stacked and app_id. Adds offscreen-css bool option (default false), only effective when "all-outputs" is true. This adds styles on outputs without focused node, according to its focused workspaces window situation. Adds an "offscreen-css-text" string option (default empty), only effective when "all-outputs" and "offscreen-style" are set. This is shown as a text on outputs without a focused node. Adds a "show-focused-workspace" bool option (default false) to indicate the workspace name if the whole workspace is focused when nodes are also present. If not set, empty text is shown, but css classes according to nodes in the workspace are still applied. Limitation: When the top level layout changes, there is no sway event so the module cannot react. Perhaps in the future recurring polling can be added to go around this limitation.
1 parent 3996764 commit 7f06852

File tree

4 files changed

+610
-66
lines changed

4 files changed

+610
-66
lines changed

include/modules/sway/window.hpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ class Window : public AIconLabel, public sigc::trackable {
1919
auto update() -> void;
2020

2121
private:
22+
void setClass(std::string classname, bool enable);
2223
void onEvent(const struct Ipc::ipc_response&);
2324
void onCmd(const struct Ipc::ipc_response&);
24-
std::tuple<std::size_t, int, std::string, std::string, std::string> getFocusedNode(
25-
const Json::Value& nodes, std::string& output);
25+
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string>
26+
getFocusedNode(const Json::Value& nodes, std::string& output);
2627
void getTree();
2728
std::string rewriteTitle(const std::string& title);
2829
void updateAppIconName();
@@ -33,11 +34,13 @@ class Window : public AIconLabel, public sigc::trackable {
3334
int windowId_;
3435
std::string app_id_;
3536
std::string app_class_;
37+
std::string layout_;
3638
std::string old_app_id_;
3739
std::size_t app_nb_;
3840
unsigned app_icon_size_{24};
3941
bool update_app_icon_{true};
4042
std::string app_icon_name_;
43+
int floating_count_;
4144
util::JsonParser parser_;
4245
std::mutex mutex_;
4346
Ipc ipc_;

man/waybar-sway-window.5.scd

+25-2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ Addressed by *sway/window*
6666
default: true ++
6767
Option to disable tooltip on hover.
6868

69+
*all-outputs*: ++
70+
typeof: bool ++
71+
default: false ++
72+
Option to show the focused window along with its workspace styles on all outputs.
73+
74+
*offscreen-css*: ++
75+
typeof: bool ++
76+
default: false ++
77+
Only effective when all-outputs is true. Adds style according to present windows on unfocused outputs instead of showing the focused window and style.
78+
79+
*offscreen-css-text*: ++
80+
typeof: string ++
81+
Only effective when both all-outputs and offscreen-style are true. On screens currently not focused, show the given text along with that workspaces styles.
82+
83+
*show-focused-workspace-name*: ++
84+
typeof: bool ++
85+
default: false ++
86+
If the workspace itself is focused and the workspace contains nodes or floating_nodes, show the workspace name. If not set, text remains empty but styles according to nodes in the workspace are still applied.
87+
6988
*rewrite*: ++
7089
typeof: object ++
7190
Rules to rewrite window title. See *rewrite rules*.
@@ -108,6 +127,10 @@ Invalid expressions (e.g., mismatched parentheses) are skipped.
108127
# STYLE
109128

110129
- *#window*
111-
- *window#waybar.empty* When no windows is in the workspace
112-
- *window#waybar.solo* When one window is in the workspace
130+
- *window#waybar.empty* When no windows are in the workspace, or screen is not focused and offscreen-text option is not set
131+
- *window#waybar.solo* When one tiled window is in the workspace
132+
- *window#waybar.floating* When there are only floating windows in the workspace
133+
- *window#waybar.stacked* When there is more than one window in the workspace and the workspace layout is stacked
134+
- *window#waybar.tabbed* When there is more than one window in the workspace and the workspace layout is tabbed
135+
- *window#waybar.tiled* When there is more than one window in the workspace and the workspace layout is splith or splitv
113136
- *window#waybar.<app_id>* Where *app_id* is the app_id or *instance* name like (*chromium*) of the only window in the workspace

src/modules/sway/window.cpp

+157-62
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
3333
ipc_.handleEvent();
3434
} catch (const std::exception& e) {
3535
spdlog::error("Window: {}", e.what());
36+
spdlog::trace("Window::Window exception");
3637
}
3738
});
3839
}
@@ -44,12 +45,13 @@ void Window::onCmd(const struct Ipc::ipc_response& res) {
4445
std::lock_guard<std::mutex> lock(mutex_);
4546
auto payload = parser_.parse(res.payload);
4647
auto output = payload["output"].isString() ? payload["output"].asString() : "";
47-
std::tie(app_nb_, windowId_, window_, app_id_, app_class_) =
48+
std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, layout_) =
4849
getFocusedNode(payload["nodes"], output);
4950
updateAppIconName();
5051
dp.emit();
5152
} catch (const std::exception& e) {
5253
spdlog::error("Window: {}", e.what());
54+
spdlog::trace("Window::onCmd exception");
5355
}
5456
}
5557

@@ -154,27 +156,48 @@ void Window::updateAppIcon() {
154156
}
155157

156158
auto Window::update() -> void {
157-
if (!old_app_id_.empty()) {
158-
bar_.window.get_style_context()->remove_class(old_app_id_);
159-
}
159+
spdlog::trace("workspace layout {}, tiled count {}, floating count {}", layout_, app_nb_,
160+
floating_count_);
161+
162+
int mode = 0;
160163
if (app_nb_ == 0) {
161-
bar_.window.get_style_context()->remove_class("solo");
162-
if (!bar_.window.get_style_context()->has_class("empty")) {
163-
bar_.window.get_style_context()->add_class("empty");
164+
if (floating_count_ == 0) {
165+
mode += 1;
166+
} else {
167+
mode += 4;
164168
}
165169
} else if (app_nb_ == 1) {
166-
bar_.window.get_style_context()->remove_class("empty");
167-
if (!bar_.window.get_style_context()->has_class("solo")) {
168-
bar_.window.get_style_context()->add_class("solo");
169-
}
170-
if (!app_id_.empty() && !bar_.window.get_style_context()->has_class(app_id_)) {
171-
bar_.window.get_style_context()->add_class(app_id_);
172-
old_app_id_ = app_id_;
173-
}
170+
mode += 2;
174171
} else {
175-
bar_.window.get_style_context()->remove_class("solo");
176-
bar_.window.get_style_context()->remove_class("empty");
172+
if (layout_ == "tabbed") {
173+
mode += 8;
174+
} else if (layout_ == "stacked") {
175+
mode += 16;
176+
} else {
177+
mode += 32;
178+
}
177179
}
180+
181+
if (!old_app_id_.empty() && ((mode & 2) == 0 || old_app_id_ != app_id_) &&
182+
bar_.window.get_style_context()->has_class(old_app_id_)) {
183+
spdlog::trace("Removing app_id class: {}", old_app_id_);
184+
bar_.window.get_style_context()->remove_class(old_app_id_);
185+
old_app_id_ = "";
186+
}
187+
188+
setClass("empty", ((mode & 1) > 0));
189+
setClass("solo", ((mode & 2) > 0));
190+
setClass("floating", ((mode & 4) > 0));
191+
setClass("tabbed", ((mode & 8) > 0));
192+
setClass("stacked", ((mode & 16) > 0));
193+
setClass("tiled", ((mode & 32) > 0));
194+
195+
if ((mode & 2) > 0 && !app_id_.empty() && !bar_.window.get_style_context()->has_class(app_id_)) {
196+
spdlog::trace("Adding app_id class: {}", app_id_);
197+
bar_.window.get_style_context()->add_class(app_id_);
198+
old_app_id_ = app_id_;
199+
}
200+
178201
label_.set_markup(
179202
fmt::format(format_, fmt::arg("title", rewriteTitle(window_)), fmt::arg("app_id", app_id_)));
180203
if (tooltipEnabled()) {
@@ -187,75 +210,147 @@ auto Window::update() -> void {
187210
AIconLabel::update();
188211
}
189212

190-
int leafNodesInWorkspace(const Json::Value& node) {
213+
void Window::setClass(std::string classname, bool enable) {
214+
if (enable) {
215+
if (!bar_.window.get_style_context()->has_class(classname)) {
216+
bar_.window.get_style_context()->add_class(classname);
217+
}
218+
} else {
219+
bar_.window.get_style_context()->remove_class(classname);
220+
}
221+
}
222+
223+
std::pair<int, int> leafNodesInWorkspace(const Json::Value& node) {
191224
auto const& nodes = node["nodes"];
192225
auto const& floating_nodes = node["floating_nodes"];
193226
if (nodes.empty() && floating_nodes.empty()) {
194-
if (node["type"] == "workspace")
195-
return 0;
196-
else
197-
return 1;
227+
if (node["type"].asString() == "workspace")
228+
return {0, 0};
229+
else if (node["type"].asString() == "floating_con") {
230+
return {0, 1};
231+
} else {
232+
return {1, 0};
233+
}
198234
}
199235
int sum = 0;
200-
if (!nodes.empty()) {
201-
for (auto const& node : nodes) sum += leafNodesInWorkspace(node);
236+
int floating_sum = 0;
237+
for (auto const& node : nodes) {
238+
std::pair all_leaf_nodes = leafNodesInWorkspace(node);
239+
sum += all_leaf_nodes.first;
240+
floating_sum += all_leaf_nodes.second;
202241
}
203-
if (!floating_nodes.empty()) {
204-
for (auto const& node : floating_nodes) sum += leafNodesInWorkspace(node);
242+
for (auto const& node : floating_nodes) {
243+
std::pair all_leaf_nodes = leafNodesInWorkspace(node);
244+
sum += all_leaf_nodes.first;
245+
floating_sum += all_leaf_nodes.second;
205246
}
206-
return sum;
247+
return {sum, floating_sum};
207248
}
208249

209-
std::tuple<std::size_t, int, std::string, std::string, std::string> gfnWithWorkspace(
210-
const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_,
211-
Json::Value& parentWorkspace) {
250+
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string>
251+
gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_,
252+
const Bar& bar_, Json::Value& parentWorkspace,
253+
const Json::Value& immediateParent) {
212254
for (auto const& node : nodes) {
213-
if (node["output"].isString()) {
214-
output = node["output"].asString();
215-
}
216-
// found node
217-
if (node["focused"].asBool() && (node["type"] == "con" || node["type"] == "floating_con")) {
218-
if ((!config_["all-outputs"].asBool() && output == bar_.output->name) ||
219-
config_["all-outputs"].asBool()) {
220-
auto app_id = node["app_id"].isString() ? node["app_id"].asString()
221-
: node["window_properties"]["instance"].asString();
222-
const auto app_class = node["window_properties"]["class"].isString()
223-
? node["window_properties"]["class"].asString()
224-
: "";
225-
int nb = node.size();
226-
if (parentWorkspace != 0) nb = leafNodesInWorkspace(parentWorkspace);
227-
return {nb, node["id"].asInt(), Glib::Markup::escape_text(node["name"].asString()), app_id,
228-
app_class};
255+
if (node["type"].asString() == "output") {
256+
if ((!config_["all-outputs"].asBool() || config_["offscreen-css"].asBool()) &&
257+
(node["name"].asString() != bar_.output->name)) {
258+
continue;
259+
}
260+
output = node["name"].asString();
261+
} else if (node["type"].asString() == "workspace") {
262+
// needs to be a string comparison, because filterWorkspace is the current_workspace
263+
if (node["name"].asString() != immediateParent["current_workspace"].asString()) {
264+
continue;
229265
}
266+
if (node["focused"].asBool()) {
267+
std::pair all_leaf_nodes = leafNodesInWorkspace(node);
268+
return {all_leaf_nodes.first,
269+
all_leaf_nodes.second,
270+
node["id"].asInt(),
271+
(((all_leaf_nodes.first > 0) || (all_leaf_nodes.second > 0)) &&
272+
(config_["show-focused-workspace-name"].asBool()))
273+
? node["name"].asString()
274+
: "",
275+
"",
276+
"",
277+
node["layout"].asString()};
278+
}
279+
parentWorkspace = node;
280+
} else if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") &&
281+
(node["focused"].asBool())) {
282+
// found node
283+
spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name,
284+
output, node["name"].asString());
285+
auto app_id = node["app_id"].isString() ? node["app_id"].asString()
286+
: node["window_properties"]["instance"].asString();
287+
const auto app_class = node["window_properties"]["class"].isString()
288+
? node["window_properties"]["class"].asString()
289+
: "";
290+
int nb = node.size();
291+
int floating_count = 0;
292+
std::string workspace_layout = "";
293+
if (!parentWorkspace.isNull()) {
294+
std::pair all_leaf_nodes = leafNodesInWorkspace(parentWorkspace);
295+
nb = all_leaf_nodes.first;
296+
floating_count = all_leaf_nodes.second;
297+
workspace_layout = parentWorkspace["layout"].asString();
298+
}
299+
return {nb,
300+
floating_count,
301+
node["id"].asInt(),
302+
Glib::Markup::escape_text(node["name"].asString()),
303+
app_id,
304+
app_class,
305+
workspace_layout};
230306
}
307+
231308
// iterate
232-
if (node["type"] == "workspace") parentWorkspace = node;
233-
auto [nb, id, name, app_id, app_class] =
234-
gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace);
235-
if (id > -1 && !name.empty()) {
236-
return {nb, id, name, app_id, app_class};
237-
}
238-
// Search for floating node
239-
std::tie(nb, id, name, app_id, app_class) =
240-
gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace);
241-
if (id > -1 && !name.empty()) {
242-
return {nb, id, name, app_id, app_class};
309+
auto [nb, f, id, name, app_id, app_class, workspace_layout] =
310+
gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace, node);
311+
auto [nb2, f2, id2, name2, app_id2, app_class2, workspace_layout2] =
312+
gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace, node);
313+
314+
// if ((id > 0 || ((id2 < 0 || name2.empty()) && id > -1)) && !name.empty()) {
315+
if ((id > 0) || (id2 < 0 && id > -1)) {
316+
return {nb, f, id, name, app_id, app_class, workspace_layout};
317+
} else if (id2 > 0 && !name2.empty()) {
318+
return {nb2, f2, id2, name2, app_id2, app_class, workspace_layout2};
243319
}
244320
}
245-
return {0, -1, "", "", ""};
321+
322+
// this only comes into effect when no focused children are present
323+
if (config_["all-outputs"].asBool() && config_["offscreen-css"].asBool() &&
324+
immediateParent["type"].asString() == "workspace") {
325+
std::pair all_leaf_nodes = leafNodesInWorkspace(immediateParent);
326+
// using an empty string as default ensures that no window depending styles are set due to the
327+
// checks above for !name.empty()
328+
return {all_leaf_nodes.first,
329+
all_leaf_nodes.second,
330+
0,
331+
(all_leaf_nodes.first > 0 || all_leaf_nodes.second > 0)
332+
? config_["offscreen-css-text"].asString()
333+
: "",
334+
"",
335+
"",
336+
immediateParent["layout"].asString()};
337+
}
338+
339+
return {0, 0, -1, "", "", "", ""};
246340
}
247341

248-
std::tuple<std::size_t, int, std::string, std::string, std::string> Window::getFocusedNode(
249-
const Json::Value& nodes, std::string& output) {
250-
Json::Value placeholder = 0;
251-
return gfnWithWorkspace(nodes, output, config_, bar_, placeholder);
342+
std::tuple<std::size_t, int, int, std::string, std::string, std::string, std::string>
343+
Window::getFocusedNode(const Json::Value& nodes, std::string& output) {
344+
Json::Value placeholder = Json::Value::null;
345+
return gfnWithWorkspace(nodes, output, config_, bar_, placeholder, placeholder);
252346
}
253347

254348
void Window::getTree() {
255349
try {
256350
ipc_.sendCmd(IPC_GET_TREE);
257351
} catch (const std::exception& e) {
258352
spdlog::error("Window: {}", e.what());
353+
spdlog::trace("Window::getTree exception");
259354
}
260355
}
261356

0 commit comments

Comments
 (0)