import AXorcist
import Commander
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation
/// Command for interacting with macOS menu bar items (status items).
@MainActor
struct MenuBarCommand: ParsableCommand, OutputFormattable {
nonisolated(unsafe) static var commandDescription: CommandDescription {
MainActorCommandDescription.describe {
CommandDescription(
commandName: "menubar",
abstract: "Interact with macOS menu bar items (status items)",
discussion: """
The menubar command provides specialized support for interacting with menu bar items
(also known as status items) on macOS. These are the icons that appear on the right
side of the menu bar.
FEATURES:
⢠Fuzzy matching - Partial text and case-insensitive search
⢠Index-based clicking - Use item number from list output
⢠Smart error messages - Shows available items when not found
⢠JSON output support - For scripting and automation
EXAMPLES:
# List all menu bar items with indices
peekaboo menubar list
peekaboo menubar list --json-output # JSON format
# Click by exact or partial name (case-insensitive)
peekaboo menubar click "Wi-Fi" # Exact match
peekaboo menubar click "wi" # Partial match
peekaboo menubar click "Bluetooth" # Click Bluetooth icon
# Click by index from the list
peekaboo menubar click --index 3 # Click the 3rd item
NOTE: Menu bar items are different from regular application menus. For application
menus (File, Edit, etc.), use the 'menu' command instead.
""",
showHelpOnEmptyInvocation: true
)
}
}
@Argument(help: "Action to perform (list or click)")
var action: String
@Argument(help: "Name of the menu bar item to click (for click action)")
var itemName: String?
@Option(help: "Index of the menu bar item (0-based)")
var index: Int?
@Flag(help: "Include raw debug fields (window owner/layer) in JSON output")
var includeRawDebug: Bool = false
@RuntimeStorage private var runtime: CommandRuntime?
private var resolvedRuntime: CommandRuntime {
guard let runtime else {
preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
}
return runtime
}
private var services: any PeekabooServiceProviding { self.resolvedRuntime.services }
private var logger: Logger { self.resolvedRuntime.logger }
var outputLogger: Logger { self.logger }
private var configuration: CommandRuntime.Configuration { self.resolvedRuntime.configuration }
var jsonOutput: Bool { self.configuration.jsonOutput }
private var isVerbose: Bool { self.configuration.verbose }
@MainActor
mutating func run(using runtime: CommandRuntime) async throws {
self.runtime = runtime
switch self.action.lowercased() {
case "list":
try await self.listMenuBarItems()
case "click":
try await self.clickMenuBarItem()
default:
throw PeekabooError.invalidInput("Unknown action '\(self.action)'. Use 'list' or 'click'.")
}
}
@MainActor
private func listMenuBarItems() async throws {
let startTime = Date()
do {
self.logger.debug("Listing menu bar items includeRawDebug=\(self.includeRawDebug)")
let menuBarItems = try await MenuServiceBridge.listMenuBarItems(
menu: self.services.menu,
includeRaw: self.includeRawDebug
)
if self.jsonOutput {
let output = ListJSONOutput(
success: true,
menuBarItems: menuBarItems.map { item in
JSONMenuBarItem(
title: item.title,
raw_title: item.rawTitle,
bundle_id: item.bundleIdentifier,
owner_name: item.ownerName,
identifier: item.identifier,
ax_identifier: item.axIdentifier,
ax_description: item.axDescription,
raw_window_id: item.rawWindowID,
raw_window_layer: item.rawWindowLayer,
raw_owner_pid: item.rawOwnerPID,
raw_source: item.rawSource,
index: item.index,
isVisible: item.isVisible,
description: item.description
)
},
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output, logger: self.outputLogger)
} else {
if menuBarItems.isEmpty {
print("No menu bar items found.")
} else {
print("š Menu Bar Items:")
for item in menuBarItems {
var info = " [\(item.index)] \(item.title ?? "Untitled")"
if !item.isVisible {
info += " (hidden)"
}
if let desc = item.description, self.isVerbose {
info += " - \(desc)"
}
print(info)
}
print("\nš” Tip: Use 'peekaboo menubar click \"name\"' to click a menu bar item")
}
}
} catch {
if self.jsonOutput {
let output = JSONErrorOutput(
success: false,
error: error.localizedDescription,
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output, logger: self.outputLogger)
} else {
throw error
}
}
}
@MainActor
private func clickMenuBarItem() async throws {
let startTime = Date()
do {
let result: PeekabooCore.ClickResult
if let idx = self.index {
result = try await MenuServiceBridge.clickMenuBarItem(at: idx, menu: self.services.menu)
} else if let name = self.itemName {
result = try await MenuServiceBridge.clickMenuBarItem(named: name, menu: self.services.menu)
} else {
throw PeekabooError.invalidInput("Please provide either a menu bar item name or use --index")
}
if self.jsonOutput {
let output = ClickJSONOutput(
success: true,
clicked: result.elementDescription,
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output, logger: self.outputLogger)
} else {
print("ā
Clicked menu bar item: \(result.elementDescription)")
if self.isVerbose {
print("ā±ļø Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s")
}
}
} catch {
if self.jsonOutput {
let output = JSONErrorOutput(
success: false,
error: error.localizedDescription,
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output, logger: self.outputLogger)
} else {
// Provide helpful hints for common errors
if error.localizedDescription.contains("not found") {
print("ā Error: \(error.localizedDescription)")
print("\nš” Hints:")
print(" ⢠Menu bar items often require clicking on their icon coordinates")
print(" ⢠Try 'peekaboo see' first to get element IDs")
print(" ⢠Use 'peekaboo menubar list' to see available items")
} else {
throw error
}
}
}
}
}
// MARK: - JSON Output Types
private struct JSONMenuBarItem: Codable {
let title: String?
let raw_title: String?
let bundle_id: String?
let owner_name: String?
let identifier: String?
let ax_identifier: String?
let ax_description: String?
let raw_window_id: CGWindowID?
let raw_window_layer: Int?
let raw_owner_pid: pid_t?
let raw_source: String?
let index: Int
let isVisible: Bool
let description: String?
}
private struct ListJSONOutput: Codable {
let success: Bool
let menuBarItems: [JSONMenuBarItem]
let executionTime: TimeInterval
}
private struct ClickJSONOutput: Codable {
let success: Bool
let clicked: String
let executionTime: TimeInterval
}
private struct JSONErrorOutput: Codable {
let success: Bool
let error: String
let executionTime: TimeInterval
}
extension MenuBarCommand: AsyncRuntimeCommand {}
@MainActor
extension MenuBarCommand: CommanderBindableCommand {
mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
self.action = try values.decodePositional(0, label: "action")
self.itemName = try values.decodeOptionalPositional(1, label: "itemName")
self.index = try values.decodeOption("index", as: Int.self)
self.includeRawDebug = values.flag("includeRawDebug")
}
}