mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 08:14:38 +08:00
Adds a Navigation feature: one settings pane, one switch, two commands that move you somewhere rather than changing something. Switch Windows sweeps every regular app's standard windows over AX, minimized ones included, orders them most-recently-used and raises the chosen one. Recency comes from a single CGWindowListCopyWindowInfo call for per-app front rank — public API, no Screen Recording grant and no long-lived activation observer. Search Menu Bar Items is today's Search Menu Items, moved out of Settings > Commands into the new pane and renamed. Its raw id is unchanged, so recorded shortcuts, aliases and visibility keys survive. It also gains a Disabled Applications list: MenuSearchTarget.classify answers .excluded before the menu-bar test, so an excluded app starts no walk at all rather than having a read menu filtered afterwards. The exclusion list is the Clipboard section lifted into a shared DisabledApplicationsSection, and AppPickerPopover moves to Features/Launcher/Settings/ where its three consumers already pointed.
124 lines
4.6 KiB
Swift
124 lines
4.6 KiB
Swift
import Foundation
|
|
|
|
@main
|
|
@MainActor
|
|
struct WindowSwitchTests {
|
|
static var failures = 0
|
|
|
|
static func expect(_ condition: @autoclosure () -> Bool, _ message: String) {
|
|
if !condition() {
|
|
failures += 1
|
|
print("FAIL: \(message)")
|
|
}
|
|
}
|
|
|
|
static func entry(
|
|
_ handle: Int, app: String = "Safari", title: String = "Window",
|
|
minimized: Bool = false, rank: Int = 0
|
|
) -> WindowSwitchEntry {
|
|
WindowSwitchEntry(
|
|
handle: handle, appName: app, bundleID: "com.example.\(app.lowercased())",
|
|
iconURL: nil, iconStamp: 0, title: title, isMinimized: minimized, appRank: rank)
|
|
}
|
|
|
|
static func main() {
|
|
identity()
|
|
displayTitle()
|
|
searchMapping()
|
|
ordering()
|
|
orderingIsTotal()
|
|
ranking()
|
|
rankingLimit()
|
|
|
|
print(failures == 0 ? "Window switch tests passed" : "\(failures) window switch tests failed")
|
|
exit(failures == 0 ? 0 : 1)
|
|
}
|
|
|
|
static func identity() {
|
|
expect(entry(7).id == "7", "the id is the handle, which is unique inside one sweep")
|
|
}
|
|
|
|
static func displayTitle() {
|
|
expect(entry(0, title: "Notes.md").displayTitle == "Notes.md", "a titled window shows it")
|
|
expect(
|
|
entry(0, app: "Preview", title: "").displayTitle == "Preview",
|
|
"an untitled window reads as its app rather than as a blank row")
|
|
expect(
|
|
entry(0, app: "Preview", title: " ").displayTitle == "Preview",
|
|
"a whitespace-only title is no title")
|
|
}
|
|
|
|
static func searchMapping() {
|
|
let aliases = entry(0, app: "Safari", title: "Inbox").searchFields().aliases
|
|
expect(
|
|
aliases.contains { $0.text == "Inbox" && $0.role == .name },
|
|
"the window title is the name a query matches")
|
|
expect(
|
|
aliases.contains { $0.text == "Safari" && $0.role == .owner },
|
|
"the app rides as owner: every window of one app shares it")
|
|
}
|
|
|
|
static func ordering() {
|
|
let sorted = WindowSwitchOrder.sorted([
|
|
entry(0, app: "Mail", rank: 2),
|
|
entry(1, app: "Safari", minimized: true, rank: 0),
|
|
entry(2, app: "Safari", rank: 0),
|
|
entry(3, app: "Safari", rank: 0),
|
|
entry(4, app: "Zed", rank: .max)
|
|
])
|
|
expect(
|
|
sorted.map(\.handle) == [2, 3, 0, 4, 1],
|
|
"front app first, then by rank, unranked next, minimized last: \(sorted.map(\.handle))")
|
|
}
|
|
|
|
static func orderingIsTotal() {
|
|
let entries = (0..<12).map {
|
|
entry($0, app: ["Mail", "Safari", "Zed"][$0 % 3], minimized: $0 % 4 == 0, rank: $0 % 3)
|
|
}
|
|
let once = WindowSwitchOrder.sorted(entries)
|
|
let twice = WindowSwitchOrder.sorted(entries.reversed())
|
|
expect(
|
|
once.map(\.handle) == twice.map(\.handle),
|
|
"the order is total, so a shuffled sweep sorts identically")
|
|
expect(
|
|
once.map(\.handle).sorted() == entries.map(\.handle).sorted(),
|
|
"sorting drops nothing")
|
|
let split = once.firstIndex(where: \.isMinimized) ?? once.count
|
|
expect(
|
|
once[split...].allSatisfy(\.isMinimized),
|
|
"minimized windows form one run at the end, never interleaved")
|
|
}
|
|
|
|
static func ranking() {
|
|
let entries = [
|
|
entry(0, app: "Mail", title: "Drafts"),
|
|
entry(1, app: "Safari", title: "Inbox"),
|
|
entry(2, app: "Safari", title: "Inbox archive")
|
|
]
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "").map(\.handle) == [0, 1, 2],
|
|
"an empty query keeps the order it was handed")
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "Inbox").first?.handle == 1,
|
|
"the exact title beats the one that only starts with it")
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "Safari").map(\.handle) == [1, 2],
|
|
"the app name matches every one of its windows, in the order given")
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "zzz").isEmpty,
|
|
"a query nothing matches ranks nothing")
|
|
}
|
|
|
|
static func rankingLimit() {
|
|
let entries = (0..<(WindowSwitchQuery.resultLimit + 50)).map {
|
|
entry($0, app: "Safari", title: "Tab \($0)")
|
|
}
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "").count == WindowSwitchQuery.resultLimit,
|
|
"an empty query is capped too, so a huge sweep never builds a huge list")
|
|
expect(
|
|
WindowSwitchQuery.rank(entries, for: "Tab").count == WindowSwitchQuery.resultLimit,
|
|
"the cap holds under a query every row matches")
|
|
}
|
|
}
|