SNI implementation for tray icons 🎉 (#12)

Implementation for SNI (StatusNotifierItem) d-bus protocol for tray
icons.

Implements https://github.com/scorpion-26/gBar/issues/5
This commit is contained in:
scorpion-26 2023-05-04 16:50:13 +02:00 committed by GitHub
commit ea857e5ff6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 844 additions and 30 deletions

View file

@ -19,10 +19,12 @@ jobs:
pacman-key --populate archlinux pacman-key --populate archlinux
- name: Download pacman packages - name: Download pacman packages
run: | run: |
pacman -Syu --noconfirm base-devel gcc git ninja meson gtk-layer-shell pulseaudio wayland pacman -Syu --noconfirm base-devel gcc git ninja meson gtk-layer-shell pulseaudio wayland libdbusmenu-gtk3
- name: Download gBar - name: Download gBar
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
with:
submodules: recursive
- name: Run meson - name: Run meson
run: | run: |
@ -35,14 +37,14 @@ jobs:
name: Build using Nix name: Build using Nix
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download gBar
uses: actions/checkout@v3.3.0
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v20 uses: cachix/install-nix-action@v20
- name: Download gBar
uses: actions/checkout@v3.3.0
with:
submodules: recursive
- name: Build Nix flake - name: Build Nix flake
run: | run: |
nix build --print-build-logs nix build --print-build-logs

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "thirdparty/stb"]
path = thirdparty/stb
url = https://github.com/nothings/stb

View file

