Skip to content
Closed
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
5 changes: 5 additions & 0 deletions cmd/hexecute/run_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ func runMain(app *models.App, _ *config.Settings) {
if err := window.RegisterHotkey(keyCode, modifiers); err != nil {
log.Fatal("Failed to register global hotkey:", err)
}

// Add a menu-bar item so the accessory agent (no Dock icon) is visible and
// can be cast or quit from the menu bar, not only via the hot key.
window.SetupMenuBar()

log.Printf("Hexecute is running; press %s (or relaunch the app) to cast a gesture.", spec)

for {
Expand Down
7 changes: 7 additions & 0 deletions pkg/cocoa/cocoa.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func (w *CocoaWindow) RegisterHotkey(keyCode, modifiers uint32) error {
return nil
}

// SetupMenuBar adds a status-bar (menu bar) item for the resident agent, with a
// menu to cast on demand or quit. macOS-only; called from the background agent
// path, so it is not part of the platform.Window interface.
func (w *CocoaWindow) SetupMenuBar() {
C.cocoa_setup_menu_bar()
}

// WaitForShow blocks until a show is requested, either by the registered global
// hot key or by a relaunch of the resident agent (reopen Apple event).
func (w *CocoaWindow) WaitForShow() {
Expand Down
4 changes: 4 additions & 0 deletions pkg/cocoa/cocoa.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ void cocoa_hide(void);
// Returns 0 on success.
int cocoa_register_hotkey(uint32_t keyCode, uint32_t modifiers);

// cocoa_setup_menu_bar adds a status-bar (menu bar) item for the resident agent,
// with a menu to cast on demand or quit. Idempotent; must run on the main thread.
void cocoa_setup_menu_bar(void);

// cocoa_wait_for_show blocks, pumping the event loop, until a show is requested
// by the hot key or by a relaunch (reopen Apple event).
void cocoa_wait_for_show(void);
Expand Down
83 changes: 83 additions & 0 deletions pkg/cocoa/cocoa.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:

static HexAppDelegate *g_app_delegate = nil;

// Menu-bar status item for the resident agent. The agent runs as an accessory
// (no Dock icon), so the status item is the only visible affordance: it shows
// Hexecute is running and offers a menu to cast on demand or quit.
static NSStatusItem *g_status_item = nil;

// Targets for the status-item menu. cast funnels through request_show (the same
// path as the hot key and relaunch); quit terminates the resident agent.
@interface HexStatusController : NSObject
@end

@implementation HexStatusController
- (void)cast:(id)sender {
(void)sender;
request_show();
}
- (void)quit:(id)sender {
(void)sender;
[NSApp terminate:nil];
}
@end

static HexStatusController *g_status_controller = nil;

// Convert a macOS virtual key code to an XKB keysym. Only the keys the app
// reacts to need mapping; everything else reports 0 (no key).
static uint32_t map_keycode(unsigned short keyCode) {
Expand Down Expand Up @@ -284,6 +307,61 @@ int cocoa_register_hotkey(uint32_t keyCode, uint32_t modifiers) {
}
}

// cocoa_setup_menu_bar adds a status-bar item (right side of the menu bar) for
// the resident agent, with a menu to cast on demand or quit. Idempotent and a
// no-op if a status item already exists. Must run on the main thread.
void cocoa_setup_menu_bar(void) {
@autoreleasepool {
if (g_status_item) {
return;
}

g_status_controller = [[HexStatusController alloc] init];

// statusItemWithLength: returns an autoreleased item and the system
// status bar does not keep a reliable strong reference, so under manual
// reference counting (cgo compiles this without ARC) it must be retained
// or it is deallocated when this pool drains and the icon never appears.
g_status_item = [[[NSStatusBar systemStatusBar]
statusItemWithLength:NSVariableStatusItemLength] retain];

// Prefer a template SF Symbol (the system tints it for light/dark menu
// bars); wand.and.stars matches the spell-casting metaphor. Fall back to
// a glyph title if the symbol is unavailable. LSMinimumSystemVersion is
// 11.0, where imageWithSystemSymbolName exists.
NSImage *icon = nil;
if (@available(macOS 11.0, *)) {
icon = [NSImage imageWithSystemSymbolName:@"wand.and.stars"
accessibilityDescription:@"Hexecute"];
}
if (icon) {
[icon setTemplate:YES];
g_status_item.button.image = icon;
} else {
g_status_item.button.title = @"✦";
}
g_status_item.button.toolTip = @"Hexecute";

NSMenu *menu = [[NSMenu alloc] init];

NSMenuItem *castItem = [[NSMenuItem alloc] initWithTitle:@"Cast Gesture"
action:@selector(cast:)
keyEquivalent:@""];
[castItem setTarget:g_status_controller];
[menu addItem:castItem];

[menu addItem:[NSMenuItem separatorItem]];

NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:@"Quit Hexecute"
action:@selector(quit:)
keyEquivalent:@"q"];
[quitItem setTarget:g_status_controller];
[menu addItem:quitItem];

g_status_item.menu = menu;
}
}

// cocoa_wait_for_show blocks (pumping the event loop, so the hot key, reopen
// Apple event, and other events are dispatched) until a show is requested by the
// hot key or a relaunch.
Expand Down Expand Up @@ -413,6 +491,11 @@ void cocoa_clear_last_key(void) {

void cocoa_destroy(void) {
[NSCursor unhide];
if (g_status_item) {
[[NSStatusBar systemStatusBar] removeStatusItem:g_status_item];
[g_status_item release];
g_status_item = nil;
}
if (g_context) {
[g_context clearDrawable];
g_context = nil;
Expand Down
Loading