From 1ef26863969e4ea7467302d371993480d8480ab3 Mon Sep 17 00:00:00 2001 From: rico <565636992@qq.com> Date: Mon, 20 Apr 2026 13:08:50 +0800 Subject: [PATCH] feat(status-item): add right-click context menu support Add native NSMenu support for status item right-click / ctrl-click: Swift: - Track statusMenu on AppDelegate, handle rightMouseUp and ctrl+click to pop up NSMenu via NSMenu.popUpContextMenu - Parse 'status-menu' JSON command to build menu items dynamically - Emit {type:'menu', id:'...'} on stdout when a menu item is selected Node wrapper (glimpse.mjs): - Add setMenu(items) method to send 'status-menu' command - Emit 'menu' event with selected item id This enables agents to add quit, toggle, and action items to the status bar icon without building a custom popover UI. Amp-Thread-ID: https://ampcode.com/threads/T-019da911-1c0d-734f-bbfa-754a6c09a6a1 --- src/glimpse.mjs | 7 +++++++ src/glimpse.swift | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/glimpse.mjs b/src/glimpse.mjs index 290681a..84740a1 100644 --- a/src/glimpse.mjs +++ b/src/glimpse.mjs @@ -113,6 +113,9 @@ class GlimpseWindow extends EventEmitter { case 'click': this.emit('click'); break; + case 'menu': + this.emit('menu', msg.id); + break; case 'closed': if (!this.#closed) { this.#closed = true; @@ -257,6 +260,10 @@ class GlimpseStatusItem extends GlimpseWindow { this._write({ type: 'title', title }); } + setMenu(items) { + this._write({ type: 'status-menu', items }); + } + resize(width, height) { this._write({ type: 'resize', width, height }); } diff --git a/src/glimpse.swift b/src/glimpse.swift index 07584e4..6294728 100644 --- a/src/glimpse.swift +++ b/src/glimpse.swift @@ -334,6 +334,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScri var nsStatusItem: NSStatusItem? var popover: NSPopover? var popoverViewController: StatusItemViewController? + var statusMenu: NSMenu? nonisolated init(config: Config) { self.config = config @@ -467,6 +468,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScri button.title = config.title == "Glimpse" ? "G" : config.title button.action = #selector(statusItemClicked(_:)) button.target = self + button.sendAction(on: [.leftMouseUp, .rightMouseUp]) } // Load blank page to trigger first ready @@ -475,6 +477,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScri @objc func statusItemClicked(_ sender: Any?) { guard let button = nsStatusItem?.button, let popover = popover else { return } + if let event = NSApp.currentEvent, + (event.type == .rightMouseUp || (event.type == .leftMouseUp && event.modifierFlags.contains(.control))), + let statusMenu { + NSMenu.popUpContextMenu(statusMenu, with: event, for: button) + return + } if popover.isShown { popover.performClose(sender) } else { @@ -483,6 +491,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScri writeToStdout(["type": "click"]) } + @objc func statusMenuItemSelected(_ sender: NSMenuItem) { + if let id = sender.representedObject as? String { + writeToStdout(["type": "menu", "id": id]) + } + } + // MARK: - Follow Cursor func computeTargetPosition(mouse: NSPoint) -> NSPoint { @@ -766,6 +780,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScri } else { window.setContentSize(size) } + case "status-menu": + guard config.statusItem else { + log("status-menu not supported outside status-item mode") + return + } + let items = json["items"] as? [[String: Any]] ?? [] + let menu = NSMenu() + for item in items { + guard let id = item["id"] as? String, let title = item["title"] as? String else { + continue + } + let menuItem = NSMenuItem(title: title, action: #selector(statusMenuItemSelected(_:)), keyEquivalent: "") + menuItem.target = self + menuItem.representedObject = id + menu.addItem(menuItem) + } + statusMenu = menu.items.isEmpty ? nil : menu case "close": closeAndExit() default: