From 4440a264760d37b3b516b791fc68957e1d26c5ef Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 8 Sep 2026 18:29:50 +0100 Subject: [PATCH] App scaffold & wizard shell (#1) XcodeGen-managed project (project.yml), SwiftUI single-window app 1280x800 / min 1100x700, dark theme tokens from docs/21, 270pt sidebar with logo/preset select/Calibrate/stepper 1-5, notice banner, five stage placeholders, ICCeryCore SPM package with AppPaths + WizardStage, Swift Testing plumbing. Sandbox off, hardened runtime on, bundle id com.gronod.iccery2. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 5 +- Makefile | 23 ++++ Packages/ICCeryCore/Package.swift | 13 +++ .../Sources/ICCeryCore/Paths/AppPaths.swift | 51 +++++++++ .../ICCeryCore/Wizard/WizardStage.swift | 45 ++++++++ .../AccentColor.colorset/Contents.json | 20 ++++ .../AppIcon.appiconset/Contents.json | 58 ++++++++++ Resources/Assets.xcassets/Contents.json | 6 + .../ICCery-logo.imageset/Contents.json | 16 +++ .../ICCery-logo.imageset/ICCery-logo.svg | 47 ++++++++ Resources/ICCery.entitlements | 8 ++ Sources/ICCery/ICCeryApp.swift | 33 ++++++ Sources/ICCery/NoticeBanner.swift | 62 +++++++++++ Sources/ICCery/RootView.swift | 48 ++++++++ Sources/ICCery/SidebarView.swift | 105 ++++++++++++++++++ Sources/ICCery/StagePlaceholderView.swift | 24 ++++ Sources/ICCery/Theme.swift | 21 ++++ Sources/ICCery/WizardViewModel.swift | 62 +++++++++++ Tests/ICCeryCoreTests/AppPathsTests.swift | 27 +++++ project.yml | 80 +++++++++++++ 20 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 Packages/ICCeryCore/Package.swift create mode 100644 Packages/ICCeryCore/Sources/ICCeryCore/Paths/AppPaths.swift create mode 100644 Packages/ICCeryCore/Sources/ICCeryCore/Wizard/WizardStage.swift create mode 100644 Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Resources/Assets.xcassets/Contents.json create mode 100644 Resources/Assets.xcassets/ICCery-logo.imageset/Contents.json create mode 100644 Resources/Assets.xcassets/ICCery-logo.imageset/ICCery-logo.svg create mode 100644 Resources/ICCery.entitlements create mode 100644 Sources/ICCery/ICCeryApp.swift create mode 100644 Sources/ICCery/NoticeBanner.swift create mode 100644 Sources/ICCery/RootView.swift create mode 100644 Sources/ICCery/SidebarView.swift create mode 100644 Sources/ICCery/StagePlaceholderView.swift create mode 100644 Sources/ICCery/Theme.swift create mode 100644 Sources/ICCery/WizardViewModel.swift create mode 100644 Tests/ICCeryCoreTests/AppPathsTests.swift create mode 100644 project.yml diff --git a/.gitignore b/.gitignore index 6e57a6a..d131153 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,10 @@ DerivedData/ Package.resolved # Fetched Argyll sidecars (release artefacts, not git blobs — #127) -Resources/argyll/ +Vendor/Argyll/ + +# XcodeGen output (regenerate with `make gen`) +ICCery.xcodeproj/ # macOS .DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70e8da4 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +SCHEME := ICCery +DEST := 'platform=macOS' + +.PHONY: gen build test universal fetch-argyll clean + +gen: + xcodegen generate + +build: gen + xcodebuild build -scheme $(SCHEME) -destination $(DEST) + +test: gen + xcodebuild build test -scheme $(SCHEME) -destination $(DEST) + +universal: gen + xcodebuild build -scheme $(SCHEME) -destination $(DEST) \ + ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO + +fetch-argyll: + scripts/fetch-argyll.sh + +clean: + rm -rf ICCery.xcodeproj DerivedData Packages/ICCeryCore/.build diff --git a/Packages/ICCeryCore/Package.swift b/Packages/ICCeryCore/Package.swift new file mode 100644 index 0000000..7de992e --- /dev/null +++ b/Packages/ICCeryCore/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "ICCeryCore", + platforms: [.macOS(.v14)], + products: [ + .library(name: "ICCeryCore", targets: ["ICCeryCore"]), + ], + targets: [ + .target(name: "ICCeryCore"), + ] +) diff --git a/Packages/ICCeryCore/Sources/ICCeryCore/Paths/AppPaths.swift b/Packages/ICCeryCore/Sources/ICCeryCore/Paths/AppPaths.swift new file mode 100644 index 0000000..24a280f --- /dev/null +++ b/Packages/ICCeryCore/Sources/ICCeryCore/Paths/AppPaths.swift @@ -0,0 +1,51 @@ +import Foundation + +/// Well-known filesystem locations for the ICCery host process. +/// +/// macOS paths (docs/02 §Persistence): +/// - App data: `~/Library/Application Support//` +/// - Log file: `~/Library/Logs//iccery.log` +/// - Bundled Argyll tools: `/Contents/Resources/Argyll/` +public enum AppPaths { + + /// `com.gronod.iccery2` — read from the main bundle so tests can override. + public static var bundleIdentifier: String { + Bundle.main.bundleIdentifier ?? "com.gronod.iccery2" + } + + /// `~/Library/Application Support/com.gronod.iccery2` + public static var appDataDir: URL { + FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent(bundleIdentifier, isDirectory: true) + } + + /// `~/Library/Logs/com.gronod.iccery2` + public static var logDir: URL { + FileManager.default + .urls(for: .libraryDirectory, in: .userDomainMask)[0] + .appendingPathComponent("Logs", isDirectory: true) + .appendingPathComponent(bundleIdentifier, isDirectory: true) + } + + /// `~/Library/Logs/com.gronod.iccery2/iccery.log` + public static var logFile: URL { + logDir.appendingPathComponent("iccery.log", isDirectory: false) + } + + /// `/Contents/Resources/Argyll` — bundled sidecar root. + public static var bundledArgyllDir: URL { + Bundle.main.resourceURL? + .appendingPathComponent("Argyll", isDirectory: true) + ?? URL(fileURLWithPath: "/nonexistent") + } + + /// Creates the app data and log directories if missing. + @discardableResult + public static func ensureDirectories() throws -> (appData: URL, logs: URL) { + let fm = FileManager.default + try fm.createDirectory(at: appDataDir, withIntermediateDirectories: true) + try fm.createDirectory(at: logDir, withIntermediateDirectories: true) + return (appDataDir, logDir) + } +} diff --git a/Packages/ICCeryCore/Sources/ICCeryCore/Wizard/WizardStage.swift b/Packages/ICCeryCore/Sources/ICCeryCore/Wizard/WizardStage.swift new file mode 100644 index 0000000..c34c8a0 --- /dev/null +++ b/Packages/ICCeryCore/Sources/ICCeryCore/Wizard/WizardStage.swift @@ -0,0 +1,45 @@ +import Foundation + +/// The five wizard stages plus Stage 0 (calibration), matching the v1 +/// `data-stage` contract in docs/21. Stepper buttons 1–5 map to +/// `.generate` … `.verifyInstall`; `.calibrate` lives outside the stepper. +public enum WizardStage: Int, CaseIterable, Sendable, Codable { + case calibrate = 0 + case generate = 1 + case layOutPrint = 2 + case measure = 3 + case buildProfile = 4 + case verifyInstall = 5 + + /// Sidebar stepper position (1–5); `nil` for the out-of-band calibrate stage. + public var stepperIndex: Int? { + self == .calibrate ? nil : rawValue + } + + public var title: String { + switch self { + case .calibrate: return "Printer Calibration" + case .generate: return "Generate Target" + case .layOutPrint: return "Lay Out & Print" + case .measure: return "Measure Chart" + case .buildProfile: return "Build Profile" + case .verifyInstall: return "Verify & Install" + } + } + + public var symbolName: String { + switch self { + case .calibrate: return "slider.horizontal.3" + case .generate: return "square.grid.3x3" + case .layOutPrint: return "printer" + case .measure: return "eyedropper.halffull" + case .buildProfile: return "paintpalette" + case .verifyInstall: return "checkmark.seal" + } + } + + /// Stages shown in the sidebar stepper, in order. + public static var stepperStages: [WizardStage] { + [.generate, .layOutPrint, .measure, .buildProfile, .verifyInstall] + } +} diff --git a/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..64563b3 --- /dev/null +++ b/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.800", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Contents.json b/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/ICCery-logo.imageset/Contents.json b/Resources/Assets.xcassets/ICCery-logo.imageset/Contents.json new file mode 100644 index 0000000..a1ec0b2 --- /dev/null +++ b/Resources/Assets.xcassets/ICCery-logo.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "ICCery-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Resources/Assets.xcassets/ICCery-logo.imageset/ICCery-logo.svg b/Resources/Assets.xcassets/ICCery-logo.imageset/ICCery-logo.svg new file mode 100644 index 0000000..2b7b7e2 --- /dev/null +++ b/Resources/Assets.xcassets/ICCery-logo.imageset/ICCery-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ICCery + diff --git a/Resources/ICCery.entitlements b/Resources/ICCery.entitlements new file mode 100644 index 0000000..df6ecba --- /dev/null +++ b/Resources/ICCery.entitlements @@ -0,0 +1,8 @@ + + + + + + + diff --git a/Sources/ICCery/ICCeryApp.swift b/Sources/ICCery/ICCeryApp.swift new file mode 100644 index 0000000..9dba46a --- /dev/null +++ b/Sources/ICCery/ICCeryApp.swift @@ -0,0 +1,33 @@ +import AppKit +import SwiftUI + +@main +struct ICCeryApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + @State private var model = WizardViewModel() + + 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 give later +/// milestones a hook to `killAll` Argyll children before teardown +/// (#147/#149 — wired once ProcessManager exists in #2). +final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true + } + + func applicationWillTerminate(_ notification: Notification) { + // Issue #2+: ProcessManager.shared.killAll() + } +} diff --git a/Sources/ICCery/NoticeBanner.swift b/Sources/ICCery/NoticeBanner.swift new file mode 100644 index 0000000..c0bfb67 --- /dev/null +++ b/Sources/ICCery/NoticeBanner.swift @@ -0,0 +1,62 @@ +import SwiftUI + +/// Banner notice model — the v2 equivalent of `#wizardNotification` +/// (docs/21 §Banner). +struct Notice: Identifiable, Equatable { + enum Kind: Equatable { + case info, warning, error + + var symbolName: String { + switch self { + case .info: return "info.circle" + case .warning: return "exclamationmark.triangle" + case .error: return "xmark.octagon" + } + } + + var tint: Color { + switch self { + case .info: return Theme.accent + case .warning: return .orange + case .error: return .red + } + } + } + + let id = UUID() + let kind: Kind + let text: String + /// Auto-dismiss interval; `nil` keeps the banner until closed. + var autoHideAfter: TimeInterval? = 6 +} + +struct NoticeBanner: View { + let notice: Notice + let onClose: () -> Void + + var body: some View { + HStack(spacing: 10) { + Image(systemName: notice.kind.symbolName) + .foregroundStyle(notice.kind.tint) + Text(notice.text) + .font(.callout) + .foregroundStyle(Theme.text) + .lineLimit(3) + Spacer() + Button(action: onClose) { + Image(systemName: "xmark") + } + .buttonStyle(.plain) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Theme.panel) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundStyle(Theme.border), + alignment: .bottom + ) + } +} diff --git a/Sources/ICCery/RootView.swift b/Sources/ICCery/RootView.swift new file mode 100644 index 0000000..6d5f619 --- /dev/null +++ b/Sources/ICCery/RootView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +/// Root layout: 270 pt sidebar + main stage area with the notification +/// banner pinned to the top (docs/21 §Shell). +struct RootView: View { + @Bindable var model: WizardViewModel + @State private var showingSettings = false + @State private var showingAbout = false + + var body: some View { + HStack(spacing: 0) { + SidebarView( + model: model, + onOpenSettings: { showingSettings = true }, + onOpenAbout: { showingAbout = true } + ) + + Rectangle() + .fill(Theme.border) + .frame(width: 1) + + VStack(spacing: 0) { + if let notice = model.notice { + NoticeBanner(notice: notice, onClose: model.dismissNotice) + } + StagePlaceholderView(stage: model.stage) + } + } + .frame(minWidth: 1100, minHeight: 700) + .background(Theme.background) + .sheet(isPresented: $showingSettings) { + // Full settings dialog lands in issue #5. + VStack(spacing: 12) { + Text("Settings").font(.headline) + Text("Implemented in issue #5.") + .foregroundStyle(.secondary) + Button("Close") { showingSettings = false } + } + .padding(24) + .frame(width: 420) + } + .alert("ICCery 2.0.0", isPresented: $showingAbout) { + Button("OK") {} + } message: { + Text("Native macOS printer profiling workstation.\nFull About dialog lands in issue #31.") + } + } +} diff --git a/Sources/ICCery/SidebarView.swift b/Sources/ICCery/SidebarView.swift new file mode 100644 index 0000000..a165035 --- /dev/null +++ b/Sources/ICCery/SidebarView.swift @@ -0,0 +1,105 @@ +import SwiftUI +import ICCeryCore + +/// 270 pt sidebar (docs/21 §Shell): logo, settings/about buttons, preset +/// select, Calibrate Printer + status chip, and the 1–5 stepper. +struct SidebarView: View { + @Bindable var model: WizardViewModel + var onOpenSettings: () -> Void + var onOpenAbout: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + Image("ICCery-logo") + .resizable() + .scaledToFit() + .frame(height: 40) + Spacer() + Button(action: onOpenSettings) { + Image(systemName: "gearshape") + } + .buttonStyle(.plain) + .help("Settings") + Button(action: onOpenAbout) { + Image(systemName: "info.circle") + } + .buttonStyle(.plain) + .help("About ICCery") + } + .padding(12) + + Divider().overlay(Theme.border) + + // Preset select (`#presetSelect`). Preset engine lands in #11. + Picker("Preset", selection: .constant("none")) { + Text("No preset").tag("none") + } + .pickerStyle(.menu) + .padding(.horizontal, 12) + .padding(.vertical, 8) + + // Calibrate Printer (`#btnCalibratePrinter`); `#calStatusChip` + // is hidden until the calibration library lands in #29. + Button(action: { model.enterCalibration() }) { + Label("Calibrate Printer", systemImage: "slider.horizontal.3") + .frame(maxWidth: .infinity) + } + .controlSize(.large) + .padding(.horizontal, 12) + + Divider().overlay(Theme.border) + .padding(.vertical, 8) + + // Stepper 1–5. + VStack(alignment: .leading, spacing: 2) { + ForEach(WizardStage.stepperStages, id: \.self) { stage in + StepperRow( + stage: stage, + isActive: model.stage == stage + ) { + model.go(to: stage) + } + } + } + .padding(.horizontal, 6) + + Spacer() + } + .frame(width: Theme.Metrics.sidebarWidth) + .background(Theme.panel) + } +} + +private struct StepperRow: View { + let stage: WizardStage + let isActive: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 10) { + ZStack { + Circle() + .fill(isActive ? Theme.accent : Theme.border) + .frame(width: 26, height: 26) + Text("\(stage.stepperIndex ?? 0)") + .font(.callout.bold()) + .foregroundStyle(isActive ? .white : Theme.text) + } + Label(stage.title, systemImage: stage.symbolName) + .font(.callout) + .foregroundStyle(isActive ? Theme.text : .secondary) + Spacer() + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background( + RoundedRectangle(cornerRadius: Theme.Metrics.cornerMedium) + .fill(isActive ? Theme.accent.opacity(0.15) : .clear) + ) + } +} diff --git a/Sources/ICCery/StagePlaceholderView.swift b/Sources/ICCery/StagePlaceholderView.swift new file mode 100644 index 0000000..ae92b96 --- /dev/null +++ b/Sources/ICCery/StagePlaceholderView.swift @@ -0,0 +1,24 @@ +import SwiftUI +import ICCeryCore + +/// Placeholder stage surface for M1. Real stage UIs arrive in M2–M5 +/// (issues #7–#31); Stage 0 lands in M6 (issue #29). +struct StagePlaceholderView: View { + let stage: WizardStage + + var body: some View { + VStack(spacing: 16) { + Image(systemName: stage.symbolName) + .font(.system(size: 44)) + .foregroundStyle(Theme.accent) + Text(stage.title) + .font(.title2) + .foregroundStyle(Theme.text) + Text("This stage is not implemented yet — see the milestone plan.") + .font(.callout) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Theme.background) + } +} diff --git a/Sources/ICCery/Theme.swift b/Sources/ICCery/Theme.swift new file mode 100644 index 0000000..c65ca0c --- /dev/null +++ b/Sources/ICCery/Theme.swift @@ -0,0 +1,21 @@ +import SwiftUI + +/// Design tokens carried over from the v1 stylesheet (docs/21 §Design tokens). +enum Theme { + static let background = Color(red: 0x1e / 255, green: 0x1e / 255, blue: 0x1e / 255) + static let panel = Color(red: 0x25 / 255, green: 0x25 / 255, blue: 0x26 / 255) + static let text = Color(red: 0xd4 / 255, green: 0xd4 / 255, blue: 0xd4 / 255) + static let accent = Color(red: 0x00 / 255, green: 0x7a / 255, blue: 0xcc / 255) + static let border = Color(red: 0x33 / 255, green: 0x33 / 255, blue: 0x33 / 255) + /// v1 window/titlebar backing colour (docs/02 §Window contract). + static let windowChrome = Color(red: 0x1a / 255, green: 0x1a / 255, blue: 0x22 / 255) + + enum Metrics { + static let sidebarWidth: CGFloat = 270 + static let buttonSmall: CGFloat = 28 + static let buttonMedium: CGFloat = 36 + static let buttonLarge: CGFloat = 40 + static let cornerSmall: CGFloat = 4 + static let cornerMedium: CGFloat = 6 + } +} diff --git a/Sources/ICCery/WizardViewModel.swift b/Sources/ICCery/WizardViewModel.swift new file mode 100644 index 0000000..4c405bf --- /dev/null +++ b/Sources/ICCery/WizardViewModel.swift @@ -0,0 +1,62 @@ +import Foundation +import Observation +import ICCeryCore + +/// Wizard shell state (issue #1). Artefact gating, persistence and the +/// "open existing" flow land in issue #4. +@MainActor +@Observable +final class WizardViewModel { + /// Currently displayed stage. + var stage: WizardStage = .generate + + /// Banner notice currently displayed (`#wizardNotification`). + var notice: Notice? + + /// Target basename shared across stages (`targetBasename`). + var basename: String = "" + + /// Working directory for all Argyll artefacts. + var workingDirectory: URL? + + /// Printer queue selected in Stage 2; retained across stages. + var printerName: String? + + private var noticeDismissTask: Task? + + /// `true` while Stage 0 (printer calibration) is shown instead of a + /// stepper stage. + var isCalibrating: Bool { stage == .calibrate } + + func go(to stage: WizardStage) { + self.stage = stage + } + + func enterCalibration() { + stage = .calibrate + } + + func exitCalibration() { + stage = .generate + } + + func showNotice(_ text: String, kind: Notice.Kind = .info, autoHideAfter: TimeInterval? = 6) { + noticeDismissTask?.cancel() + let notice = Notice(kind: kind, text: text, autoHideAfter: autoHideAfter) + self.notice = notice + if let delay = notice.autoHideAfter { + noticeDismissTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(delay)) + guard !Task.isCancelled else { return } + if self?.notice?.id == notice.id { + self?.notice = nil + } + } + } + } + + func dismissNotice() { + noticeDismissTask?.cancel() + notice = nil + } +} diff --git a/Tests/ICCeryCoreTests/AppPathsTests.swift b/Tests/ICCeryCoreTests/AppPathsTests.swift new file mode 100644 index 0000000..2316056 --- /dev/null +++ b/Tests/ICCeryCoreTests/AppPathsTests.swift @@ -0,0 +1,27 @@ +import Testing +import Foundation +@testable import ICCeryCore + +@Suite("AppPaths") +struct AppPathsTests { + @Test func appDataDirUsesBundleID() { + #expect(AppPaths.appDataDir.path.contains("Library/Application Support/com.gronod.iccery2")) + } + + @Test func logFileIsUnderLibraryLogs() { + #expect(AppPaths.logFile.lastPathComponent == "iccery.log") + #expect(AppPaths.logFile.path.contains("Library/Logs/com.gronod.iccery2")) + } + + @Test func bundledArgyllDirIsInsideResources() { + #expect(AppPaths.bundledArgyllDir.lastPathComponent == "Argyll") + } +} + +@Suite("WizardStage") +struct WizardStageTests { + @Test func stepperOrderIsOneThroughFive() { + #expect(WizardStage.stepperStages.map(\.stepperIndex) == [1, 2, 3, 4, 5]) + #expect(WizardStage.calibrate.stepperIndex == nil) + } +} diff --git a/project.yml b/project.yml new file mode 100644 index 0000000..4618da8 --- /dev/null +++ b/project.yml @@ -0,0 +1,80 @@ +name: ICCery +options: + bundleIdPrefix: com.gronod + deploymentTarget: + macOS: "14.0" + groupSortPosition: top + +packages: + ICCeryCore: + path: Packages/ICCeryCore + +targets: + ICCery: + type: application + platform: macOS + deploymentTarget: "14.0" + sources: + - path: Sources/ICCery + - path: Resources + excludes: + - ICCery.entitlements + dependencies: + - package: ICCeryCore + product: ICCeryCore + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.gronod.iccery2 + PRODUCT_NAME: ICCery + PRODUCT_BUNDLE_PACKAGE_TYPE: APPL + GENERATE_INFOPLIST_FILE: YES + INFOPLIST_KEY_CFBundleDisplayName: ICCery + INFOPLIST_KEY_LSMinimumSystemVersion: "14.0" + INFOPLIST_KEY_NSPrincipalClass: NSApplication + INFOPLIST_KEY_NSHumanReadableCopyright: "Copyright © 2026 Gronod. AGPLv3." + MARKETING_VERSION: "2.0.0" + CURRENT_PROJECT_VERSION: "1" + ENABLE_HARDENED_RUNTIME: YES + CODE_SIGN_ENTITLEMENTS: Resources/ICCery.entitlements + CODE_SIGN_IDENTITY: "-" + CODE_SIGN_STYLE: Automatic + ENABLE_APP_SANDBOX: NO + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor + SWIFT_VERSION: "6.0" + SWIFT_STRICT_CONCURRENCY: complete + MACOSX_DEPLOYMENT_TARGET: "14.0" + ARCHS: "$(ARCHS_STANDARD)" + + ICCeryCoreTests: + type: bundle.unit-test + platform: macOS + deploymentTarget: "14.0" + sources: + - path: Tests/ICCeryCoreTests + dependencies: + - package: ICCeryCore + product: ICCeryCore + - target: ICCery + settings: + base: + BUNDLE_LOADER: "$(TEST_HOST)" + TEST_HOST: "$(BUILT_PRODUCTS_DIR)/ICCery.app/Contents/MacOS/ICCery" + GENERATE_INFOPLIST_FILE: YES + CODE_SIGN_IDENTITY: "-" + SWIFT_VERSION: "6.0" + MACOSX_DEPLOYMENT_TARGET: "14.0" + +schemes: + ICCery: + build: + targets: + ICCery: all + ICCeryCoreTests: [test] + run: + config: Debug + test: + config: Debug + gatherCoverageData: false + targets: + - ICCeryCoreTests -- 2.39.5