From 56b7dd71b30c4b2a72f34eab43e5ed2d116e4181 Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 18:17:30 -0500 Subject: [PATCH 1/7] chore: add .gitignore file to exclude build directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build From ce822a260ae49d5422f910d5ba544685f48f17fe Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 18:25:24 -0500 Subject: [PATCH 2/7] fix!: resolve errors introduced in sdbus-cpp 2.x sdbus-cpp 2.x (shipped in arch since April 2024) requires `createProxy` arguments to be strongly typed. See for more information: https://github.com/Kistler-Group/sdbus-cpp/blob/master/docs/using-sdbus-c++.md#migrating-to-sdbus-c-v2 BREAKING CHANGE: depends on sdbus-cpp >= 2.0.0 --- PKGBUILD | 2 +- src/StatusNotifierItem.cpp | 2 +- src/StatusNotifierWatcher.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 57dba64..e2fa019 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -5,7 +5,7 @@ arch=('x86_64') depends=('systemd-libs' 'fmt' - 'sdbus-cpp') + 'sdbus-cpp>=2.0.0') makedepends=('git' 'cmake') diff --git a/src/StatusNotifierItem.cpp b/src/StatusNotifierItem.cpp index c5d2696..a4842c5 100644 --- a/src/StatusNotifierItem.cpp +++ b/src/StatusNotifierItem.cpp @@ -22,7 +22,7 @@ StatusNotifierItem::~StatusNotifierItem() = default; std::expected StatusNotifierItem::connect() { return safelyExec( [this] -> std::expected { - if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), std::string( destination_ ), "/StatusNotifierItem" ) ) + if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), sdbus::ServiceName{ std::string( destination_ ) }, sdbus::ObjectPath{ "/StatusNotifierItem" } ) ) return {}; else return makeError( ErrorKind::ConnectionError ); diff --git a/src/StatusNotifierWatcher.cpp b/src/StatusNotifierWatcher.cpp index f99fa08..e613759 100644 --- a/src/StatusNotifierWatcher.cpp +++ b/src/StatusNotifierWatcher.cpp @@ -16,7 +16,7 @@ StatusNotifierWatcher::~StatusNotifierWatcher() = default; std::expected StatusNotifierWatcher::connect() { return safelyExec( [this] -> std::expected { - if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher" ) ) + if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), sdbus::ServiceName{ "org.kde.StatusNotifierWatcher" }, sdbus::ObjectPath{ "/StatusNotifierWatcher" } ) ) return {}; else return makeError( ErrorKind::ConnectionError ); From 1789c1da3fdc59fbb68eb3c0135c895d49c3ae01 Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 18:26:47 -0500 Subject: [PATCH 3/7] docs: fix build instructions --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc16c5d..d4733c9 100644 --- a/README.md +++ b/README.md @@ -56,20 +56,23 @@ This tool depends on `systemd-libs`, `sdbus-cpp` and `fmt`. There also submodule dependencies: `cxxopts` and `magic_enum` but you should generally not worry about them since they are included as submodules into this repo. ## Installation -### Archlinux +### Arch Linux Check out the repo and use [makepkg](https://wiki.archlinux.org/title/makepkg): + ```shell -git checkout https://github.com/andrewerf/tray-control.git +git clone https://github.com/andrewerf/tray-control.git cd tray-control makepkg -si ``` I hope I'll add this tool to AUR in the near future. ### Other linux -Checkout the repo and build with cmake + make: +Checkout the repo, pull submodules, and build with cmake + make: ```shell -git checkout https://github.com/andrewerf/tray-control.git +git clone https://github.com/andrewerf/tray-control.git cd tray-control +git submodule init +git submodule update mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release .. From 26cf822ec1580eb9ee15f9e64d5015b3d989f85a Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 19:12:30 -0500 Subject: [PATCH 4/7] feat: support ayatana indicators and perhaps other items not using /StatusNotifierItem as an object path --- src/StatusNotifierItem.cpp | 5 +++-- src/StatusNotifierItem.h | 5 +++-- src/StatusNotifierWatcher.cpp | 28 ++++++++++++++++++++++++---- src/StatusNotifierWatcher.h | 3 ++- src/Types.h | 15 +++++++++++++++ src/tray-activate.cpp | 1 + src/tray-show.cpp | 1 + 7 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 src/Types.h diff --git a/src/StatusNotifierItem.cpp b/src/StatusNotifierItem.cpp index a4842c5..f0ab6d2 100644 --- a/src/StatusNotifierItem.cpp +++ b/src/StatusNotifierItem.cpp @@ -6,13 +6,14 @@ #include #include "DBusUtils.h" +#include "Types.h" template static constexpr auto safelyGetSNIProperty = std::bind( safelyGetProperty, std::placeholders::_1, "org.kde.StatusNotifierItem", std::placeholders::_2 ); -StatusNotifierItem::StatusNotifierItem( std::string_view destination ): +StatusNotifierItem::StatusNotifierItem( SniAddress destination ): destination_( destination ) {} @@ -22,7 +23,7 @@ StatusNotifierItem::~StatusNotifierItem() = default; std::expected StatusNotifierItem::connect() { return safelyExec( [this] -> std::expected { - if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), sdbus::ServiceName{ std::string( destination_ ) }, sdbus::ObjectPath{ "/StatusNotifierItem" } ) ) + if ( proxy_ = sdbus::createProxy( sdbus::createSessionBusConnection(), sdbus::ServiceName{ destination_.serviceName }, sdbus::ObjectPath{ destination_.objectPath } ) ) return {}; else return makeError( ErrorKind::ConnectionError ); diff --git a/src/StatusNotifierItem.h b/src/StatusNotifierItem.h index a8fafae..2e04dea 100644 --- a/src/StatusNotifierItem.h +++ b/src/StatusNotifierItem.h @@ -8,6 +8,7 @@ #include #include "Errors.h" +#include "Types.h" namespace sdbus { @@ -27,7 +28,7 @@ class StatusNotifierItem std::string description; }; - StatusNotifierItem( std::string_view destination ); + StatusNotifierItem( SniAddress destination ); ~StatusNotifierItem(); std::expected connect(); @@ -85,5 +86,5 @@ class StatusNotifierItem private: std::unique_ptr proxy_; - std::string_view destination_; + SniAddress destination_; }; \ No newline at end of file diff --git a/src/StatusNotifierWatcher.cpp b/src/StatusNotifierWatcher.cpp index e613759..119a1a9 100644 --- a/src/StatusNotifierWatcher.cpp +++ b/src/StatusNotifierWatcher.cpp @@ -7,6 +7,7 @@ #include "Utils.h" #include "DBusUtils.h" +#include "Types.h" StatusNotifierWatcher::StatusNotifierWatcher() = default; @@ -23,17 +24,36 @@ std::expected StatusNotifierWatcher::connect() } ); } -std::expected, Error> StatusNotifierWatcher::getRegisteredStatusNotifierItemAddresses() +std::expected, Error> StatusNotifierWatcher::getRegisteredStatusNotifierItemAddresses() { return safelyGetProperty>( proxy_, "org.kde.StatusNotifierWatcher", "RegisteredStatusNotifierItems" ) >> - [] ( std::vector&& addrs ) { + [] ( std::vector&& addrs ) + -> std::expected, Error> + { + std::vector out; + out.reserve(addrs.size()); + for ( auto& addr : addrs ) { - addr = addr.substr( 0, addr.find( '/' ) ); + /* + StatusNotifierItems created by Ayatana are not stored at the normal path of /StatusNotifierItem, + so we'll split the path based on the first occurrence of `/` to separate the service name of the + process exposing a status item and its object path. + */ + auto slashIndex = addr.find( '/' ); + std::string serviceName = addr.substr( 0, slashIndex ); + + // leading `/` in object path must be retained + std::string objectPath = addr.substr( slashIndex ); + + out.push_back(SniAddress{ + std::move( serviceName ), + std::move( objectPath ), + }); } - return addrs; + return out; }; } diff --git a/src/StatusNotifierWatcher.h b/src/StatusNotifierWatcher.h index c1e1da6..504bb71 100644 --- a/src/StatusNotifierWatcher.h +++ b/src/StatusNotifierWatcher.h @@ -9,6 +9,7 @@ #include #include "Errors.h" +#include "Types.h" namespace sdbus { @@ -22,7 +23,7 @@ class StatusNotifierWatcher ~StatusNotifierWatcher(); std::expected connect(); - std::expected, Error> getRegisteredStatusNotifierItemAddresses(); + std::expected, Error> getRegisteredStatusNotifierItemAddresses(); private: std::unique_ptr proxy_; diff --git a/src/Types.h b/src/Types.h new file mode 100644 index 0000000..6a1f78b --- /dev/null +++ b/src/Types.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +struct SniAddress { + std::string serviceName; + std::string objectPath; +}; + + +// Overload the concatenation operator so that we can easily reference SNI addresses in log/error messages +inline std::ostream& operator<<(std::ostream& os, const SniAddress& addr) +{ + return os << addr.serviceName << addr.objectPath; +} \ No newline at end of file diff --git a/src/tray-activate.cpp b/src/tray-activate.cpp index 283fe46..87d554e 100644 --- a/src/tray-activate.cpp +++ b/src/tray-activate.cpp @@ -8,6 +8,7 @@ #include "StatusNotifierWatcher.h" #include "StatusNotifierItem.h" #include "Utils.h" +#include "Types.h" void exitWithMsg( std::string_view msg, int code = -1 ) diff --git a/src/tray-show.cpp b/src/tray-show.cpp index ac80d19..417bb2e 100644 --- a/src/tray-show.cpp +++ b/src/tray-show.cpp @@ -8,6 +8,7 @@ #include "StatusNotifierWatcher.h" #include "StatusNotifierItem.h" #include "Utils.h" +#include "Types.h" #define WRAP_FUNC_TO_LAMBDA( func ) \ From 7963d461442c3d75a99006a1d3eeeedd079dbcdc Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 20:13:21 -0500 Subject: [PATCH 5/7] feat(tray-show): add -j, --json arg for displaying JSON output of all available data --- src/StatusNotifierItemJson.h | 46 ++++++++++++++++++++++++++++++++++++ src/Types.h | 13 ++++++++++ src/tray-show.cpp | 38 ++++++++++++++++++++--------- 3 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 src/StatusNotifierItemJson.h diff --git a/src/StatusNotifierItemJson.h b/src/StatusNotifierItemJson.h new file mode 100644 index 0000000..1ceedd3 --- /dev/null +++ b/src/StatusNotifierItemJson.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include "StatusNotifierItem.h" + +using json = nlohmann::json; + +/* + Simple helper for nulling a key in output JSON if there's an error when fetching it. +*/ +template +inline void json_set_or_null(json& j, std::string_view key, const std::expected& value) +{ + j[key] = value ? json(*value) : json(nullptr); +} + +/* + nlohmann/json finds `to_json` methods using ADL, so we define some helper methods + here to enable automatic JSON serialization of a few types. +*/ +inline void to_json(json& j, const StatusNotifierItem::ToolTip& t) +{ + j = nlohmann::json{ + {"icon_name", t.iconName}, + {"title", t.title}, + {"description", t.description}, + }; +} + +inline void to_json(json& j, StatusNotifierItem& t) +{ + j = json::object(); + + json_set_or_null(j, "category", t.getCategory()); + json_set_or_null(j, "id", t.getId()); + json_set_or_null(j, "title", t.getTitle()); + json_set_or_null(j, "status", t.getStatus()); + json_set_or_null(j, "windowId", t.getWindowId()); + json_set_or_null(j, "iconName", t.getIconName()); + json_set_or_null(j, "overlayIconName", t.getOverlayIconName()); + json_set_or_null(j, "attentionIconName", t.getAttentionIconName()); + json_set_or_null(j, "attentionMovieName", t.getAttentionMovieName()); + + // TODO: re-add after getToolTip is implemented + // json_set_or_null(j, "toolTip", t.getToolTip()); +} diff --git a/src/Types.h b/src/Types.h index 6a1f78b..abd6d93 100644 --- a/src/Types.h +++ b/src/Types.h @@ -1,6 +1,10 @@ #pragma once #include +#include + +using json = nlohmann::json; + struct SniAddress { std::string serviceName; @@ -12,4 +16,13 @@ struct SniAddress { inline std::ostream& operator<<(std::ostream& os, const SniAddress& addr) { return os << addr.serviceName << addr.objectPath; +} + +// Implement JSON serialization for SniAddress +inline void to_json(json& j, const SniAddress& t) +{ + j = nlohmann::json{ + {"serviceName", t.serviceName}, + {"objectPath", t.objectPath}, + }; } \ No newline at end of file diff --git a/src/tray-show.cpp b/src/tray-show.cpp index 417bb2e..0d67de6 100644 --- a/src/tray-show.cpp +++ b/src/tray-show.cpp @@ -4,19 +4,22 @@ #include #include #include +#include #include "StatusNotifierWatcher.h" #include "StatusNotifierItem.h" +#include "StatusNotifierItemJson.h" #include "Utils.h" #include "Types.h" +using json = nlohmann::json; + #define WRAP_FUNC_TO_LAMBDA( func ) \ [] ( Args&& ...args ) -> decltype( auto ) \ { return func( std::forward( args )... ); } - void exitWithMsg( std::string_view msg, int code = -1 ) { std::cerr << msg << std::endl; @@ -29,6 +32,7 @@ int main( int argc, char** argv ) cxxopts::Options optionsDecl( "tray-show", "Show items in the system tray" ); optionsDecl.add_options() ( "h,help", "Print help and exit", cxxopts::value()->default_value("false") ) + ( "j,json", "Print all item properties in JSON format", cxxopts::value()->default_value("false") ) ( "v,verbose", "Show full info about each item", cxxopts::value()->default_value("false") ); const auto options = optionsDecl.parse( argc, argv ); @@ -38,6 +42,7 @@ int main( int argc, char** argv ) return 0; } const bool verboseOutput = options["verbose"].as(); + const bool jsonOutput = options["json"].as(); StatusNotifierWatcher watcher; if ( auto connRes = watcher.connect(); !connRes ) @@ -50,23 +55,34 @@ int main( int argc, char** argv ) StatusNotifierItem item( addr ); if ( auto connRes = item.connect() ) { - item.getCategory() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Category: %s\n" ); - item.getTitle() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Title: %s\n" ); - - if ( verboseOutput ) + if ( jsonOutput ) { + // Serialize, print item data with 4-space indent + json j, jAddr; + to_json( j, item ); + to_json( jAddr, addr ); + j["address"] = jAddr; + std::cout << j.dump( 4 ) << '\n'; + } + else { - item.getId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Id: %s\n" ); - item.getStatus() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Status: %s\n" ); - item.getWindowId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "WindowId: %d\n" ); - item.getIconName() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "IconName: %s\n" ); + item.getCategory() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Category: %s\n" ); + item.getTitle() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Title: %s\n" ); + + if ( verboseOutput ) + { + item.getId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Id: %s\n" ); + item.getStatus() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Status: %s\n" ); + item.getWindowId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "WindowId: %d\n" ); + item.getIconName() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "IconName: %s\n" ); + } + + std::cout << '\n'; } } else { std::cerr << "Could not connect to the StatusNotifierItem on address: " << addr << " with error: " << connRes.error().show() << '\n'; } - - std::cout << '\n'; } } } \ No newline at end of file From e976b965e87d3af592a76ca50ef860bb73f80eb5 Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 20:54:25 -0500 Subject: [PATCH 6/7] feat(tray-show): display JSON output in a single array structure instead of multiple objects, refactor existing logging for plaintext output --- src/tray-show.cpp | 64 ++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/tray-show.cpp b/src/tray-show.cpp index 0d67de6..c6dacea 100644 --- a/src/tray-show.cpp +++ b/src/tray-show.cpp @@ -15,11 +15,6 @@ using json = nlohmann::json; -#define WRAP_FUNC_TO_LAMBDA( func ) \ - [] ( Args&& ...args ) -> decltype( auto ) \ - { return func( std::forward( args )... ); } - - void exitWithMsg( std::string_view msg, int code = -1 ) { std::cerr << msg << std::endl; @@ -50,39 +45,56 @@ int main( int argc, char** argv ) if ( auto maybeAddrs = watcher.getRegisteredStatusNotifierItemAddresses() ) { + // collect all item data as JSON first, store in a vector + std::vector itemData; + itemData.reserve( maybeAddrs.value().size() ); + for ( const auto& addr : maybeAddrs.value() ) { StatusNotifierItem item( addr ); if ( auto connRes = item.connect() ) { - if ( jsonOutput ) { - // Serialize, print item data with 4-space indent - json j, jAddr; - to_json( j, item ); - to_json( jAddr, addr ); - j["address"] = jAddr; - std::cout << j.dump( 4 ) << '\n'; - } - else - { - item.getCategory() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Category: %s\n" ); - item.getTitle() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Title: %s\n" ); + // Serialize, print item data with 4-space indent + json j; + to_json( j, item ); - if ( verboseOutput ) - { - item.getId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Id: %s\n" ); - item.getStatus() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "Status: %s\n" ); - item.getWindowId() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "WindowId: %d\n" ); - item.getIconName() >> std::bind_front( WRAP_FUNC_TO_LAMBDA( fmt::printf ), "IconName: %s\n" ); - } + // Add the D-Bus address we used to fetch the information + json jAddr; + to_json( jAddr, addr ); + j["address"] = jAddr; - std::cout << '\n'; - } + // Push item data onto the vector + itemData.push_back( j ); } else { std::cerr << "Could not connect to the StatusNotifierItem on address: " << addr << " with error: " << connRes.error().show() << '\n'; } } + + // Print collected data in chosen output format + if ( jsonOutput ) { + // Serialize, print item data with 4-space indent + json j = itemData; + std::cout << j.dump( 4 ) << '\n'; + } + else + { + for ( const json& item : itemData ) + { + fmt::printf( "Category: %s\n", item.find("category")->get_ref() ); + fmt::printf( "Title: %s\n", item.find("title")->get_ref() ); + + if ( verboseOutput ) + { + fmt::printf( "Id: %s\n", item.find("id")->get_ref() ); + fmt::printf( "Status: %s\n", item.find("status")->get_ref() ); + fmt::printf( "WindowId: %s\n", item.find("windowId")->get() ); + fmt::printf( "IconName: %s\n", item.find("iconName")->get_ref() ); + } + + std::cout << '\n'; + } + } } } \ No newline at end of file From 347acf104dc338ee25ae0f8d0947f5cd5ebb729a Mon Sep 17 00:00:00 2001 From: Brad Reardon Date: Sat, 28 Feb 2026 21:00:32 -0500 Subject: [PATCH 7/7] fix(tray-show): fix attempted deserialization of null references --- src/tray-show.cpp | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/tray-show.cpp b/src/tray-show.cpp index c6dacea..3141e45 100644 --- a/src/tray-show.cpp +++ b/src/tray-show.cpp @@ -21,6 +21,25 @@ void exitWithMsg( std::string_view msg, int code = -1 ) exit( code ); } +void print_json_string (const json& obj, const char* key, const char* label_fmt) { + auto it = obj.find(key); + if (it == obj.end() || it->is_null()) return; + + // if the value isn't a string, skip (or coerce) + if (!it->is_string()) return; + + fmt::printf(label_fmt, it->get_ref().c_str()); +}; + +void print_json_u32 (const json& obj, const char* key, const char* label_fmt) { + auto it = obj.find(key); + if (it == obj.end() || it->is_null()) return; + + if (!it->is_number_unsigned()) return; + + fmt::printf(label_fmt, it->get()); +}; + int main( int argc, char** argv ) { @@ -82,17 +101,16 @@ int main( int argc, char** argv ) { for ( const json& item : itemData ) { - fmt::printf( "Category: %s\n", item.find("category")->get_ref() ); - fmt::printf( "Title: %s\n", item.find("title")->get_ref() ); + print_json_string( item, "category", "Category: %s\n" ); + print_json_string( item, "title", "Title: %s\n" ); - if ( verboseOutput ) + if (verboseOutput) { - fmt::printf( "Id: %s\n", item.find("id")->get_ref() ); - fmt::printf( "Status: %s\n", item.find("status")->get_ref() ); - fmt::printf( "WindowId: %s\n", item.find("windowId")->get() ); - fmt::printf( "IconName: %s\n", item.find("iconName")->get_ref() ); + print_json_string( item, "id", "Id: %s\n" ); + print_json_string( item, "status", "Status: %s\n" ); + print_json_u32( item, "windowId", "WindowId: %u\n" ); + print_json_string( item, "iconName", "IconName: %s\n" ); } - std::cout << '\n'; } }