Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Hyprland: Allow matching against Window Titles for Rewrite #2563

Merged
merged 8 commits into from
Oct 15, 2023
19 changes: 13 additions & 6 deletions include/modules/hyprland/workspaces.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
#include "bar.hpp"
#include "modules/hyprland/backend.hpp"
#include "util/enum.hpp"
#include "util/regex_collection.hpp"

using WindowAddress = std::string;

namespace waybar::modules::hyprland {

class Workspaces;
Expand Down Expand Up @@ -46,8 +48,8 @@ class Workspace {
void set_visible(bool value = true) { is_visible_ = value; };
void set_windows(uint value) { windows_ = value; };
void set_name(std::string value) { name_ = value; };
bool contains_window(WindowAddress addr) { return window_map_.contains(addr); }
void insert_window(WindowAddress addr, std::string window_repr);
bool contains_window(WindowAddress addr) const { return window_map_.contains(addr); }
void insert_window(WindowAddress addr, std::string window_class, std::string window_title);
std::string remove_window(WindowAddress addr);
void initialize_window_map(const Json::Value& clients_data);

Expand Down Expand Up @@ -92,9 +94,11 @@ class Workspaces : public AModule, public EventHandler {

auto get_bar_output() const -> std::string { return bar_.output->name; }

std::string get_rewrite(std::string window_class);
std::string get_rewrite(std::string window_class, std::string window_title);
std::string& get_window_separator() { return format_window_separator_; }

bool window_rewrite_config_uses_title() const { return any_window_rewrite_rule_uses_title_; }

private:
void onEvent(const std::string&) override;
void update_window_count();
Expand All @@ -111,6 +115,8 @@ class Workspaces : public AModule, public EventHandler {
void on_window_closed(std::string payload);
void on_window_moved(std::string payload);

int window_rewrite_priority_function(std::string& window_rule);

bool all_outputs_ = false;
bool show_special_ = false;
bool active_only_ = false;
Expand All @@ -129,11 +135,12 @@ class Workspaces : public AModule, public EventHandler {
bool persistent_created_ = false;

std::string format_;

std::map<std::string, std::string> icons_map_;
Json::Value window_rewrite_rules_;
std::map<std::string, std::string> regex_cache_;
util::RegexCollection window_rewrite_rules_;
bool any_window_rewrite_rule_uses_title_ = false;
std::string format_window_separator_;
std::string window_rewrite_default_;

bool with_icon_;
uint64_t monitor_id_;
std::string active_workspace_name_;
Expand Down
51 changes: 51 additions & 0 deletions include/util/regex_collection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#pragma once

#include <json/json.h>

#include <functional>
#include <regex>
#include <string>

namespace waybar::util {

struct Rule {
std::regex rule;
std::string repr;
int priority;

// Fix for Clang < 16
// See https://en.cppreference.com/w/cpp/compiler_support/20 "Parenthesized initialization of
// aggregates"
Rule(std::regex rule, std::string repr, int priority)
: rule(rule), repr(repr), priority(priority) {}
};

int default_priority_function(std::string& key);

/* A collection of regexes and strings, with a default string to return if no regexes.
* When a regex is matched, the corresponding string is returned.
* All regexes that are matched are cached, so that the regexes are only
* evaluated once against a given string.
* Regexes may be given a higher priority than others, so that they are matched
* first. The priority function is given the regex string, and should return a
* higher number for higher priority regexes.
*/
class RegexCollection {
private:
std::vector<Rule> rules;
std::map<std::string, std::string> regex_cache;
std::string default_repr;

std::string& find_match(std::string& value, bool& matched_any);

public:
RegexCollection() = default;
RegexCollection(const Json::Value& map, std::string default_repr = "",
std::function<int(std::string&)> priority_function = default_priority_function);
~RegexCollection() = default;

std::string& get(std::string& value, bool& matched_any);
std::string& get(std::string& value);
};

} // namespace waybar::util
7 changes: 5 additions & 2 deletions man/waybar-hyprland-workspaces.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Addressed by *hyprland/workspaces*
typeof: object ++
Regex rules to map window class to an icon or preferred method of representation for a workspace's window.
Keys are the rules, while the values are the methods of representation.
Rules may specify `class<...>`, `title<...>` or both in order to fine-tune the matching.

*window-rewrite-default*:
typeof: string ++
Expand Down Expand Up @@ -124,8 +125,10 @@ Additional to workspace name matching, the following *format-icons* can be set.
"format-window-separator": "\n",
"window-rewrite-default": "",
"window-rewrite": {
"firefox": "",
"foot": "",
"title<.*youtube.*>": "", // Windows whose titles contain "youtube"
"class<firefox>": "", // Windows whose classes are "firefox"
"class<firefox> title<.*github.*>": "", // Windows whose class is "firefox" and title contains "github". Note that "class" always comes first.
"foot": "", // Windows that contain "foot" in either class or title. For optimization reasons, it will only match against a title if at least one other window explicitly matches against a title.
"code": "󰨞",
}
}
Expand Down
3 changes: 2 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ src_files = files(
'src/util/ustring_clen.cpp',
'src/util/sanitize_str.cpp',
'src/util/rewrite_string.cpp',
'src/util/gtk_icon.cpp'
'src/util/gtk_icon.cpp',
'src/util/regex_collection.cpp'
)

inc_dirs = ['include']
Expand Down
12 changes: 6 additions & 6 deletions src/modules/disk.cpp
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is unrelated to this PR and was simply linted it in f8340d8

Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ auto waybar::modules::Disk::update() -> void {
float specific_free, specific_used, specific_total, divisor;

divisor = calc_specific_divisor(unit_);
specific_free = (stats.f_bavail * stats.f_frsize)/divisor;
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize)/divisor;
specific_total = (stats.f_blocks * stats.f_frsize)/divisor;
specific_free = (stats.f_bavail * stats.f_frsize) / divisor;
specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor;
specific_total = (stats.f_blocks * stats.f_frsize) / divisor;

auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
Expand All @@ -72,7 +72,7 @@ auto waybar::modules::Disk::update() -> void {
fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
}

Expand All @@ -85,7 +85,7 @@ auto waybar::modules::Disk::update() -> void {
fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free),
fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used),
fmt::arg("percentage_used", percentage_used), fmt::arg("total", total),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("path", path_), fmt::arg("specific_free", specific_free),
fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)));
}
// Call parent update
Expand All @@ -109,7 +109,7 @@ float waybar::modules::Disk::calc_specific_divisor(std::string divisor) {
return 1000.0 * 1000.0 * 1000.0 * 1000.0;
} else if (divisor == "TiB") {
return 1024.0 * 1024.0 * 1024.0 * 1024.0;
} else { //default to Bytes if it is anything that we don't recongnise
} else { // default to Bytes if it is anything that we don't recongnise
return 1.0;
}
}
104 changes: 75 additions & 29 deletions src/modules/hyprland/workspaces.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
#include "modules/hyprland/workspaces.hpp"

