vercel-labs / vercel-labs/native

Menu-bar apps: window close-policy (hide instead of quit) and tray commands for app show/quit

Open
#149 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Zig
Stars
7.7k
Forks
314
Avg merge
5h
Merged PRs (30d)
13

Description

Version: native 0.5.3 (commit 57bf56b) · macOS

We're building a menu-bar music player (internet-radio client: status_item_fn tray, audio via fx.playAudio). Two lifecycle behaviors of that app class currently have no seam, and both need the same kind of host-side support. Filing together since they're two halves of one pattern.

1. Closing the last window always quits the app

windowWillClose in the AppKit host emits shutdown when windows.count == 0. For a media app, the red close button killing playback is wrong — the expected shape is Spotify-like: close hides the window, the audio and the tray keep running, and a Dock-icon click brings the window back. There's currently no policy hook: no windowShouldClose on the delegate and no manifest option.

Proposal: a declarative policy on ShellWindow / WindowDescriptor, e.g.

.close_policy = .quit (default) | .hide_app | .hide_window

.hide_app mapping cleanly onto [NSApp hide:] (Dock click restores for free, Cmd+Q/SIGTERM/AppleScript quit still terminate normally).

Our local patch (running fine for a while now) is a windowShouldClose: on NativeSdkWindowDelegate that hides the app when a status item is installed and the closing window is "main":

- (BOOL)windowShouldClose:(NSWindow *)sender {
    (void)sender;
    if (self.host.statusItem == nil) return YES;
    NSString *label = self.host.windowLabels[@(self.windowId)];
    if ([label isEqualToString:@"main"]) {
        [NSApp hide:nil];
        return NO;
    }
    return YES;
}

2. Tray items can't open (unhide) or quit the app

Once close-hides exists, the tray needs "Open player" and "Quit" rows — but tray commands dispatch into update, and the model has no channel to [NSApp unhide:] / [NSApp activate...] / [NSApp terminate:]. (The status_item docs example even shows .command = "app.quit", but nothing handles that name.)

Proposal: well-known built-in tray commands the host handles before (or instead of) forwarding — e.g. "app.show" → unhide + activate, "app.quit" → terminate — mirroring what the docs already imply.

Our local patch reserves item ids in trayMenuItemClicked::

    if (menuItem.tag == 100) {          /* "Open player" */
        [NSApp unhide:nil];
        [NSApp activateIgnoringOtherApps:YES];
    } else if (menuItem.tag == 101) {   /* "Quit" */
        [NSApp terminate:nil];
        return;
    }

Both patches together give the full menu-bar-app close/reopen/quit loop; we'd love to drop them for first-class options. Happy to send PRs if the proposed shapes look right.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the AppKit host's NativeSdkWindowDelegate and its windowWillClose/windowShouldClose behavior, then trace ShellWindow or WindowDescriptor and the trayMenuItemClicked: entry point. Compare the status_item documentation example with the host command dispatch. Done means a declared close policy and built-in show/quit commands support the described hide, reopen, and termination flows without breaking default quitting.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, objective-c, zig
Domain
desktop
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.