diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index d76ec74a4..80a8343ca 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include "bar.hpp" #include "modules/sni/item.hpp" @@ -19,6 +21,8 @@ class Host { const std::function&)>&, const std::function&); ~Host(); + void requestReorder(); + private: void busAcquired(const Glib::RefPtr&, Glib::ustring); void nameAppeared(const Glib::RefPtr&, Glib::ustring, @@ -36,6 +40,9 @@ class Host { std::tuple getBusNameAndObjectPath(const std::string); void addRegisteredItem(const std::string& service); + void reorderItems(); + static std::string toLowerAscii(std::string s); + std::vector> items_; const std::string bus_name_; const std::string object_path_; @@ -48,6 +55,11 @@ class Host { const std::function&)> on_add_; const std::function&)> on_remove_; const std::function on_update_; + + std::unordered_map order_left_; + std::unordered_map order_right_; + bool reverse_direction_{false}; + bool reorder_pending_{false}; }; } // namespace waybar::modules::SNI diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 74d54f4cf..588ae038b 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -42,6 +42,7 @@ class Item : public sigc::trackable { Gtk::EventBox event_box; std::string category; std::string id; + std::string sort_key; std::string title; std::string icon_name; diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index dec5347fa..922ceb4b4 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -31,7 +31,28 @@ Addressed by *tray* *reverse-direction*: ++ typeof: bool ++ - Defines if new app icons should be added in a reverse order + default: false ++ + Reverses the alphabetical ordering of icons in the middle section + (icons not pinned via *order-left* or *order-right*). Items pinned + via *order-left* / *order-right* are unaffected and keep their + configured visual position. + +*order-left*: ++ + typeof: array ++ + List of tray item keys that should be pinned to the left (or top, + for vertical bars), in the given order. Keys are matched + case-insensitively against the item's sort key, which is its SNI + *Id* (or the tooltip text, lowercased, for Chrome-based apps). + The key for each registered item is printed on startup as an info + log line of the form _tray: item key='...'_ — run waybar from a + terminal to see it. + +*order-right*: ++ + typeof: array ++ + List of tray item keys that should be pinned to the right (or + bottom, for vertical bars), in the given order. Any item not + listed in *order-left* or *order-right* is placed between the two + groups in alphabetical order. *on-update*: ++ typeof: string ++ @@ -51,7 +72,9 @@ Addressed by *tray* "icons": { "blueman": "bluetooth", "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" - } + }, + "order-left": ["nm-applet", "blueman"], + "order-right": ["TelegramDesktop", "vesktop"] } ``` diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 18eac643b..1d8fd9f42 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -1,7 +1,11 @@ #include "modules/sni/host.hpp" +#include #include +#include +#include + #include "util/scope_guard.hpp" namespace waybar::modules::SNI { @@ -19,7 +23,22 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, bar_(bar), on_add_(on_add), on_remove_(on_remove), - on_update_(on_update) {} + on_update_(on_update) { + auto parse_list = [](const Json::Value& list, + std::unordered_map& out) { + if (!list.isArray()) return; + for (Json::ArrayIndex i = 0; i < list.size(); ++i) { + if (!list[i].isString()) continue; + out.emplace(toLowerAscii(list[i].asString()), static_cast(i)); + } + }; + parse_list(config_["order-left"], order_left_); + parse_list(config_["order-right"], order_right_); + + if (config_["reverse-direction"].isBool()) { + reverse_direction_ = config_["reverse-direction"].asBool(); + } +} Host::~Host() { if (bus_name_id_ > 0) { @@ -130,6 +149,7 @@ void Host::itemReady(Item& item) { [&item](const auto& candidate) { return candidate.get() == &item; }); if (it != items_.end() && (*it)->isReady()) { on_add_(*it); + requestReorder(); } } @@ -184,4 +204,74 @@ void Host::addRegisteredItem(const std::string& service) { } } +std::string Host::toLowerAscii(std::string s) { + for (auto& ch : s) { + ch = static_cast(std::tolower(static_cast(ch))); + } + return s; +} + +void Host::requestReorder() { + if (reorder_pending_) return; + reorder_pending_ = true; + Glib::signal_idle().connect_once([this] { + reorder_pending_ = false; + reorderItems(); + }); +} + +void Host::reorderItems() { + // Classify each item into one of three buckets: configured-left, configured-right, or middle + // (alphabetical). Compute this once so the comparator stays cheap. + enum class Bucket { Left, Middle, Right }; + struct Info { + Bucket bucket; + std::size_t cfg_index; // only meaningful for Left/Right + std::string key; // lowercased sort_key, empty if none + }; + + std::unordered_map info; + info.reserve(items_.size()); + for (const auto& it : items_) { + const auto key = toLowerAscii(it->sort_key); + auto left_it = order_left_.find(key); + if (!key.empty() && left_it != order_left_.end()) { + info[it.get()] = {Bucket::Left, left_it->second, key}; + continue; + } + auto right_it = order_right_.find(key); + if (!key.empty() && right_it != order_right_.end()) { + info[it.get()] = {Bucket::Right, right_it->second, key}; + continue; + } + info[it.get()] = {Bucket::Middle, 0, key}; + } + + std::stable_sort(items_.begin(), items_.end(), + [&info, this](const std::unique_ptr& a, const std::unique_ptr& b) { + const auto& ia = info[a.get()]; + const auto& ib = info[b.get()]; + if (ia.bucket != ib.bucket) { + return static_cast(ia.bucket) < static_cast(ib.bucket); + } + if (ia.bucket == Bucket::Left || ia.bucket == Bucket::Right) { + return ia.cfg_index < ib.cfg_index; + } + // Middle: alphabetical (optionally reversed) + if (ia.key != ib.key) { + return reverse_direction_ ? ia.key > ib.key : ia.key < ib.key; + } + return false; + }); + + // Rebuild UI: remove all ready items, then re-add in sorted order. + for (auto& it : items_) { + if (it->isReady()) on_remove_(it); + } + for (auto& it : items_) { + if (it->isReady()) on_add_(it); + } + on_update_(); +} + } // namespace waybar::modules::SNI diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 2f3680835..418578a92 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -163,6 +163,8 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { category = get_variant(value); } else if (name == "Id") { id = get_variant(value); + const auto old_sort_key = sort_key; + sort_key = id; /* * HACK: Electron apps seem to have the same ID, but tooltip seems correct, so use that as ID @@ -177,11 +179,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { this->proxy_->get_cached_property(value, "ToolTip"); tooltip = get_variant(value); if (!tooltip.text.empty()) { - setCustomIcon(tooltip.text.lowercase()); + // The tooltip often carries a changing status suffix, e.g. + // "Rocket.Chat: 3 unread messages". Strip everything from the first + // ':' onwards so the key stays stable across status changes. + std::string key = tooltip.text.lowercase(); + const auto colon = key.find(':'); + if (colon != std::string::npos) { + key.erase(colon); + } + while (!key.empty() && key.back() == ' ') key.pop_back(); + sort_key = key; + setCustomIcon(sort_key); } } else { setCustomIcon(id); } + if (sort_key != old_sort_key) { + spdlog::info("tray: item key='{}' (use this value in tray.order-left / tray.order-right)", + sort_key); + } } else if (name == "Title") { title = get_variant(value); if (tooltip.text.empty()) { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 114aba786..429f7da39 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -37,11 +37,9 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) void Tray::queueUpdate() { dp.emit(); } void Tray::onAdd(std::unique_ptr& item) { - if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { - box_.pack_end(item->event_box); - } else { - box_.pack_start(item->event_box); - } + // Host controls the final order via reorderItems(); we always pack_start so + // that the order of on_add_ calls maps directly to the visual order. + box_.pack_start(item->event_box); dp.emit(); }