Skip to content

Commit f7ae728

Browse files
committed
chore: Clean up and simplify the app
1 parent fa66819 commit f7ae728

File tree

7 files changed

+493
-1355
lines changed

7 files changed

+493
-1355
lines changed

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@
8989

9090
## Packaging (Terminal-First)
9191
- Always build this project via the `Makefile`; do not invoke `swift build` directly.
92-
- Only use `make build`. Do not run `make install` unless the user explicitly asks.
92+
- After making changes, run `make` so the app is rebuilt and reinstalled in `~/Applications`.
93+
- Use `make build` only when the user explicitly wants a build without reinstalling.
9394
- SwiftPM project rooted at `src/`.
9495
- Main sources live in `src/Sources/Stack/`.
9596
- `make build` will:
9697
- run `swift build --package-path src --scratch-path .build -c release`
9798
- assemble `.app` inside `.build/Stack.app`
9899
- copy `Info.plist`, `AppIcon.icns`, and `en.lproj/Localizable.strings` into the bundle
100+
- `make` will build the app, replace `~/Applications/Stack.app`, and relaunch it if it was already running.

src/Sources/Stack/AppDelegate.swift

Lines changed: 62 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
// AppDelegate.swift
33
// Stack
44
//
5-
// Handles global keyboard shortcuts and app lifecycle
5+
// Handles app lifecycle and persistence setup
66
//
77

88
import AppKit
99
import SwiftUI
1010
import SwiftData
1111
import Carbon.HIToolbox
12-
import ServiceManagement
1312

14-
// Notification for toggling the menu bar popover
1513
extension Notification.Name {
1614
static let toggleMenuBarPopover = Notification.Name("toggleMenuBarPopover")
1715
static let popoverDidShow = Notification.Name("popoverDidShow")
@@ -21,31 +19,15 @@ extension Notification.Name {
2119
final 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

Comments
 (0)