Merge pull request #2529 from Syndelis/feat/hyprland-window-workspaces

Feature: Hyprland dynamic window names on workspaces
This commit is contained in:
Alexis Rouillard 2023-10-02 19:17:42 +02:00 committed by GitHub
commit 58e506a675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 290 additions and 15 deletions

View File

@ -2,9 +2,13 @@
#include <gtkmm/button.h> #include <gtkmm/button.h>
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <json/value.h>
#include <cstddef>
#include <cstdint>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@ -13,13 +17,15 @@
#include "modules/hyprland/backend.hpp" #include "modules/hyprland/backend.hpp"
#include "util/enum.hpp" #include "util/enum.hpp"
using WindowAddress = std::string;
namespace waybar::modules::hyprland { namespace waybar::modules::hyprland {
class Workspaces; class Workspaces;
class Workspace { class Workspace {
public: public:
explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager); explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager,
const Json::Value& clients_json = Json::Value::nullRef);
std::string& select_icon(std::map<std::string, std::string>& icons_map); std::string& select_icon(std::map<std::string, std::string>& icons_map);
Gtk::Button& button() { return button_; }; Gtk::Button& button() { return button_; };
@ -40,6 +46,16 @@ class Workspace {
void set_visible(bool value = true) { is_visible_ = value; }; void set_visible(bool value = true) { is_visible_ = value; };
void set_windows(uint value) { windows_ = value; }; void set_windows(uint value) { windows_ = value; };
void set_name(std::string value) { name_ = 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);
std::string remove_window(WindowAddress addr);
void initialize_window_map(const Json::Value& clients_data);
bool on_window_opened(WindowAddress& addr, std::string& workspace_name, std::string window_repr);
bool on_window_opened(WindowAddress& addr, std::string& workspace_name, std::string& window_class,
std::string& window_title);
std::optional<std::string> on_window_closed(WindowAddress& addr);
void update(const std::string& format, const std::string& icon); void update(const std::string& format, const std::string& icon);
@ -56,6 +72,8 @@ class Workspace {
bool is_urgent_ = false; bool is_urgent_ = false;
bool is_visible_ = false; bool is_visible_ = false;
std::map<WindowAddress, std::string> window_map_;
Gtk::Button button_; Gtk::Button button_;
Gtk::Box content_; Gtk::Box content_;
Gtk::Label label_; Gtk::Label label_;
@ -74,16 +92,25 @@ class Workspaces : public AModule, public EventHandler {
auto get_bar_output() const -> std::string { return bar_.output->name; } auto get_bar_output() const -> std::string { return bar_.output->name; }
std::string get_rewrite(std::string window_class);
std::string& get_window_separator() { return format_window_separator_; }
private: private:
void onEvent(const std::string&) override; void onEvent(const std::string&) override;
void update_window_count(); void update_window_count();
void initialize_window_maps();
void sort_workspaces(); void sort_workspaces();
void create_workspace(Json::Value& value); void create_workspace(Json::Value& workspace_data,
const Json::Value& clients_data = Json::Value::nullRef);
void remove_workspace(std::string name); void remove_workspace(std::string name);
void set_urgent_workspace(std::string windowaddress); void set_urgent_workspace(std::string windowaddress);
void parse_config(const Json::Value& config); void parse_config(const Json::Value& config);
void register_ipc(); void register_ipc();
void on_window_opened(std::string payload);
void on_window_closed(std::string payload);
void on_window_moved(std::string payload);
bool all_outputs_ = false; bool all_outputs_ = false;
bool show_special_ = false; bool show_special_ = false;
bool active_only_ = false; bool active_only_ = false;
@ -103,6 +130,10 @@ class Workspaces : public AModule, public EventHandler {
std::string format_; std::string format_;
std::map<std::string, std::string> icons_map_; std::map<std::string, std::string> icons_map_;
Json::Value window_rewrite_rules_;
std::map<std::string, std::string> regex_cache_;
std::string format_window_separator_;
std::string window_rewrite_default_;
bool with_icon_; bool with_icon_;
uint64_t monitor_id_; uint64_t monitor_id_;
std::string active_workspace_name_; std::string active_workspace_name_;

View File

@ -5,4 +5,6 @@
namespace waybar::util { namespace waybar::util {
std::string rewriteString(const std::string&, const Json::Value&); std::string rewriteString(const std::string&, const Json::Value&);
} std::string rewriteStringOnce(const std::string& value, const Json::Value& rules,
bool& matched_any);
} // namespace waybar::util

View File

@ -21,6 +21,21 @@ Addressed by *hyprland/workspaces*
typeof: array ++ typeof: array ++
Based on the workspace id and state, the corresponding icon gets selected. See *icons*. Based on the workspace id and state, the corresponding icon gets selected. See *icons*.
*window-rewrite*: ++
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.
*window-rewrite-default*:
typeof: string ++
default: "?" ++
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*.
*format-window-separator*: ++
typeof: string ++
default: " " ++
The separator to be used between windows in a workspace.
*show-special*: ++ *show-special*: ++
typeof: bool ++ typeof: bool ++
default: false ++ default: false ++
@ -103,6 +118,19 @@ Additional to workspace name matching, the following *format-icons* can be set.
} }
``` ```
```
"hyprland/workspaces": {
"format": "{name}\n{windows}",
"format-window-separator": "\n",
"window-rewrite-default": "",
"window-rewrite": {
"firefox": "",
"foot": "",
"code": "󰨞",
}
}
```
# Style # Style
- *#workspaces* - *#workspaces*

View File

@ -244,7 +244,9 @@ void waybar::modules::Backlight::upsert_device(ForwardIt first, ForwardIt last,
check_nn(name); check_nn(name);
const char *actual_brightness_attr = const char *actual_brightness_attr =
strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0 ? "brightness" : "actual_brightness"; strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0
? "brightness"
: "actual_brightness";
const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr);
const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); const char *max = udev_device_get_sysattr_value(dev, "max_brightness");

View File

@ -256,7 +256,7 @@ const std::tuple<uint8_t, float, std::string, float> waybar::modules::Battery::g
std::string _status; std::string _status;
/* Check for adapter status if battery is not available */ /* Check for adapter status if battery is not available */
if(!std::ifstream(bat / "status")) { if (!std::ifstream(bat / "status")) {
std::getline(std::ifstream(adapter_ / "status"), _status); std::getline(std::ifstream(adapter_ / "status"), _status);
} else { } else {
std::getline(std::ifstream(bat / "status"), _status); std::getline(std::ifstream(bat / "status"), _status);

View File

@ -1,13 +1,17 @@
#include "modules/hyprland/workspaces.hpp" #include "modules/hyprland/workspaces.hpp"
#include <fmt/ostream.h>
#include <json/value.h> #include <json/value.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm> #include <algorithm>
#include <charconv> #include <charconv>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include "util/rewrite_string.hpp"
namespace waybar::modules::hyprland { namespace waybar::modules::hyprland {
Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config)
@ -68,6 +72,16 @@ auto Workspaces::parse_config(const Json::Value &config) -> void {
g_warning("Invalid string representation for sort-by. Falling back to default sort method."); g_warning("Invalid string representation for sort-by. Falling back to default sort method.");
} }
} }
Json::Value format_window_separator = config["format-window-separator"];
format_window_separator_ =
format_window_separator.isString() ? format_window_separator.asString() : " ";
window_rewrite_rules_ = config["window-rewrite"];
Json::Value window_rewrite_default = config["window-rewrite-default"];
window_rewrite_default_ =
window_rewrite_default.isString() ? window_rewrite_default.asString() : "?";
} }
auto Workspaces::register_ipc() -> void { auto Workspaces::register_ipc() -> void {
@ -184,8 +198,15 @@ void Workspaces::onEvent(const std::string &ev) {
} else { } else {
workspaces_to_remove_.push_back(workspace); workspaces_to_remove_.push_back(workspace);
} }
} else if (eventName == "openwindow" || eventName == "closewindow" || eventName == "movewindow") { } else if (eventName == "openwindow") {
update_window_count(); update_window_count();
on_window_opened(payload);
} else if (eventName == "closewindow") {
update_window_count();
on_window_closed(payload);
} else if (eventName == "movewindow") {
update_window_count();
on_window_moved(payload);
} else if (eventName == "urgent") { } else if (eventName == "urgent") {
set_urgent_workspace(payload); set_urgent_workspace(payload);
} else if (eventName == "renameworkspace") { } else if (eventName == "renameworkspace") {
@ -206,6 +227,67 @@ void Workspaces::onEvent(const std::string &ev) {
dp.emit(); dp.emit();
} }
void Workspaces::on_window_opened(std::string payload) {
size_t last_comma_idx = 0;
size_t next_comma_idx = payload.find(',');
std::string window_address = payload.substr(last_comma_idx, next_comma_idx - last_comma_idx);
last_comma_idx = next_comma_idx;
next_comma_idx = payload.find(',', next_comma_idx + 1);
std::string workspace_name =
payload.substr(last_comma_idx + 1, next_comma_idx - last_comma_idx - 1);
last_comma_idx = next_comma_idx;
next_comma_idx = payload.find(',', next_comma_idx + 1);
std::string window_class =
payload.substr(last_comma_idx + 1, next_comma_idx - last_comma_idx - 1);
std::string window_title = payload.substr(next_comma_idx + 1, payload.length() - next_comma_idx);
for (auto &workspace : workspaces_) {
if (workspace->on_window_opened(window_address, workspace_name, window_class, window_title)) {
break;
}
}
}
void Workspaces::on_window_closed(std::string addr) {
for (auto &workspace : workspaces_) {
if (workspace->on_window_closed(addr)) {
break;
}
}
}
void Workspaces::on_window_moved(std::string payload) {
size_t last_comma_idx = 0;
size_t next_comma_idx = payload.find(',');
std::string window_address = payload.substr(last_comma_idx, next_comma_idx - last_comma_idx);
std::string workspace_name =
payload.substr(next_comma_idx + 1, payload.length() - next_comma_idx);
std::string window_repr;
// Take the window's representation from the old workspace...
for (auto &workspace : workspaces_) {
try {
window_repr = workspace->on_window_closed(window_address).value();
break;
} catch (const std::bad_optional_access &e) {
// window was not found in this workspace
continue;
}
}
// ...and add it to the new workspace
for (auto &workspace : workspaces_) {
if (workspace->on_window_opened(window_address, workspace_name, window_repr)) {
break;
}
}
}
void Workspaces::update_window_count() { void Workspaces::update_window_count() {
const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces");
for (auto &workspace : workspaces_) { for (auto &workspace : workspaces_) {
@ -224,22 +306,86 @@ void Workspaces::update_window_count() {
} }
} }
void Workspaces::create_workspace(Json::Value &value) { void Workspaces::initialize_window_maps() {
Json::Value clients_data = gIPC->getSocket1JsonReply("clients");
for (auto &workspace : workspaces_) {
workspace->initialize_window_map(clients_data);
}
}
void Workspace::initialize_window_map(const Json::Value &clients_data) {
window_map_.clear();
for (auto client : clients_data) {
if (client["workspace"]["id"].asInt() == id()) {
// substr(2, ...) is necessary because Hyprland's JSON follows this format:
// 0x{ADDR}
// While Hyprland's IPC follows this format:
// {ADDR}
WindowAddress client_address = client["address"].asString();
client_address = client_address.substr(2, client_address.length() - 2);
insert_window(client_address, client["class"].asString());
}
}
}
void Workspace::insert_window(WindowAddress addr, std::string window_class) {
auto window_repr = workspace_manager_.get_rewrite(window_class);
if (!window_repr.empty()) {
window_map_.emplace(addr, window_repr);
}
};
std::string Workspace::remove_window(WindowAddress addr) {
std::string window_repr = window_map_[addr];
window_map_.erase(addr);
return window_repr;
}
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);
return true;
} else {
return false;
}
}
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);
return true;
} else {
return false;
}
}
std::optional<std::string> Workspace::on_window_closed(WindowAddress &addr) {
if (window_map_.contains(addr)) {
return remove_window(addr);
} else {
return {};
}
}
void Workspaces::create_workspace(Json::Value &workspace_data, const Json::Value &clients_data) {
// replace the existing persistent workspace if it exists // replace the existing persistent workspace if it exists
auto workspace = std::find_if( auto workspace = std::find_if(
workspaces_.begin(), workspaces_.end(), [&](std::unique_ptr<Workspace> const &x) { workspaces_.begin(), workspaces_.end(), [&](std::unique_ptr<Workspace> const &x) {
auto name = value["name"].asString(); auto name = workspace_data["name"].asString();
return x->is_persistent() && return x->is_persistent() &&
((name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name()); ((name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name());
}); });
if (workspace != workspaces_.end()) { if (workspace != workspaces_.end()) {
// replace workspace, but keep persistent flag // replace workspace, but keep persistent flag
workspaces_.erase(workspace); workspaces_.erase(workspace);
value["persistent"] = true; workspace_data["persistent"] = true;
} }
// create new workspace // create new workspace
workspaces_.emplace_back(std::make_unique<Workspace>(value, *this)); workspaces_.emplace_back(std::make_unique<Workspace>(workspace_data, *this, clients_data));
Gtk::Button &new_workspace_button = workspaces_.back()->button(); Gtk::Button &new_workspace_button = workspaces_.back()->button();
box_.pack_start(new_workspace_button, false, false); box_.pack_start(new_workspace_button, false, false);
sort_workspaces(); sort_workspaces();
@ -362,10 +508,12 @@ void Workspaces::init() {
create_persistent_workspaces(); create_persistent_workspaces();
const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces");
const Json::Value clients_json = gIPC->getSocket1JsonReply("clients");
for (Json::Value workspace_json : workspaces_json) { for (Json::Value workspace_json : workspaces_json) {
if ((all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && if ((all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) &&
(!workspace_json["name"].asString().starts_with("special") || show_special())) { (!workspace_json["name"].asString().starts_with("special") || show_special())) {
create_workspace(workspace_json); create_workspace(workspace_json, clients_json);
} }
} }
@ -382,7 +530,8 @@ Workspaces::~Workspaces() {
std::lock_guard<std::mutex> lg(mutex_); std::lock_guard<std::mutex> lg(mutex_);
} }
Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager) Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager,
const Json::Value &clients_data)
: workspace_manager_(workspace_manager), : workspace_manager_(workspace_manager),
id_(workspace_data["id"].asInt()), id_(workspace_data["id"].asInt()),
name_(workspace_data["name"].asString()), name_(workspace_data["name"].asString()),
@ -407,6 +556,8 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma
button_.set_relief(Gtk::RELIEF_NONE); button_.set_relief(Gtk::RELIEF_NONE);
content_.set_center_widget(label_); content_.set_center_widget(label_);
button_.add(content_); button_.add(content_);
initialize_window_map(clients_data);
} }
void add_or_remove_class(const Glib::RefPtr<Gtk::StyleContext> &context, bool condition, void add_or_remove_class(const Glib::RefPtr<Gtk::StyleContext> &context, bool condition,
@ -440,8 +591,22 @@ void Workspace::update(const std::string &format, const std::string &icon) {
add_or_remove_class(style_context, is_urgent(), "urgent"); add_or_remove_class(style_context, is_urgent(), "urgent");
add_or_remove_class(style_context, is_visible(), "visible"); add_or_remove_class(style_context, is_visible(), "visible");
std::string windows;
auto window_separator = workspace_manager_.get_window_separator();
bool is_not_first = false;
for (auto &[_pid, window_repr] : window_map_) {
if (is_not_first) {
windows.append(window_separator);
}
is_not_first = true;
windows.append(window_repr);
}
label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()),
fmt::arg("name", name()), fmt::arg("icon", icon))); fmt::arg("name", name()), fmt::arg("icon", icon),
fmt::arg("windows", windows)));
} }
void Workspaces::sort_workspaces() { void Workspaces::sort_workspaces() {
@ -601,4 +766,23 @@ 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_;
}
regex_cache_.emplace(window_class, window_class_rewrite);
return window_class_rewrite;
}
} // namespace waybar::modules::hyprland } // namespace waybar::modules::hyprland

