diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 6c62ac316..ebf355636 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -6,6 +6,9 @@ #include #include +#include + +#include #include "bar.hpp" #include "modules/sni/item.hpp" @@ -19,6 +22,8 @@ class Host { const std::function&)>&); ~Host(); + void requestReorder(); + private: void busAcquired(const Glib::RefPtr&, Glib::ustring); void nameAppeared(const Glib::RefPtr&, Glib::ustring, @@ -32,6 +37,9 @@ class Host { std::tuple getBusNameAndObjectPath(const std::string); void addRegisteredItem(std::string service); + void reorderItems(); // remove/sort/add + static std::string toLowerAscii(std::string s); + std::vector> items_; const std::string bus_name_; const std::string object_path_; @@ -43,6 +51,13 @@ class Host { const Bar& bar_; const std::function&)> on_add_; const std::function&)> on_remove_; + + bool reorder_pending_{false}; + std::size_t next_seq_{0}; + std::unordered_map seq_; + std::unordered_map order_index_; + std::vector order_list_; // normalized keys in configured order + bool unknown_after_{true}; // configured icons first, unknown icons after (default) }; } // namespace waybar::modules::SNI diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 503ab637e..cd08258cd 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -18,6 +18,8 @@ namespace waybar::modules::SNI { +class Host; + struct ToolTip { Glib::ustring icon_name; Glib::ustring text; @@ -25,7 +27,7 @@ struct ToolTip { class Item : public sigc::trackable { public: - Item(const std::string&, const std::string&, const Json::Value&, const Bar&); + Item(Host& host, const std::string&, const std::string&, const Json::Value&, const Bar&); ~Item(); std::string bus_name; @@ -38,6 +40,7 @@ class Item : public sigc::trackable { std::string category; std::string id; + std::string sort_key; std::string title; std::string icon_name; Glib::RefPtr icon_pixmap; @@ -58,6 +61,7 @@ class Item : public sigc::trackable { bool item_is_menu = true; private: + Host& host_; void onConfigure(GdkEventConfigure* ev); void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index dec5347fa..03bbb4f28 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -42,6 +42,24 @@ Addressed by *tray* default: false ++ Enables this module to consume all left over space dynamically. +*order*: ++ + typeof: array ++ + A list of tray item keys in the desired order. ++ + Items listed here are ordered according to this list. ++ + Items not present in the list are ordered alphabetically by their key. ++ + Matching is performed case-insensitively. ++ + The values must match the key printed in the log line: + + tray: item key='' ... + +*order-unknown*: ++ + typeof: string ++ + default: after ++ + Controls where items not present in *order* are placed. + + *after* – configured items come first, unconfigured items follow. ++ + *before* – unconfigured items come first, configured items follow. + # EXAMPLES ``` @@ -51,7 +69,12 @@ Addressed by *tray* "icons": { "blueman": "bluetooth", "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" - } + }, + "order": [ + "TelegramDesktop", + "signal desktop", + ], + "order-unknown": "after", } ``` diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 54faa16c9..538ec3f51 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -4,6 +4,11 @@ #include "util/scope_guard.hpp" +#include +#include +#include +#include + namespace waybar::modules::SNI { Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, @@ -17,7 +22,38 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, config_(config), bar_(bar), on_add_(on_add), - on_remove_(on_remove) {} + on_remove_(on_remove) { + // Parse "order" list: map key -> index (0..n-1) + order_list_.clear(); + if (config_["order"].isArray()) { + order_list_.reserve(config_["order"].size()); + for (Json::ArrayIndex i = 0; i < config_["order"].size(); ++i) { + const auto& v = config_["order"][i]; + if (!v.isString()) continue; + auto key = toLowerAscii(v.asString()); + order_list_.push_back(key); + order_index_[key] = static_cast(order_list_.size() - 1); + } + } + + // Unknown placement: "after" (default) or "before" + if (config_["order-unknown"].isString()) { + const auto s = toLowerAscii(config_["order-unknown"].asString()); + if (s == "before") unknown_after_ = false; + else if (s == "after") unknown_after_ = true; + } + + if (!order_list_.empty()) { + std::string line = "tray: configured order:"; + for (const auto& k : order_list_) { + line += " ["; + line += k; + line += "]"; + } + line += unknown_after_ ? " unknown=after" : " unknown=before"; + spdlog::info("{}", line); + } +} Host::~Host() { if (bus_name_id_ > 0) { @@ -139,8 +175,142 @@ void Host::addRegisteredItem(std::string service) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item(bus_name, object_path, config_, bar_)); + items_.emplace_back(new Item(*this, bus_name, object_path, config_, bar_)); + seq_[items_.back().get()] = next_seq_++; on_add_(items_.back()); + requestReorder(); + } +} + +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]() { + this->reorderItems(); + this->reorder_pending_ = false; + }); +} + +void Host::reorderItems() { + // 1) Remove all items from UI + for (auto& it : items_) { + on_remove_(it); + } + + // 2) Sort canonical item storage by sort_key (stable) + std::stable_sort(items_.begin(), items_.end(), + [this](const std::unique_ptr& a, const std::unique_ptr& b) { + + const auto& ka_raw = a->sort_key; + const auto& kb_raw = b->sort_key; + const bool a_has = !ka_raw.empty(); + const bool b_has = !kb_raw.empty(); + + // keep empty keys deterministic + if (a_has != b_has) return a_has; + + const auto ka = a_has ? toLowerAscii(ka_raw) : std::string{}; + const auto kb = b_has ? toLowerAscii(kb_raw) : std::string{}; + + const auto ia = a_has ? order_index_.find(ka) : order_index_.end(); + const auto ib = b_has ? order_index_.find(kb) : order_index_.end(); + + const bool a_cfg = (ia != order_index_.end()); + const bool b_cfg = (ib != order_index_.end()); + + if (a_cfg != b_cfg) { + // unknown_after_==true -> configured first + // unknown_after_==false -> unknown first + return unknown_after_ ? a_cfg : !a_cfg; + } + + if (a_cfg && b_cfg) { + if (ia->second != ib->second) return ia->second < ib->second; + // fall through to alpha/seq + } + + if (a_has && b_has && ka != kb) return ka < kb; + + return seq_[a.get()] < seq_[b.get()]; + }); + + // 3) Add all items back to UI in a way that matches Tray's packing direction. + const bool reverse = + config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool(); + + // IMPORTANT: + // Tray::onAdd() uses pack_start when reverse-direction is false. If we add items + // in sorted order with pack_start, the visual order is reversed. Therefore we + // iterate backwards in that case. + if (!reverse) { + for (auto& it : items_) { + on_add_(it); + } + } else { + for (auto it = items_.rbegin(); it != items_.rend(); ++it) { + on_add_(*it); + } + } + { + std::string line; + line.reserve(256); + line += "tray: host sorted order:"; + for (const auto& it : items_) { + const auto& key = it->sort_key; + const auto seq_it = seq_.find(it.get()); + const auto seq = (seq_it != seq_.end()) ? seq_it->second : 999999u; + + line += " ["; + line += key.empty() ? "" : key; + line += "#"; + line += std::to_string(seq); + line += "]"; + } + spdlog::info("{}", line); + } + + if (!order_list_.empty()) { + std::unordered_set seen; + seen.reserve(items_.size()); + + for (const auto& it : items_) { + if (!it->sort_key.empty()) { + seen.insert(toLowerAscii(it->sort_key)); + } + } + + std::string found = "tray: order found:"; + std::string missing = "tray: order missing:"; + + bool any_found = false; + bool any_missing = false; + + for (const auto& k : order_list_) { + if (seen.find(k) != seen.end()) { + found += " ["; + found += k; + found += "]"; + any_found = true; + } else { + missing += " ["; + missing += k; + missing += "]"; + any_missing = true; + } + } + + if (any_found) spdlog::info("{}", found); + if (any_missing) spdlog::info("{}", missing); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index ef2543b50..890daa44c 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -14,6 +14,8 @@ #include "util/format.hpp" #include "util/gtk_icon.hpp" +#include "modules/sni/host.hpp" + template <> struct fmt::formatter : formatter { bool is_printable(const Glib::VariantBase& value) const { @@ -37,8 +39,9 @@ namespace waybar::modules::SNI { static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name; static const unsigned UPDATE_DEBOUNCE_TIME = 10; -Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar) - : bus_name(bn), +Item::Item(Host& host, const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar) + : host_(host), + bus_name(bn), object_path(op), icon_size(16), effective_icon_size(0), @@ -151,6 +154,7 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { category = get_variant(value); } else if (name == "Id") { id = get_variant(value); + sort_key = id; // default /* * HACK: Electron apps seem to have the same ID, but tooltip seems correct, so use that as ID @@ -165,11 +169,18 @@ 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()); + sort_key = tooltip.text.lowercase(); + setCustomIcon(sort_key); } } else { setCustomIcon(id); } + // Single log line that users can copy into later ordering config: + spdlog::info("tray: item key='{}' (id='{}') title='{}' icon='{}' bus='{}' path='{}'", + sort_key, id, title, icon_name, bus_name, object_path); + if (!sort_key.empty()) { + host_.requestReorder(); + } } else if (name == "Title") { title = get_variant(value); if (tooltip.text.empty()) {