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
15 changes: 15 additions & 0 deletions include/modules/sni/host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include <json/json.h>

#include <tuple>
#include <vector>

#include <unordered_map>

#include "bar.hpp"
#include "modules/sni/item.hpp"
Expand All @@ -19,6 +22,8 @@ class Host {
const std::function<void(std::unique_ptr<Item>&)>&);
~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 @@ -32,6 +37,9 @@ class Host {
std::tuple<std::string, std::string> getBusNameAndObjectPath(const std::string);
void addRegisteredItem(std::string service);

void reorderItems(); // remove/sort/add
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 @@ -43,6 +51,13 @@ class Host {
const Bar& bar_;
const std::function<void(std::unique_ptr<Item>&)> on_add_;
const std::function<void(std::unique_ptr<Item>&)> on_remove_;

bool reorder_pending_{false};
std::size_t next_seq_{0};
std::unordered_map<Item*, std::size_t> seq_;
std::unordered_map<std::string, std::size_t> order_index_;
std::vector<std::string> order_list_; // normalized keys in configured order
bool unknown_after_{true}; // configured icons first, unknown icons after (default)
};

} // namespace waybar::modules::SNI
6 changes: 5 additions & 1 deletion include/modules/sni/item.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@

namespace waybar::modules::SNI {

class Host;

struct ToolTip {
Glib::ustring icon_name;
Glib::ustring text;
};

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;
Expand All @@ -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<Gdk::Pixbuf> icon_pixmap;
Expand All @@ -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<Gio::AsyncResult>& result);
void setProperty(const Glib::ustring& name, Glib::VariantBase& value);
Expand Down
25 changes: 24 additions & 1 deletion man/waybar-tray.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -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='<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

```
Expand All @@ -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",
}

```
Expand Down
174 changes: 172 additions & 2 deletions src/modules/sni/host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

#include "util/scope_guard.hpp"

#include <algorithm>
#include <cctype>
#include <glibmm/main.h>
#include <unordered_set>

namespace waybar::modules::SNI {

Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
Expand All @@ -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<std::size_t>(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) {
Expand Down Expand Up @@ -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<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]() {
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<Item>& a, const std::unique_ptr<Item>& 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() ? "<empty>" : key;
line += "#";
line += std::to_string(seq);
line += "]";
}
spdlog::info("{}", line);
}

if (!order_list_.empty()) {
std::unordered_set<std::string> 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);
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/modules/sni/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "util/format.hpp"
#include "util/gtk_icon.hpp"

#include "modules/sni/host.hpp"

template <>
struct fmt::formatter<Glib::VariantBase> : formatter<std::string> {
bool is_printable(const Glib::VariantBase& value) const {
Expand All @@ -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),
Expand Down Expand Up @@ -151,6 +154,7 @@ 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);
sort_key = id; // default

/*
* HACK: Electron apps seem to have the same ID, but tooltip seems correct, so use that as ID
Expand All @@ -165,11 +169,18 @@ 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());
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<std::string>(value);
if (tooltip.text.empty()) {
Expand Down