#include <fmt/ostream.h>
#include <json/value.h>
#include <spdlog/spdlog.h>

#include <algorithm>
#include <charconv>
#include <memory>
#include <optional>
#include <string>

#include "util/rewrite_string.hpp"
#include "util/regex_collection.hpp"

namespace waybar::modules::hyprland {

int Workspaces::window_rewrite_priority_function(std::string &window_rule) {
// Rules that match against title are prioritized
// Rules that don't specify if they're matching against either title or class are deprioritized
bool has_title = window_rule.find("title") != std::string::npos;
bool has_class = window_rule.find("class") != std::string::npos;

if (has_title && has_class) {
any_window_rewrite_rule_uses_title_ = true;
return 3;
} else if (has_title) {
any_window_rewrite_rule_uses_title_ = true;
return 2;
} else if (has_class) {
return 1;
} else {
return 0;
}
}

Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config)
: AModule(config, "workspaces", id, false, false),
bar_(bar),
Expand Down Expand Up @@ -77,11 +94,16 @@ auto Workspaces::parse_config(const Json::Value &config) -> void {
format_window_separator_ =
format_window_separator.isString() ? format_window_separator.asString() : " ";

window_rewrite_rules_ = config["window-rewrite"];
Json::Value window_rewrite = config["window-rewrite"];

Json::Value window_rewrite_default_config = config["window-rewrite-default"];
std::string window_rewrite_default =
window_rewrite_default_config.isString() ? window_rewrite_default_config.asString() : "?";

Json::Value window_rewrite_default = config["window-rewrite-default"];
window_rewrite_default_ =
window_rewrite_default.isString() ? window_rewrite_default.asString() : "?";
window_rewrite_rules_ = util::RegexCollection(
window_rewrite, window_rewrite_default, [this](std::string &window_rule) {
return this->window_rewrite_priority_function(window_rule);
});
}

