From 03ac8e4db5db106b5d7b4396efe06822e0613eca Mon Sep 17 00:00:00 2001 From: "Hugo Mallinson (Gartner)" Date: Mon, 21 Jul 2025 13:07:53 +0100 Subject: [PATCH 1/2] Supports multiple instances. - Added serial number class to manage unique serials for each instance. - Added menu item to spawn a new instance. - Changed window title to display the screen number --- DeskPad/AppDelegate.swift | 18 +++- .../Screen/ScreenViewController.swift | 11 ++- DeskPad/SerialNumberManager.swift | 84 +++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 DeskPad/SerialNumberManager.swift diff --git a/DeskPad/AppDelegate.swift b/DeskPad/AppDelegate.swift index 8e044cc..4025a3b 100644 --- a/DeskPad/AppDelegate.swift +++ b/DeskPad/AppDelegate.swift @@ -11,12 +11,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_: Notification) { let viewController = ScreenViewController() window = NSWindow(contentViewController: viewController) + window.bind(NSBindingName.title, to: viewController, withKeyPath: "title") window.delegate = viewController - window.title = "DeskPad" + // window.title = "DeskPad" window.makeKeyAndOrderFront(nil) window.titlebarAppearsTransparent = true window.isMovableByWindowBackground = true - window.titleVisibility = .hidden + // window.titleVisibility = .hidden window.backgroundColor = .white window.contentMinSize = CGSize(width: 400, height: 300) window.contentMaxSize = CGSize(width: 3840, height: 2160) @@ -26,6 +27,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { let mainMenu = NSMenu() let mainMenuItem = NSMenuItem() let subMenu = NSMenu(title: "MainMenu") + let newMenuItem = NSMenuItem( + title: "Create New Screen", + action: #selector(spawnNewInstance), + keyEquivalent: "n" + ) + subMenu.addItem(newMenuItem) let quitMenuItem = NSMenuItem( title: "Quit", action: #selector(NSApp.terminate), @@ -42,4 +49,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { return true } + + @objc func spawnNewInstance() { + let task = Process() + task.launchPath = "/usr/bin/open" + task.arguments = ["-n", Bundle.main.bundlePath] + try? task.run() + } } diff --git a/DeskPad/Frontend/Screen/ScreenViewController.swift b/DeskPad/Frontend/Screen/ScreenViewController.swift index b47186c..2360a71 100644 --- a/DeskPad/Frontend/Screen/ScreenViewController.swift +++ b/DeskPad/Frontend/Screen/ScreenViewController.swift @@ -20,16 +20,19 @@ class ScreenViewController: SubscriberViewController, NSWindowDe override func viewDidLoad() { super.viewDidLoad() + SerialNumberManager.shared.claimSerial() + let serial = SerialNumberManager.shared.claimedSerial ?? 0x0001 + title = "Screen \(serial)" let descriptor = CGVirtualDisplayDescriptor() descriptor.setDispatchQueue(DispatchQueue.main) - descriptor.name = "DeskPad Display" + descriptor.name = "DeskPad Display \(serial)" descriptor.maxPixelsWide = 3840 descriptor.maxPixelsHigh = 2160 descriptor.sizeInMillimeters = CGSize(width: 1600, height: 1000) descriptor.productID = 0x1234 descriptor.vendorID = 0x3456 - descriptor.serialNum = 0x0001 + descriptor.serialNum = serial let display = CGVirtualDisplay(descriptor: descriptor) store.dispatch(ScreenViewAction.setDisplayID(display.displayID)) @@ -120,4 +123,8 @@ class ScreenViewController: SubscriberViewController, NSWindowDe ) store.dispatch(MouseLocationAction.requestMove(toPoint: onScreenPoint)) } + + func applicationWillTerminate(_: Notification) { + SerialNumberManager.shared.releaseSerial() + } } diff --git a/DeskPad/SerialNumberManager.swift b/DeskPad/SerialNumberManager.swift new file mode 100644 index 0000000..318152f --- /dev/null +++ b/DeskPad/SerialNumberManager.swift @@ -0,0 +1,84 @@ +import Cocoa + +class SerialNumberManager { + static let shared = SerialNumberManager() + + private(set) var claimedSerial: UInt32? + + private let fileCoordinator = NSFileCoordinator() + private let registryURL: URL + + private init() { + let fileManager = FileManager.default + guard let appSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + fatalError("Could not find Application Support directory.") + } + + let bundleID = Bundle.main.bundleIdentifier ?? "com.example.YourAppBundleID" + let appDirectoryURL = appSupportURL.appendingPathComponent(bundleID, isDirectory: true) + + try? fileManager.createDirectory(at: appDirectoryURL, withIntermediateDirectories: true, attributes: nil) + + registryURL = appDirectoryURL.appendingPathComponent("serials.json", isDirectory: false) + } + + func claimSerial() { + var coordinationError: NSError? + + fileCoordinator.coordinate(writingItemAt: registryURL, options: .forMerging, error: &coordinationError) { url in + var activeSerials = [pid_t: UInt32]() + if let data = try? Data(contentsOf: url), + let decoded = try? JSONDecoder().decode([pid_t: UInt32].self, from: data) + { + activeSerials = decoded + } + + for (pid, _) in activeSerials { + if NSRunningApplication(processIdentifier: pid) == nil { + activeSerials.removeValue(forKey: pid) + } + } + + let usedSerials = Set(activeSerials.values) + var newSerial: UInt32 = 1 + while usedSerials.contains(newSerial) { + newSerial += 1 + } + + let myPID = ProcessInfo.processInfo.processIdentifier + activeSerials[myPID] = newSerial + self.claimedSerial = newSerial + + if let data = try? JSONEncoder().encode(activeSerials) { + try? data.write(to: url, options: .atomic) + } + } + + if let error = coordinationError { + print("🚨 Coordination Error during claim: \(error)") + } + } + + func releaseSerial() { + var coordinationError: NSError? + + fileCoordinator.coordinate(writingItemAt: registryURL, options: .forMerging, error: &coordinationError) { url in + guard let data = try? Data(contentsOf: url), + var activeSerials = try? JSONDecoder().decode([pid_t: Int].self, from: data) + else { + return + } + + let myPID = ProcessInfo.processInfo.processIdentifier + if activeSerials.removeValue(forKey: myPID) != nil { + if let data = try? JSONEncoder().encode(activeSerials) { + try? data.write(to: url, options: .atomic) + } + } + } + + if let error = coordinationError { + print("🚨 Coordination Error during release: \(error)") + } + } +} From 31d18c1cc701e3e0893e0eb3fc2c8b3e1768a87e Mon Sep 17 00:00:00 2001 From: "Hugo Mallinson (Gartner)" Date: Fri, 10 Oct 2025 10:10:34 +0100 Subject: [PATCH 2/2] Added project file update --- DeskPad.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DeskPad.xcodeproj/project.pbxproj b/DeskPad.xcodeproj/project.pbxproj index 98777f5..2c189c8 100644 --- a/DeskPad.xcodeproj/project.pbxproj +++ b/DeskPad.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 6DC044522801877F00281728 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC044512801877F00281728 /* AppDelegate.swift */; }; 6DC044562801878100281728 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6DC044552801878100281728 /* Assets.xcassets */; }; 6DC04461280191EB00281728 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC04460280191EB00281728 /* main.swift */; }; + 70EA23072E38CF4A00B8ED62 /* SerialNumberManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70EA23062E38CF4A00B8ED62 /* SerialNumberManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -42,6 +43,7 @@ 6DC044512801877F00281728 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6DC044552801878100281728 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6DC0445A2801878100281728 /* DeskPad.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DeskPad.entitlements; sourceTree = ""; }; + 70EA23062E38CF4A00B8ED62 /* SerialNumberManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialNumberManager.swift; sourceTree = ""; }; 6DC04460280191EB00281728 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -133,6 +135,7 @@ children = ( 6DC04460280191EB00281728 /* main.swift */, 6D2F1485280C20C800A3A2E5 /* SubscriberViewController.swift */, + 70EA23062E38CF4A00B8ED62 /* SerialNumberManager.swift */, 6DC044512801877F00281728 /* AppDelegate.swift */, 6D2F1483280C201B00A3A2E5 /* Backend */, 6D2F1484280C202700A3A2E5 /* Frontend */, @@ -247,6 +250,7 @@ 6D2F148C280C20D000A3A2E5 /* Store.swift in Sources */, 6D2F1486280C20C800A3A2E5 /* SubscriberViewController.swift in Sources */, 6D68E1AF287ABB9900CD574A /* ScreenConfigurationSideEffect.swift in Sources */, + 70EA23072E38CF4A00B8ED62 /* SerialNumberManager.swift in Sources */, 6D41B0A12879FABE007CEB2F /* MouseLocationState.swift in Sources */, 6D2F148A280C20D000A3A2E5 /* AppState.swift in Sources */, 6D2F148B280C20D000A3A2E5 /* SideEffectsMiddleware.swift in Sources */,