diff --git a/include/factory.hpp b/include/factory.hpp index 47ef530..c24e654 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -24,6 +24,7 @@ #ifdef HAVE_HYPRLAND #include "modules/hyprland/backend.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/workspaces.hpp" #endif #if defined(__linux__) && !defined(NO_FILESYSTEM) #include "modules/battery.hpp" diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index f80159b..e21426a 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -18,10 +18,6 @@ private: void onEvent(const std::string&); const Bar& bar_; - IPC ipc; - unsigned app_icon_size_{24}; - bool update_app_icon_{true}; - std::string app_icon_name_; util::JsonParser parser_; std::string lastView; }; diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp new file mode 100644 index 0000000..8d182d5 --- /dev/null +++ b/include/modules/hyprland/workspaces.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/json.hpp" +#include +#include +#include + +namespace waybar::modules::hyprland { + +class Workspaces : public AModule, public sigc::trackable { + public: + Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); + ~Workspaces() = default; + auto update() -> void; + + private: + void onEvent(const std::string&); + bool handleScroll(GdkEventScroll*); + void configOnLaunch(const Json::Value&); + void updateButtons(); + void parseInitHyprlandWorkspaces(); + Gtk::Button& addButton(const std::string&); + std::string getIcon(const std::string&); + std::deque getAllSortedWS(); + + bool isNumber(const std::string&); + + Gtk::Box box_; + const Bar& bar_; + std::deque workspaces; + std::deque persistentWorkspaces; + std::unordered_map buttons_; + std::string focusedWorkspace; + + std::mutex mutex_; +}; + +} // namespace waybar::modules::sway diff --git a/meson.build b/meson.build index 3c32006..39e9cdc 100644 --- a/meson.build +++ b/meson.build @@ -205,6 +205,7 @@ if true add_project_arguments('-DHAVE_HYPRLAND', language: 'cpp') src_files += 'src/modules/hyprland/backend.cpp' src_files += 'src/modules/hyprland/window.cpp' + src_files += 'src/modules/hyprland/workspaces.cpp' endif if libnl.found() and libnlgen.found() diff --git a/src/factory.cpp b/src/factory.cpp index 6df69d5..2c6fe63 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -61,6 +61,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "hyprland/window") { return new waybar::modules::hyprland::Window(id, bar_, config_[name]); } + if (ref == "hyprland/workspaces") { + return new waybar::modules::hyprland::Workspaces(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 index 16deb18..88705d1 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -1,8 +1,9 @@ #include "modules/hyprland/backend.hpp" -#include -#include +#include +#include #include +#include #include #include #include @@ -12,9 +13,13 @@ #include #include -#include +#include +#include +#include -void waybar::modules::hyprland::IPC::startIPC() { +namespace waybar::modules::hyprland { + +void IPC::startIPC() { // will start IPC and relay events to parseIPC std::thread([&]() { @@ -84,7 +89,7 @@ void waybar::modules::hyprland::IPC::startIPC() { }).detach(); } -void waybar::modules::hyprland::IPC::parseIPC(const std::string& ev) { +void IPC::parseIPC(const std::string& ev) { // todo std::string request = ev.substr(0, ev.find_first_of('>')); @@ -95,11 +100,73 @@ void waybar::modules::hyprland::IPC::parseIPC(const std::string& ev) { } } -void waybar::modules::hyprland::IPC::registerForIPC(const std::string& 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); +} + } \ No newline at end of file diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp new file mode 100644 index 0000000..0dad13a --- /dev/null +++ b/src/modules/hyprland/workspaces.cpp @@ -0,0 +1,228 @@ +#include "modules/hyprland/workspaces.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace waybar::modules::hyprland { + +Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) + : AModule(config, "workspaces", id, false, !config["disable-scroll"].asBool()), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + + event_box_.add(box_); + + if (config["enable-bar-scroll"].asBool()) { + auto &window = const_cast(bar_).window; + window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + + modulesReady = true; + + if (!gIPC.get()) { + gIPC = std::make_unique(); + } + + // register for hyprland ipc + gIPC->registerForIPC("createworkspace", [&](const std::string &ev) { this->onEvent(ev); }); + gIPC->registerForIPC("destroyworkspace", [&](const std::string &ev) { this->onEvent(ev); }); + gIPC->registerForIPC("activemon", [&](const std::string &ev) { this->onEvent(ev); }); + gIPC->registerForIPC("workspace", [&](const std::string &ev) { this->onEvent(ev); }); + + // parse cfg stuff + configOnLaunch(config); + + // parse workspaces already existing + parseInitHyprlandWorkspaces(); +} + +void Workspaces::parseInitHyprlandWorkspaces() { + std::istringstream WORKSPACES; + WORKSPACES.str(gIPC->getSocket1Reply("workspaces")); + + std::string line; + while (std::getline(WORKSPACES, line)) { + if (line.find("workspace ID") == 0) { + auto workspaceName = line.substr(line.find_first_of('(') + 1).substr(0, line.find_first_of(')') - line.find_first_of('(') - 1); + workspaces.emplace_back(workspaceName); + } + } +} + +void Workspaces::configOnLaunch(const Json::Value& cfg) { + if (cfg["persistent_workspaces"].isObject()) { + spdlog::info("persistent"); + const Json::Value &persistentWorkspacesJSON = cfg["persistent_workspaces"]; + + for (auto &wn : persistentWorkspacesJSON.getMemberNames()) { + persistentWorkspaces.emplace_back(wn); + spdlog::info("persistent ws {}", wn); + } + } +} + +std::deque Workspaces::getAllSortedWS() { + std::deque result; + for (auto& ws : workspaces) + result.emplace_back(ws); + for (auto& ws : persistentWorkspaces) + result.emplace_back(ws); + + std::sort(result.begin(), result.end(), [&](const std::string& ws1, const std::string& ws2) { + if (isNumber(ws1) && isNumber(ws2)) { + return std::stoi(ws1) < std::stoi(ws2); + } else if (isNumber(ws1)) { + return true; + } else { + return false; + } + }); + + return result; +} + +std::string Workspaces::getIcon(const std::string &name) { + return config_["format-icons"][name].asString(); +} + +auto Workspaces::update() -> void { + updateButtons(); +} + +void Workspaces::updateButtons() { + mutex_.lock(); + + auto ws = getAllSortedWS(); + + for (auto it = ws.begin(); it != ws.end(); ++it) { + auto bit = buttons_.find(*it); + + auto &button = bit == buttons_.end() ? addButton(*it) : bit->second; + + if (focusedWorkspace == *it) { + button.get_style_context()->add_class("focused"); + } else { + button.get_style_context()->remove_class("focused"); + } + + std::string label = *it; + if (config_["format"].isString()) { + auto format = config_["format"].asString(); + label = fmt::format(format, fmt::arg("icon", getIcon(*it)), fmt::arg("name", *it)); + } + + button.set_label(label); + + button.show(); + } + + AModule::update(); + + mutex_.unlock(); +} + +bool Workspaces::isNumber(const std::string& str) { + for (auto &c : str) { + if (!(isdigit(c) != 0 || c == '-')) return false; + } + return true; +} + +Gtk::Button &Workspaces::addButton(const std::string& name) { + auto pair = buttons_.emplace(name, name); + auto &&button = pair.first->second; + box_.pack_start(button, false, false, 0); + button.set_name(name); + button.set_relief(Gtk::RELIEF_NONE); + if (!config_["disable-click"].asBool()) { + button.signal_pressed().connect([&, name]{ + if (isNumber(name)) { + gIPC->getSocket1Reply("dispatch workspace " + name); + spdlog::info("executing {}", "dispatch workspace " + name); + } + else { + gIPC->getSocket1Reply("dispatch workspace name:" + name); + spdlog::info("executing {}", "dispatch workspace name:" + name); + } + }); + } + + return button; +} + +void Workspaces::onEvent(const std::string& ev) { + const auto EVENT = ev.substr(0, ev.find_first_of('>')); + const auto WORKSPACE = ev.substr(ev.find_last_of('>') + 1); + + mutex_.lock(); + + if (EVENT == "activemon") { + focusedWorkspace = WORKSPACE.substr(WORKSPACE.find_first_of(',') + 1); + } else if (EVENT == "workspace") { + focusedWorkspace = WORKSPACE; + } else if (EVENT == "createworkspace") { + workspaces.emplace_back(WORKSPACE); + + // remove the buttons for reorder + buttons_.clear(); + } else { + const auto it = std::remove(workspaces.begin(), workspaces.end(), WORKSPACE); + + if (it != workspaces.end()) + workspaces.erase(it); + + // also remove the buttons + buttons_.clear(); + } + + dp.emit(); + + mutex_.unlock(); +} + +bool Workspaces::handleScroll(GdkEventScroll *e) { + if (gdk_event_get_pointer_emulated((GdkEvent *)e)) { + /** + * Ignore emulated scroll events on window + */ + return false; + } + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + mutex_.lock(); + + if (dir == SCROLL_DIR::UP) { + gIPC->getSocket1Reply("dispatch workspace +1"); + } else if (dir == SCROLL_DIR::DOWN) { + gIPC->getSocket1Reply("dispatch workspace -1"); + } + + mutex_.unlock(); + + return true; +} +};