Skip to content
Merged
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
2 changes: 2 additions & 0 deletions native/patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ this accepted outcome.
operation carries the exact process generation and title array, avoiding the existing String
API's splitting, fuzzy normalization and intermediate presses. Fresh bounded raw AX traversal
requires a unique path and supported enabled leaf before one AXPress; lazy missing paths refuse.
Titles match exact UTF-16, including ellipses. A title with no match refuses with its path level,
the bounded sibling titles already read, and a lone sibling that only adds a trailing ellipsis.
The whole native operation runs off MainActor while retaining the existing process write lane
until actual return, with cancellation checked before dispatch. Per-element AX messaging timeouts
do not race or abandon native work. `cannotComplete` and other ambiguous delivery failures remain
Expand Down
36 changes: 33 additions & 3 deletions native/patches/peekaboo-menu.patch
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
diff --git a/Core/PeekabooAutomationKit/Sources/PeekabooAutomationKit/Services/UI/MenuCommand.swift b/Core/PeekabooAutomationKit/Sources/PeekabooAutomationKit/Services/UI/MenuCommand.swift
new file mode 100644
index 000000000..094ee51ee
index 000000000..5954f209d
--- /dev/null
+++ b/Core/PeekabooAutomationKit/Sources/PeekabooAutomationKit/Services/UI/MenuCommand.swift
@@ -0,0 +1,285 @@
@@ -0,0 +1,315 @@
+import ApplicationServices
+import Foundation
+import os
Expand Down Expand Up @@ -258,6 +258,7 @@ index 000000000..094ee51ee
+ var chain: [AXUIElement] = [menu]
+ for (index, title) in self.request.path.enumerated() {
+ var matches: [AXUIElement] = []
+ var candidates: [String] = []
+ for child in try self.children(menu) {
+ let role = try self.role(child)
+ // Separators and known non-item roles cannot compete for this literal title.
Expand All @@ -267,12 +268,13 @@ index 000000000..094ee51ee
+ guard let candidate = rawValue as? String else {
+ throw self.refusal("A candidate menu title is unavailable, so its uniqueness cannot be established.")
+ }
+ candidates.append(candidate)
+ if candidate.utf16.elementsEqual(title.utf16) { matches.append(child) }
+ }
+ guard matches.count == 1, let item = matches.first else {
+ throw DesktopActionFailure.preDispatchRefusal(
+ reason: .invalidRequest,
+ message: matches.isEmpty ? "The literal menu path is unavailable without opening ancestor menus." : "The literal menu path is ambiguous.")
+ message: self.mismatch(at: index, matches: matches.count, candidates: candidates))
+ }
+ chain.append(item)
+ if index == self.request.path.count - 1 { return chain }
Expand All @@ -288,6 +290,34 @@ index 000000000..094ee51ee
+ }
+ throw self.refusal("The literal menu path contains no command.")
+ }
+
+ /// Names the failed component and the sibling titles already read; matching itself stays literal.
+ func mismatch(at index: Int, matches: Int, candidates: [String]) -> String {
+ let title = self.request.path[index]
+ let place = index == 0 ? "in the menu bar" : "under \(String(reflecting: self.request.path[index - 1]))"
+ let level = "level \(index + 1) of \(self.request.path.count)"
+ if matches > 1 {
+ return "The literal menu path is ambiguous: \(matches) items titled \(String(reflecting: title)) \(place) (\(level))."
+ }
+ var message = "No menu item titled \(String(reflecting: title)) \(place) (\(level))"
+ let near = candidates.filter {
+ $0.utf16.elementsEqual((title + "\u{2026}").utf16) || $0.utf16.elementsEqual((title + "...").utf16)
+ }
+ message += near.count == 1 ? "; did you mean \(String(reflecting: near[0]))?" : "."
+ message += " Titles must match exactly."
+ let titles = candidates.filter { !$0.isEmpty }.map { String(reflecting: $0) }
+ guard !titles.isEmpty else { return message + " No titled items were read \(place)." }
+ var listed: [String] = []
+ var length = 0
+ for quoted in titles {
+ guard listed.count < 30, length + quoted.count <= 300 else { break }
+ listed.append(quoted)
+ length += quoted.count + 2
+ }
+ if listed.isEmpty { return message + " The \(titles.count) titles \(place) are too long to list." }
+ let rest = titles.count - listed.count
+ return message + " Titles \(place): \(listed.joined(separator: ", "))" + (rest > 0 ? ", and \(rest) more." : ".")
+ }
+}
diff --git a/Core/PeekabooCore/Sources/PeekabooBridge/PeekabooBridgeMenuCommand.swift b/Core/PeekabooCore/Sources/PeekabooBridge/PeekabooBridgeMenuCommand.swift
new file mode 100644
Expand Down
Loading