@ -88,6 +88,7 @@ Bar:
- Disk: Free/Total - Disk: Free/Total
- Network: Current upload and download speed - Network: Current upload and download speed
- Update checking (Non-Arch systems need to be configured manually) - Update checking (Non-Arch systems need to be configured manually)
- Tray icons
Bluetooth: Bluetooth:
- Scanning of nearby bluetooth devices - Scanning of nearby bluetooth devices
@ -162,3 +163,10 @@ See *Configuration for your system*
### The icons are not showing! ### The icons are not showing!
Please install a Nerd Font from https://www.nerdfonts.com (I use Caskaydia Cove NF), and change style.css/style.scss accordingly (Refer to 'I want to customize the colors' for that). You _will_ a Nerd Font with version 2.3.0 or newer (For more details see [this comment](https://github.com/scorpion-26/gBar/issues/5#issuecomment-1442037005)) Please install a Nerd Font from https://www.nerdfonts.com (I use Caskaydia Cove NF), and change style.css/style.scss accordingly (Refer to 'I want to customize the colors' for that). You _will_ a Nerd Font with version 2.3.0 or newer (For more details see [this comment](https://github.com/scorpion-26/gBar/issues/5#issuecomment-1442037005))
### The tray doesn't show
Some apps sometimes don't actively query for tray applications. A fix for this is to start gBar before the tray app
If it still doesn't show, please open an issue with your application
The tray icons are confirmed to work with Discord, Telegram, OBS and KeePassXC
### Clicking on the tray opens a glitchy transparent menu
This is semi-intentional and a known bug (See https://github.com/scorpion-26/gBar/pull/12#issuecomment-1529143790 for an explanation). You can make it opaque by setting the background-color property of .popup in style.css/style.scss

View file

@ -3,6 +3,10 @@
font-family: "CaskaydiaCove Nerd Font"; font-family: "CaskaydiaCove Nerd Font";
} }
.popup {
color: #50fa7b;
}
.bar, tooltip { .bar, tooltip {
background-color: #282a36; background-color: #282a36;
border-radius: 16px; border-radius: 16px;

View file

@ -27,6 +27,10 @@ $textsize: 16px;
//background-color: #00cc00 //background-color: #00cc00
} }
.popup {
color: #50fa7b;
}
.bar,tooltip{ .bar,tooltip{
background-color: $bg; background-color: $bg;

View file

@ -71,6 +71,20 @@ NetworkAdapter: eno1
# Disables the network widget when set to false # Disables the network widget when set to false
NetworkWidget: true NetworkWidget: true
# Enables tray icons
EnableSNI: true
# SNIIconSize sets the icon size for a SNI icon.
# SNIPaddingTop Can be used to push the Icon down. Negative values are allowed
# For both: The first parameter is a filter of the tooltip(The text that pops up, when the icon is hovered) of the icon
# Scale everything down to 25 pixels ('*' as filter means everything)
SNIIconSize: *, 25
# Explicitly make OBS a bit smaller than default
SNIIconSize: OBS, 23
# Nudges the Discord icon a bit down
# SNIPaddingTop: Discord, 5
# These set the range for the network widget. The widget changes colors at six intervals: # These set the range for the network widget. The widget changes colors at six intervals:
# - Below Min...Bytes ("under") # - Below Min...Bytes ("under")
# - Between ]0%;25%]. 0% = Min...Bytes; 100% = Max...Bytes ("low") # - Between ]0%;25%]. 0% = Min...Bytes; 100% = Max...Bytes ("low")

View file

@ -29,6 +29,8 @@
gtk3 gtk3
gtk-layer-shell gtk-layer-shell
libpulseaudio libpulseaudio
stb
libdbusmenu-gtk3
]; ];
}); });
in { in {

View file

@ -2,7 +2,7 @@ project('gBar',
['c', 'cpp'], ['c', 'cpp'],
version: '0.0.1', version: '0.0.1',
license: 'MIT', license: 'MIT',
meson_version: '>=0.49.0', meson_version: '>=0.53.0',
default_options: ['cpp_std=c++17', default_options: ['cpp_std=c++17',
'warning_level=3', 'warning_level=3',
'default_library=static', 'default_library=static',
@ -26,6 +26,8 @@ ext_workspace_header = custom_target('generate-ext-workspace-header',
gtk = dependency('gtk+-3.0') gtk = dependency('gtk+-3.0')
gtk_layer_shell = dependency('gtk-layer-shell-0') gtk_layer_shell = dependency('gtk-layer-shell-0')
pulse = dependency('libpulse')
headers = [ headers = [
'src/Common.h', 'src/Common.h',
'src/Log.h', 'src/Log.h',
@ -37,6 +39,25 @@ headers = [
'src/CSS.h' 'src/CSS.h'
] ]
sources = [
ext_workspace_src,
ext_workspace_header,
'src/Window.cpp',
'src/Widget.cpp',
'src/System.cpp',
'src/Bar.cpp',
'src/Workspaces.cpp',
'src/AudioFlyin.cpp',
'src/BluetoothDevices.cpp',
'src/Plugin.cpp',
'src/Config.cpp',
'src/CSS.cpp',
'src/Log.cpp',
'src/SNI.cpp',
]
dependencies = [gtk, gtk_layer_shell, pulse, wayland_client ]
if get_option('WithHyprland') if get_option('WithHyprland')
add_global_arguments('-DWITH_HYPRLAND', language: 'cpp') add_global_arguments('-DWITH_HYPRLAND', language: 'cpp')
headers += 'src/Workspaces.h' headers += 'src/Workspaces.h'
@ -59,27 +80,55 @@ endif
if get_option('WithSys') if get_option('WithSys')
add_global_arguments('-DWITH_SYS', language: 'cpp') add_global_arguments('-DWITH_SYS', language: 'cpp')
endif endif
if get_option('WithSNI')
add_global_arguments('-DWITH_SNI', language: 'cpp')
# DBus protocols
dbus_gen = find_program('gdbus-codegen')
sni_item_src = custom_target('generate-sni-item-src',
input: ['protocols/sni-item.xml'],
output: ['sni-item.c'],
command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@'])
sni_item_header = custom_target('generate-sni-item-header',
input: ['protocols/sni-item.xml'],
output: ['sni-item.h'],
command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@'])
sni_watcher_src = custom_target('generate-sni-watcher-src',
input: ['protocols/sni-watcher.xml'],
output: ['sni-watcher.c'],
command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@'])
sni_watcher_header = custom_target('generate-sni-watcher-header',
input: ['protocols/sni-watcher.xml'],
output: ['sni-watcher.h'],
command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@'])
libdbusmenu = dependency('dbusmenu-gtk3-0.4')
sources += sni_item_src
sources += sni_item_header
sources += sni_watcher_src
sources += sni_watcher_header
dependencies += libdbusmenu
endif
add_global_arguments('-DUSE_LOGFILE', language: 'cpp') add_global_arguments('-DUSE_LOGFILE', language: 'cpp')
pulse = dependency('libpulse') # stb
fs = import('fs')
stb = include_directories('thirdparty')
if fs.exists('thirdparty/stb/stb_image.h')
add_global_arguments('-DHAS_STB', language: 'cpp')
endif
libgBar = library('gBar', libgBar = library('gBar',
[ ext_workspace_src, sources,
ext_workspace_header, dependencies: dependencies,
'src/Window.cpp', include_directories: stb,
'src/Widget.cpp',
'src/System.cpp',
'src/Bar.cpp',
'src/Workspaces.cpp',
'src/AudioFlyin.cpp',
'src/BluetoothDevices.cpp',
'src/Plugin.cpp',
'src/Config.cpp',
'src/CSS.cpp',
'src/Log.cpp',
],
dependencies: [gtk, gtk_layer_shell, pulse, wayland_client],
install: true) install: true)
pkg = import('pkgconfig') pkg = import('pkgconfig')

View file

@ -4,6 +4,9 @@ option('WithHyprland', type: 'boolean', value : true)
# Workspaces general, enables Wayland protocol # Workspaces general, enables Wayland protocol
option('WithWorkspaces', type: 'boolean', value : true) option('WithWorkspaces', type: 'boolean', value : true)
# Tray icons, requires stb git submodule
option('WithSNI', type: 'boolean', value : true)
option('WithNvidia', type: 'boolean', value : true) option('WithNvidia', type: 'boolean', value : true)
option('WithAMD', type: 'boolean', value : true) option('WithAMD', type: 'boolean', value : true)
option('WithBlueZ', type: 'boolean', value : true) option('WithBlueZ', type: 'boolean', value : true)

46
protocols/sni-item.xml Normal file
View file

@ -0,0 +1,46 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierItem">
<annotation name="org.gtk.GDBus.C.Name" value="item"/>
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="u" access="read"/>
<property name="IconName" type="s" access="read"/>
<property name="IconPixmap" type="a(iiay)" access="read"/>
<property name="OverlayIconName" type="s" access="read"/>
<property name="OverlayIconPixmap" type="a(iiay)" access="read"/>
<property name="AttentionIconName" type="s" access="read"/>
<property name="AttentionIconPixmap" type="a(iiay)" access="read"/>
<property name="AttentionMovieName" type="s" access="read"/>
<property name="ToolTip" type="(sa(iiay)ss)" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<property name="Menu" type="o" access="read"/>
<method name="ContextMenu">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="Activate">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="SecondaryActivate">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="Scroll">
<arg type="i" direction="in" name="delta"/>
<arg type="s" direction="in" name="orientation"/>
</method>
<signal name="NewTitle"/>
<signal name="NewIcon"/>
<signal name="NewAttentionIcon"/>
<signal name="NewOverlayIcon"/>
<signal name="NewToolTip"/>
<signal name="NewStatus">
<arg type="s" direction="in" name="status"/>
</signal>
</interface>
</node>

23
protocols/sni-watcher.xml Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierWatcher">
<annotation name="org.gtk.GDBus.C.Name" value="watcher"/>
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<method name="RegisterStatusNotifierItem">
<arg type="s" direction="in" name="service"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg type="s" direction="in" name="service"/>
</method>
<signal name="StatusNotifierItemRegistered">
<arg type="s" direction="out" name="service"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" direction="out" name="service"/>
</signal>
<signal name="StatusNotifierHostRegistered"/>
</interface>
</node>

View file

@ -3,6 +3,7 @@
#include "System.h" #include "System.h"
#include "Common.h" #include "Common.h"
#include "Config.h" #include "Config.h"
#include "SNI.h"
#include <mutex> #include <mutex>
namespace Bar namespace Bar
@ -646,6 +647,10 @@ namespace Bar
right->SetSpacing({8, false}); right->SetSpacing({8, false});
right->SetHorizontalTransform({-1, true, Alignment::Right}); right->SetHorizontalTransform({-1, true, Alignment::Right});
{ {
#ifdef WITH_SNI
SNI::WidgetSNI(*right);
#endif
WidgetPackages(*right); WidgetPackages(*right);
WidgetAudio(*right); WidgetAudio(*right);

View file

@ -27,6 +27,14 @@ void ApplyProperty<uint32_t>(uint32_t& propertyToSet, const std::string_view& va
propertyToSet = atoi(valStr.c_str()); propertyToSet = atoi(valStr.c_str());
} }
template<>
void ApplyProperty<int32_t>(int32_t& propertyToSet, const std::string_view& value)
{
// Why, C++?
std::string valStr = std::string(value);
propertyToSet = atoi(valStr.c_str());
}
template<> template<>
void ApplyProperty<double>(double& propertyToSet, const std::string_view& value) void ApplyProperty<double>(double& propertyToSet, const std::string_view& value)
{ {
@ -53,6 +61,30 @@ void ApplyProperty<bool>(bool& propertyToSet, const std::string_view& value)
} }
} }
template<>
void ApplyProperty<std::pair<std::string, uint32_t>>(std::pair<std::string, uint32_t>& propertyToSet, const std::string_view& value)
{
// TODO: Ignore escaped delimiter (e.g. \, is the same as ,)
const char delim = ',';
const char* whitespace = " \t";
size_t delimPos = value.find(delim);
if (delimPos == std::string::npos)
{
propertyToSet = {std::string(value), 0};
return;
}
std::string_view before = value.substr(0, delimPos);
std::string_view after = value.substr(delimPos + 1);
// Strip whitespaces for before
size_t beginBefore = before.find_first_not_of(whitespace);
size_t endBefore = before.find_last_not_of(whitespace);
before = before.substr(beginBefore, endBefore - beginBefore + 1);
ApplyProperty(propertyToSet.first, before);
ApplyProperty(propertyToSet.second, after);
}
template<typename T> template<typename T>
void AddConfigVar(const std::string& propertyName, T& propertyToSet, std::string_view line, bool& setConfig) void AddConfigVar(const std::string& propertyName, T& propertyToSet, std::string_view line, bool& setConfig)
{ {
@ -147,6 +179,7 @@ void Config::Load()
AddConfigVar("WorkspaceScrollOnMonitor", config.workspaceScrollOnMonitor, lineView, foundProperty); AddConfigVar("WorkspaceScrollOnMonitor", config.workspaceScrollOnMonitor, lineView, foundProperty);
AddConfigVar("WorkspaceScrollInvert", config.workspaceScrollInvert, lineView, foundProperty); AddConfigVar("WorkspaceScrollInvert", config.workspaceScrollInvert, lineView, foundProperty);
AddConfigVar("UseHyprlandIPC", config.useHyprlandIPC, lineView, foundProperty); AddConfigVar("UseHyprlandIPC", config.useHyprlandIPC, lineView, foundProperty);
AddConfigVar("EnableSNI", config.enableSNI, lineView, foundProperty);
AddConfigVar("MinUploadBytes", config.minUploadBytes, lineView, foundProperty); AddConfigVar("MinUploadBytes", config.minUploadBytes, lineView, foundProperty);
AddConfigVar("MaxUploadBytes", config.maxUploadBytes, lineView, foundProperty); AddConfigVar("MaxUploadBytes", config.maxUploadBytes, lineView, foundProperty);
@ -160,6 +193,21 @@ void Config::Load()
AddConfigVar("AudioMinVolume", config.audioMinVolume, lineView, foundProperty); AddConfigVar("AudioMinVolume", config.audioMinVolume, lineView, foundProperty);
AddConfigVar("AudioMaxVolume", config.audioMaxVolume, lineView, foundProperty); AddConfigVar("AudioMaxVolume", config.audioMaxVolume, lineView, foundProperty);
std::pair<std::string, uint32_t> buf;
bool hasntFoundProperty = !foundProperty;
AddConfigVar("SNIIconSize", buf, lineView, foundProperty);
if (foundProperty && hasntFoundProperty)
{
// This was found
config.sniIconSizes[buf.first] = buf.second;
}
hasntFoundProperty = !foundProperty;
AddConfigVar("SNIPaddingTop", buf, lineView, foundProperty);
if (foundProperty && hasntFoundProperty)
{
config.sniPaddingTop[buf.first] = buf.second;
}
if (foundProperty == false) if (foundProperty == false)
{ {
LOG("Warning: unknown config var: " << lineView); LOG("Warning: unknown config var: " << lineView);

View file

@ -2,6 +2,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
#include <unordered_map>
class Config class Config
{ {
@ -27,6 +28,7 @@ public:
bool workspaceScrollOnMonitor = true; // Scroll through workspaces on monitor instead of all bool workspaceScrollOnMonitor = true; // Scroll through workspaces on monitor instead of all
bool workspaceScrollInvert = false; // Up = +1, instead of Up = -1 bool workspaceScrollInvert = false; // Up = +1, instead of Up = -1
bool useHyprlandIPC = false; // Use Hyprland IPC instead of ext_workspaces protocol (Less buggy, but also less performant) bool useHyprlandIPC = false; // Use Hyprland IPC instead of ext_workspaces protocol (Less buggy, but also less performant)
bool enableSNI = true; // Enable tray icon
// Controls for color progression of the network widget // Controls for color progression of the network widget
uint32_t minUploadBytes = 0; // Bottom limit of the network widgets upload. Everything below it is considered "under" uint32_t minUploadBytes = 0; // Bottom limit of the network widgets upload. Everything below it is considered "under"
@ -38,6 +40,10 @@ public:
uint32_t checkUpdateInterval = 5 * 60; // Interval to run the "checkPackagesCommand". In seconds uint32_t checkUpdateInterval = 5 * 60; // Interval to run the "checkPackagesCommand". In seconds
// SNIIconSize: ["Title String"], ["Size"]
std::unordered_map<std::string, uint32_t> sniIconSizes;
std::unordered_map<std::string, int32_t> sniPaddingTop;
// Only affects outputs (i.e.: speakers, not microphones). This remaps the range of the volume; In percent // Only affects outputs (i.e.: speakers, not microphones). This remaps the range of the volume; In percent
double audioMinVolume = 0.f; // Map the minimum volume to this value double audioMinVolume = 0.f; // Map the minimum volume to this value
double audioMaxVolume = 100.f; // Map the maximum volume to this value double audioMaxVolume = 100.f; // Map the maximum volume to this value
@ -73,6 +79,12 @@ public:
bool hasBlueZ = false; bool hasBlueZ = false;
#endif #endif
#if defined WITH_SNI && defined HAS_STB
bool hasSNI = true;
#else
bool hasSNI = false;
#endif
bool hasNet = true; bool hasNet = true;
bool hasPackagesScript = true; bool hasPackagesScript = true;

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <sstream> #include <sstream>
#include <iostream>
#ifdef USE_LOGFILE #ifdef USE_LOGFILE
#define LOG(x) \ #define LOG(x) \

485
src/SNI.cpp Normal file
View file

@ -0,0 +1,485 @@
#include "SNI.h"
#include "Log.h"
#include "Widget.h"
#include "Config.h"
#ifdef WITH_SNI
#include <sni-watcher.h>
#include <sni-item.h>
#include <gio/gio.h>
#include <libdbusmenu-gtk/menu.h>
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <fstream>
namespace SNI
{
sniWatcher* watcherSkeleton;
guint watcherID;
GDBusConnection* dbusConnection = nullptr;
guint hostID;
struct Item
{
std::string name;
std::string object;
size_t w;
size_t h;
uint8_t* iconData = nullptr;
std::string tooltip;
std::string menuObjectPath;
EventBox* gtkEvent;
};
std::vector<Item> items;
std::unordered_map<std::string, Item> clientsToQuery;
// Gtk stuff, TODO: Allow more than one instance
// Simply removing the gtk_drawing_areas doesn't trigger proper redrawing
// HACK: Make an outer permanent and an inner box, which will be deleted and readded
Widget* parentBox;
Widget* iconBox;
static Item CreateItem(std::string&& name, std::string&& object)
{
Item item{};
item.name = name;
item.object = object;
auto getProperty = [&](const char* prop) -> GVariant*
{
GError* err = nullptr;
GVariant* params[2];
params[0] = g_variant_new_string("org.kde.StatusNotifierItem");
params[1] = g_variant_new_string(prop);
GVariant* param = g_variant_new_tuple(params, 2);
GVariant* res = g_dbus_connection_call_sync(dbusConnection, name.c_str(), object.c_str(), "org.freedesktop.DBus.Properties", "Get", param,
G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err);
if (err)
{
g_error_free(err);
return nullptr;
}
// There's probably a better method than to use 3 variants
// g_variant_unref(params[0]);
// g_variant_unref(params[1]);
// g_variant_unref(param);
return res;
};
GVariant* iconPixmap = getProperty("IconPixmap");
if (iconPixmap)
{
// Only get first item
GVariant* arr = nullptr;
g_variant_get(iconPixmap, "(v)", &arr);
GVariantIter* arrIter = nullptr;
g_variant_get(arr, "a(iiay)", &arrIter);
int width;
int height;
GVariantIter* data = nullptr;
g_variant_iter_next(arrIter, "(iiay)", &width, &height, &data);
LOG("SNI: Width: " << width);
LOG("SNI: Height: " << height);
item.w = width;
item.h = height;
item.iconData = new uint8_t[width * height * 4];
uint8_t px = 0;
int i = 0;
while (g_variant_iter_next(data, "y", &px))
{
item.iconData[i] = px;
i++;
}
for (int i = 0; i < width * height; i++)
{
struct Px
{
// This should be bgra...
// Since source is ARGB32 in network order(=big-endian)
// and x86 Linux is little-endian, we *should* swap b and r...
uint8_t a, r, g, b;
};
Px& pixel = ((Px*)item.iconData)[i];
// Swap to create rgba
pixel = {pixel.r, pixel.g, pixel.b, pixel.a};
}
g_variant_iter_free(data);
g_variant_iter_free(arrIter);
g_variant_unref(arr);
g_variant_unref(iconPixmap);
}
else
{
// Get icon theme path
GVariant* themePathVariant = getProperty("IconThemePath"); // Not defined by freedesktop, I think ayatana does this...
GVariant* iconNameVariant = getProperty("IconName");
std::string iconPath;
if (themePathVariant && iconNameVariant)
{
// Why GLib?
GVariant* themePathStr = nullptr;
g_variant_get(themePathVariant, "(v)", &themePathStr);
GVariant* iconNameStr = nullptr;
g_variant_get(iconNameVariant, "(v)", &iconNameStr);
const char* themePath = g_variant_get_string(themePathStr, nullptr);
const char* iconName = g_variant_get_string(iconNameStr, nullptr);
iconPath = std::string(themePath) + "/" + iconName + ".png"; // TODO: Find out if this is always png
g_variant_unref(themePathVariant);
g_variant_unref(themePathStr);
g_variant_unref(iconNameVariant);
g_variant_unref(iconNameStr);
}
else if (iconNameVariant)
{
GVariant* iconNameStr = nullptr;
g_variant_get(iconNameVariant, "(v)", &iconNameStr);
const char* iconName = g_variant_get_string(iconNameStr, nullptr);
iconPath = std::string(iconName);
g_variant_unref(iconNameVariant);
g_variant_unref(iconNameStr);
}
else
{
LOG("SNI: Unknown path!");
return item;
}
int width, height, channels;
stbi_uc* pixels = stbi_load(iconPath.c_str(), &width, &height, &channels, STBI_rgb_alpha);
if (!pixels)
{
LOG("SNI: Cannot open " << iconPath);
return item;
}
item.w = width;
item.h = height;
item.iconData = new uint8_t[width * height * 4];
// Already rgba32
memcpy(item.iconData, pixels, width * height * 4);
stbi_image_free(pixels);
}
// Query tooltip(Steam e.g. doesn't have one)
GVariant* tooltip = getProperty("ToolTip");
if (tooltip)
{
GVariant* tooltipVar;
g_variant_get_child(tooltip, 0, "v", &tooltipVar);
const gchar* title;
// Both telegram and discord only set the "title" component
g_variant_get_child(tooltipVar, 2, "s", &title);
item.tooltip = title;
LOG("SNI: Title: " << item.tooltip);
g_variant_unref(tooltip);
g_variant_unref(tooltipVar);
}
// Query menu
GVariant* menuPath = getProperty("Menu");
if (menuPath)
{
GVariant* menuVariant;
g_variant_get_child(menuPath, 0, "v", &menuVariant);
const char* objectPath;
g_variant_get(menuVariant, "o", &objectPath);
LOG("SNI: Menu object path: " << objectPath);
item.menuObjectPath = objectPath;
g_variant_unref(menuVariant);
g_variant_unref(menuPath);
}
return item;
}
static void InvalidateWidget();
static void DBusNameVanished(GDBusConnection*, const char* name, void*)
{
auto it = std::find_if(items.begin(), items.end(),
[&](const Item& item)
{
return item.name == name;
});
if (it != items.end())
{
LOG("SNI: " << name << " vanished!");
items.erase(it);
InvalidateWidget();
}
else
{
LOG("SNI: Cannot remove unregistered bus name " << name);
}
}
static void ItemPropertyChanged(GDBusConnection*, const char*, const char* object, const char*, const char*, GVariant*, void* name)
{
std::string nameStr = (const char*)name;
LOG("SNI: Reloading " << (const char*)name << " " << object);
// We don't care about *what* changed, just remove and reload
auto it = std::find_if(items.begin(), items.end(),
[&](const Item& item)
{
return item.name == nameStr;
});
if (it != items.end())
{
items.erase(it);
}
else
{
LOG("SNI: Coudn't remove item " << nameStr << " when reloading");
}
clientsToQuery[nameStr] = {nameStr, std::string(object)};
}
static TimerResult UpdateWidgets(Box&)
{
if (RuntimeConfig::Get().hasSNI == false || Config::Get().enableSNI == false)
{
// Don't bother
return TimerResult::Delete;
}
for (auto& [name, client] : clientsToQuery)
{
LOG("SNI: Creating Item " << client.name << " " << client.object);
Item item = CreateItem(std::move(client.name), std::move(client.object));
// Add handler for removing
g_bus_watch_name_on_connection(dbusConnection, item.name.c_str(), G_BUS_NAME_WATCHER_FLAGS_NONE, nullptr, DBusNameVanished, nullptr,
nullptr);
// Add handler for icon change
char* staticBuf = new char[item.name.size() + 1]{0x0};
memcpy(staticBuf, item.name.c_str(), item.name.size());
g_dbus_connection_signal_subscribe(
dbusConnection, item.name.c_str(), "org.kde.StatusNotifierItem", nullptr, nullptr, nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
ItemPropertyChanged, staticBuf,
+[](void* ptr)
{
LOG("SNI: Delete static name buffer for " << (char*)ptr);
delete[] (char*)ptr;
});
items.push_back(std::move(item));
}
if (clientsToQuery.size() > 0)
{
InvalidateWidget();
}
clientsToQuery.clear();
return TimerResult::Ok;
}
// SNI implements the GTK-Thingies itself internally
static void InvalidateWidget()
{
LOG("SNI: Clearing old children");
parentBox->RemoveChild(iconBox);
auto container = Widget::Create<Box>();
container->SetSpacing({4, false});
iconBox = container.get();
for (auto& item : items)
{
if (item.iconData)
{
auto eventBox = Widget::Create<EventBox>();
item.gtkEvent = eventBox.get();
eventBox->SetOnCreate(
[&](Widget& w)
{
auto clickFn = [](GtkWidget*, GdkEventButton* event, void* data) -> gboolean
{
if (event->button == 1)
{
Item* item = (Item*)data;
GtkMenu* menu = (GtkMenu*)dbusmenu_gtkmenu_new(item->name.data(), item->menuObjectPath.data());
LOG(menu);
gtk_menu_attach_to_widget(menu, item->gtkEvent->Get(), nullptr);
gtk_menu_popup_at_pointer(menu, (GdkEvent*)event);
LOG(item->menuObjectPath << " click");
}
return GDK_EVENT_STOP;
};
g_signal_connect(w.Get(), "button-release-event", G_CALLBACK(+clickFn), &item);
});
LOG("SNI: Add " << item.name << " to widget");
auto texture = Widget::Create<Texture>();
bool wasExplicitOverride = false;
for (auto& [filter, size] : Config::Get().sniIconSizes)
{
if (item.tooltip.find(filter) != std::string::npos)
{
wasExplicitOverride = true;
texture->ForceHeight(size);
}
else if (filter == "*" && !wasExplicitOverride)
{
texture->ForceHeight(size);
}
}
wasExplicitOverride = false;
for (auto& [filter, padding] : Config::Get().sniPaddingTop)
{
if (item.tooltip.find(filter) != std::string::npos)
{
LOG("Padding " << padding);
wasExplicitOverride = true;
texture->AddPaddingTop(padding);
}
else if (filter == "*" && !wasExplicitOverride)
{
texture->AddPaddingTop(padding);
}
}
texture->SetHorizontalTransform({0, true, Alignment::Fill});
texture->SetBuf(item.w, item.h, item.iconData);
texture->SetTooltip(item.tooltip);
eventBox->AddChild(std::move(texture));
iconBox->AddChild(std::move(eventBox));
}
}
parentBox->AddChild(std::move(container));
}
void WidgetSNI(Widget& parent)
{
if (RuntimeConfig::Get().hasSNI == false || Config::Get().enableSNI == false)
{
return;
}
// Add parent box
auto box = Widget::Create<Box>();
auto container = Widget::Create<Box>();
container->AddTimer<Box>(UpdateWidgets, 1000, TimerDispatchBehaviour::LateDispatch);
iconBox = container.get();
parentBox = box.get();
InvalidateWidget();
box->AddChild(std::move(container));
parent.AddChild(std::move(box));
}
// Methods
static bool RegisterItem(sniWatcher* watcher, GDBusMethodInvocation* invocation, const char* service)
{
std::string name;
std::string object;
if (strncmp(service, "/", 1) == 0)
{
// service is object (used by e.g. ayatana -> steam, discord)
object = service;
name = g_dbus_method_invocation_get_sender(invocation);
}
else
{
// service is bus name (used by e.g. Telegram)
name = service;
object = "/StatusNotifierItem";
}
auto it = std::find_if(items.begin(), items.end(),
[&](const Item& item)
{
return item.name == name && item.object == object;
});
if (it != items.end())
{
LOG("Rejecting " << name << " " << object);
return false;
}
sni_watcher_emit_status_notifier_item_registered(watcher, service);
sni_watcher_complete_register_status_notifier_item(watcher, invocation);
LOG("SNI: Registered Item " << name << " " << object);
clientsToQuery[name] = {name, std::move(object)};
return true;
}
static void RegisterHost(sniWatcher*, GDBusMethodInvocation*, const char*)
{
LOG("TODO: Implement RegisterHost!");
}
// Signals
static void ItemRegistered(sniWatcher*, const char*, void*)
{
// Don't care, since watcher and host will always be from gBar (at least for now)
}
static void ItemUnregistered(sniWatcher*, const char*, void*)
{
// Don't care, since watcher and host will always be from gBar (at least for now)
}
void Init()
{
if (RuntimeConfig::Get().hasSNI == false || Config::Get().enableSNI == false)
{
return;
}
auto busAcquired = [](GDBusConnection* connection, const char*, void*)
{
GError* err = nullptr;
g_dbus_interface_skeleton_export((GDBusInterfaceSkeleton*)watcherSkeleton, connection, "/StatusNotifierWatcher", &err);
if (err)
{
LOG("SNI: Failed to connect to dbus! Disabling SNI. Error: " << err->message);
RuntimeConfig::Get().hasSNI = false;
g_error_free(err);
return;
}
dbusConnection = connection;
// Connect methods and signals
g_signal_connect(watcherSkeleton, "handle-register-status-notifier-item", G_CALLBACK(RegisterItem), nullptr);
g_signal_connect(watcherSkeleton, "handle-register-status-notifier-host", G_CALLBACK(RegisterHost), nullptr);
g_signal_connect(watcherSkeleton, "status-notifier-item-registered", G_CALLBACK(ItemRegistered), nullptr);
g_signal_connect(watcherSkeleton, "status-notifier-item-unregistered", G_CALLBACK(ItemUnregistered), nullptr);
// Host is always available
sni_watcher_set_is_status_notifier_host_registered(watcherSkeleton, true);
};
auto emptyCallback = [](GDBusConnection*, const char*, void*) {};
auto lostName = [](GDBusConnection*, const char* msg, void*)
{
LOG("SNI: Lost Name! Disabling SNI!");
RuntimeConfig::Get().hasSNI = false;
};
auto flags = G_BUS_NAME_OWNER_FLAGS_REPLACE | G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
g_bus_own_name(G_BUS_TYPE_SESSION, "org.kde.StatusNotifierWatcher", (GBusNameOwnerFlags)flags, +busAcquired, +emptyCallback, +lostName,
nullptr, nullptr);
watcherSkeleton = sni_watcher_skeleton_new();
std::string hostName = "org.kde.StatusNotifierHost-" + std::to_string(getpid());
g_bus_own_name(G_BUS_TYPE_SESSION, hostName.c_str(), (GBusNameOwnerFlags)flags, +emptyCallback, +emptyCallback, +emptyCallback, nullptr,
nullptr);
// Host is always available
sni_watcher_set_is_status_notifier_host_registered(watcherSkeleton, true);
sni_watcher_emit_status_notifier_host_registered(watcherSkeleton);
}
void Shutdown() {}
}
#endif

10
src/SNI.h Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#ifdef WITH_SNI
class Widget;
namespace SNI
{
void Init();
void WidgetSNI(Widget& parent);
void Shutdown();
}
#endif

View file

@ -5,6 +5,7 @@
#include "PulseAudio.h" #include "PulseAudio.h"
#include "Workspaces.h" #include "Workspaces.h"
#include "Config.h" #include "Config.h"
#include "SNI.h"
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
@ -646,6 +647,10 @@ namespace System
PulseAudio::Init(); PulseAudio::Init();
#ifdef WITH_SNI
SNI::Init();
#endif
CheckNetwork(); CheckNetwork();
} }
void FreeResources() void FreeResources()
@ -662,6 +667,10 @@ namespace System
#ifdef WITH_BLUEZ #ifdef WITH_BLUEZ
StopBTScan(); StopBTScan();
#endif #endif
#ifdef WITH_SNI
SNI::Shutdown();
#endif
Logging::Shutdown(); Logging::Shutdown();
} }
} }

View file

@ -122,10 +122,32 @@ void Widget::RemoveChild(size_t idx)
if (m_Widget) if (m_Widget)
{ {
auto& child = *m_Childs[idx]; auto& child = *m_Childs[idx];
gtk_container_remove((GtkContainer*)child.m_Widget, m_Widget); gtk_container_remove((GtkContainer*)m_Widget, child.m_Widget);
child.m_Widget = nullptr;
} }
m_Childs.erase(m_Childs.begin() + idx); m_Childs.erase(m_Childs.begin() + idx);
} }
void Widget::RemoveChild(Widget* widget)
{
auto it = std::find_if(m_Childs.begin(), m_Childs.end(),
[&](std::unique_ptr<Widget>& other)
{
return other.get() == widget;
});
if (it != m_Childs.end())
{
if (m_Widget)
{
gtk_container_remove((GtkContainer*)m_Widget, it->get()->m_Widget);
it->get()->m_Widget = nullptr;
}
m_Childs.erase(it);
}
else
{
LOG("Invalid child!");
}
}
void Widget::SetVisible(bool visible) void Widget::SetVisible(bool visible)
{ {
@ -151,6 +173,9 @@ void Widget::ApplyPropertiesToWidget()
gtk_widget_set_valign(m_Widget, Utils::ToGtkAlign(m_VerticalTransform.alignment)); gtk_widget_set_valign(m_Widget, Utils::ToGtkAlign(m_VerticalTransform.alignment));
gtk_widget_set_hexpand(m_Widget, m_HorizontalTransform.expand); gtk_widget_set_hexpand(m_Widget, m_HorizontalTransform.expand);
gtk_widget_set_vexpand(m_Widget, m_VerticalTransform.expand); gtk_widget_set_vexpand(m_Widget, m_VerticalTransform.expand);
if (m_OnCreate)
m_OnCreate(*this);
} }
void Box::SetOrientation(Orientation orientation) void Box::SetOrientation(Orientation orientation)
@ -475,6 +500,38 @@ void NetworkSensor::Draw(cairo_t* cr)
gdk_rgba_free(colDown); gdk_rgba_free(colDown);
} }
Texture::~Texture()
{
if (m_Pixbuf)
g_free(m_Pixbuf);
if (m_Bytes)
g_free(m_Bytes);
}
void Texture::SetBuf(size_t width, size_t height, uint8_t* buf)
{
m_Width = width;
m_Height = height;
m_Bytes = g_bytes_new(buf, m_Width * m_Height * 4);
m_Pixbuf = gdk_pixbuf_new_from_bytes((GBytes*)m_Bytes, GDK_COLORSPACE_RGB, true, 8, m_Width, m_Height, m_Width * 4);
}
void Texture::Draw(cairo_t* cr)
{
GtkAllocation dim;
gtk_widget_get_allocation(m_Widget, &dim);
double height = m_ForcedHeight != 0 ? m_ForcedHeight : dim.height;
double scale = (double)height / (double)m_Height;
double width = (double)m_Width * scale;
gtk_widget_set_size_request(m_Widget, width + 2, height);
cairo_scale(cr, scale, scale);
cairo_rectangle(cr, (dim.width - width) / 2.0, m_Padding + (dim.height - height) / 2.0, m_Width, m_Height);
gdk_cairo_set_source_pixbuf(cr, m_Pixbuf, (dim.width - width) / 2.0, m_Padding + (dim.height - height) / 2.0);
cairo_fill(cr);
}
void Revealer::SetTransition(Transition transition) void Revealer::SetTransition(Transition transition)
{ {
m_Transition = transition; m_Transition = transition;

View file

@ -116,6 +116,7 @@ public:
void AddChild(std::unique_ptr<Widget>&& widget); void AddChild(std::unique_ptr<Widget>&& widget);
void RemoveChild(size_t idx); void RemoveChild(size_t idx);
void RemoveChild(Widget* widget);
std::vector<std::unique_ptr<Widget>>& GetWidgets() { return m_Childs; } std::vector<std::unique_ptr<Widget>>& GetWidgets() { return m_Childs; }
@ -156,6 +157,8 @@ public:
void SetVisible(bool visible); void SetVisible(bool visible);
void SetOnCreate(Callback<Widget>&& onCreate) { m_OnCreate = onCreate; }
protected: protected:
void PropagateToParent(GdkEvent* event); void PropagateToParent(GdkEvent* event);
void ApplyPropertiesToWidget(); void ApplyPropertiesToWidget();
@ -168,6 +171,8 @@ protected:
std::string m_Tooltip; std::string m_Tooltip;
Transform m_HorizontalTransform; // X Transform m_HorizontalTransform; // X
Transform m_VerticalTransform; // Y Transform m_VerticalTransform; // Y
Callback<Widget> m_OnCreate;
}; };
class Box : public Widget class Box : public Widget
@ -263,6 +268,29 @@ private:
std::unique_ptr<Box> contextDown; std::unique_ptr<Box> contextDown;
}; };
class Texture : public CairoArea
{
public:
Texture() = default;
virtual ~Texture();
// Non-Owning, ARGB32
void SetBuf(size_t width, size_t height, uint8_t* buf);
void ForceHeight(size_t height) { m_ForcedHeight = height; };
void AddPaddingTop(int32_t topPadding) { m_Padding = topPadding; };
private:
void Draw(cairo_t* cr) override;
size_t m_Width;
size_t m_Height;
size_t m_ForcedHeight = 0;
int32_t m_Padding = 0;
GBytes* m_Bytes;
GdkPixbuf* m_Pixbuf;
};
class Revealer : public Widget class Revealer : public Widget
{ {
public: public:

1
thirdparty/stb vendored Submodule

@ -0,0 +1 @@
Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9