diff --git a/include/modules/hyprland/windowcount.hpp b/include/modules/hyprland/windowcount.hpp new file mode 100644 index 00000000..3972c66a --- /dev/null +++ b/include/modules/hyprland/windowcount.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/json.hpp" + +namespace waybar::modules::hyprland { + +class WindowCount : public waybar::AAppIconLabel, public EventHandler { + public: + WindowCount(const std::string&, const waybar::Bar&, const Json::Value&); + ~WindowCount() override; + + auto update() -> void override; + + private: + struct Workspace { + int id; + int windows; + std::string last_window; + std::string last_window_title; + + static auto parse(const Json::Value& value) -> Workspace; + }; + + struct WindowCountData { + bool floating; + int monitor = -1; + std::string class_name; + std::string initial_class_name; + std::string title; + std::string initial_title; + bool fullscreen; + bool grouped; + + static auto parse(const Json::Value&) -> WindowCountData; + }; + + static auto getActiveWorkspace(const std::string&) -> Workspace; + static auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string& ev) override; + void queryActiveWorkspace(); + void setClass(const std::string&, bool enable); + + bool separateOutputs_; + std::mutex mutex_; + const Bar& bar_; + util::JsonParser parser_; + WindowCountData windowData_; + Workspace workspace_; + std::string soloClass_; + std::string lastSoloClass_; + bool solo_; + bool allFloating_; + bool swallowing_; + bool fullscreen_; + bool focused_; +}; + +} // namespace waybar::modules::hyprland diff --git a/man/waybar-hyprland-windowcount.5.scd b/man/waybar-hyprland-windowcount.5.scd new file mode 100644 index 00000000..4e9c5d18 --- /dev/null +++ b/man/waybar-hyprland-windowcount.5.scd @@ -0,0 +1,89 @@ +waybar-hyprland-window(5) + +# NAME + +waybar - hyprland window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in Hyprland. + +# CONFIGURATION + +Addressed by *hyprland/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. On {} the current window title is displayed. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite window title. See *rewrite rules*. + +*separate-outputs*: ++ + typeof: bool ++ + Show the active window of the monitor the bar belongs to, instead of the focused window. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +# FORMAT REPLACEMENTS +See the output of "hyprctl clients" for examples + +*{title}*: The current title of the focused window. + +*{initialTitle}*: The initial title of the focused window. + +*{class}*: The current class of the focused window. + +*{initialClass}*: The initial class of the focused window. + +# REWRITE RULES + +*rewrite* is an object where keys are regular expressions and values are +rewrite rules if the expression matches. Rules may contain references to +captures of the expression. + +Regular expression and replacement follow ECMA-script rules. + +If no expression matches, the title is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"hyprland/window": { + "format": "{}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` + +# STYLE + +- *#window* +- *window#waybar.empty #window* When no windows are in the workspace + +The following classes are applied to the entire Waybar rather than just the +window widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.solo* When one tiled window is visible in the workspace + (floating windows may be present) +- *window#waybar.* Where ** is the *class* (e.g. *chromium*) of + the solo tiled window in the workspace (use *hyprctl clients* to see classes) +- *window#waybar.floating* When there are only floating windows in the workspace +- *window#waybar.fullscreen* When there is a fullscreen window in the workspace; + useful with Hyprland's *fullscreen, 1* mode +- *window#waybar.swallowing* When there is a swallowed window in the workspace diff --git a/meson.build b/meson.build index 8daa6c9c..1a7a94df 100644 --- a/meson.build +++ b/meson.build @@ -306,6 +306,7 @@ if true 'src/modules/hyprland/language.cpp', 'src/modules/hyprland/submap.cpp', 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/windowcount.cpp', 'src/modules/hyprland/workspace.cpp', 'src/modules/hyprland/workspaces.cpp', 'src/modules/hyprland/windowcreationpayload.cpp', diff --git a/src/factory.cpp b/src/factory.cpp index ca10ef95..2344160f 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -34,6 +34,7 @@ #include "modules/hyprland/language.hpp" #include "modules/hyprland/submap.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/windowcount.hpp" #include "modules/hyprland/workspaces.hpp" #endif #if defined(__FreeBSD__) || defined(__linux__) @@ -196,6 +197,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "hyprland/window") { return new waybar::modules::hyprland::Window(id, bar_, config_[name]); } + if (ref == "hyprland/windowcount") { + return new waybar::modules::hyprland::WindowCount(id, bar_, config_[name]); + } if (ref == "hyprland/language") { return new waybar::modules::hyprland::Language(id, bar_, config_[name]); } diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp new file mode 100644 index 00000000..52c25978 --- /dev/null +++ b/src/modules/hyprland/windowcount.cpp @@ -0,0 +1,233 @@ +#include "modules/hyprland/windowcount.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "modules/hyprland/backend.hpp" +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::hyprland { + +WindowCount::WindowCount(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { + modulesReady = true; + separateOutputs_ = config["separate-outputs"].asBool(); + + if (!gIPC) { + gIPC = std::make_unique(); + } + + queryActiveWorkspace(); + update(); + dp.emit(); + + // register for hyprland ipc + gIPC->registerForIPC("activewindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); + gIPC->registerForIPC("changefloatingmode", this); + gIPC->registerForIPC("fullscreen", this); +} + +WindowCount::~WindowCount() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +auto WindowCount::update() -> void { + // fix ampersands + std::lock_guard lg(mutex_); + + std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title); + std::string windowAddress = workspace_.last_window; + + windowData_.title = windowName; + + if (!format_.empty()) { + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name)), + config_["rewrite"])); + } else { + label_.hide(); + } + + if (focused_) { + image_.show(); + } else { + image_.hide(); + } + + setClass("empty", workspace_.windows == 0); + setClass("solo", solo_); + setClass("floating", allFloating_); + setClass("swallowing", swallowing_); + setClass("fullscreen", fullscreen_); + + if (!lastSoloClass_.empty() && soloClass_ != lastSoloClass_) { + if (bar_.window.get_style_context()->has_class(lastSoloClass_)) { + bar_.window.get_style_context()->remove_class(lastSoloClass_); + spdlog::trace("Removing solo class: {}", lastSoloClass_); + } + } + + if (!soloClass_.empty() && soloClass_ != lastSoloClass_) { + bar_.window.get_style_context()->add_class(soloClass_); + spdlog::trace("Adding solo class: {}", soloClass_); + } + lastSoloClass_ = soloClass_; + + AAppIconLabel::update(); +} + +auto WindowCount::getActiveWorkspace() -> Workspace { + const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); + + if (workspace.isObject()) { + return Workspace::parse(workspace); + } + + return {}; +} + +auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspace { + const auto monitors = gIPC->getSocket1JsonReply("monitors"); + if (monitors.isArray()) { + auto monitor = std::find_if(monitors.begin(), monitors.end(), [&](Json::Value monitor) { + return monitor["name"] == monitorName; + }); + if (monitor == std::end(monitors)) { + spdlog::warn("Monitor not found: {}", monitorName); + return Workspace{-1, 0, "", ""}; + } + const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + + const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); + if (workspaces.isArray()) { + auto workspace = std::find_if(workspaces.begin(), workspaces.end(), + [&](Json::Value workspace) { return workspace["id"] == id; }); + if (workspace == std::end(workspaces)) { + spdlog::warn("No workspace with id {}", id); + return Workspace{-1, 0, "", ""}; + } + return Workspace::parse(*workspace); + }; + }; + + return {}; +} + +auto WindowCount::Workspace::parse(const Json::Value& value) -> WindowCount::Workspace { + return Workspace{ + value["id"].asInt(), + value["windows"].asInt(), + value["lastwindow"].asString(), + value["lastwindowtitle"].asString(), + }; +} + +auto WindowCount::WindowCountData::parse(const Json::Value& value) -> WindowCount::WindowCountData { + return WindowCountData{value["floating"].asBool(), value["monitor"].asInt(), + value["class"].asString(), value["initialClass"].asString(), + value["title"].asString(), value["initialTitle"].asString(), + value["fullscreen"].asBool(), !value["grouped"].empty()}; +} + +void WindowCount::queryActiveWorkspace() { + std::lock_guard lg(mutex_); + + if (separateOutputs_) { + workspace_ = getActiveWorkspace(this->bar_.output->name); + } else { + workspace_ = getActiveWorkspace(); + } + + focused_ = true; + if (workspace_.windows > 0) { + const auto clients = gIPC->getSocket1JsonReply("clients"); + if (clients.isArray()) { + auto activeWindowCount = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { + return window["address"] == workspace_.last_window; + }); + + if (activeWindowCount == std::end(clients)) { + focused_ = false; + return; + } + + windowData_ = WindowCountData::parse(*activeWindowCount); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindowCounts; + std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspaceWindowCounts), + [&](Json::Value window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); + }); + swallowing_ = + std::any_of(workspaceWindowCounts.begin(), workspaceWindowCounts.end(), [&](Json::Value window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); + std::vector visibleWindowCounts; + std::copy_if(workspaceWindowCounts.begin(), workspaceWindowCounts.end(), + std::back_inserter(visibleWindowCounts), + [&](Json::Value window) { return !window["hidden"].asBool(); }); + solo_ = 1 == std::count_if(visibleWindowCounts.begin(), visibleWindowCounts.end(), + [&](Json::Value window) { return !window["floating"].asBool(); }); + allFloating_ = std::all_of(visibleWindowCounts.begin(), visibleWindowCounts.end(), + [&](Json::Value window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; + + // Fullscreen windows look like they are solo + if (fullscreen_) { + solo_ = true; + } + + // Grouped windows have a tab bar and therefore don't look fullscreen or solo + if (windowData_.grouped) { + fullscreen_ = false; + solo_ = false; + } + + if (solo_) { + soloClass_ = windowData_.class_name; + } else { + soloClass_ = ""; + } + }; + } else { + focused_ = false; + windowData_ = WindowCountData{}; + allFloating_ = false; + swallowing_ = false; + fullscreen_ = false; + solo_ = false; + soloClass_ = ""; + } +} + +void WindowCount::onEvent(const std::string& ev) { + queryActiveWorkspace(); + + dp.emit(); +} + +void WindowCount::setClass(const std::string& classname, bool enable) { + if (enable) { + if (!bar_.window.get_style_context()->has_class(classname)) { + bar_.window.get_style_context()->add_class(classname); + } + } else { + bar_.window.get_style_context()->remove_class(classname); + } +} + +} // namespace waybar::modules::hyprland