- AppSettings: snake_case Codable model — argyll_binary_dir, default_instrument (stored, never applied to argv), log_level (nil -> Debug debug / Info release), delta_e thresholds (2.0/5.0), custom_presets, enable_i1pro2_leds, calibration_stale_days 30, default_install_location user, ask_before_overwrite_profile, open_color_panel_after_install - Validation with the exact contract strings; save() refuses invalid settings; corrupt/missing JSON -> defaults; settingsDidChange notification posted on save (for #20) - LogSink: rolling file at ~/Library/Logs/com.gronod.iccery2/ iccery.log, 5 MiB x 5 segments, runtime setLevel applied at startup and on save (#158); AppLogger gates os_log+file through it - SettingsView sheet: Argyll dir picker, instrument (display-only caveat), i1Pro2 LEDs, ΔE fields + inline errors, stale days, install location, overwrite + ColorSync toggles, log level, open-log-folder / copy-path / copy-excerpt - v1 settings path never read; writes atomic via AtomicFileWriter - 13 new tests; 60/60 green Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
51 lines
1.7 KiB
Swift
51 lines
1.7 KiB
Swift
import AppKit
|
||
import ICCeryCore
|
||
import SwiftUI
|
||
|
||
@main
|
||
struct ICCeryApp: App {
|
||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||
@State private var model = WizardViewModel()
|
||
|
||
init() {
|
||
try? AppPaths.ensureDirectories()
|
||
// Log level is runtime state — apply persisted settings at
|
||
// startup (#158); the Settings sheet re-applies on save.
|
||
LogSink.shared.applySettings(SettingsStore().load())
|
||
}
|
||
|
||
var body: some Scene {
|
||
// Single fixed window (docs/21 §Shell: 1280×800, min 1100×700).
|
||
Window("ICCery", id: "main") {
|
||
RootView(model: model)
|
||
.frame(minWidth: 1100, minHeight: 700)
|
||
.preferredColorScheme(.dark)
|
||
}
|
||
.defaultSize(width: 1280, height: 800)
|
||
.windowResizability(.contentMinSize)
|
||
.defaultPosition(.center)
|
||
}
|
||
}
|
||
|
||
/// AppDelegate: quit when the single window closes, and `killAll` Argyll
|
||
/// children before teardown (#147/#149). Termination is deferred until
|
||
/// `killAll` has signaled every child so `chartread` can park an XY head
|
||
/// when the UI already sent `q\n`.
|
||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||
private var terminationRequested = false
|
||
|
||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||
true
|
||
}
|
||
|
||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||
guard !terminationRequested else { return .terminateNow }
|
||
terminationRequested = true
|
||
Task {
|
||
await ProcessManager.shared.killAll()
|
||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
||
}
|
||
return .terminateLater
|
||
}
|
||
}
|