diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build 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/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 .. diff --git a/src/StatusNotifierItem.cpp b/src/StatusNotifierItem.cpp index c5d2696..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(), std::string( destination_ ), "/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/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/StatusNotifierWatcher.cpp b/src/StatusNotifierWatcher.cpp index f99fa08..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; @@ -16,24 +17,43 @@ 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 ); } ); } -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..abd6d93 --- /dev/null +++ b/src/Types.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +using json = nlohmann::json; + + +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; +} + +// 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-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..3141e45 100644 --- a/src/tray-show.cpp +++ b/src/tray-show.cpp @@ -4,16 +4,15 @@ #include #include #include +#include #include "StatusNotifierWatcher.h" #include "StatusNotifierItem.h" +#include "StatusNotifierItemJson.h" #include "Utils.h" +#include "Types.h" - -#define WRAP_FUNC_TO_LAMBDA( func ) \ - [] ( Args&& ...args ) -> decltype( auto ) \ - { return func( std::forward( args )... ); } - +using json = nlohmann::json; void exitWithMsg( std::string_view msg, int code = -1 ) @@ -22,12 +21,32 @@ 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 ) { 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 ); @@ -37,6 +56,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 ) @@ -44,28 +64,55 @@ 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() ) { - 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; + + // 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 ) + { + print_json_string( item, "category", "Category: %s\n" ); + print_json_string( item, "title", "Title: %s\n" ); - std::cout << '\n'; + if (verboseOutput) + { + 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'; + } } } } \ No newline at end of file