diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..7748e45
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "thirdparty/stb"]
+ path = thirdparty/stb
+ url = https://github.com/nothings/stb
diff --git a/meson.build b/meson.build
index c858f21..a443769 100644
--- a/meson.build
+++ b/meson.build
@@ -23,6 +23,28 @@ ext_workspace_header = custom_target('generate-ext-workspace-header',
output: ['ext-workspace-unstable-v1.h'],
command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'])
+# 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@'])
+
gtk = dependency('gtk+-3.0')
gtk_layer_shell = dependency('gtk-layer-shell-0')
@@ -64,9 +86,16 @@ add_global_arguments('-DUSE_LOGFILE', language: 'cpp')
pulse = dependency('libpulse')
+# stb
+stb = include_directories('thirdparty')
+
libgBar = library('gBar',
[ ext_workspace_src,
ext_workspace_header,
+ sni_item_src,
+ sni_item_header,
+ sni_watcher_src,
+ sni_watcher_header,
'src/Window.cpp',
'src/Widget.cpp',
'src/System.cpp',
@@ -78,8 +107,10 @@ libgBar = library('gBar',
'src/Config.cpp',
'src/CSS.cpp',
'src/Log.cpp',
+ 'src/SNI.cpp',
],
dependencies: [gtk, gtk_layer_shell, pulse, wayland_client],
+ include_directories: stb,
install: true)
pkg = import('pkgconfig')
diff --git a/protocols/sni-item.xml b/protocols/sni-item.xml
new file mode 100644
index 0000000..74afed2
--- /dev/null
+++ b/protocols/sni-item.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/protocols/sni-watcher.xml b/protocols/sni-watcher.xml
new file mode 100644
index 0000000..0948962
--- /dev/null
+++ b/protocols/sni-watcher.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bar.cpp b/src/Bar.cpp
index 589e8f3..e0f302e 100644
--- a/src/Bar.cpp
+++ b/src/Bar.cpp
@@ -3,6 +3,7 @@
#include "System.h"
#include "Common.h"
#include "Config.h"
+#include "SNI.h"
#include
namespace Bar
@@ -646,6 +647,8 @@ namespace Bar
right->SetSpacing({8, false});
right->SetHorizontalTransform({-1, true, Alignment::Right});
{
+ SNI::WidgetSNI(*right);
+
WidgetPackages(*right);
WidgetAudio(*right);
diff --git a/src/Log.h b/src/Log.h
index 79df5a3..33a7ea1 100644
--- a/src/Log.h
+++ b/src/Log.h
@@ -1,5 +1,6 @@
#pragma once
#include
+#include
#ifdef USE_LOGFILE
#define LOG(x) \
diff --git a/src/SNI.cpp b/src/SNI.cpp
new file mode 100644
index 0000000..04b8aea
--- /dev/null
+++ b/src/SNI.cpp
@@ -0,0 +1,287 @@
+#include "SNI.h"
+#include "Log.h"
+#include "Widget.h"
+
+#include
+#include
+#include
+
+#define STB_IMAGE_IMPLEMENTATION
+#include
+
+#include
+
+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::vector- items;
+
+ // 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;
+
+ // SNI implements the GTK-Thingies itself internally
+ static void InvalidateWidget()
+ {
+ parentBox->RemoveChild(iconBox);
+
+ auto container = Widget::Create();
+ iconBox = container.get();
+ for (auto& item : items)
+ {
+ if (item.iconData)
+ {
+ auto texture = Widget::Create();
+ texture->SetHorizontalTransform({32, true, Alignment::Fill});
+ texture->SetBuf(item.w, item.h, item.iconData);
+ iconBox->AddChild(std::move(texture));
+ }
+ }
+ parentBox->AddChild(std::move(container));
+ }
+
+ void WidgetSNI(Widget& parent)
+ {
+ // Add parent box
+ auto box = Widget::Create();
+ auto container = Widget::Create();
+ iconBox = container.get();
+ parentBox = box.get();
+ InvalidateWidget();
+ box->AddChild(std::move(container));
+ parent.AddChild(std::move(box));
+ }
+
+ 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(width);
+ LOG(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);
+ }
+ return item;
+ }
+
+ // Methods
+ static void RegisterItem(sniWatcher*, 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;
+ }
+ // TODO: Add mechanism to remove items
+ LOG("SNI: Registered Item " << name << " " << object);
+ Item item = CreateItem(std::move(name), std::move(object));
+ items.push_back(std::move(item));
+ InvalidateWidget();
+ }
+ 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()
+ {
+ auto busAcquired = [](GDBusConnection* connection, const char*, void*)
+ {
+ GError* err = nullptr;
+ g_dbus_interface_skeleton_export((GDBusInterfaceSkeleton*)watcherSkeleton, connection, "/StatusNotifierWatcher", &err);
+ if (err)
+ {
+ LOG("Failed to connect to dbus! Error: " << err->message);
+ 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*, void*)
+ {
+ LOG("Lost name!");
+ };
+ 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);
+ }
+
+ void Shutdown() {}
+}
diff --git a/src/SNI.h b/src/SNI.h
new file mode 100644
index 0000000..0a9128e
--- /dev/null
+++ b/src/SNI.h
@@ -0,0 +1,8 @@
+#pragma once
+class Widget;
+namespace SNI
+{
+ void Init();
+ void WidgetSNI(Widget& parent);
+ void Shutdown();
+}
diff --git a/src/System.cpp b/src/System.cpp
index f39b1c3..f5499b0 100644
--- a/src/System.cpp
+++ b/src/System.cpp
@@ -5,6 +5,7 @@
#include "PulseAudio.h"
#include "Workspaces.h"
#include "Config.h"
+#include "SNI.h"
#include
#include
@@ -646,6 +647,8 @@ namespace System
PulseAudio::Init();
+ SNI::Init();
+
CheckNetwork();
}
void FreeResources()
@@ -662,6 +665,8 @@ namespace System
#ifdef WITH_BLUEZ
StopBTScan();
#endif
+ SNI::Shutdown();
+
Logging::Shutdown();
}
}
diff --git a/src/Widget.cpp b/src/Widget.cpp
index 0e62a27..59b36c3 100644
--- a/src/Widget.cpp
+++ b/src/Widget.cpp
@@ -122,10 +122,32 @@ void Widget::RemoveChild(size_t idx)
if (m_Widget)
{
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);
}
+void Widget::RemoveChild(Widget* widget)
+{
+ auto it = std::find_if(m_Childs.begin(), m_Childs.end(),
+ [&](std::unique_ptr& 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)
{
@@ -475,6 +497,30 @@ void NetworkSensor::Draw(cairo_t* cr)
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)
+{
+ // TODO: W + H
+ cairo_rectangle(cr, 0.f, 0.f, 32.f, 32.f);
+ gdk_cairo_set_source_pixbuf(cr, m_Pixbuf, 0, 0);
+ cairo_fill(cr);
+}
+
void Revealer::SetTransition(Transition transition)
{
m_Transition = transition;
diff --git a/src/Widget.h b/src/Widget.h
index 03b1094..150e71f 100644
--- a/src/Widget.h
+++ b/src/Widget.h
@@ -116,6 +116,7 @@ public:
void AddChild(std::unique_ptr&& widget);
void RemoveChild(size_t idx);
+ void RemoveChild(Widget* widget);
std::vector>& GetWidgets() { return m_Childs; }
@@ -263,6 +264,24 @@ private:
std::unique_ptr contextDown;
};
+class Texture : public CairoArea
+{
+public:
+ Texture() = default;
+ virtual ~Texture();
+
+ // Non-Owning, ARGB32
+ void SetBuf(size_t width, size_t height, uint8_t* buf);
+
+private:
+ void Draw(cairo_t* cr) override;
+
+ size_t m_Width;
+ size_t m_Height;
+ GBytes* m_Bytes;
+ GdkPixbuf* m_Pixbuf;
+};
+
class Revealer : public Widget
{
public:
diff --git a/thirdparty/stb b/thirdparty/stb
new file mode 160000
index 0000000..5736b15
--- /dev/null
+++ b/thirdparty/stb
@@ -0,0 +1 @@
+Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9