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
40 changes: 32 additions & 8 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
objects = {

/* Begin PBXBuildFile section */
2A28492B2F22B4B700F6CE42 /* Scribe in Frameworks */ = {isa = PBXBuildFile; productRef = 2A28492A2F22B4B700F6CE42 /* Scribe */; };
2A28B6292EE5050C00A1E26B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 2A28B6282EE5050C00A1E26B /* Defaults */; };
2A28B62C2EE5057C00A1E26B /* Luminare in Frameworks */ = {isa = PBXBuildFile; productRef = 2A28B62B2EE5057C00A1E26B /* Luminare */; };
3EFD33252EE6DFA4007D9601 /* Scribe in Frameworks */ = {isa = PBXBuildFile; productRef = 3EFD33242EE6DFA4007D9601 /* Scribe */; };
2A8EDDBC2F23743B005457F8 /* Scribe in Frameworks */ = {isa = PBXBuildFile; productRef = 2A8EDDBB2F23743B005457F8 /* Scribe */; };
3ED0A7B92F21DF6800A58629 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 3ED0A7B82F21DF6800A58629 /* ZIPFoundation */; };
A883642F298B7288005D6C19 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A883642E298B7288005D6C19 /* ServiceManagement.framework */; };
F06D768A2DFF7A77007EEDA9 /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F06D76892DFF7A77007EEDA9 /* SkyLight.framework */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -55,11 +57,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3EFD33252EE6DFA4007D9601 /* Scribe in Frameworks */,
2A28B6292EE5050C00A1E26B /* Defaults in Frameworks */,
2A28B62C2EE5057C00A1E26B /* Luminare in Frameworks */,
2A8EDDBC2F23743B005457F8 /* Scribe in Frameworks */,
2A28492B2F22B4B700F6CE42 /* Scribe in Frameworks */,
F06D768A2DFF7A77007EEDA9 /* SkyLight.framework in Frameworks */,
A883642F298B7288005D6C19 /* ServiceManagement.framework in Frameworks */,
3ED0A7B92F21DF6800A58629 /* ZIPFoundation in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -119,7 +123,9 @@
packageProductDependencies = (
2A28B6282EE5050C00A1E26B /* Defaults */,
2A28B62B2EE5057C00A1E26B /* Luminare */,
3EFD33242EE6DFA4007D9601 /* Scribe */,
3ED0A7B82F21DF6800A58629 /* ZIPFoundation */,
2A28492A2F22B4B700F6CE42 /* Scribe */,
2A8EDDBB2F23743B005457F8 /* Scribe */,
);
productName = WindowManager;
productReference = A8E59C35297F5E9A0064D4BA /* Loop.app */;
Expand All @@ -132,7 +138,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 2600;
TargetAttributes = {
A8E59C34297F5E9A0064D4BA = {
Expand Down Expand Up @@ -163,7 +169,8 @@
packageReferences = (
2A28B6272EE5050C00A1E26B /* XCRemoteSwiftPackageReference "Defaults" */,
2A28B62A2EE5057C00A1E26B /* XCRemoteSwiftPackageReference "luminare" */,
3EFD33232EE6DFA4007D9601 /* XCRemoteSwiftPackageReference "Scribe" */,
3ED0A7B72F21DF6800A58629 /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
2A8EDDBA2F23743B005457F8 /* XCRemoteSwiftPackageReference "Scribe" */,
);
productRefGroup = A8E59C36297F5E9A0064D4BA /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -543,17 +550,29 @@
kind = branch;
};
};
3EFD33232EE6DFA4007D9601 /* XCRemoteSwiftPackageReference "Scribe" */ = {
2A8EDDBA2F23743B005457F8 /* XCRemoteSwiftPackageReference "Scribe" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SenpaiHunters/Scribe";
requirement = {
branch = main;
kind = branch;
};
};
3ED0A7B72F21DF6800A58629 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/weichsel/ZIPFoundation";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.20;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
2A28492A2F22B4B700F6CE42 /* Scribe */ = {
isa = XCSwiftPackageProductDependency;
productName = Scribe;
};
2A28B6282EE5050C00A1E26B /* Defaults */ = {
isa = XCSwiftPackageProductDependency;
package = 2A28B6272EE5050C00A1E26B /* XCRemoteSwiftPackageReference "Defaults" */;
Expand All @@ -564,11 +583,16 @@
package = 2A28B62A2EE5057C00A1E26B /* XCRemoteSwiftPackageReference "luminare" */;
productName = Luminare;
};
3EFD33242EE6DFA4007D9601 /* Scribe */ = {
2A8EDDBB2F23743B005457F8 /* Scribe */ = {
isa = XCSwiftPackageProductDependency;
package = 3EFD33232EE6DFA4007D9601 /* XCRemoteSwiftPackageReference "Scribe" */;
package = 2A8EDDBA2F23743B005457F8 /* XCRemoteSwiftPackageReference "Scribe" */;
productName = Scribe;
};
3ED0A7B82F21DF6800A58629 /* ZIPFoundation */ = {
isa = XCSwiftPackageProductDependency;
package = 3ED0A7B72F21DF6800A58629 /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
productName = ZIPFoundation;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A8E59C2D297F5E9A0064D4BA /* Project object */;
Expand Down
57 changes: 44 additions & 13 deletions Loop/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_: Notification) {
configureLogging()

// Check for and terminate other running Loop instances to prevent accessibility conflicts
terminateOtherLoopInstances()

Task {
await Defaults.iCloud.waitForSyncCompletion()
}

// Wait for other instances to terminate before proceeding with TCC operations
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
AccessibilityManager.requestAccess()
}

if !launchedAsLoginItem {
SettingsWindowManager.shared.show()
} else {
Expand All @@ -51,10 +59,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
UNUserNotificationCenter.current().delegate = self
AppDelegate.requestNotificationAuthorization()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AccessibilityManager.requestAccess()
}

// Register for URL handling
NSAppleEventManager.shared().setEventHandler(
self,
Expand All @@ -64,6 +68,42 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
)
}

/// Terminates any other running instances of Loop to prevent accessibility permission conflicts.
private func terminateOtherLoopInstances() {
let currentProcessId = ProcessInfo.processInfo.processIdentifier
let bundleId = Bundle.main.bundleIdentifier ?? "com.MrKai77.Loop"

let runningApps = NSWorkspace.shared.runningApplications
let otherLoopInstances = runningApps.filter {
$0.bundleIdentifier == bundleId && $0.processIdentifier != currentProcessId
}

guard !otherLoopInstances.isEmpty else {
Log.info("No other Loop instances found", category: .appDelegate)
return
}

Log.info("Found \(otherLoopInstances.count) other Loop instance(s), terminating them to prevent accessibility conflicts. TCC operations will be delayed.", category: .appDelegate)

for instance in otherLoopInstances {
Log.info("Terminating Loop instance (PID: \(instance.processIdentifier))", category: .appDelegate)
instance.terminate()

// If the instance doesn't terminate within 2 seconds, force terminate
Task {
try? await Task.sleep(for: .seconds(2))

if instance.isTerminated == false {
Log.warn("Force terminating Loop instance (PID: \(instance.processIdentifier))", category: .appDelegate)
instance.forceTerminate()
}
}
}

// Give the other instances time to terminate cleanly
Thread.sleep(forTimeInterval: 1.0)
}

/// Applies baseline logging configuration for Scribe.
private func configureLogging() {
LogManager.shared.configuration.includeFileAndLineNumber = false
Expand Down Expand Up @@ -93,15 +133,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
StashManager.shared.onApplicationWillTerminate()
}

static func relaunch(after seconds: TimeInterval = 0.5) -> Never {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch()
NSApp.terminate(nil)
exit(0)
}

func application(_: NSApplication, open urls: [URL]) {
for url in urls {
urlCommandHandler.handle(url)
Expand Down
2 changes: 1 addition & 1 deletion Loop/App/LoopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct LoopApp: App {
Divider()

Text(
"Version \(Bundle.main.appVersion ?? "Unknown") (\(Bundle.main.appBuild ?? 0))",
"Version \(VersionDisplay.current.fullDisplay)",
comment: "Format: Version [version, e.g. 1.3.0] ([build number, e.g. 1500])"
)
.font(.system(size: 11, weight: .semibold))
Expand Down
2 changes: 1 addition & 1 deletion Loop/Core/LoopManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ extension LoopManager {
}

Task {
if updater.shouldAutoPresentUpdateWindow {
if await updater.shouldAutoPresentUpdateWindow {
await updater.showUpdateWindowIfEligible()
}
}
Expand Down
4 changes: 4 additions & 0 deletions Loop/Extensions/Bundle+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extension Bundle {
getInfo("CFBundleShortVersionString")
}

var bundleURL: URL {
URL(fileURLWithPath: bundlePath)
}

func getInfo(_ str: String) -> String? {
infoDictionary?[str] as? String
}
Expand Down
1 change: 1 addition & 0 deletions Loop/Extensions/Defaults+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ extension Defaults.Keys {
// Development versions should check for development updates by default.
static let includeDevelopmentVersions = Key<Bool>("includeDevelopmentVersions", default: true, iCloud: true)
#endif
static let automaticallyUpdate = Key<Bool>("automaticallyUpdate", default: false, iCloud: true)
}

// MARK: - Hidden Settings
Expand Down
21 changes: 21 additions & 0 deletions Loop/Extensions/Int64+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Int64+Extensions.swift
// Loop
//
// Created by Kami on 2026-01-22.
//

import Foundation

extension Int64 {
var formattedBytes: String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useMB]
formatter.countStyle = .file
formatter.includesUnit = true
formatter.includesCount = true
formatter.isAdaptive = false // Always use MB, no GB/KB
formatter.allowsNonnumericFormatting = false
return formatter.string(fromByteCount: self)
}
}
Loading