Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions include/modules/sni/host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <json/json.h>

#include <tuple>
#include <unordered_map>
#include <vector>

#include "bar.hpp"
#include "modules/sni/item.hpp"
Expand All @@ -19,6 +21,8 @@ class Host {
const std::function<void(std::unique_ptr<Item>&)>&, const std::function<void()>&);
~Host();

void requestReorder();

private:
void busAcquired(const Glib::RefPtr<Gio::DBus::Connection>&, Glib::ustring);
void nameAppeared(const Glib::RefPtr<Gio::DBus::Connection>&, Glib::ustring,
Expand All @@ -36,6 +40,9 @@ class Host {
std::tuple<std::string, std::string> getBusNameAndObjectPath(const std::string);
void addRegisteredItem(const std::string& service);

void reorderItems();
static std::string toLowerAscii(std::string s);

std::vector<std::unique_ptr<Item>> items_;
const std::string bus_name_;
const std::string object_path_;
Expand All @@ -48,6 +55,11 @@ class Host {
const std::function<void(std::unique_ptr<Item>&)> on_add_;
const std::function<void(std::unique_ptr<Item>&)> on_remove_;
const std::function<void()> on_update_;

std::unordered_map<std::string, std::size_t> order_left_;
std::unordered_map<std::string, std::size_t> order_right_;
bool reverse_direction_{false};
bool reorder_pending_{false};
};

} // namespace waybar::modules::SNI
1 change: 1 addition & 0 deletions include/modules/sni/item.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
27 changes: 25 additions & 2 deletions man/waybar-tray.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -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 ++
Expand All @@ -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"]
}

```
Expand Down
92 changes: 91 additions & 1 deletion src/modules/sni/host.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include "modules/sni/host.hpp"

#include <glibmm/main.h>
#include <spdlog/spdlog.h>

#include <algorithm>
#include <cctype>

#include "util/scope_guard.hpp"

namespace waybar::modules::SNI {
Expand All @@ -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<std::string, std::size_t>& 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<std::size_t>(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) {
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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<char>(std::tolower(static_cast<unsigned char>(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<Item*, Info> 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<Item>& a, const std::unique_ptr<Item>& b) {
const auto& ia = info[a.get()];
const auto& ib = info[b.get()];
if (ia.bucket != ib.bucket) {
return static_cast<int>(ia.bucket) < static_cast<int>(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
18 changes: 17 additions & 1 deletion src/modules/sni/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
category = get_variant<std::string>(value);
} else if (name == "Id") {
id = get_variant<std::string>(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
Expand All @@ -177,11 +179,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
this->proxy_->get_cached_property(value, "ToolTip");
tooltip = get_variant<ToolTip>(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<std::string>(value);
if (tooltip.text.empty()) {
Expand Down
8 changes: 3 additions & 5 deletions src/modules/sni/tray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>& 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();
}

Expand Down