Skip to content

Commit aea4e55

Browse files
committed
Don't mistake Xcode's welcome window for a project; bump to 1.3.9
With Xcode showing only its "Welcome to Xcode" screen, Notchy opened a tab named after that window whose working directory was $HOME. The AppleScript queries answer correctly there — zero workspace documents — but both encoded "no projects" and "the script never ran" as nil, so an authoritative empty answer was read as a failure and sent detection to the window-title fallback. That fallback sees the welcome window like any other, and its projects carry no path, which directoryPath turns into the home directory. Separate the two with a QueryOutcome enum: .success is honored even when empty, and only .unavailable — Xcode not scriptable, automation permission denied — falls back to window titles. The frontmost query now returns "" explicitly for zero documents rather than relying on the implicit value of a skipped if, and Xcode not running is reported as a successful empty result, which it is. A welcome tab created before this fix is persisted and stays until closed by hand; it just won't come back. 116 tests in 9 suites pass.
1 parent e4c7ad9 commit aea4e55

2 files changed

Lines changed: 40 additions & 29 deletions

File tree

Notchy.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@
378378
"@executable_path/../Frameworks",
379379
);
380380
MACOSX_DEPLOYMENT_TARGET = 15.6;
381-
MARKETING_VERSION = 1.3.8;
381+
MARKETING_VERSION = 1.3.9;
382382
PRODUCT_BUNDLE_IDENTIFIER = li.luy.notchy;
383383
PRODUCT_NAME = "$(TARGET_NAME)";
384384
REGISTER_APP_GROUPS = YES;
@@ -418,7 +418,7 @@
418418
"@executable_path/../Frameworks",
419419
);
420420
MACOSX_DEPLOYMENT_TARGET = 15.6;
421-
MARKETING_VERSION = 1.3.8;
421+
MARKETING_VERSION = 1.3.9;
422422
PRODUCT_BUNDLE_IDENTIFIER = li.luy.notchy;
423423
PRODUCT_NAME = "$(TARGET_NAME)";
424424
REGISTER_APP_GROUPS = YES;
@@ -440,7 +440,7 @@
440440
DEVELOPMENT_TEAM = RHVTXHK83V;
441441
GENERATE_INFOPLIST_FILE = YES;
442442
MACOSX_DEPLOYMENT_TARGET = 15.6;
443-
MARKETING_VERSION = 1.3.8;
443+
MARKETING_VERSION = 1.3.9;
444444
PRODUCT_BUNDLE_IDENTIFIER = li.luy.NotchyTests;
445445
PRODUCT_NAME = "$(TARGET_NAME)";
446446
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -461,7 +461,7 @@
461461
DEVELOPMENT_TEAM = RHVTXHK83V;
462462
GENERATE_INFOPLIST_FILE = YES;
463463
MACOSX_DEPLOYMENT_TARGET = 15.6;
464-
MARKETING_VERSION = 1.3.8;
464+
MARKETING_VERSION = 1.3.9;
465465
PRODUCT_BUNDLE_IDENTIFIER = li.luy.NotchyTests;
466466
PRODUCT_NAME = "$(TARGET_NAME)";
467467
SWIFT_APPROACHABLE_CONCURRENCY = YES;

Notchy/XcodeDetector.swift

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,35 @@ struct XcodeProject: Equatable {
1717
class XcodeDetector {
1818
static let shared = XcodeDetector()
1919

20+
/// Outcome of an AppleScript query. `.success` is authoritative even when it
21+
/// carries no projects — Xcode showing only its "Welcome to Xcode" window has
22+
/// zero workspace documents, and that must not be confused with a failed
23+
/// query. `.unavailable` means the script never ran (Xcode not scriptable,
24+
/// automation permission denied), which is the only case worth falling back
25+
/// to window titles for.
26+
private enum QueryOutcome<T> {
27+
case success(T)
28+
case unavailable
29+
}
30+
2031
/// Detects the frontmost Xcode project
2132
func detectFrontmostProject() -> XcodeProject? {
22-
if let project = queryFrontViaAppleScript() {
23-
return project
24-
}
25-
if let project = queryViaWindowTitle() {
33+
switch queryFrontViaAppleScript() {
34+
case .success(let project):
2635
return project
36+
case .unavailable:
37+
return queryViaWindowTitle()
2738
}
28-
return nil
2939
}
3040

3141
/// Detects ALL open Xcode workspace documents
3242
func detectAllProjects() -> [XcodeProject] {
33-
if let projects = queryAllViaAppleScript(), !projects.isEmpty {
43+
switch queryAllViaAppleScript() {
44+
case .success(let projects):
3445
return projects
46+
case .unavailable:
47+
return allProjectsViaWindowTitles()
3548
}
36-
return allProjectsViaWindowTitles()
3749
}
3850

3951
// MARK: - AppleScript
@@ -44,31 +56,30 @@ class XcodeDetector {
4456
}
4557
}
4658

47-
private func queryFrontViaAppleScript() -> XcodeProject? {
48-
guard isXcodeRunning() else { return nil }
59+
private func queryFrontViaAppleScript() -> QueryOutcome<XcodeProject?> {
60+
guard isXcodeRunning() else { return .success(nil) }
4961

5062
let script = """
5163
tell application "Xcode"
52-
if (count of workspace documents) > 0 then
53-
set activeDoc to front workspace document
54-
set docPath to path of activeDoc
55-
set docName to name of activeDoc
56-
return docName & "|||" & docPath
57-
end if
64+
if (count of workspace documents) is 0 then return ""
65+
set activeDoc to front workspace document
66+
set docPath to path of activeDoc
67+
set docName to name of activeDoc
68+
return docName & "|||" & docPath
5869
end tell
5970
"""
6071

61-
guard let appleScript = NSAppleScript(source: script) else { return nil }
72+
guard let appleScript = NSAppleScript(source: script) else { return .unavailable }
6273
var error: NSDictionary?
6374
let result = appleScript.executeAndReturnError(&error)
64-
if error != nil { return nil }
75+
if error != nil { return .unavailable }
6576

66-
guard let resultString = result.stringValue else { return nil }
67-
return Self.parseProject(from: resultString)
77+
guard let resultString = result.stringValue else { return .unavailable }
78+
return .success(Self.parseProject(from: resultString))
6879
}
6980

70-
private func queryAllViaAppleScript() -> [XcodeProject]? {
71-
guard isXcodeRunning() else { return nil }
81+
private func queryAllViaAppleScript() -> QueryOutcome<[XcodeProject]> {
82+
guard isXcodeRunning() else { return .success([]) }
7283

7384
let script = """
7485
tell application "Xcode"
@@ -82,14 +93,14 @@ class XcodeDetector {
8293
end tell
8394
"""
8495

85-
guard let appleScript = NSAppleScript(source: script) else { return nil }
96+
guard let appleScript = NSAppleScript(source: script) else { return .unavailable }
8697
var error: NSDictionary?
8798
let result = appleScript.executeAndReturnError(&error)
88-
if error != nil { return nil }
99+
if error != nil { return .unavailable }
89100

90-
guard let resultString = result.stringValue, !resultString.isEmpty else { return nil }
101+
guard let resultString = result.stringValue else { return .unavailable }
91102

92-
return Self.parseProjectList(from: resultString)
103+
return .success(Self.parseProjectList(from: resultString))
93104
}
94105

95106
// MARK: - Parsing (pure, unit-tested)

0 commit comments

Comments
 (0)