diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index b32dc41d..353edb7a 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -18,11 +18,15 @@ class Workspace { int id() const { return id_; }; std::string name() const { return name_; }; std::string output() const { return output_; }; - int active() const { return active_; }; + bool active() const { return active_; }; bool is_special() const { return is_special_; }; + bool is_persistent() const { return is_persistent_; }; + bool is_empty() const { return windows_ == 0; }; auto handle_clicked(GdkEventButton* bt) -> bool; void set_active(bool value = true) { active_ = value; }; + void set_persistent(bool value = true) { is_persistent_ = value; }; + void set_windows(uint value) { windows_ = value; }; void update(const std::string& format, const std::string& icon); @@ -30,9 +34,10 @@ class Workspace { int id_; std::string name_; std::string output_; - int windows_; - bool active_; - bool is_special_; + uint windows_; + bool active_ = false; + bool is_special_ = false; + bool is_persistent_ = false; Gtk::Button button_; Gtk::Box content_; @@ -53,6 +58,7 @@ class Workspaces : public AModule, public EventHandler { private: void onEvent(const std::string&) override; + void update_window_count(); void sort_workspaces(); void create_workspace(Json::Value& value); void remove_workspace(std::string name); @@ -60,10 +66,16 @@ class Workspaces : public AModule, public EventHandler { bool all_outputs_ = false; bool show_special_ = false; + void fill_persistent_workspaces(); + void create_persistent_workspaces(); + std::vector persistent_workspaces_to_create_; + bool persistent_created_ = false; + std::string format_; std::map icons_map_; bool with_icon_; - std::string active_workspace_name; + uint64_t monitor_id_; + std::string active_workspace_name_; std::vector> workspaces_; std::vector workspaces_to_create_; std::vector workspaces_to_remove_; diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8e5d1148..787d812d 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -55,6 +55,9 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value gIPC->registerForIPC("destroyworkspace", this); gIPC->registerForIPC("focusedmon", this); gIPC->registerForIPC("moveworkspace", this); + gIPC->registerForIPC("openwindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); } auto Workspaces::update() -> void { @@ -71,7 +74,7 @@ auto Workspaces::update() -> void { workspaces_to_create_.clear(); for (auto &workspace : workspaces_) { - workspace->set_active(workspace->name() == active_workspace_name); + workspace->set_active(workspace->name() == active_workspace_name_); std::string &workspace_icon = icons_map_[""]; if (with_icon_) { workspace_icon = workspace->select_icon(icons_map_); @@ -87,7 +90,7 @@ void Workspaces::onEvent(const std::string &ev) { std::string payload = ev.substr(eventName.size() + 2); if (eventName == "workspace") { - active_workspace_name = payload; + active_workspace_name_ = payload; } else if (eventName == "destroyworkspace") { workspaces_to_remove_.push_back(payload); @@ -97,14 +100,14 @@ void Workspaces::onEvent(const std::string &ev) { for (Json::Value workspace_json : workspaces_json) { if (workspace_json["name"].asString() == payload && (all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && - (show_special() || workspace_json["name"].asString().find("special") != 0)) { + (show_special() || workspace_json["name"].asString().starts_with("special"))) { workspaces_to_create_.push_back(workspace_json); break; } } } else if (eventName == "focusedmon") { - active_workspace_name = payload.substr(payload.find(',') + 1); + active_workspace_name_ = payload.substr(payload.find(',') + 1); } else if (eventName == "moveworkspace" && !all_outputs()) { std::string workspace = payload.substr(0, payload.find(',')); @@ -121,13 +124,47 @@ void Workspaces::onEvent(const std::string &ev) { } else { workspaces_to_remove_.push_back(workspace); } + } else if (eventName == "openwindow" || eventName == "closewindow" || eventName == "movewindow") { + update_window_count(); } dp.emit(); } +void Workspaces::update_window_count() { + const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : workspaces_) { + auto workspace_json = std::find_if( + workspaces_json.begin(), workspaces_json.end(), + [&](Json::Value const &x) { return x["name"].asString() == workspace->name(); }); + if (workspace_json != workspaces_json.end()) { + try { + workspace->set_windows((*workspace_json)["windows"].asUInt()); + } catch (const std::exception &e) { + spdlog::error("Failed to update window count: {}", e.what()); + } + } else { + workspace->set_windows(0); + } + } +} + void Workspaces::create_workspace(Json::Value &value) { - workspaces_.push_back(std::make_unique(value)); + // 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(); + 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; + } + + // create new workspace + workspaces_.emplace_back(std::make_unique(value)); Gtk::Button &new_workspace_button = workspaces_.back()->button(); box_.pack_start(new_workspace_button, false, false); sort_workspaces(); @@ -139,6 +176,12 @@ void Workspaces::remove_workspace(std::string name) { [&](std::unique_ptr &x) { return x->name() == name; }); if (workspace == workspaces_.end()) { + // happens when a workspace on another monitor is destroyed + return; + } + + if ((*workspace)->is_persistent()) { + // don't remove persistent workspaces, create_workspace will take care of replacement return; } @@ -146,16 +189,89 @@ void Workspaces::remove_workspace(std::string name) { workspaces_.erase(workspace); } +void Workspaces::fill_persistent_workspaces() { + if (config_["persistent_workspaces"].isObject() && !all_outputs()) { + const Json::Value persistent_workspaces = config_["persistent_workspaces"]; + const std::vector keys = persistent_workspaces.getMemberNames(); + + for (const std::string &key : keys) { + const Json::Value &value = persistent_workspaces[key]; + if (value.isInt()) { + // value is a number => create that many workspaces for this monitor + // only add if either: + // 1. key is "*" and this monitor is not already defined in the config + // 2. key is the current monitor name + if ((key == "*" && std::find(keys.begin(), keys.end(), bar_.output->name) == keys.end()) || + key == bar_.output->name) { + int amount = value.asInt(); + spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, + bar_.output->name); + for (int i = 0; i < amount; i++) { + persistent_workspaces_to_create_.emplace_back( + std::to_string(monitor_id_ * amount + i + 1)); + } + } + + } else if (value.isArray() && !value.empty()) { + // value is an array => key is a workspace name + // values are monitor names this workspace should be shown on + for (const Json::Value &monitor : value) { + if (monitor.isString() && monitor.asString() == bar_.output->name) { + persistent_workspaces_to_create_.emplace_back(key); + break; + } + } + } + } + } +} + +void Workspaces::create_persistent_workspaces() { + for (const std::string &workspace_name : persistent_workspaces_to_create_) { + Json::Value new_workspace; + try { + // numbered persistent workspaces get the name as ID + new_workspace["id"] = workspace_name == "special" ? -99 : std::stoi(workspace_name); + } catch (const std::exception &e) { + // named persistent workspaces start with ID=0 + new_workspace["id"] = 0; + } + new_workspace["name"] = workspace_name; + new_workspace["monitor"] = bar_.output->name; + new_workspace["windows"] = 0; + new_workspace["persistent"] = true; + + create_workspace(new_workspace); + } +} + void Workspaces::init() { - active_workspace_name = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + active_workspace_name_ = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + + // get monitor ID from name (used by persistent workspaces) + monitor_id_ = 0; + auto monitors = gIPC->getSocket1JsonReply("monitors"); + auto current_monitor = std::find_if( + monitors.begin(), monitors.end(), + [this](const Json::Value &m) { return m["name"].asString() == bar_.output->name; }); + if (current_monitor == monitors.end()) { + spdlog::error("Monitor '{}' does not have an ID? Using 0", bar_.output->name); + } else { + monitor_id_ = (*current_monitor)["id"].asInt(); + } + + fill_persistent_workspaces(); + create_persistent_workspaces(); const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); for (Json::Value workspace_json : workspaces_json) { if ((all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && - (workspace_json["name"].asString().find("special") != 0 || show_special())) + (workspace_json["name"].asString().starts_with("special") || show_special())) create_workspace(workspace_json); } + update_window_count(); + sort_workspaces(); dp.emit(); @@ -171,16 +287,19 @@ Workspace::Workspace(const Json::Value &workspace_data) : id_(workspace_data["id"].asInt()), name_(workspace_data["name"].asString()), output_(workspace_data["monitor"].asString()), // TODO:allow using monitor desc - windows_(workspace_data["id"].asInt()), - active_(true), - is_special_(false) { - if (name_.find("name:") == 0) { + windows_(workspace_data["windows"].asInt()), + active_(true) { + if (name_.starts_with("name:")) { name_ = name_.substr(5); - } else if (name_.find("special") == 0) { - name_ = id_ == -99 ? name_ : name_.substr(13); + } else if (name_.starts_with("special")) { + name_ = id_ == -99 ? name_ : name_.substr(8); is_special_ = true; } + if (workspace_data.isMember("persistent")) { + is_persistent_ = workspace_data["persistent"].asBool(); + } + button_.add_events(Gdk::BUTTON_PRESS_MASK); button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked), false); @@ -202,28 +321,45 @@ void add_or_remove_class(const Glib::RefPtr &context, bool co void Workspace::update(const std::string &format, const std::string &icon) { auto style_context = button_.get_style_context(); add_or_remove_class(style_context, active(), "active"); + add_or_remove_class(style_context, is_special(), "special"); + add_or_remove_class(style_context, is_empty(), "persistent"); - label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), - fmt::arg("name", name()), fmt::arg("icon", icon))); + label_.set_markup( + fmt::format(fmt::runtime(format), fmt::arg("name", name()), fmt::arg("icon", icon))); } void Workspaces::sort_workspaces() { std::sort(workspaces_.begin(), workspaces_.end(), [](std::unique_ptr &a, std::unique_ptr &b) { - // normal -> named -> special -> named special + // normal -> named persistent -> named -> special -> named special + + // both normal (includes numbered persistent) => sort by ID if (a->id() > 0 && b->id() > 0) { return a->id() < b->id(); } - if (a->id() < 0 && b->id() < 0) { - if ((a->is_special()) ^ (a->is_special())) { - return a->id() > b->id(); - } - return a->id() < b->id(); + + // one normal, one special => normal first + if ((a->is_special()) ^ (b->is_special())) { + return b->is_special(); } + + // only one normal, one named if ((a->id() > 0) ^ (b->id() > 0)) { - return a->id() > b->id(); + return a->id() > 0; } - return false; + + // both special + if (a->is_special() && b->is_special()) { + // if one is -99 => put it last + if (a->id() == -99 || b->id() == -99) { + return b->id() == -99; + } + // both are 0 (not yet named persistents) / both are named specials (-98 <= ID <=-1) + return a->name() < b->name(); + } + + // sort non-special named workspaces by name (ID <= -1377) + return a->name() < b->name(); }); for (size_t i = 0; i < workspaces_.size(); ++i) { @@ -239,11 +375,25 @@ std::string &Workspace::select_icon(std::map &icons_ma } } + if (is_special()) { + auto special_icon_it = icons_map.find("special"); + if (special_icon_it != icons_map.end()) { + return special_icon_it->second; + } + } + auto named_icon_it = icons_map.find(std::to_string(id())); if (named_icon_it != icons_map.end()) { return named_icon_it->second; } + if (is_persistent()) { + auto persistent_icon_it = icons_map.find("persistent"); + if (persistent_icon_it != icons_map.end()) { + return persistent_icon_it->second; + } + } + auto default_icon_it = icons_map.find("default"); if (default_icon_it != icons_map.end()) { return default_icon_it->second; @@ -253,12 +403,12 @@ std::string &Workspace::select_icon(std::map &icons_ma auto Workspace::handle_clicked(GdkEventButton *bt) -> bool { try { - if (id() > 0) { // normal + if (id() > 0) { // normal or numbered persistent gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); - } else if (!is_special()) { // named normal - gIPC->getSocket1Reply("dispatch workspace name" + name()); + } else if (!is_special()) { // named + gIPC->getSocket1Reply("dispatch workspace name:" + name()); } else if (id() != -99) { // named special - gIPC->getSocket1Reply("dispatch togglespecialworkspace name" + name()); + gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); } else { // special gIPC->getSocket1Reply("dispatch togglespecialworkspace"); }