diff --git a/README.md b/README.md index fefde2c..addee57 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ #### Current features - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) +- Hyprland (Focused window name) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery diff --git a/include/factory.hpp b/include/factory.hpp index 1e79bd7..47ef530 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -21,6 +21,10 @@ #include "modules/river/tags.hpp" #include "modules/river/window.hpp" #endif +#ifdef HAVE_HYPRLAND +#include "modules/hyprland/backend.hpp" +#include "modules/hyprland/window.hpp" +#endif #if defined(__linux__) && !defined(NO_FILESYSTEM) #include "modules/battery.hpp" #endif diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp new file mode 100644 index 0000000..b9d1c99 --- /dev/null +++ b/include/modules/hyprland/backend.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace waybar::modules::hyprland { +class IPC { +public: + IPC() { startIPC(); } + + void registerForIPC(const std::string&, std::function); + + std::string getSocket1Reply(const std::string& rq); + +private: + + void startIPC(); + void parseIPC(const std::string&); + + std::mutex callbackMutex; + std::deque>> callbacks; +}; + +inline std::unique_ptr gIPC; +inline bool modulesReady = false; +}; + diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp new file mode 100644 index 0000000..e844655 --- /dev/null +++ b/include/modules/hyprland/window.hpp @@ -0,0 +1,28 @@ +#include + +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/json.hpp" + +namespace waybar::modules::hyprland { + +class Window : public waybar::ALabel { +public: + Window(const std::string&, const waybar::Bar&, const Json::Value&); + ~Window() = default; + + auto update() -> void; + +private: + void onEvent(const std::string&); + + std::mutex mutex_; + const Bar& bar_; + util::JsonParser parser_; + std::string lastView; +}; + +} \ No newline at end of file diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index d80cc25..c99ba7f 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -21,7 +21,7 @@ class Window : public AIconLabel, public sigc::trackable { private: void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - std::tuple getFocusedNode( + std::tuple getFocusedNode( const Json::Value& nodes, std::string& output); void getTree(); std::string rewriteTitle(const std::string& title); @@ -35,6 +35,7 @@ class Window : public AIconLabel, public sigc::trackable { std::string app_class_; std::string old_app_id_; std::size_t app_nb_; + std::string shell_; unsigned app_icon_size_{24}; bool update_app_icon_{true}; std::string app_icon_name_; diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd new file mode 100644 index 0000000..4be137d --- /dev/null +++ b/man/waybar-hyprland-window.5.scd @@ -0,0 +1,31 @@ +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: {} ++ + The format, how information should be displayed. On {} the current window title is displayed. + + +# EXAMPLES + +``` +"hyprland/window": { + "format": "{}" +} +``` + +# STYLE + +- *#window* diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index e475cea..6e5ebdb 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -14,8 +14,8 @@ Addressed by *sway/window* *format*: ++ typeof: string ++ - default: {} ++ - The format, how information should be displayed. On {} data gets inserted. + default: {title} ++ + The format, how information should be displayed. *rotate*: ++ typeof: integer ++ @@ -80,6 +80,15 @@ Addressed by *sway/window* default: 24 ++ Option to change the size of the application icon. +# FORMAT REPLACEMENTS + +*{title}*: The title of the focused window. + +*{app_id}*: The app_id of the focused window. + +*{shell}*: The shell of the focused window. It's 'xwayland' when the window is +running through xwayland, otherwise it's 'xdg-shell'. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are diff --git a/meson.build b/meson.build index 441ccfb..3c32006 100644 --- a/meson.build +++ b/meson.build @@ -201,6 +201,12 @@ if true src_files += 'src/modules/river/window.cpp' endif +if true + add_project_arguments('-DHAVE_HYPRLAND', language: 'cpp') + src_files += 'src/modules/hyprland/backend.cpp' + src_files += 'src/modules/hyprland/window.cpp' +endif + if libnl.found() and libnlgen.found() add_project_arguments('-DHAVE_LIBNL', language: 'cpp') src_files += 'src/modules/network.cpp' diff --git a/src/factory.cpp b/src/factory.cpp index 841465f..6df69d5 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -56,6 +56,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "river/window") { return new waybar::modules::river::Window(id, bar_, config_[name]); } +#endif +#ifdef HAVE_HYPRLAND + if (ref == "hyprland/window") { + return new waybar::modules::hyprland::Window(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp new file mode 100644 index 0000000..ae73a25 --- /dev/null +++ b/src/modules/hyprland/backend.cpp @@ -0,0 +1,171 @@ +#include "modules/hyprland/backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace waybar::modules::hyprland { + +void IPC::startIPC() { + // will start IPC and relay events to parseIPC + + std::thread([&]() { + // check for hyprland + const char* HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + + if (!HIS) { + spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); + return; + } + + if (!modulesReady) return; + + spdlog::info("Hyprland IPC starting"); + + struct sockaddr_un addr; + int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (socketfd == -1) { + spdlog::error("Hyprland IPC: socketfd failed"); + return; + } + + addr.sun_family = AF_UNIX; + + // socket path + std::string socketPath = "/tmp/hypr/" + std::string(HIS) + "/.socket2.sock"; + + strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); + + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + int l = sizeof(struct sockaddr_un); + + if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { + spdlog::error("Hyprland IPC: Unable to connect?"); + return; + } + + auto file = fdopen(socketfd, "r"); + + while (1) { + // read + + char buffer[1024]; // Hyprland socket2 events are max 1024 bytes + auto recievedCharPtr = fgets(buffer, 1024, file); + + if (!recievedCharPtr) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + + callbackMutex.lock(); + + std::string messageRecieved(buffer); + + messageRecieved = messageRecieved.substr(0, messageRecieved.find_first_of('\n')); + + spdlog::debug("hyprland IPC received {}", messageRecieved); + + parseIPC(messageRecieved); + + callbackMutex.unlock(); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }).detach(); +} + +void IPC::parseIPC(const std::string& ev) { + // todo + std::string request = ev.substr(0, ev.find_first_of('>')); + + for (auto& [eventname, handler] : callbacks) { + if (eventname == request) { + handler(ev); + } + } +} + +void IPC::registerForIPC(const std::string& ev, std::function fn) { + callbackMutex.lock(); + + callbacks.emplace_back(std::make_pair(ev, fn)); + + callbackMutex.unlock(); +} + +std::string IPC::getSocket1Reply(const std::string& rq) { + // basically hyprctl + + const auto SERVERSOCKET = socket(AF_UNIX, SOCK_STREAM, 0); + + if (SERVERSOCKET < 0) { + spdlog::error("Hyprland IPC: Couldn't open a socket (1)"); + return ""; + } + + const auto SERVER = gethostbyname("localhost"); + + if (!SERVER) { + spdlog::error("Hyprland IPC: Couldn't get host (2)"); + return ""; + } + + // get the instance signature + auto instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + + if (!instanceSig) { + spdlog::error("Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)"); + return ""; + } + + std::string instanceSigStr = std::string(instanceSig); + + sockaddr_un serverAddress = {0}; + serverAddress.sun_family = AF_UNIX; + + std::string socketPath = "/tmp/hypr/" + instanceSigStr + "/.socket.sock"; + + strcpy(serverAddress.sun_path, socketPath.c_str()); + + if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) { + spdlog::error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); + return ""; + } + + auto sizeWritten = write(SERVERSOCKET, rq.c_str(), rq.length()); + + if (sizeWritten < 0) { + spdlog::error("Hyprland IPC: Couldn't write (4)"); + return ""; + } + + char buffer[8192] = {0}; + + sizeWritten = read(SERVERSOCKET, buffer, 8192); + + if (sizeWritten < 0) { + spdlog::error("Hyprland IPC: Couldn't read (5)"); + return ""; + } + + close(SERVERSOCKET); + + return std::string(buffer); +} + +} // namespace waybar::modules::hyprland \ No newline at end of file diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp new file mode 100644 index 0000000..c292333 --- /dev/null +++ b/src/modules/hyprland/window.cpp @@ -0,0 +1,63 @@ +#include "modules/hyprland/window.hpp" + +#include + +#include "modules/hyprland/backend.hpp" + +namespace waybar::modules::hyprland { + +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "window", id, "{}", 0, true), bar_(bar) { + modulesReady = true; + + if (!gIPC.get()) { + gIPC = std::make_unique(); + } + + label_.hide(); + ALabel::update(); + + // register for hyprland ipc + gIPC->registerForIPC("activewindow", [&](const std::string& ev) { this->onEvent(ev); }); +} + +auto Window::update() -> void { + // fix ampersands + std::lock_guard lg(mutex_); + + if (!format_.empty()) { + label_.show(); + label_.set_markup(fmt::format(format_, lastView)); + } else { + label_.hide(); + } + + ALabel::update(); +} + +void Window::onEvent(const std::string& ev) { + std::lock_guard lg(mutex_); + auto windowName = ev.substr(ev.find_first_of(',') + 1).substr(0, 256); + + auto replaceAll = [](std::string str, const std::string& from, + const std::string& to) -> std::string { + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + return str; + }; + + windowName = replaceAll(windowName, "&", "&"); + + if (windowName == lastView) return; + + lastView = windowName; + + spdlog::debug("hyprland window onevent with {}", windowName); + + dp.emit(); +} + +} // namespace waybar::modules::hyprland \ No newline at end of file diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9b878f9..0f58343 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -166,18 +166,17 @@ void waybar::modules::MPD::setLabel() { if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt()); try { - label_.set_markup( - fmt::format(format, fmt::arg("artist", Glib::Markup::escape_text(artist).raw()), - fmt::arg("albumArtist", Glib::Markup::escape_text(album_artist).raw()), - fmt::arg("album", Glib::Markup::escape_text(album).raw()), - fmt::arg("title", Glib::Markup::escape_text(title).raw()), - fmt::arg("date", Glib::Markup::escape_text(date).raw()), - fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), - fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), - fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), - fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), - fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon), - fmt::arg("filename", filename))); + label_.set_markup(fmt::format( + format, fmt::arg("artist", Glib::Markup::escape_text(artist).raw()), + fmt::arg("albumArtist", Glib::Markup::escape_text(album_artist).raw()), + fmt::arg("album", Glib::Markup::escape_text(album).raw()), + fmt::arg("title", Glib::Markup::escape_text(title).raw()), + fmt::arg("date", Glib::Markup::escape_text(date).raw()), fmt::arg("volume", volume), + fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), + fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), + fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), + fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), + fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename))); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error: {}", e.what()); } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 773cfda..7cd0045 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -314,9 +314,7 @@ void Item::updateImage() { } Glib::RefPtr Item::getIconPixbuf() { - if (!icon_name.empty()) { - try { std::ifstream temp(icon_name); if (temp.is_open()) { @@ -347,7 +345,8 @@ Glib::RefPtr Item::getIconPixbuf() { if (icon_name.empty()) { spdlog::error("Item '{}': No icon name or pixmap given.", id); } else { - spdlog::error("Item '{}': Could not find an icon named '{}' and no pixmap given.", id, icon_name); + spdlog::error("Item '{}': Could not find an icon named '{}' and no pixmap given.", id, + icon_name); } return getIconByName("image-missing", getScaledIconSize()); diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index dbff90a..3d63743 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -15,7 +15,7 @@ namespace waybar::modules::sway { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) - : AIconLabel(config, "window", id, "{}", 0, true), bar_(bar), windowId_(-1) { + : AIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), windowId_(-1) { // Icon size if (config_["icon-size"].isUInt()) { app_icon_size_ = config["icon-size"].asUInt(); @@ -44,7 +44,7 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); auto output = payload["output"].isString() ? payload["output"].asString() : ""; - std::tie(app_nb_, windowId_, window_, app_id_, app_class_) = + std::tie(app_nb_, windowId_, window_, app_id_, app_class_, shell_) = getFocusedNode(payload["nodes"], output); updateAppIconName(); dp.emit(); @@ -175,8 +175,8 @@ auto Window::update() -> void { bar_.window.get_style_context()->remove_class("solo"); bar_.window.get_style_context()->remove_class("empty"); } - label_.set_markup( - fmt::format(format_, fmt::arg("title", rewriteTitle(window_)), fmt::arg("app_id", app_id_))); + label_.set_markup(fmt::format(format_, fmt::arg("title", rewriteTitle(window_)), + fmt::arg("app_id", app_id_), fmt::arg("shell", shell_))); if (tooltipEnabled()) { label_.set_tooltip_text(window_); } @@ -206,7 +206,7 @@ int leafNodesInWorkspace(const Json::Value& node) { return sum; } -std::tuple gfnWithWorkspace( +std::tuple gfnWithWorkspace( const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_, Json::Value& parentWorkspace) { for (auto const& node : nodes) { @@ -222,31 +222,34 @@ std::tuple gfnWithWorks const auto app_class = node["window_properties"]["class"].isString() ? node["window_properties"]["class"].asString() : ""; + + const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; + int nb = node.size(); if (parentWorkspace != 0) nb = leafNodesInWorkspace(parentWorkspace); - return {nb, node["id"].asInt(), Glib::Markup::escape_text(node["name"].asString()), app_id, - app_class}; + return {nb, node["id"].asInt(), Glib::Markup::escape_text(node["name"].asString()), + app_id, app_class, shell}; } } // iterate if (node["type"] == "workspace") parentWorkspace = node; - auto [nb, id, name, app_id, app_class] = + auto [nb, id, name, app_id, app_class, shell] = gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace); if (id > -1 && !name.empty()) { - return {nb, id, name, app_id, app_class}; + return {nb, id, name, app_id, app_class, shell}; } // Search for floating node - std::tie(nb, id, name, app_id, app_class) = + std::tie(nb, id, name, app_id, app_class, shell) = gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace); if (id > -1 && !name.empty()) { - return {nb, id, name, app_id, app_class}; + return {nb, id, name, app_id, app_class, shell}; } } - return {0, -1, "", "", ""}; + return {0, -1, "", "", "", ""}; } -std::tuple Window::getFocusedNode( - const Json::Value& nodes, std::string& output) { +std::tuple +Window::getFocusedNode(const Json::Value& nodes, std::string& output) { Json::Value placeholder = 0; return gfnWithWorkspace(nodes, output, config_, bar_, placeholder); }