View File

@ -1,5 +1,6 @@
#include "util/rewrite_string.hpp" #include "util/rewrite_string.hpp"
#include <fmt/core.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <regex> #include <regex>
@ -17,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) {
try { try {
// malformated regexes will cause an exception. // malformated regexes will cause an exception.
// in this case, log error and try the next rule. // in this case, log error and try the next rule.
const std::regex rule{it.key().asString()}; const std::regex rule{it.key().asString(), std::regex_constants::icase};
if (std::regex_match(value, rule)) { if (std::regex_match(value, rule)) {
res = std::regex_replace(res, rule, it->asString()); res = std::regex_replace(res, rule, it->asString());
} }
@ -29,4 +30,31 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) {
return res; return res;
} }
std::string rewriteStringOnce(const std::string& value, const Json::Value& rules,
bool& matched_any) {
if (!rules.isObject()) {
return value;
}
matched_any = false;
std::string res = value;
for (auto it = rules.begin(); it != rules.end(); ++it) {
if (it.key().isString() && it->isString()) {
try {
const std::regex rule{it.key().asString(), std::regex_constants::icase};
if (std::regex_match(value, rule)) {
matched_any = true;
return std::regex_replace(res, rule, it->asString());
}
} catch (const std::regex_error& e) {
spdlog::error("Invalid rule {}: {}", it.key().asString(), e.what());
}
}
}
return value;
}
} // namespace waybar::util } // namespace waybar::util