From 3ad3c72a6f62184df1719ae7f1d65bee7ac8c634 Mon Sep 17 00:00:00 2001 From: Roel Van Gils Date: Fri, 12 Dec 2025 00:07:02 +0100 Subject: [PATCH 1/2] fix: resolve menu bar item source apps on macOS Tahoe (26.x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS 26 (Tahoe), Apple changed the ownership of third-party menu bar items. These items are now owned by "Control Centre" instead of their source applications. This breaks the alias feature as SketchyBar can no longer identify which app owns which menu bar item. This fix uses the Accessibility API to resolve the real source application for each menu bar item: - Adds source_pid.h/m: New module that uses AXUIElementRef and the kAXExtrasMenuBarAttribute to find the real source app by matching window bounds to accessibility element frames - Updates alias.c: Integrates the source PID lookup in both print_all_menu_items() and alias_find_window() functions - On macOS < 26, the existing behavior is preserved The approach is similar to how Ice menu bar manager solved this issue in their macos-26 branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- makefile | 2 +- src/alias.c | 83 +++++++++++--- src/source_pid.h | 26 +++++ src/source_pid.m | 277 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 15 deletions(-) create mode 100644 src/source_pid.h create mode 100644 src/source_pid.m diff --git a/makefile b/makefile index 648fc47..a3007ed 100644 --- a/makefile +++ b/makefile @@ -18,7 +18,7 @@ _OBJ = alias.o background.o bar_item.o custom_events.o event.o graph.o \ image.o mouse.o shadow.o font.o text.o message.o mouse.o bar.o color.o \ window.o bar_manager.o display.o group.o mach.o popup.o \ animation.o workspace.om volume.o slider.o power.o wifi.om media.om \ - hotload.o app_windows.o + hotload.o app_windows.o source_pid.om OBJ = $(patsubst %, $(ODIR)/%, $(_OBJ)) diff --git a/src/alias.c b/src/alias.c index 19edd6f..5f16ddc 100644 --- a/src/alias.c +++ b/src/alias.c @@ -1,5 +1,6 @@ #include "alias.h" #include "misc/helpers.h" +#include "source_pid.h" #include #include @@ -15,6 +16,13 @@ void print_all_menu_items(FILE* rsp) { } #endif + + // On macOS 26+, check if we need to use accessibility API workaround + bool use_source_pid_workaround = source_pid_needs_workaround(); + if (use_source_pid_workaround) { + source_pid_cache_refresh(); + } + CFArrayRef window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID ); int window_count = CFArrayGetCount(window_list); @@ -55,11 +63,25 @@ void print_all_menu_items(FILE* rsp) { if (layer != MENUBAR_LAYER) continue; CGRect bounds = CGRectNull; if (!CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) continue; + char* owner_copy = cfstring_copy(owner_ref); if (string_equals(owner_copy, "Window Server")) { free(owner_copy); continue; } + + // On macOS 26+, try to find the real source application name + // when the owner is "Control Centre" (items are now owned by Control Center) + if (use_source_pid_workaround && + (string_equals(owner_copy, "Control Centre") || + string_equals(owner_copy, "Control Center"))) { + char* source_name = source_name_for_window(bounds); + if (source_name) { + free(owner_copy); + owner_copy = source_name; + } + } + owner[item_count] = owner_copy; name[item_count] = cfstring_copy(name_ref); x_pos[item_count++] = bounds.origin.x; @@ -132,6 +154,12 @@ static void alias_find_window(struct alias* alias) { kCGNullWindowID ); int window_count = CFArrayGetCount(window_list); + // On macOS 26+, check if we need to use accessibility API workaround + bool use_source_pid_workaround = source_pid_needs_workaround(); + if (use_source_pid_workaround) { + source_pid_cache_refresh(); + } + for (int i = 0; i < window_count; ++i) { CFDictionaryRef dictionary = CFArrayGetValueAtIndex(window_list, i); if (!dictionary) continue; @@ -145,19 +173,39 @@ static void alias_find_window(struct alias* alias) { CFStringRef name_ref = CFDictionaryGetValue(dictionary, kCGWindowName); if (!name_ref) continue; if (!owner_ref) continue; + + // Get bounds first (needed for source PID lookup on macOS 26+) + CFDictionaryRef bounds_ref = CFDictionaryGetValue(dictionary, kCGWindowBounds); + if (!bounds_ref) continue; + CGRect bounds; + if (!CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) continue; + char* owner = cfstring_copy(owner_ref); char* name = cfstring_copy(name_ref); - if (!(alias->owner && strcmp(alias->owner, owner) == 0 - && ((alias->name && strcmp(alias->name, name) == 0) - || (!alias->name && strcmp(name, "") != 0) ))) { - free(owner); - free(name); - continue; + // On macOS 26+, resolve the real owner name if it's Control Centre + char* resolved_owner = owner; + if (use_source_pid_workaround && + (string_equals(owner, "Control Centre") || + string_equals(owner, "Control Center"))) { + char* source_name = source_name_for_window(bounds); + if (source_name) { + resolved_owner = source_name; + } } + + bool owner_matches = alias->owner && strcmp(alias->owner, resolved_owner) == 0; + bool name_matches = (alias->name && strcmp(alias->name, name) == 0) + || (!alias->name && strcmp(name, "") != 0); + + if (resolved_owner != owner) free(resolved_owner); free(owner); free(name); + if (!(owner_matches && name_matches)) { + continue; + } + CFNumberRef layer_ref = CFDictionaryGetValue(dictionary, kCGWindowLayer); if (!layer_ref) continue; @@ -165,19 +213,26 @@ static void alias_find_window(struct alias* alias) { CFNumberGetValue(layer_ref, CFNumberGetType(layer_ref), &layer); if (layer != MENUBAR_LAYER) continue; - CFNumberGetValue(owner_pid_ref, - CFNumberGetType(owner_pid_ref), - &alias->pid ); + // Get the source PID on macOS 26+, otherwise use the window owner PID + if (use_source_pid_workaround) { + pid_t source_pid = source_pid_for_window(bounds); + if (source_pid != 0) { + alias->pid = source_pid; + } else { + CFNumberGetValue(owner_pid_ref, + CFNumberGetType(owner_pid_ref), + &alias->pid ); + } + } else { + CFNumberGetValue(owner_pid_ref, + CFNumberGetType(owner_pid_ref), + &alias->pid ); + } CFNumberRef window_id_ref = CFDictionaryGetValue(dictionary, kCGWindowNumber); if (!window_id_ref) continue; - CFDictionaryRef bounds_ref = CFDictionaryGetValue(dictionary, kCGWindowBounds); - if (!bounds_ref) continue; - - CGRect bounds; - CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds); uint64_t wid; CFNumberGetValue(window_id_ref, diff --git a/src/source_pid.h b/src/source_pid.h new file mode 100644 index 0000000..40c7579 --- /dev/null +++ b/src/source_pid.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +// In macOS 26 (Tahoe), menu bar item windows are owned by Control Center +// instead of their source applications. This module provides functions to +// find the actual source PID using the Accessibility API. + +// Check if we're running on macOS 26 or later where this workaround is needed +bool source_pid_needs_workaround(void); + +// Get the source PID for a menu bar item window by matching its bounds +// to accessibility elements in running applications' extras menu bars. +// Returns 0 if the source PID cannot be determined. +pid_t source_pid_for_window(CGRect window_bounds); + +// Get the source application name for a menu bar item window. +// Returns NULL if not found. Caller must free the returned string. +char* source_name_for_window(CGRect window_bounds); + +// Initialize the source PID cache (call once at startup) +void source_pid_cache_init(void); + +// Refresh the cached running applications list +void source_pid_cache_refresh(void); diff --git a/src/source_pid.m b/src/source_pid.m new file mode 100644 index 0000000..821332f --- /dev/null +++ b/src/source_pid.m @@ -0,0 +1,277 @@ +#include "source_pid.h" +#include "misc/helpers.h" +#include +#include +#include + +// The Accessibility API attribute for the extras menu bar (status items area) +// This is documented in Apple's Accessibility Programming Guide +#define kAXExtrasMenuBarAttribute CFSTR("AXExtrasMenuBar") + +// Cache for running applications and their extras menu bars +typedef struct { + pid_t pid; + char* name; + AXUIElementRef app_element; + AXUIElementRef extras_menu_bar; + bool has_extras_menu_bar; +} cached_app_t; + +static cached_app_t* g_cached_apps = NULL; +static int g_cached_apps_count = 0; +static pthread_mutex_t g_cache_mutex = PTHREAD_MUTEX_INITIALIZER; +static bool g_initialized = false; + +// Helper to calculate distance between two points +static double point_distance(CGPoint a, CGPoint b) { + double dx = a.x - b.x; + double dy = a.y - b.y; + return sqrt(dx * dx + dy * dy); +} + +// Helper to get the center of a rect +static CGPoint rect_center(CGRect rect) { + return CGPointMake(rect.origin.x + rect.size.width / 2.0, + rect.origin.y + rect.size.height / 2.0); +} + +bool source_pid_needs_workaround(void) { + // macOS Tahoe (26.x) changed menu bar item ownership to Control Center + // Check at runtime using NSProcessInfo + NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + // macOS Tahoe is version 26.x + return version.majorVersion >= 26; +} + +void source_pid_cache_init(void) { + if (g_initialized) return; + g_initialized = true; + source_pid_cache_refresh(); +} + +static void free_cached_app(cached_app_t* app) { + if (app->name) { + free(app->name); + app->name = NULL; + } + if (app->extras_menu_bar) { + CFRelease(app->extras_menu_bar); + app->extras_menu_bar = NULL; + } + if (app->app_element) { + CFRelease(app->app_element); + app->app_element = NULL; + } +} + +void source_pid_cache_refresh(void) { + pthread_mutex_lock(&g_cache_mutex); + + // Free existing cache + if (g_cached_apps) { + for (int i = 0; i < g_cached_apps_count; i++) { + free_cached_app(&g_cached_apps[i]); + } + free(g_cached_apps); + g_cached_apps = NULL; + g_cached_apps_count = 0; + } + + // Get list of running applications + NSArray* running_apps = [[NSWorkspace sharedWorkspace] runningApplications]; + g_cached_apps_count = (int)[running_apps count]; + g_cached_apps = calloc(g_cached_apps_count, sizeof(cached_app_t)); + + int actual_count = 0; + for (NSRunningApplication* app in running_apps) { + // Skip apps that can't have menu bar items + if (app.activationPolicy == NSApplicationActivationPolicyProhibited) { + continue; + } + + pid_t pid = app.processIdentifier; + + cached_app_t* cached = &g_cached_apps[actual_count]; + cached->pid = pid; + cached->name = app.localizedName ? strdup([app.localizedName UTF8String]) : NULL; + cached->app_element = AXUIElementCreateApplication(pid); + cached->extras_menu_bar = NULL; + cached->has_extras_menu_bar = false; + + actual_count++; + } + g_cached_apps_count = actual_count; + + pthread_mutex_unlock(&g_cache_mutex); +} + +// Try to get the extras menu bar for an application (lazy initialization) +static AXUIElementRef get_extras_menu_bar(cached_app_t* app) { + if (app->extras_menu_bar) { + return app->extras_menu_bar; + } + + if (app->has_extras_menu_bar == false && app->app_element == NULL) { + return NULL; + } + + // Check if accessibility is trusted + if (!AXIsProcessTrusted()) { + return NULL; + } + + // Try to get the extras menu bar attribute + AXUIElementRef extras_bar = NULL; + AXError error = AXUIElementCopyAttributeValue(app->app_element, + kAXExtrasMenuBarAttribute, + (CFTypeRef*)&extras_bar); + + if (error == kAXErrorSuccess && extras_bar) { + app->extras_menu_bar = extras_bar; + app->has_extras_menu_bar = true; + return extras_bar; + } + + // Mark that we've tried and failed + app->has_extras_menu_bar = false; + return NULL; +} + +// Get the frame of an accessibility element +static bool get_ax_frame(AXUIElementRef element, CGRect* out_frame) { + AXValueRef position_value = NULL; + AXValueRef size_value = NULL; + CGPoint position; + CGSize size; + + AXError err = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, + (CFTypeRef*)&position_value); + if (err != kAXErrorSuccess || !position_value) { + return false; + } + + err = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, + (CFTypeRef*)&size_value); + if (err != kAXErrorSuccess || !size_value) { + CFRelease(position_value); + return false; + } + + bool success = false; + if (AXValueGetValue(position_value, kAXValueCGPointType, &position) && + AXValueGetValue(size_value, kAXValueCGSizeType, &size)) { + out_frame->origin = position; + out_frame->size = size; + success = true; + } + + CFRelease(position_value); + CFRelease(size_value); + return success; +} + +// Check if an accessibility element is enabled +static bool is_ax_element_enabled(AXUIElementRef element) { + CFBooleanRef enabled_ref = NULL; + AXError err = AXUIElementCopyAttributeValue(element, kAXEnabledAttribute, + (CFTypeRef*)&enabled_ref); + if (err != kAXErrorSuccess || !enabled_ref) { + return false; + } + + bool enabled = CFBooleanGetValue(enabled_ref); + CFRelease(enabled_ref); + return enabled; +} + +// Get children of an accessibility element +static CFArrayRef get_ax_children(AXUIElementRef element) { + CFArrayRef children = NULL; + AXUIElementCopyAttributeValue(element, kAXChildrenAttribute, + (CFTypeRef*)&children); + return children; +} + +pid_t source_pid_for_window(CGRect window_bounds) { + if (!source_pid_needs_workaround()) { + return 0; + } + + if (!AXIsProcessTrusted()) { + return 0; + } + + source_pid_cache_init(); + + pthread_mutex_lock(&g_cache_mutex); + + CGPoint window_center = rect_center(window_bounds); + pid_t result_pid = 0; + + // Prioritize apps that already have a cached extras menu bar + // by iterating through them first + for (int pass = 0; pass < 2 && result_pid == 0; pass++) { + for (int i = 0; i < g_cached_apps_count && result_pid == 0; i++) { + cached_app_t* app = &g_cached_apps[i]; + + // First pass: only check apps with cached extras menu bars + // Second pass: try to get extras menu bars for remaining apps + if (pass == 0 && !app->has_extras_menu_bar) continue; + if (pass == 1 && app->has_extras_menu_bar) continue; + + AXUIElementRef extras_bar = get_extras_menu_bar(app); + if (!extras_bar) continue; + + CFArrayRef children = get_ax_children(extras_bar); + if (!children) continue; + + CFIndex child_count = CFArrayGetCount(children); + for (CFIndex j = 0; j < child_count; j++) { + AXUIElementRef child = (AXUIElementRef)CFArrayGetValueAtIndex(children, j); + + if (!is_ax_element_enabled(child)) continue; + + CGRect child_frame; + if (!get_ax_frame(child, &child_frame)) continue; + + // Check if the centers are close enough (within 1 point) + CGPoint child_center = rect_center(child_frame); + double distance = point_distance(window_center, child_center); + + if (distance <= 1.0) { + result_pid = app->pid; + break; + } + } + + CFRelease(children); + } + } + + pthread_mutex_unlock(&g_cache_mutex); + return result_pid; +} + +char* source_name_for_window(CGRect window_bounds) { + if (!source_pid_needs_workaround()) { + return NULL; + } + + pid_t source_pid = source_pid_for_window(window_bounds); + if (source_pid == 0) { + return NULL; + } + + pthread_mutex_lock(&g_cache_mutex); + + char* result = NULL; + for (int i = 0; i < g_cached_apps_count; i++) { + if (g_cached_apps[i].pid == source_pid && g_cached_apps[i].name) { + result = strdup(g_cached_apps[i].name); + break; + } + } + + pthread_mutex_unlock(&g_cache_mutex); + return result; +} From 8323d1c4a9beea37841b0bfadb4a80f605aca043 Mon Sep 17 00:00:00 2001 From: Roel Van Gils Date: Fri, 12 Dec 2025 09:43:52 +0100 Subject: [PATCH 2/2] improve: user-friendly menu items query and cache throttling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reformatted --query default_menu_items output to show a clean table with app names and ready-to-use alias commands - Added 5-second throttle to source_pid_cache_refresh() to prevent performance issues when multiple aliases update simultaneously - Moved throttle check inside mutex lock to prevent race conditions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/alias.c | 24 +++++++++++++++++------- src/source_pid.m | 13 +++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/alias.c b/src/alias.c index 5f16ddc..dc0b3dd 100644 --- a/src/alias.c +++ b/src/alias.c @@ -88,7 +88,11 @@ void print_all_menu_items(FILE* rsp) { } if (item_count > 0) { - fprintf(rsp, "[\n"); + fprintf(rsp, "Available menu bar items for aliases:\n"); + fprintf(rsp, "=====================================\n\n"); + fprintf(rsp, "%-30s %s\n", "APP NAME", "ALIAS COMMAND"); + fprintf(rsp, "%-30s %s\n", "--------", "-------------"); + int counter = 0; for (int i = 0; i < item_count; i++) { float current_pos = x_pos[0]; @@ -103,19 +107,24 @@ void print_all_menu_items(FILE* rsp) { if (!name[current_pos_id] || !owner[current_pos_id]) continue; if (strcmp(name[current_pos_id], "") != 0) { - if (counter++ > 0) { - fprintf(rsp, ", \n"); - } - fprintf(rsp, "\t\"%s,%s\"", owner[current_pos_id], - name[current_pos_id] ); + // Show clean app name on the left, full alias command on the right + fprintf(rsp, "%-30s --add alias \"%s,%s\" \n", + owner[current_pos_id], + owner[current_pos_id], + name[current_pos_id]); + counter++; } x_pos[current_pos_id] = -9999.f; } - fprintf(rsp, "\n]\n"); + fprintf(rsp, "\nFound %d menu bar items.\n", counter); + fprintf(rsp, "Position can be: left, center, right\n"); + for (int i = 0; i < window_count; i++) { if (owner[i]) free(owner[i]); if (name[i]) free(name[i]); } + } else { + fprintf(rsp, "No menu bar items found.\n"); } CFRelease(window_list); } @@ -198,6 +207,7 @@ static void alias_find_window(struct alias* alias) { bool name_matches = (alias->name && strcmp(alias->name, name) == 0) || (!alias->name && strcmp(name, "") != 0); + if (resolved_owner != owner) free(resolved_owner); free(owner); free(name); diff --git a/src/source_pid.m b/src/source_pid.m index 821332f..57d5d2b 100644 --- a/src/source_pid.m +++ b/src/source_pid.m @@ -21,6 +21,8 @@ static int g_cached_apps_count = 0; static pthread_mutex_t g_cache_mutex = PTHREAD_MUTEX_INITIALIZER; static bool g_initialized = false; +static uint64_t g_last_refresh_time = 0; +static const uint64_t CACHE_REFRESH_INTERVAL_NS = 5000000000ULL; // 5 seconds in nanoseconds // Helper to calculate distance between two points static double point_distance(CGPoint a, CGPoint b) { @@ -65,8 +67,19 @@ static void free_cached_app(cached_app_t* app) { } void source_pid_cache_refresh(void) { + // Throttle refreshes to avoid performance issues + uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC); + pthread_mutex_lock(&g_cache_mutex); + // Check throttle inside the lock to avoid race conditions + if (g_last_refresh_time > 0 && (now - g_last_refresh_time) < CACHE_REFRESH_INTERVAL_NS) { + pthread_mutex_unlock(&g_cache_mutex); + return; // Skip refresh if we refreshed recently + } + + g_last_refresh_time = now; + // Free existing cache if (g_cached_apps) { for (int i = 0; i < g_cached_apps_count; i++) {