diff --git a/flake.nix b/flake.nix index 24fcae9f..15e70b63 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,7 @@ inherit (nixpkgs) lib; genSystems = lib.genAttrs [ "x86_64-linux" + "aarch64-linux" ]; pkgsFor = genSystems (system: diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index a6024a84..c9992695 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -22,6 +22,7 @@ class Custom : public ALabel { private: void delayWorker(); void continuousWorker(); + void waitingWorker(); void parseOutputRaw(); void parseOutputJson(); void handleEvent(); diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 14b9ba0e..1a01758e 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -2,9 +2,13 @@ #include #include +#include +#include +#include #include #include +#include #include #include @@ -13,13 +17,15 @@ #include "modules/hyprland/backend.hpp" #include "util/enum.hpp" +using WindowAddress = std::string; namespace waybar::modules::hyprland { class Workspaces; class Workspace { 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& icons_map); Gtk::Button& button() { return button_; }; @@ -40,6 +46,16 @@ 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); + 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 on_window_closed(WindowAddress& addr); void update(const std::string& format, const std::string& icon); @@ -56,6 +72,8 @@ class Workspace { bool is_urgent_ = false; bool is_visible_ = false; + std::map window_map_; + Gtk::Button button_; Gtk::Box content_; Gtk::Label label_; @@ -74,16 +92,25 @@ 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_window_separator() { return format_window_separator_; } + private: void onEvent(const std::string&) override; void update_window_count(); + void initialize_window_maps(); 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 set_urgent_workspace(std::string windowaddress); void parse_config(const Json::Value& config); 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 show_special_ = false; bool active_only_ = false; @@ -103,6 +130,10 @@ class Workspaces : public AModule, public EventHandler { std::string format_; std::map icons_map_; + Json::Value window_rewrite_rules_; + std::map regex_cache_; + std::string format_window_separator_; + std::string window_rewrite_default_; bool with_icon_; uint64_t monitor_id_; std::string active_workspace_name_; diff --git a/include/util/rewrite_string.hpp b/include/util/rewrite_string.hpp index 2ab39ad8..3352a47a 100644 --- a/include/util/rewrite_string.hpp +++ b/include/util/rewrite_string.hpp @@ -5,4 +5,6 @@ namespace waybar::util { 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 diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 861d5f1f..3d8c05d1 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -58,6 +58,12 @@ class SleeperThread { bool isRunning() const { return do_run_; } + auto sleep() { + std::unique_lock lk(mutex_); + CancellationGuard cancel_lock; + return condvar_.wait(lk); + } + auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 52bc9f64..b13cee6e 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -100,6 +100,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: true ++ Option to disable tooltip on hover. +*bat-compatibility*: ++ + typeof: bool ++ + default: false ++ + Option to enable battery compatibility if not detected. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index aeb419f9..a038e355 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -34,7 +34,8 @@ Addressed by *custom/* typeof: integer ++ The interval (in seconds) in which the information gets polled. ++ Use *once* if you want to execute the module only on startup. ++ - You can update it manually with a signal. If no *interval* is defined, it is assumed that the out script loops it self. + You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops it self. ++ + If a *signal* is defined then the script will run once on startup and will will only update with a signal. *restart-interval*: ++ typeof: integer ++ @@ -45,7 +46,8 @@ Addressed by *custom/* *signal*: ++ typeof: integer ++ The signal number used to update the module. ++ - The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + If no interval is defined then a signal will be the only way to update the module. *format*: ++ typeof: string ++ diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 99d21804..02be176d 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -21,6 +21,21 @@ Addressed by *hyprland/workspaces* typeof: array ++ 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*: ++ typeof: bool ++ 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 - *#workspaces* diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 27871048..b3ca85fc 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -244,7 +244,9 @@ void waybar::modules::Backlight::upsert_device(ForwardIt first, ForwardIt last, check_nn(name); const char *actual_brightness_attr = - strncmp(name, "amdgpu_bl", 9) == 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 *max = udev_device_get_sysattr_value(dev, "max_brightness"); diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 3ced890d..70268c8a 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -100,9 +100,11 @@ void waybar::modules::Battery::refreshBatteries() { } auto dir_name = node.path().filename(); auto bat_defined = config_["bat"].isString(); + bool bat_compatibility = config_["bat-compatibility"].asBool(); if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && - fs::exists(node.path() / "uevent") && fs::exists(node.path() / "status") && + fs::exists(node.path() / "uevent") && + (fs::exists(node.path() / "status") || bat_compatibility) && fs::exists(node.path() / "type")) { std::string type; std::ifstream(node.path() / "type") >> type; @@ -252,7 +254,13 @@ const std::tuple waybar::modules::Battery::g for (auto const& item : batteries_) { auto bat = item.first; std::string _status; - std::getline(std::ifstream(bat / "status"), _status); + + /* Check for adapter status if battery is not available */ + if (!std::ifstream(bat / "status")) { + std::getline(std::ifstream(adapter_ / "status"), _status); + } else { + std::getline(std::ifstream(bat / "status"), _status); + } // Some battery will report current and charge in μA/μAh. // Scale these by the voltage to get μW/μWh. diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 5a246aff..4889b7a3 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -11,7 +11,9 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, fp_(nullptr), pid_(-1) { dp.emit(); - if (interval_.count() > 0) { + if (!config_["signal"].empty() && config_["interval"].empty()) { + waitingWorker(); + } else if (interval_.count() > 0) { delayWorker(); } else if (config_["exec"].isString()) { continuousWorker(); @@ -92,6 +94,26 @@ void waybar::modules::Custom::continuousWorker() { }; } +void waybar::modules::Custom::waitingWorker() { + thread_ = [this] { + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString()); + } + dp.emit(); + } + thread_.sleep(); + }; +} + void waybar::modules::Custom::refresh(int sig) { if (sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index f38b5050..277a83ea 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -1,13 +1,17 @@ #include "modules/hyprland/workspaces.hpp" +#include #include #include #include #include #include +#include #include +#include "util/rewrite_string.hpp" + namespace waybar::modules::hyprland { 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."); } } + + 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 { @@ -134,6 +148,14 @@ auto Workspaces::update() -> void { AModule::update(); } +bool isDoubleSpecial(std::string &workspace_name) { + // Hyprland's IPC sometimes reports the creation of workspaces strangely named + // `special:special:`. This function checks for that and is used + // to avoid creating (and then removing) such workspaces. + // See hyprwm/Hyprland#3424 for more info. + return workspace_name.find("special:special:") != std::string::npos; +} + void Workspaces::onEvent(const std::string &ev) { std::lock_guard lock(mutex_); std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); @@ -143,15 +165,16 @@ void Workspaces::onEvent(const std::string &ev) { active_workspace_name_ = payload; } else if (eventName == "destroyworkspace") { - workspaces_to_remove_.push_back(payload); - + if (!isDoubleSpecial(payload)) { + workspaces_to_remove_.push_back(payload); + } } else if (eventName == "createworkspace") { const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); for (Json::Value workspace_json : workspaces_json) { std::string name = workspace_json["name"].asString(); if (name == payload && (all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && - (show_special() || !name.starts_with("special"))) { + (show_special() || !name.starts_with("special")) && !isDoubleSpecial(payload)) { workspaces_to_create_.push_back(workspace_json); break; } @@ -175,8 +198,15 @@ void Workspaces::onEvent(const std::string &ev) { } else { workspaces_to_remove_.push_back(workspace); } - } else if (eventName == "openwindow" || eventName == "closewindow" || eventName == "movewindow") { + } else if (eventName == "openwindow") { 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") { set_urgent_workspace(payload); } else if (eventName == "renameworkspace") { @@ -197,6 +227,67 @@ void Workspaces::onEvent(const std::string &ev) { 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() { const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); for (auto &workspace : workspaces_) { @@ -215,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 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 auto workspace = std::find_if( workspaces_.begin(), workspaces_.end(), [&](std::unique_ptr const &x) { - auto name = value["name"].asString(); + auto name = workspace_data["name"].asString(); return x->is_persistent() && ((name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name()); }); if (workspace != workspaces_.end()) { // replace workspace, but keep persistent flag workspaces_.erase(workspace); - value["persistent"] = true; + workspace_data["persistent"] = true; } // create new workspace - workspaces_.emplace_back(std::make_unique(value, *this)); + workspaces_.emplace_back(std::make_unique(workspace_data, *this, clients_data)); Gtk::Button &new_workspace_button = workspaces_.back()->button(); box_.pack_start(new_workspace_button, false, false); sort_workspaces(); @@ -238,8 +393,10 @@ void Workspaces::create_workspace(Json::Value &value) { } void Workspaces::remove_workspace(std::string name) { - auto workspace = std::find_if(workspaces_.begin(), workspaces_.end(), - [&](std::unique_ptr &x) { return x->name() == name; }); + auto workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), [&](std::unique_ptr &x) { + return (name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name(); + }); if (workspace == workspaces_.end()) { // happens when a workspace on another monitor is destroyed @@ -351,10 +508,12 @@ void Workspaces::init() { create_persistent_workspaces(); const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); + const Json::Value clients_json = gIPC->getSocket1JsonReply("clients"); + for (Json::Value workspace_json : workspaces_json) { if ((all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && (!workspace_json["name"].asString().starts_with("special") || show_special())) { - create_workspace(workspace_json); + create_workspace(workspace_json, clients_json); } } @@ -371,7 +530,8 @@ Workspaces::~Workspaces() { std::lock_guard 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), id_(workspace_data["id"].asInt()), name_(workspace_data["name"].asString()), @@ -396,6 +556,8 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma button_.set_relief(Gtk::RELIEF_NONE); content_.set_center_widget(label_); button_.add(content_); + + initialize_window_map(clients_data); } void add_or_remove_class(const Glib::RefPtr &context, bool condition, @@ -429,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_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()), - fmt::arg("name", name()), fmt::arg("icon", icon))); + fmt::arg("name", name()), fmt::arg("icon", icon), + fmt::arg("windows", windows))); } void Workspaces::sort_workspaces() { @@ -549,7 +725,8 @@ std::string &Workspace::select_icon(std::map &icons_ma if (default_icon_it != icons_map.end()) { return default_icon_it->second; } - return icons_map[""]; + + return name_; } auto Workspace::handle_clicked(GdkEventButton *bt) -> bool { @@ -589,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 diff --git a/src/util/rewrite_string.cpp b/src/util/rewrite_string.cpp index 40c71e99..475f8a3d 100644 --- a/src/util/rewrite_string.cpp +++ b/src/util/rewrite_string.cpp @@ -1,5 +1,6 @@ #include "util/rewrite_string.hpp" +#include #include #include @@ -17,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) { try { // malformated regexes will cause an exception. // 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)) { 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; } + +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