22// AppDelegate.swift
33// Stack
44//
5- // Handles global keyboard shortcuts and app lifecycle
5+ // Handles app lifecycle and persistence setup
66//
77
88import AppKit
99import SwiftUI
1010import SwiftData
1111import Carbon. HIToolbox
12- import ServiceManagement
1312
14- // Notification for toggling the menu bar popover
1513extension Notification . Name {
1614 static let toggleMenuBarPopover = Notification . Name ( " toggleMenuBarPopover " )
1715 static let popoverDidShow = Notification . Name ( " popoverDidShow " )
@@ -21,31 +19,15 @@ extension Notification.Name {
2119final class AppDelegate : NSObject , NSApplicationDelegate {
2220 private let distributedNotificationCenter = DistributedNotificationCenter . default ( )
2321 private var hotKeyRef : EventHotKeyRef ?
24- private var taskManager = TaskManager ( )
22+ private let taskManager = TaskManager ( )
2523 private var modelContainer : ModelContainer ?
2624
27- // Store reference for the event handler callback
28- nonisolated ( unsafe) static var shared : AppDelegate ?
29-
3025 func applicationDidFinishLaunching( _ notification: Notification ) {
31- AppDelegate . shared = self
32-
33- // Hide dock icon for menu bar only app
3426 NSApp . setActivationPolicy ( . accessory)
35-
36- // Ensure app launches at login
37- setupLaunchAtLogin ( )
38-
39- // Setup model container
4027 setupModelContainer ( )
41-
42- // Setup the status item with content view
4328 setupStatusItem ( )
44-
45- // Set up global keyboard shortcut (Control + Option + S)
4629 registerHotKey ( )
4730
48- // Stop the current task timer when the screen locks
4931 distributedNotificationCenter. addObserver (
5032 self ,
5133 selector: #selector( handleScreenLocked ( _: ) ) ,
@@ -54,22 +36,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
5436 )
5537 }
5638
57- private func setupLaunchAtLogin( ) {
58- let service = SMAppService . mainApp
59-
60- // Check if already enabled
61- if service. status != . enabled {
62- do {
63- try service. register ( )
64- print ( " Launch at login enabled " )
65- } catch {
66- print ( " Failed to enable launch at login: \( error) " )
67- }
68- } else {
69- print ( " Launch at login already enabled " )
70- }
71- }
72-
7339 func applicationWillTerminate( _ notification: Notification ) {
7440 taskManager. prepareForTermination ( )
7541
@@ -85,52 +51,75 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
8551 let schema = Schema ( [ StackTask . self] )
8652
8753 do {
88- try configureModelContainer (
89- schema: schema,
90- configuration: ModelConfiguration ( schema: schema, isStoredInMemoryOnly: false ) ,
91- storageMode: . persistent
92- )
93- } catch let persistentStoreError {
94- print ( " Failed to create persistent ModelContainer: \( persistentStoreError) " )
54+ let storeURL = try persistentStoreURL ( )
9555
9656 do {
97- try configureModelContainer (
98- schema: schema,
99- configuration: ModelConfiguration ( schema: schema, isStoredInMemoryOnly: true ) ,
100- storageMode: . recoveryInMemory,
101- warningMessage: String ( localized: " error.persistenceRecovery " )
102- )
103- } catch let recoveryStoreError {
104- presentStartupFailure (
105- persistentStoreError: persistentStoreError,
106- recoveryStoreError: recoveryStoreError
107- )
57+ try configureModelContainer ( schema: schema, storeURL: storeURL)
58+ } catch let persistentStoreError {
59+ print ( " Failed to create persistent ModelContainer: \( persistentStoreError) " )
60+ let warningMessage = try resetPersistentStore ( at: storeURL)
61+ try configureModelContainer ( schema: schema, storeURL: storeURL, warningMessage: warningMessage)
10862 }
63+ } catch {
64+ presentStartupFailure ( error)
10965 }
11066 }
11167
11268 private func configureModelContainer(
11369 schema: Schema ,
114- configuration: ModelConfiguration ,
115- storageMode: TaskStorageMode ,
70+ storeURL: URL ,
11671 warningMessage: String ? = nil
11772 ) throws {
73+ let configuration = ModelConfiguration ( schema: schema, url: storeURL)
11874 modelContainer = try ModelContainer ( for: schema, configurations: [ configuration] )
11975
12076 if let context = modelContainer? . mainContext {
121- taskManager. configurePersistence ( context: context, storageMode: storageMode, warningMessage: warningMessage)
77+ taskManager. configurePersistence ( context: context, warningMessage: warningMessage)
78+ }
79+ }
80+
81+ private func persistentStoreURL( ) throws -> URL {
82+ let fileManager = FileManager . default
83+ let appSupportDirectory = try fileManager. url (
84+ for: . applicationSupportDirectory,
85+ in: . userDomainMask,
86+ appropriateFor: nil ,
87+ create: true
88+ )
89+ let stackDirectory = appSupportDirectory. appendingPathComponent ( " com.erazemk.Stack " , isDirectory: true )
90+
91+ try fileManager. createDirectory ( at: stackDirectory, withIntermediateDirectories: true )
92+
93+ return stackDirectory. appendingPathComponent ( " Stack.store " )
94+ }
95+
96+ private func resetPersistentStore( at storeURL: URL ) throws -> String {
97+ let fileManager = FileManager . default
98+ let parentDirectory = storeURL. deletingLastPathComponent ( )
99+ let archiveDirectory = parentDirectory. appendingPathComponent ( " ArchivedStores " , isDirectory: true )
100+ let timestamp = ISO8601DateFormatter ( ) . string ( from: Date ( ) ) . replacingOccurrences ( of: " : " , with: " - " )
101+ let storeFilePrefix = storeURL. lastPathComponent
102+
103+ try fileManager. createDirectory ( at: archiveDirectory, withIntermediateDirectories: true )
104+
105+ let siblingFiles = try fileManager. contentsOfDirectory ( at: parentDirectory, includingPropertiesForKeys: nil )
106+ for fileURL in siblingFiles where fileURL. lastPathComponent. hasPrefix ( storeFilePrefix) {
107+ let archivedURL = archiveDirectory. appendingPathComponent ( " \( timestamp) - \( fileURL. lastPathComponent) " )
108+ try ? fileManager. removeItem ( at: archivedURL)
109+ try fileManager. moveItem ( at: fileURL, to: archivedURL)
122110 }
111+
112+ return String ( localized: " error.persistenceReset " )
123113 }
124114
125- private func presentStartupFailure( persistentStoreError : Error , recoveryStoreError : Error ) {
115+ private func presentStartupFailure( _ error : Error ) {
126116 let alert = NSAlert ( )
127117 alert. alertStyle = . critical
128118 alert. messageText = String ( localized: " error.startupFailureTitle " )
129119 alert. informativeText = """
130120 \( String ( localized: " error.startupFailureMessage " ) )
131121
132- Persistent store error: \( persistentStoreError. localizedDescription)
133- Recovery store error: \( recoveryStoreError. localizedDescription)
122+ \( error. localizedDescription)
134123 """
135124 alert. addButton ( withTitle: String ( localized: " OK " ) )
136125
@@ -142,64 +131,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
142131 private func setupStatusItem( ) {
143132 let contentView = ContentView ( taskManager: taskManager)
144133 . onAppear {
145- // Update button when popover appears
146134 self . updateStatusButton ( )
147135 }
148136
149137 StatusItemController . shared. setup ( with: contentView)
150138 updateStatusButton ( )
151-
152- // Observe task changes to update the status button
153- NotificationCenter . default. addObserver (
154- self ,
155- selector: #selector( updateStatusButton) ,
156- name: . NSManagedObjectContextDidSave,
157- object: nil
158- )
159-
160- // Observe popover show to check for auto-clear of completed tasks
161- NotificationCenter . default. addObserver (
162- self ,
163- selector: #selector( handlePopoverDidShow) ,
164- name: . popoverDidShow,
165- object: nil
166- )
167- }
168-
169- @objc private func handlePopoverDidShow( ) {
170- taskManager. checkAndClearCompletedTasksIfNeeded ( )
171139 }
172140
173141 @objc private func handleScreenLocked( _ notification: Notification ) {
174- Task { @MainActor in
175- taskManager. stopCurrentTaskTimer ( )
176- }
142+ taskManager. stopCurrentTaskTimer ( )
177143 }
178144
179145 @objc private func updateStatusButton( ) {
180- let title = taskManager. currentTask? . title
181- let isRunning = taskManager. isCurrentTaskRunning
182- StatusItemController . shared. updateButton ( title: title, isRunning: isRunning)
146+ StatusItemController . shared. updateButton (
147+ title: taskManager. currentTask? . title,
148+ isRunning: taskManager. isCurrentTaskRunning
149+ )
183150 }
184151
185152 private func registerHotKey( ) {
186- // Define the hotkey: Control + Option + S
187153 let modifiers : UInt32 = UInt32 ( controlKey | optionKey)
188154 let keyCode : UInt32 = UInt32 ( kVK_ANSI_S)
189155
190- // Create hotkey ID
191156 var hotKeyID = EventHotKeyID ( )
192- hotKeyID. signature = OSType ( 0x5354_434B ) // "STCK" in hex
157+ hotKeyID. signature = OSType ( 0x5354_434B )
193158 hotKeyID. id = 1
194159
195- // Install event handler
196- var eventType = EventTypeSpec ( eventClass: OSType ( kEventClassKeyboard) , eventKind: UInt32 ( kEventHotKeyPressed) )
197-
160+ var eventType = EventTypeSpec (
161+ eventClass: OSType ( kEventClassKeyboard) ,
162+ eventKind: UInt32 ( kEventHotKeyPressed)
163+ )
198164 var eventHandlerRef : EventHandlerRef ?
199165 let handlerStatus = InstallEventHandler (
200166 GetApplicationEventTarget ( ) ,
201- { ( _, _, _) -> OSStatus in
202- // Post notification to toggle popover
167+ { _, _, _ in
203168 DispatchQueue . main. async {
204169 NotificationCenter . default. post ( name: . toggleMenuBarPopover, object: nil )
205170 }
@@ -213,11 +178,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
213178
214179 if handlerStatus != noErr {
215180 print ( " Failed to install event handler: \( handlerStatus) " )
216- } else {
217- print ( " Event handler installed successfully " )
218181 }
219182
220- // Register the hotkey
221183 let registerStatus = RegisterEventHotKey (
222184 keyCode,
223185 modifiers,
@@ -229,14 +191,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
229191
230192 if registerStatus != noErr {
231193 print ( " Failed to register hotkey: \( registerStatus) " )
232- } else {
233- print ( " Global hotkey registered successfully " )
234194 }
235195 }
236196
237197 private func unregisterHotKey( ) {
238- if let hotKeyRef = hotKeyRef {
239- UnregisterEventHotKey ( hotKeyRef)
240- }
198+ guard let hotKeyRef else { return }
199+ UnregisterEventHotKey ( hotKeyRef)
241200 }
242201}
0 commit comments