Skip to content
Merged
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
7 changes: 7 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Version 0.10.0
~~~~~~~~~~~~~
Released: 2026-01-28

Features:
* [#711] Add macOS menu bar status item (NSStatusItem) with system tray icon.

Version 0.9.4
~~~~~~~~~~~~~
Released: 2025-02-17
Expand Down
9 changes: 7 additions & 2 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ endif
if (cc.has_argument('-Wno-c99-designator'))
add_project_arguments('-Wno-c99-designator', language : 'cpp')
endif
if host_machine.system() == 'darwin' and get_option('werror')
add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'cpp')
if host_machine.system() == 'darwin'
add_languages('objcpp')
add_project_arguments('-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_70', language : 'objcpp')
if get_option('werror')
add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'cpp')
add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'objcpp')
endif
endif

so_version = 2
Expand Down
197 changes: 197 additions & 0 deletions src/iptux/AppIndicatorMac.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: GPL-2.0-or-later

#include "config.h"
#include "AppIndicator.h"

#include <glib/gi18n.h>
#include <gtk/gtk.h>

#import <Cocoa/Cocoa.h>

// Objective-C helper class for NSStatusItem callbacks.
// Must be at global scope (Objective-C declarations cannot appear inside a C++
// namespace).
@interface IptuxStatusItemHelper : NSObject {
GActionGroup* actionGroup_;
iptux::IptuxAppIndicator* owner_;
}
- (instancetype)initWithActionGroup:(GActionGroup*)actionGroup
owner:(iptux::IptuxAppIndicator*)owner;
- (void)openMainWindow:(id)sender;
- (void)openPreferences:(id)sender;
- (void)quit:(id)sender;
- (void)statusItemClicked:(id)sender;
@end

@implementation IptuxStatusItemHelper
- (instancetype)initWithActionGroup:(GActionGroup*)actionGroup
owner:(iptux::IptuxAppIndicator*)owner {
self = [super init];
if (self) {
actionGroup_ = actionGroup;
owner_ = owner;
}
return self;
}

- (void)openMainWindow:(id)sender {
(void)sender;
g_action_group_activate_action(actionGroup_, "open_main_window", NULL);
}

- (void)openPreferences:(id)sender {
(void)sender;
g_action_group_activate_action(actionGroup_, "preferences", NULL);
}

- (void)quit:(id)sender {
(void)sender;
g_action_group_activate_action(actionGroup_, "quit", NULL);
}

- (void)statusItemClicked:(id)sender {
(void)sender;
Comment on lines +50 to +53

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statusItemClicked: is currently dead code: it isn’t wired to the NSStatusItem button (no target/action assignment), so this callback (and the owner_ field it depends on) will never be invoked. Either hook this up via the status item button’s target/action if you want click-to-open behavior, or remove the unused method/field to avoid confusion.

Copilot uses AI. Check for mistakes.
owner_->sigActivateMainWindow.emit();
}
@end

static NSImage* pixbufToNSImage(GdkPixbuf* pixbuf) {
int width = gdk_pixbuf_get_width(pixbuf);
int height = gdk_pixbuf_get_height(pixbuf);
int stride = gdk_pixbuf_get_rowstride(pixbuf);
gboolean hasAlpha = gdk_pixbuf_get_has_alpha(pixbuf);
const guchar* pixels = gdk_pixbuf_get_pixels(pixbuf);

int bitsPerSample = 8;
int channels = hasAlpha ? 4 : 3;

NSBitmapImageRep* rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:width
pixelsHigh:height
bitsPerSample:bitsPerSample
samplesPerPixel:channels
hasAlpha:hasAlpha
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:stride
bitsPerPixel:bitsPerSample * channels];

memcpy([rep bitmapData], pixels, height * stride);

NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
[image addRepresentation:rep];
[rep release];

return [image autorelease];
}

static NSImage* loadIcon(const char* iconName, int size) {
auto theme = gtk_icon_theme_get_default();
GError* error = nullptr;
auto pixbuf = gtk_icon_theme_load_icon(theme, iconName, size,
GtkIconLookupFlags(0), &error);
if (!pixbuf) {
g_warning("Couldn't load icon %s: %s", iconName, error->message);
g_error_free(error);
return nil;
}
Comment thread
lidaobing marked this conversation as resolved.
NSImage* image = pixbufToNSImage(pixbuf);
[image retain];
g_object_unref(pixbuf);

// Resize for menu bar (typically 18x18 points)
[image setSize:NSMakeSize(18, 18)];
return image;
}

namespace iptux {

class IptuxAppIndicatorPrivate {
public:
IptuxAppIndicatorPrivate() {}
~IptuxAppIndicatorPrivate() {
if (statusItem) {
[[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
[statusItem release];
}
if (helper) {
[helper release];
}
if (normalIcon) {
[normalIcon release];
}
if (attentionIcon) {
[attentionIcon release];
}
}

NSStatusItem* statusItem = nil;
IptuxStatusItemHelper* helper = nil;
NSImage* normalIcon = nil;
NSImage* attentionIcon = nil;
};

IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) {
this->priv = std::make_shared<IptuxAppIndicatorPrivate>();

priv->helper = [[IptuxStatusItemHelper alloc] initWithActionGroup:action_group
owner:this];

// Load icons
priv->normalIcon = loadIcon("iptux-icon", 64);
priv->attentionIcon = loadIcon("iptux-attention", 64);

// Create status item
priv->statusItem =
Comment on lines +144 to +146

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Objective-C++ file doesn’t appear to follow the project’s clang-format style (e.g., very long lines here). Please run clang-format on AppIndicatorMac.mm so it matches the existing formatting seen in src/iptux/AppIndicator.cpp:36-56 and the rest of the codebase.

Copilot uses AI. Check for mistakes.
[[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain];
Comment thread
lidaobing marked this conversation as resolved.

if (priv->normalIcon) {
priv->statusItem.button.image = priv->normalIcon;
}
priv->statusItem.button.toolTip = @"Iptux";

// Build the menu
NSMenu* menu = [[NSMenu alloc] init];

NSMenuItem* openItem =
[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Open Iptux")]
action:@selector(openMainWindow:)
keyEquivalent:@""];
openItem.target = priv->helper;
[menu addItem:openItem];
[openItem release];

[menu addItem:[NSMenuItem separatorItem]];

NSMenuItem* prefsItem =
[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Preferences")]
action:@selector(openPreferences:)
keyEquivalent:@""];
prefsItem.target = priv->helper;
[menu addItem:prefsItem];
[prefsItem release];

NSMenuItem* quitItem =
[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:_("Quit")]
action:@selector(quit:)
keyEquivalent:@""];
quitItem.target = priv->helper;
[menu addItem:quitItem];
[quitItem release];

priv->statusItem.menu = menu;
[menu release];
}

void IptuxAppIndicator::SetUnreadCount(int count) {
if (!priv->statusItem) return;

if (count > 0 && priv->attentionIcon) {
priv->statusItem.button.image = priv->attentionIcon;
} else if (priv->normalIcon) {
priv->statusItem.button.image = priv->normalIcon;
}
}

} // namespace iptux
5 changes: 4 additions & 1 deletion src/iptux/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ sources = files([
])
dependencies = [gtk_dep, jsoncpp_dep, sigc_dep]

if appindicator_dep.found()
if host_machine.system() == 'darwin'
sources += ['AppIndicatorMac.mm']
dependencies += [dependency('appleframeworks', modules: ['Cocoa'])]
elif appindicator_dep.found()
sources += ['AppIndicator.cpp']
dependencies += [appindicator_dep]
else
Expand Down
Loading