auto Workspaces::register_ipc() -> void {
Expand All @@ -101,6 +123,13 @@ auto Workspaces::register_ipc() -> void {
gIPC->registerForIPC("closewindow", this);
gIPC->registerForIPC("movewindow", this);
gIPC->registerForIPC("urgent", this);

if (window_rewrite_config_uses_title()) {
spdlog::info(
"Registering for Hyprland's 'windowtitle' events because a user-defined window "
"rewrite rule uses the 'title' field.");
gIPC->registerForIPC("windowtitle", this);
}
}

auto Workspaces::update() -> void {
Expand Down Expand Up @@ -222,6 +251,25 @@ void Workspaces::onEvent(const std::string &ev) {
break;
}
}
} else if (eventName == "windowtitle") {
auto window_workspace =
std::find_if(workspaces_.begin(), workspaces_.end(),
[payload](auto &workspace) { return workspace->contains_window(payload); });

if (window_workspace != workspaces_.end()) {
Json::Value clients_data = gIPC->getSocket1JsonReply("clients");
std::string json_window_address = fmt::format("0x{}", payload);

auto client = std::find_if(clients_data.begin(), clients_data.end(),
[json_window_address](auto &client) {
return client["address"].asString() == json_window_address;
});

if (!client->empty()) {
(*window_workspace)
->insert_window(payload, (*client)["class"].asString(), (*client)["title"].asString());
}
}
}

dp.emit();
Expand Down Expand Up @@ -323,15 +371,22 @@ void Workspace::initialize_window_map(const Json::Value &clients_data) {
// {ADDR}
WindowAddress client_address = client["address"].asString();
client_address = client_address.substr(2, client_address.length() - 2);
insert_window(client_address, client["class"].asString());
insert_window(client_address, client["class"].asString(), client["title"].asString());
}
}
}

void Workspace::insert_window(WindowAddress addr, std::string window_class) {
auto window_repr = workspace_manager_.get_rewrite(window_class);
void Workspace::insert_window(WindowAddress addr, std::string window_class,
std::string window_title) {
if (window_class.empty() &&
(!workspace_manager_.window_rewrite_config_uses_title() || window_title.empty())) {
return;
}

auto window_repr = workspace_manager_.get_rewrite(window_class, window_title);

if (!window_repr.empty()) {
window_map_.emplace(addr, window_repr);
window_map_[addr] = window_repr;
}
};

Expand All @@ -345,7 +400,7 @@ std::string Workspace::remove_window(WindowAddress addr) {
bool Workspace::on_window_opened(WindowAddress &addr, std::string &workspace_name,
std::string window_repr) {
if (workspace_name == name()) {
window_map_.emplace(addr, window_repr);
window_map_[addr] = window_repr;
return true;
} else {
return false;
Expand All @@ -355,7 +410,7 @@ bool Workspace::on_window_opened(WindowAddress &addr, std::string &workspace_nam
bool Workspace::on_window_opened(WindowAddress &addr, std::string &workspace_name,
std::string &window_class, std::string &window_title) {
if (workspace_name == name()) {
insert_window(addr, window_class);
insert_window(addr, window_class, window_title);
return true;
} else {
return false;
Expand Down Expand Up @@ -766,23 +821,14 @@ void Workspaces::set_urgent_workspace(std::string windowaddress) {
}
}

std::string Workspaces::get_rewrite(std::string window_class) {
if (regex_cache_.contains(window_class)) {
return regex_cache_[window_class];
}

bool matched_any;

std::string window_class_rewrite =
waybar::util::rewriteStringOnce(window_class, window_rewrite_rules_, matched_any);

if (!matched_any) {
window_class_rewrite = window_rewrite_default_;
std::string Workspaces::get_rewrite(std::string window_class, std::string window_title) {
std::string window_repr_key;
if (window_rewrite_config_uses_title()) {
window_repr_key = fmt::format("class<{}> title<{}>", window_class, window_title);
} else {
window_repr_key = fmt::format("class<{}>", window_class);
}

regex_cache_.emplace(window_class, window_class_rewrite);

return window_class_rewrite;
return window_rewrite_rules_.get(window_repr_key);
}

} // namespace waybar::modules::hyprland
Loading