├── .swift-version
├── .swiftformat
├── Aware.xcodeproj
├── .gitignore
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── LICENSE
├── Assets
├── aware-macos-icon.sketch
├── icon-pngs
│ ├── icon_16x16.png
│ ├── icon_32x32.png
│ ├── icon_128x128.png
│ ├── icon_256x256.png
│ ├── icon_512x512.png
│ ├── icon_128x128@2x.png
│ ├── icon_16x16@2x.png
│ ├── icon_256x256@2x.png
│ ├── icon_32x32@2x.png
│ └── icon_512x512@2x.png
└── aware-visionos-icon.sketch
├── Aware
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon_16x16.png
│ │ ├── icon_32x32.png
│ │ ├── icon_128x128.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_512x512@2x.png
│ │ └── Contents.json
│ └── AppIcon.solidimagestack
│ │ ├── Back.solidimagestacklayer
│ │ ├── Contents.json
│ │ └── Content.imageset
│ │ │ ├── Back.png
│ │ │ └── Contents.json
│ │ ├── Front.solidimagestacklayer
│ │ ├── Contents.json
│ │ └── Content.imageset
│ │ │ ├── Front.png
│ │ │ └── Contents.json
│ │ ├── Middle.solidimagestacklayer
│ │ ├── Contents.json
│ │ └── Content.imageset
│ │ │ ├── Middle.png
│ │ │ └── Contents.json
│ │ └── Contents.json
├── Aware.entitlements
├── App.swift
├── Info.plist
├── visionOS
│ ├── NotificationName+Nonisolated.swift
│ ├── TimerWindow.swift
│ ├── TimerTextView.swift
│ ├── TimerView.swift
│ ├── Settings.bundle
│ │ └── Root.plist
│ ├── BackgroundTask.swift
│ └── ActivityMonitor.swift
├── Shared
│ ├── Duration+Extensions.swift
│ ├── AsyncStream+Extensions.swift
│ ├── NotificationCenter+Observer.swift
│ ├── LogExport.swift
│ ├── UTCClock.swift
│ ├── UserDefaults+AsyncSequence.swift
│ ├── NotificationCenter+AsyncSequence.swift
│ ├── SuspendingClock+Drift.swift
│ ├── TimerFormatStyle.swift
│ └── TimerState.swift
├── macOS
│ ├── View+NSStatusItem.swift
│ ├── View+NSWindow.swift
│ ├── NSApplication+Activate.swift
│ ├── NSEvent+AsyncStream.swift
│ ├── MenuBarTimelineView.swift
│ ├── MenuBar.swift
│ ├── SettingsView.swift
│ └── ActivityMonitor.swift
└── PrivacyInfo.xcprivacy
├── .swiftlint.yml
├── .swift-format
├── ci_scripts
└── ci_pre_xcodebuild.sh
├── README.md
├── AwareTests
├── AsyncStreamTests.swift
├── UTCClockTests.swift
├── NotificationCenterTests.swift
├── TimerStateTests.swift
└── TimerFormatStyleTests.swift
├── .github
└── workflows
│ └── ci.yml
└── itunes-connect.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.0
2 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --ifdef no-indent
2 |
--------------------------------------------------------------------------------
/Aware.xcodeproj/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2016 Joshua Peek, Patrick Marsceill. All rights reserved.
2 |
--------------------------------------------------------------------------------
/Assets/aware-macos-icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/aware-macos-icon.sketch
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_16x16.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_32x32.png
--------------------------------------------------------------------------------
/Assets/aware-visionos-icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/aware-visionos-icon.sketch
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_128x128.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_256x256.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_512x512.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_128x128@2x.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_16x16@2x.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_256x256@2x.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_32x32@2x.png
--------------------------------------------------------------------------------
/Assets/icon-pngs/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Assets/icon-pngs/icon_512x512@2x.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - cyclomatic_complexity
3 | - function_body_length
4 | - identifier_name
5 |
6 | trailing_comma:
7 | mandatory_comma: true
8 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Aware.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Back.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Front.png
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josh/Aware/HEAD/Aware/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Middle.png
--------------------------------------------------------------------------------
/.swift-format:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "lineLength": 120,
4 | "indentation": {
5 | "spaces": 4
6 | },
7 | "indentConditionalCompilationBlocks": false,
8 | "rules": {
9 | "UseLetInEveryBoundCaseVariable": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Aware/Aware.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ci_scripts/ci_pre_xcodebuild.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -ex
4 |
5 | if [ "$CI_PRODUCT_PLATFORM" = 'macOS' ] && [ "$CI_XCODEBUILD_ACTION" = 'build-for-testing' ]; then
6 | sed -i'~' 's/ENABLE_HARDENED_RUNTIME = YES;/ENABLE_HARDENED_RUNTIME = NO;/g' \
7 | "$CI_PRIMARY_REPOSITORY_PATH/$CI_XCODE_PROJECT/project.pbxproj"
8 | fi
9 |
--------------------------------------------------------------------------------
/Aware.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Back.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Front.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Middle.png",
5 | "idiom" : "vision",
6 | "scale" : "2x"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.solidimagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "layers" : [
7 | {
8 | "filename" : "Front.solidimagestacklayer"
9 | },
10 | {
11 | "filename" : "Middle.solidimagestacklayer"
12 | },
13 | {
14 | "filename" : "Back.solidimagestacklayer"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/Aware/App.swift:
--------------------------------------------------------------------------------
1 | //
2 | // App.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct AwareApp: App {
12 | var body: some Scene {
13 | #if os(macOS)
14 | MenuBar()
15 | Settings {
16 | SettingsView()
17 | }
18 | #endif
19 |
20 | #if os(visionOS)
21 | TimerWindow()
22 | #endif
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Aware/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ITSAppUsesNonExemptEncryption
6 |
7 | UIBackgroundModes
8 |
9 | fetch
10 | processing
11 |
12 | BGTaskSchedulerPermittedIdentifiers
13 |
14 | fetchActivityMonitor
15 | processingActivityMonitor
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Aware/visionOS/NotificationName+Nonisolated.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationName+Nonisolated.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/24/24.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import UIKit
10 |
11 | // Seems like an SDK bug these notification name constants are marked as isolated to the main actor.
12 | extension UIApplication {
13 | nonisolated static let nonisolatedDidEnterBackgroundNotification = Notification.Name(
14 | "UIApplicationDidEnterBackgroundNotification")
15 | nonisolated static let nonisolatedWillEnterForegroundNotification = Notification.Name(
16 | "UIApplicationWillEnterForegroundNotification")
17 | }
18 |
19 | #endif
20 |
--------------------------------------------------------------------------------
/Aware/visionOS/TimerWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerWindow.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/9/24.
6 | //
7 |
8 | #if os(visionOS)
9 |
10 | import OSLog
11 | import SwiftUI
12 |
13 | private nonisolated(unsafe) let logger = Logger(
14 | subsystem: "com.awaremac.Aware", category: "TimerWindow"
15 | )
16 |
17 | struct TimerWindow: Scene {
18 | var body: some Scene {
19 | WindowGroup {
20 | TimerView()
21 | }
22 | .defaultSize(width: 240, height: 135)
23 | .windowResizability(.contentSize)
24 | .windowStyle(.plain)
25 | .backgroundTask(fetchActivityMonitorTask)
26 | .backgroundTask(processingActivityMonitorTask)
27 | }
28 | }
29 |
30 | #endif
31 |
--------------------------------------------------------------------------------
/Aware/Shared/Duration+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Duration+Extensions.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/15/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Duration {
11 | /// Construct a Duration given a number of minutes represented as a BinaryInteger.
12 | /// - Returns: A Duration representing a given number of minutes.
13 | static func minutes(_ minutes: some BinaryInteger) -> Duration {
14 | seconds(minutes * 60)
15 | }
16 |
17 | /// Construct a Duration given a number of hours represented as a BinaryInteger.
18 | /// - Returns: A Duration representing a given number of hours.
19 | static func hours(_ hours: some BinaryInteger) -> Duration {
20 | minutes(hours * 60)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Aware/macOS/View+NSStatusItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+NSStatusItem.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/25/24.
6 | //
7 |
8 | #if canImport(AppKit)
9 |
10 | import AppKit
11 | import SwiftUI
12 |
13 | extension View {
14 | @MainActor
15 | func bindStatusItem(_ statusItem: Binding) -> some View {
16 | onAppear {
17 | let statusItems = NSApp.windows.filter { window in
18 | window.className == "NSStatusBarWindow"
19 | }.compactMap { window in
20 | window.value(forKey: "statusItem") as? NSStatusItem
21 | }
22 |
23 | assert(!statusItems.isEmpty, "no NSStatusItems found")
24 | assert(statusItems.count == 1, "multiple NSStatusItems found")
25 | statusItem.wrappedValue = statusItems.first
26 | }
27 | }
28 | }
29 |
30 | #endif
31 |
--------------------------------------------------------------------------------
/Aware/macOS/View+NSWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+NSWindow.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/25/24.
6 | //
7 |
8 | #if canImport(AppKit)
9 |
10 | import AppKit
11 | import SwiftUI
12 |
13 | private struct WindowAccessorView: NSViewRepresentable {
14 | @Binding var window: NSWindow?
15 |
16 | func makeNSView(context _: Context) -> NSView {
17 | let view = NSView()
18 | view.translatesAutoresizingMaskIntoConstraints = false
19 | Task {
20 | assert(view.window != nil, "window accessor fail to detect window")
21 | self.window = view.window
22 | }
23 | return view
24 | }
25 |
26 | func updateNSView(_: NSView, context _: Context) {}
27 | }
28 |
29 | extension View {
30 | func bindWindow(_ window: Binding) -> some View {
31 | background(WindowAccessorView(window: window))
32 | }
33 | }
34 |
35 | #endif
36 |
--------------------------------------------------------------------------------
/Aware/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyTrackingDomains
8 |
9 | NSPrivacyCollectedDataTypes
10 |
11 | NSPrivacyAccessedAPITypes
12 |
13 |
14 | NSPrivacyAccessedAPIType
15 | NSPrivacyAccessedAPICategorySystemBootTime
16 | NSPrivacyAccessedAPITypeReasons
17 |
18 | 35F9.1
19 |
20 |
21 |
22 | NSPrivacyAccessedAPIType
23 | NSPrivacyAccessedAPICategoryUserDefaults
24 | NSPrivacyAccessedAPITypeReasons
25 |
26 | CA92.1
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Aware/macOS/NSApplication+Activate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSApplication+Activate.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/25/24.
6 | //
7 |
8 | #if canImport(AppKit)
9 |
10 | import AppKit
11 | import OSLog
12 |
13 | private nonisolated(unsafe) let logger = Logger(
14 | subsystem: "com.awaremac.Aware", category: "NSApp+Activate"
15 | )
16 |
17 | extension NSApplication {
18 | func activateAggressively() {
19 | let start: ContinuousClock.Instant = .now
20 | let deadline: ContinuousClock.Instant = start.advanced(by: .seconds(3))
21 |
22 | Task(priority: .high) {
23 | while isActive == false && deadline > .now && !Task.isCancelled {
24 | activate()
25 | await Task.yield()
26 | }
27 |
28 | assert(isActive, "expected app to be active")
29 | logger.debug("\(self) application took \(.now - start) to activate")
30 | }
31 | }
32 | }
33 |
34 | #endif
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aware
2 |
3 | Aware is a menubar app for macOS and visionOS that displays how long you've been actively using your computer.
4 |
5 | 
6 | 
7 |
8 | ## Installing the app
9 |
10 |
11 |
12 | [View in Mac App Store](https://itunes.apple.com/us/app/aware/id1082170746?mt=12) or [download the latest release from GitHub](https://github.com/josh/Aware/releases/latest).
13 |
14 | ## Development information
15 |
16 | Requires Xcode 10.2
17 |
18 | ``` sh
19 | $ git clone https://github.com/josh/Aware
20 | $ cd Aware/
21 | $ open Aware.xcodeproj/
22 | ```
23 |
24 | ## License
25 |
26 | Copyright © 2016 Joshua Peek, Patrick Marsceill. All rights reserved.
27 |
--------------------------------------------------------------------------------
/AwareTests/AsyncStreamTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Aware
4 |
5 | final class AsyncStreamTests: XCTestCase {
6 | func testSimpleAsyncStream() async throws {
7 | @Sendable func answer() async -> Int {
8 | 42
9 | }
10 |
11 | let stream = AsyncStream { [answer] yield in
12 | let n = await answer()
13 | yield(n)
14 | yield(n + 1)
15 | yield(n + 2)
16 | }
17 |
18 | var numbers: [Int] = []
19 | for await n in stream {
20 | numbers.append(n)
21 | }
22 |
23 | XCTAssertEqual(numbers, [42, 43, 44])
24 | }
25 |
26 | func testMapAsyncStream() async throws {
27 | let stream1 = AsyncStream { continuation in
28 | continuation.yield(1)
29 | continuation.yield(2)
30 | continuation.yield(3)
31 | continuation.finish()
32 | }
33 |
34 | let stream2 = AsyncStream { yield in
35 | for await n in stream1 {
36 | yield(n * 2)
37 | }
38 | }
39 |
40 | var numbers: [Int] = []
41 | for await n in stream2 {
42 | numbers.append(n)
43 | }
44 |
45 | XCTAssertEqual(numbers, [2, 4, 6])
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Aware/Shared/AsyncStream+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncStream+Extensions.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/24/24.
6 | //
7 |
8 | extension AsyncStream {
9 | /// Annoying wrapper to get
10 | /// typealias Yield = @discardableResult (Element) -> Continuation.YieldResult
11 | struct Yield: Sendable {
12 | private let continuation: Continuation
13 |
14 | fileprivate init(continuation: Continuation) {
15 | self.continuation = continuation
16 | }
17 |
18 | @discardableResult
19 | func callAsFunction(_ value: Element) -> Continuation.YieldResult {
20 | continuation.yield(value)
21 | }
22 | }
23 |
24 | init(
25 | _ elementType: Element.Type = Element.self,
26 | bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded,
27 | _ build: @Sendable @escaping (Yield) async -> Void
28 | ) {
29 | self.init(elementType, bufferingPolicy: limit) { continuation in
30 | let task = Task {
31 | await build(Yield(continuation: continuation))
32 | continuation.finish()
33 | }
34 | continuation.onTermination = { _ in
35 | task.cancel()
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Aware/visionOS/TimerTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerTextView.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/19/24.
6 | //
7 |
8 | #if os(visionOS)
9 |
10 | import SwiftUI
11 |
12 | struct TimerTextView: View {
13 | var duration: Duration = .seconds(0)
14 | var format = TimerFormatStyle(style: .condensedAbbreviated, showSeconds: false)
15 | var glassBackground: Bool = true
16 |
17 | var body: some View {
18 | Text(duration, format: format)
19 | .lineLimit(1)
20 | .padding()
21 | .font(.system(size: 900, weight: .medium))
22 | .minimumScaleFactor(0.01)
23 | .frame(maxWidth: .infinity, maxHeight: .infinity)
24 | .glassBackgroundEffect(displayMode: glassBackground ? .always : .never)
25 | }
26 | }
27 |
28 | #Preview("0m", traits: .fixedLayout(width: 240, height: 135)) {
29 | TimerTextView()
30 | }
31 |
32 | #Preview("15m", traits: .fixedLayout(width: 240, height: 135)) {
33 | TimerTextView(duration: .minutes(15))
34 | }
35 |
36 | #Preview("1h", traits: .fixedLayout(width: 240, height: 135)) {
37 | TimerTextView(duration: .hours(1))
38 | }
39 |
40 | #Preview("1h 15m", traits: .fixedLayout(width: 240, height: 135)) {
41 | TimerTextView(duration: .minutes(75))
42 | }
43 |
44 | #endif
45 |
--------------------------------------------------------------------------------
/Aware/macOS/NSEvent+AsyncStream.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSEvent+AsyncStream.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/8/24.
6 | //
7 |
8 | #if canImport(AppKit)
9 |
10 | import AppKit
11 |
12 | extension NSEvent {
13 | struct Monitor: @unchecked Sendable {
14 | /// The event handler object
15 | let eventMonitor: Any?
16 |
17 | fileprivate init(_ eventMonitor: Any?) {
18 | self.eventMonitor = eventMonitor
19 | }
20 |
21 | /// Removes the specified event monitor.
22 | func cancel() {
23 | assert(eventMonitor != nil, "event monitor failed to install")
24 | if let eventMonitor {
25 | NSEvent.removeMonitor(eventMonitor)
26 | }
27 | }
28 | }
29 |
30 | static func globalEvents(matching mask: NSEvent.EventTypeMask) -> AsyncStream {
31 | AsyncStream(bufferingPolicy: .bufferingNewest(7)) { continuation in
32 | let monitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { event in
33 | continuation.yield(event)
34 | }
35 |
36 | let eventMonitor = Monitor(monitor)
37 | continuation.onTermination = { [eventMonitor] _ in
38 | eventMonitor.cancel()
39 | }
40 | }
41 | }
42 | }
43 |
44 | #endif
45 |
--------------------------------------------------------------------------------
/AwareTests/UTCClockTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Aware
4 |
5 | final class UTCClockTests: XCTestCase {
6 | func testSleep() async throws {
7 | try await UTCClock().sleep(for: .seconds(1))
8 | }
9 |
10 | func testAdvancedByDuration() throws {
11 | let now = UTCClock().now
12 | let then = now.advanced(by: .seconds(15))
13 | XCTAssertEqual(now.duration(to: then), .seconds(15))
14 | XCTAssertEqual(then.duration(to: now), .seconds(-15))
15 | }
16 |
17 | func testDurationToTimeInterval() throws {
18 | XCTAssertEqual(Duration.seconds(0).timeInterval, 0.0)
19 | XCTAssertEqual(Duration.seconds(60).timeInterval, 60.0)
20 |
21 | XCTAssertEqual(Duration.milliseconds(0).timeInterval, 0.0)
22 | XCTAssertEqual(Duration.milliseconds(50).timeInterval, 0.05)
23 | XCTAssertEqual(Duration.milliseconds(1500).timeInterval, 1.5)
24 |
25 | XCTAssertEqual(Duration.microseconds(0).timeInterval, 0.0)
26 | XCTAssertEqual(Duration.microseconds(50).timeInterval, 0.00005)
27 | }
28 |
29 | func testTimeIntervalToDuration() throws {
30 | XCTAssertEqual(Duration(timeInterval: 0.0), .seconds(0))
31 | XCTAssertEqual(Duration(timeInterval: 60.0), .seconds(60))
32 |
33 | XCTAssertEqual(Duration(timeInterval: 0.05), .milliseconds(50))
34 | XCTAssertEqual(Duration(timeInterval: 1.5), .milliseconds(1500))
35 |
36 | XCTAssertEqual(Duration(timeInterval: 0.00005), .microseconds(50))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Aware/Shared/NotificationCenter+Observer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationCenter+Observer.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/16/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension NotificationCenter {
11 | struct Observer: @unchecked Sendable {
12 | /// The notification center this observer uses.
13 | private let center: NotificationCenter
14 |
15 | /// Reference to internal non-sendable `NSNotificationReceiver` object.
16 | /// See apple/swift-corelibs-foundation for source.
17 | private let observer: AnyObject
18 |
19 | /// Create sendable Observer wrapper around `NSNotificationReceiver`.
20 | fileprivate init(center: NotificationCenter, observer: AnyObject) {
21 | self.center = center
22 | self.observer = observer
23 | }
24 |
25 | /// Removes observer from the notification center's dispatch table.
26 | func cancel() {
27 | center.removeObserver(observer)
28 | }
29 | }
30 |
31 | /// Adds an entry to the notification center to receive notifications that passed to the provided block.
32 | func observe(
33 | for name: Notification.Name,
34 | object: AnyObject? = nil,
35 | using block: @Sendable @escaping (Notification) -> Void
36 | ) -> Observer {
37 | let observer = addObserver(
38 | forName: name,
39 | object: object,
40 | queue: nil,
41 | using: block
42 | )
43 | return Observer(center: self, observer: observer)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Aware/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon_16x16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon_16x16@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon_32x32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon_32x32@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon_128x128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon_128x128@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon_256x256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon_256x256@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon_512x512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon_512x512@2x.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Aware/macOS/MenuBarTimelineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBarTimelineView.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/7/24.
6 | //
7 |
8 | #if os(macOS)
9 |
10 | import SwiftUI
11 |
12 | // Using TimelineView within MenuBarExtra content seems to beachball on macOS Sonoma 14.4.
13 | // Reported Feedback FB13678902 on Mar 7, 2024.
14 | struct MenuBarTimelineView: View
15 | where Schedule: TimelineSchedule, Content: View
16 | {
17 | let schedule: Schedule
18 | let content: (MenuBarTimelineViewDefaultContext) -> Content
19 |
20 | private let startDate = Date()
21 |
22 | @State private var context: MenuBarTimelineViewDefaultContext = .init(date: .now)
23 |
24 | init(
25 | _ schedule: Schedule,
26 | @ViewBuilder content: @escaping (MenuBarTimelineViewDefaultContext) -> Content
27 | ) {
28 | self.schedule = schedule
29 | self.content = content
30 | }
31 |
32 | var body: some View {
33 | content(context)
34 | .task(id: startDate) {
35 | for date in schedule.entries(from: startDate, mode: .normal) {
36 | let duration: Duration = .init(timeInterval: date.timeIntervalSinceNow)
37 | if duration > .zero {
38 | do {
39 | try await Task.sleep(for: duration)
40 | } catch is CancellationError {
41 | return
42 | } catch {
43 | assertionFailure("sleep threw unknown error")
44 | return
45 | }
46 | }
47 | assert(date <= Date.now, "didn't sleep long enough")
48 | context = .init(date: date)
49 | }
50 | }
51 | }
52 | }
53 |
54 | struct MenuBarTimelineViewDefaultContext {
55 | let date: Date
56 | }
57 |
58 | #endif
59 |
--------------------------------------------------------------------------------
/Aware/Shared/LogExport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogExport.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/23/24.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | private nonisolated(unsafe) let logger = Logger(
12 | subsystem: "com.awaremac.Aware", category: "LogExport"
13 | )
14 |
15 | enum LogExportError: Error {
16 | case missingLibraryDirectory
17 | }
18 |
19 | func exportLogs() async throws -> URL {
20 | logger.info("Starting OSLog export")
21 |
22 | let store = try OSLogStore(scope: .currentProcessIdentifier)
23 | let predicate = NSPredicate(format: "subsystem == 'com.awaremac.Aware'")
24 | let date = Date.now.addingTimeInterval(-3600)
25 | let position = store.position(date: date)
26 |
27 | await Task.yield()
28 |
29 | logger.debug("Starting to gather entries from OSLogStore")
30 | let clock = ContinuousClock()
31 | let start = clock.now
32 | let entries = try store.getEntries(at: position, matching: predicate)
33 | logger.debug("Finished gathering logs from OSLogStore in \(clock.now - start)")
34 |
35 | try Task.checkCancellation()
36 |
37 | var data = Data()
38 | for entry in entries {
39 | guard let entry = entry as? OSLogEntryLog else { continue }
40 | let date = entry.date.formatted(date: .omitted, time: .standard)
41 | let category = entry.category
42 | let message = entry.composedMessage
43 | let line = "[\(date)] [\(category)] \(message)\n"
44 | data.append(contentsOf: line.utf8)
45 | }
46 |
47 | try Task.checkCancellation()
48 |
49 | guard let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else {
50 | throw LogExportError.missingLibraryDirectory
51 | }
52 | let fileURL =
53 | libraryURL
54 | .appendingPathComponent("Logs", isDirectory: true)
55 | .appendingPathComponent("Aware.log")
56 | try data.write(to: fileURL)
57 |
58 | logger.info("Finished OSLog export")
59 |
60 | return fileURL
61 | }
62 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | env:
6 | CI_DERIVED_DATA_PATH: "${{ github.workspace }}/DerivedData"
7 | CI_RESULT_BUNDLE_PATH: "${{ github.workspace }}/resultbundle.xcresult"
8 | CI_XCODE_PROJECT: Aware.xcodeproj
9 | CI_XCODE_SCHEME: Aware
10 |
11 | jobs:
12 | build:
13 | runs-on: macos-14
14 | timeout-minutes: 10
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Set Xcode version
20 | run: sudo xcode-select --switch /Applications/Xcode_15.3.app
21 |
22 | - name: Print Xcode version
23 | run: xcodebuild -version -sdk
24 |
25 | - name: Resolve package dependencies
26 | run: |
27 | xcodebuild -resolvePackageDependencies \
28 | -project "$CI_XCODE_PROJECT" \
29 | -scheme "$CI_XCODE_SCHEME" \
30 | -derivedDataPath "$CI_DERIVED_DATA_PATH" \
31 | | xcpretty
32 | exit ${PIPESTATUS[0]}
33 |
34 | - name: Build
35 | run: |
36 | xcodebuild build-for-testing \
37 | -scheme "$CI_XCODE_SCHEME" \
38 | -project "$CI_XCODE_PROJECT" \
39 | -derivedDataPath "$CI_DERIVED_DATA_PATH" \
40 | -resultBundlePath "build-for-testing.xcresult" \
41 | CODE_SIGN_IDENTITY=- \
42 | AD_HOC_CODE_SIGNING_ALLOWED=YES \
43 | | xcpretty
44 | exit ${PIPESTATUS[0]}
45 |
46 | - name: Test
47 | run: |
48 | xcodebuild test-without-building \
49 | -scheme "$CI_XCODE_SCHEME" \
50 | -project "$CI_XCODE_PROJECT" \
51 | -derivedDataPath "$CI_DERIVED_DATA_PATH" \
52 | -resultBundlePath "test-without-building.xcresult" \
53 | -test-timeouts-enabled YES \
54 | -maximum-test-execution-time-allowance 1800 \
55 | | xcpretty
56 | exit ${PIPESTATUS[0]}
57 |
58 | - name: Upload xcresult
59 | uses: actions/upload-artifact@v4
60 | with:
61 | name: ${{ matrix.platform }}-test-xcresult
62 | path: "*.xcresult"
63 | if-no-files-found: error
64 | retention-days: 7
65 |
--------------------------------------------------------------------------------
/Aware/Shared/UTCClock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UTCClock.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/8/24.
6 | //
7 |
8 | import Foundation
9 |
10 | // Backport proposed Foundation UTCClock
11 | // https://github.com/apple/swift-evolution/blob/main/proposals/0329-clock-instant-duration.md#clocks-outside-of-the-standard-library
12 | struct UTCClock: Clock {
13 | struct Instant: InstantProtocol {
14 | let date: Date
15 |
16 | init(_ date: Date) {
17 | self.date = date
18 | }
19 |
20 | static var now: Self { .init(.now) }
21 |
22 | static func < (lhs: Self, rhs: Self) -> Bool {
23 | lhs.date < rhs.date
24 | }
25 |
26 | func advanced(by duration: Duration) -> Self {
27 | Self(date.addingTimeInterval(duration.timeInterval))
28 | }
29 |
30 | func duration(to other: Self) -> Duration {
31 | Duration(timeInterval: other.date.timeIntervalSince(date))
32 | }
33 | }
34 |
35 | let minimumResolution: Duration = .nanoseconds(100)
36 | var now: Instant { .now }
37 |
38 | func sleep(for duration: Duration, tolerance: Duration? = nil) async throws {
39 | try await ContinuousClock().sleep(for: duration, tolerance: tolerance)
40 | }
41 |
42 | func sleep(until deadline: Instant, tolerance: Duration?) async throws {
43 | try await sleep(for: now.duration(to: deadline), tolerance: tolerance)
44 | }
45 | }
46 |
47 | // extension Date: InstantProtocol {
48 | // public func advanced(by duration: Duration) -> Date {
49 | // addingTimeInterval(duration.timeInterval)
50 | // }
51 | //
52 | // public func duration(to other: Date) -> Duration {
53 | // Duration(timeInterval: other.timeIntervalSince(self))
54 | // }
55 | // }
56 |
57 | extension Duration {
58 | init(timeInterval: TimeInterval) {
59 | let seconds = Int64(timeInterval)
60 | let attoseconds = Int64((timeInterval - TimeInterval(seconds)) * 1_000_000_000_000_000_000)
61 | self.init(secondsComponent: seconds, attosecondsComponent: attoseconds)
62 | }
63 |
64 | var timeInterval: TimeInterval {
65 | TimeInterval(components.seconds)
66 | + (TimeInterval(components.attoseconds) / 1_000_000_000_000_000_000)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Aware/Shared/UserDefaults+AsyncSequence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+AsyncSequence.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/23/24.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | private nonisolated(unsafe) let logger = Logger(
12 | subsystem: "com.awaremac.Aware", category: "UserDefaults+AsyncSequence"
13 | )
14 |
15 | extension UserDefaults {
16 | fileprivate class Observer: NSObject, @unchecked Sendable {
17 | private let store: UserDefaults
18 | private let keyPath: String
19 | private let block: @Sendable (Element?) -> Void
20 |
21 | init(store: UserDefaults, keyPath: String, block: @escaping @Sendable (Element?) -> Void) {
22 | self.store = store
23 | self.keyPath = keyPath
24 | self.block = block
25 | }
26 |
27 | override func observeValue(
28 | forKeyPath keyPath: String?,
29 | of object: Any?,
30 | change: [NSKeyValueChangeKey: Any]?,
31 | context: UnsafeMutableRawPointer?
32 | ) {
33 | assert(keyPath == self.keyPath, "unexpected keyPath")
34 | assert(object as? UserDefaults == store, "unexpected store")
35 | assert(context == nil, "unexpected context")
36 | block(change?[.newKey] as? Element)
37 | }
38 |
39 | func cancel() {
40 | store.removeObserver(self, forKeyPath: keyPath)
41 | }
42 | }
43 |
44 | func updates(
45 | forKeyPath keyPath: String,
46 | type _: Element.Type = Element.self,
47 | initial: Bool = false
48 | ) -> AsyncStream {
49 | .init(bufferingPolicy: .bufferingNewest(1)) { continuation in
50 | let observer = Observer(store: self, keyPath: keyPath) { value in
51 | logger.debug(
52 | "Yielding UserDefaults \"\(keyPath, privacy: .public)\" value: \(String(describing: value))")
53 | continuation.yield(value)
54 | }
55 |
56 | let options: NSKeyValueObservingOptions = initial ? [.initial, .new] : [.new]
57 | addObserver(observer, forKeyPath: keyPath, options: options, context: nil)
58 |
59 | continuation.onTermination = { _ in
60 | logger.debug("Canceling UserDefaults \"\(keyPath, privacy: .public)\" observer")
61 | observer.cancel()
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/itunes-connect.md:
--------------------------------------------------------------------------------
1 | # iTunes Connect App
2 |
3 | ## Localizable Information
4 |
5 | **Name** Aware
6 | **Privacy Policy URL** https://awaremac.com/privacy
7 |
8 | ## General Information
9 |
10 | **Bundle ID** `com.awaremac.Aware`
11 | **SKU** Aware1
12 | **Primary Language** English
13 | **Primary Category** Productivity
14 | **License Agreement** Apple's Standard License Agreement
15 | **Rating** Ages 4+
16 |
17 | ## Pricing and Availability
18 |
19 | **Price** USD 0 (Free)
20 | **Availability** Available in all territories
21 | **Volume Purchase Program** Available with a volume discount for educational institutions
22 | **Bitcode Auto-Recompilation** Use bitcode auto-recompilation
23 |
24 | ## General App Information
25 |
26 | **Rating** Ages 4+
27 | **Copyright** 2016 Joshua Peek, Patrick Marsceill.
28 |
29 | ## macOS App
30 |
31 | **Screenshots**
32 |
33 | 
34 | 
35 |
36 | **Description**
37 |
38 | A simple menubar app for macOS that tracks how long you've been actively using your computer.
39 |
40 | Aware tells you how long you've been using your computer in hours and minutes. It knows this because it detects the movements of your mouse and the keystrokes on your keyboard. After a short time of inactivity (a break), Aware will pause the timer, then reset and start again when more activity is detected.
41 |
42 | There are no assumptions made as to what you do with this information. No popups or alarms to tell you to take a break, just a record of time tracked in your menubar for easy access.
43 |
44 | **Keywords** aware,time,timer,activity,usage,break
45 | **Support URL** http://awaremac.com
46 | **Marketing URL** http://awaremac.com
47 |
48 | ## visionOS App
49 |
50 | **Description**
51 |
52 | A simple usage timer for visionOS that shows you how long you've been continuously wearing Vision Pro.
53 |
54 | Open Aware and place the timer window anywhere you'd like. The timer keeps running until you take off the device and restarts from zero when you put it back on to start a new session. No need to close the app, the timer resets itself.
55 |
56 | Use Aware to remind yourself to take breaks from wearing Vision Pro or gauge your productivity after a long session.
57 |
58 | **Keywords** aware,time,timer,activity,usage,break
59 | **Support URL** http://awaremac.com
60 | **Marketing URL** http://awaremac.com
61 |
--------------------------------------------------------------------------------
/Aware/Shared/NotificationCenter+AsyncSequence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationCenter+AsyncSequence.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/23/24.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | private nonisolated(unsafe) let logger = Logger(
12 | subsystem: "com.awaremac.Aware", category: "NotificationCenter+AsyncSequence"
13 | )
14 |
15 | extension NotificationCenter {
16 | /// Returns an asynchronous sequence of notifications produced by this center for multiple notification names
17 | /// and optional source object. Similar to calling `AsyncAlgorithms` `merge` over multiple
18 | /// `notifications(named:object:)`.
19 | /// - Parameters:
20 | /// - names: An array of notification names.
21 | /// - object: A source object of notifications.
22 | /// - Returns: A merged asynchronous sequence of notifications from the center.
23 | func mergeNotifications(
24 | named names: [Notification.Name],
25 | object: AnyObject? = nil
26 | ) -> MergedNotifications {
27 | let stream = AsyncStream(bufferingPolicy: .bufferingNewest(7)) { continuation in
28 | let observers = names.map { name in
29 | logger.debug("Listening for \(name.rawValue, privacy: .public) notifications")
30 | return observe(for: name, object: object) { notification in
31 | logger.debug("Received \(name.rawValue, privacy: .public)")
32 | continuation.yield(notification)
33 | }
34 | }
35 |
36 | continuation.onTermination = { _ in
37 | logger.debug("Canceling notification observers")
38 | for observer in observers {
39 | observer.cancel()
40 | }
41 | }
42 | }
43 |
44 | return MergedNotifications(stream: stream)
45 | }
46 | }
47 |
48 | struct MergedNotifications: AsyncSequence, @unchecked Sendable {
49 | typealias Element = Notification
50 |
51 | private let stream: AsyncStream
52 |
53 | fileprivate init(stream: AsyncStream) {
54 | self.stream = stream
55 | }
56 |
57 | func makeAsyncIterator() -> Iterator {
58 | Iterator(iterator: stream.makeAsyncIterator())
59 | }
60 |
61 | struct Iterator: AsyncIteratorProtocol {
62 | private var iterator: AsyncStream.Iterator
63 |
64 | fileprivate init(iterator: AsyncStream.Iterator) {
65 | self.iterator = iterator
66 | }
67 |
68 | mutating func next() async -> Notification? {
69 | await iterator.next()
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Aware/visionOS/TimerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerView.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/19/24.
6 | //
7 |
8 | #if os(visionOS)
9 |
10 | import OSLog
11 | import SwiftUI
12 |
13 | private nonisolated(unsafe) let logger = Logger(
14 | subsystem: "com.awaremac.Aware", category: "TimerView"
15 | )
16 |
17 | struct TimerView: View {
18 | @State private var timerState = TimerState()
19 |
20 | @AppStorage("showSeconds") private var showSeconds: Bool = false
21 | @AppStorage("formatStyle") private var timerFormatStyle: TimerFormatStyle.Style = .condensedAbbreviated
22 | @AppStorage("glassBackground") private var glassBackground: Bool = true
23 |
24 | @AppStorage("backgroundTaskInterval") private var backgroundTaskInterval: Int = 300
25 | @AppStorage("backgroundGracePeriod") private var backgroundGracePeriod: Int = 7200
26 | @AppStorage("lockGracePeriod") private var lockGracePeriod: Int = 60
27 | @AppStorage("maxSuspendingClockDrift") private var maxSuspendingClockDrift: Int = 10
28 |
29 | private var timerFormat: TimerFormatStyle {
30 | TimerFormatStyle(style: timerFormatStyle, showSeconds: showSeconds)
31 | }
32 |
33 | private var activityMonitorConfiguration: ActivityMonitor.Configuration {
34 | ActivityMonitor.Configuration(
35 | backgroundTaskInterval: .seconds(backgroundTaskInterval),
36 | backgroundGracePeriod: .seconds(backgroundGracePeriod),
37 | lockGracePeriod: .seconds(lockGracePeriod),
38 | maxSuspendingClockDrift: .seconds(maxSuspendingClockDrift)
39 | )
40 | }
41 |
42 | var body: some View {
43 | Group {
44 | if let start = timerState.start {
45 | TimelineView(.periodic(from: start.date, by: timerFormat.refreshInterval)) { context in
46 | let duration = timerState.duration(to: UTCClock.Instant(context.date))
47 | TimerTextView(duration: duration, format: timerFormat, glassBackground: glassBackground)
48 | }
49 | } else {
50 | TimerTextView(duration: .zero, format: timerFormat, glassBackground: glassBackground)
51 | }
52 | }
53 | .task(id: activityMonitorConfiguration) {
54 | let activityMonitor = ActivityMonitor(initialState: timerState, configuration: activityMonitorConfiguration)
55 | logger.log("Starting ActivityMonitor updates task: \(timerState, privacy: .public)")
56 | for await state in activityMonitor.updates() {
57 | logger.log("Received ActivityMonitor state: \(state, privacy: .public)")
58 | timerState = state
59 | }
60 | logger.log("Finished ActivityMonitor updates task: \(timerState, privacy: .public)")
61 | }
62 | }
63 | }
64 |
65 | #Preview(traits: .fixedLayout(width: 200, height: 100)) {
66 | TimerView()
67 | }
68 |
69 | #endif
70 |
--------------------------------------------------------------------------------
/Aware/macOS/MenuBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuBar.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/16/24.
6 | //
7 |
8 | #if os(macOS)
9 |
10 | import AppKit
11 | import OSLog
12 | import SwiftUI
13 |
14 | private nonisolated(unsafe) let logger = Logger(
15 | subsystem: "com.awaremac.Aware", category: "MenuBar"
16 | )
17 |
18 | struct MenuBar: Scene {
19 | var body: some Scene {
20 | MenuBarExtra {
21 | MenuBarContentView()
22 | } label: {
23 | TimerMenuBarLabel()
24 | }
25 | }
26 | }
27 |
28 | struct TimerMenuBarLabel: View {
29 | @State private var timerState = TimerState()
30 | @State private var statusItem: NSStatusItem?
31 |
32 | // User configurable idle time in seconds (defaults to 2 minutes)
33 | @AppStorage("userIdleSeconds") private var userIdleSeconds: Int = 120
34 |
35 | @AppStorage("formatStyle") private var timerFormatStyle: TimerFormatStyle.Style = .condensedAbbreviated
36 | @AppStorage("showSeconds") private var showSeconds: Bool = false
37 |
38 | private var timerFormat: TimerFormatStyle {
39 | TimerFormatStyle(style: timerFormatStyle, showSeconds: showSeconds)
40 | }
41 |
42 | private var activityMonitorConfiguration: ActivityMonitor.Configuration {
43 | ActivityMonitor.Configuration(
44 | userIdle: .seconds(max(1, userIdleSeconds))
45 | )
46 | }
47 |
48 | var body: some View {
49 | Group {
50 | if let start = timerState.start {
51 | MenuBarTimelineView(.periodic(from: start.date, by: timerFormat.refreshInterval)) { context in
52 | let duration = timerState.duration(to: UTCClock.Instant(context.date))
53 | Text(duration, format: timerFormat)
54 | }
55 | } else {
56 | Text(.seconds(0), format: timerFormat)
57 | }
58 | }
59 | .task(id: activityMonitorConfiguration) {
60 | let activityMonitor = ActivityMonitor(initialState: timerState, configuration: activityMonitorConfiguration)
61 | logger.log("Starting ActivityMonitor updates task: \(timerState, privacy: .public)")
62 | for await state in activityMonitor.updates() {
63 | logger.log("Received ActivityMonitor state: \(state, privacy: .public)")
64 | timerState = state
65 | }
66 | logger.log("Finished ActivityMonitor updates task: \(timerState, privacy: .public)")
67 | }
68 | .bindStatusItem($statusItem)
69 | .onChange(of: timerState.isIdle) { _, isIdle in
70 | assert(statusItem?.button != nil, "missing statusItem button")
71 | statusItem?.button?.appearsDisabled = isIdle
72 | }
73 | }
74 | }
75 |
76 | struct MenuBarContentView: View {
77 | var body: some View {
78 | SettingsLink()
79 | .keyboardShortcut(",")
80 | Divider()
81 | Button("Quit") {
82 | NSApplication.shared.terminate(nil)
83 | }.keyboardShortcut("q")
84 | }
85 | }
86 |
87 | #endif
88 |
--------------------------------------------------------------------------------
/Aware/Shared/SuspendingClock+Drift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SuspendingClock+Drift.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/23/24.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | private nonisolated(unsafe) let logger = Logger(
12 | subsystem: "com.awaremac.Aware", category: "SuspendingClock+Drift"
13 | )
14 |
15 | #if canImport(AppKit)
16 | import AppKit
17 |
18 | private let notificationCenter: NotificationCenter = NSWorkspace.shared.notificationCenter
19 |
20 | private let notifications: [Notification.Name] = [
21 | NSWorkspace.willSleepNotification,
22 | NSWorkspace.didWakeNotification,
23 | NSWorkspace.screensDidSleepNotification,
24 | NSWorkspace.screensDidWakeNotification,
25 | NSWorkspace.willPowerOffNotification,
26 | ]
27 |
28 | #elseif canImport(UIKit)
29 | import UIKit
30 |
31 | private let notificationCenter: NotificationCenter = .default
32 |
33 | private let notifications: [Notification.Name] = [
34 | UIApplication.nonisolatedDidEnterBackgroundNotification,
35 | UIApplication.nonisolatedWillEnterForegroundNotification,
36 | ]
37 |
38 | #endif
39 |
40 | extension SuspendingClock {
41 | /// Monitor the system's suspending clock's drift compared to the system's continous clock.
42 | /// When the drift exceeds the threshold, return from this async function.
43 | /// - Parameter threshold: The minium duration of acceptable drift
44 | /// - Throws: `CancellationError` when task is canceled
45 | /// - Returns: The drift duration above the `threshold`
46 | @discardableResult
47 | func monitorDrift(threshold: Duration) async throws -> Duration {
48 | logger.info("Starting SuspendingClock drift monitor")
49 | defer { logger.info("Finished SuspendingClock drift monitor") }
50 |
51 | let continuousClock = ContinuousClock()
52 | let suspendingClock = self
53 |
54 | let continuousStart = continuousClock.now
55 | let suspendingStart = suspendingClock.now
56 | var drift: Duration = .zero
57 |
58 | for await notification in notificationCenter.mergeNotifications(named: notifications) {
59 | logger.log("Received \(notification.name.rawValue, privacy: .public)")
60 |
61 | let continuousDuration = continuousClock.now - continuousStart
62 | assert(continuousDuration > .zero)
63 |
64 | let suspendingDuration = suspendingClock.now - suspendingStart
65 | assert(suspendingDuration > .zero)
66 |
67 | drift = continuousDuration - suspendingDuration
68 | assert(drift >= .milliseconds(-1), "suspending clock running ahead of continuous clock")
69 |
70 | logger.debug(
71 | """
72 | continuous \(continuousDuration, privacy: .public) - \
73 | suspending \(suspendingDuration, privacy: .public) = \
74 | drift \(drift, privacy: .public)
75 | """
76 | )
77 |
78 | if drift > threshold {
79 | logger.log("suspending drift exceeded threshold: \(drift, privacy: .public)")
80 | break
81 | }
82 |
83 | try Task.checkCancellation()
84 | }
85 |
86 | try Task.checkCancellation()
87 | return drift
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Aware/Shared/TimerFormatStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerFormatStyle.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/22/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TimerFormatStyle: FormatStyle, Codable {
11 | enum Style: String, CaseIterable, Codable {
12 | // 1 hr, 15 min
13 | case abbreviated
14 |
15 | // 1h 15m
16 | case condensedAbbreviated
17 |
18 | // 1hr 15min
19 | case narrow
20 |
21 | // 1 hour, 15 minutes
22 | case wide
23 |
24 | // one hour, fifteen minutes
25 | case spellOut
26 |
27 | // 1:15
28 | case digits
29 |
30 | var exampleText: String {
31 | switch self {
32 | case .abbreviated: return "1 hr, 15 min"
33 | case .condensedAbbreviated: return "1h 15m"
34 | case .narrow: return "1hr 15min"
35 | case .wide: return "1 hour, 15 minutes"
36 | case .spellOut: return "one hour, fifteen minutes"
37 | case .digits: return "1:15"
38 | }
39 | }
40 | }
41 |
42 | var style: Style
43 | var showSeconds: Bool
44 |
45 | func format(_ value: Duration) -> String {
46 | let clampedValue = value < .zero ? .zero : value
47 |
48 | switch style {
49 | case .digits:
50 | let pattern: Duration.TimeFormatStyle.Pattern =
51 | showSeconds
52 | ? .hourMinuteSecond(padHourToLength: 1)
53 | : .hourMinute(padHourToLength: 1, roundSeconds: .down)
54 | let format: Duration.TimeFormatStyle = .time(pattern: pattern)
55 | return format.format(clampedValue)
56 |
57 | case .abbreviated, .condensedAbbreviated, .narrow, .wide, .spellOut:
58 | let referenceDate = Date(timeIntervalSinceReferenceDate: 0)
59 | let timeInterval = TimeInterval(clampedValue.components.seconds)
60 | let range = referenceDate ..< Date(timeIntervalSinceReferenceDate: timeInterval)
61 |
62 | let componentsFormatFields: Set =
63 | if showSeconds {
64 | [.hour, .minute, .second]
65 | } else {
66 | [.hour, .minute]
67 | }
68 |
69 | let componentsFormatStyle: Date.ComponentsFormatStyle.Style =
70 | switch style {
71 | case .abbreviated: .abbreviated
72 | case .condensedAbbreviated: .condensedAbbreviated
73 | case .narrow: .narrow
74 | case .spellOut: .spellOut
75 | case .wide: .wide
76 | case .digits: fatalError("unreachable")
77 | }
78 |
79 | let formatStyle: Date.ComponentsFormatStyle = .components(
80 | style: componentsFormatStyle, fields: componentsFormatFields
81 | )
82 |
83 | return formatStyle.format(range)
84 | }
85 | }
86 |
87 | /// Return interval formatted text needs to be updated at depending on if seconds are shown.
88 | /// Also see
89 | var refreshInterval: TimeInterval {
90 | showSeconds ? 1.0 : 60.0
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Aware/visionOS/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | Type
9 | PSMultiValueSpecifier
10 | Title
11 | Format Style
12 | Key
13 | formatStyle
14 | DefaultValue
15 | condensedAbbreviated
16 | Values
17 |
18 | abbreviated
19 | condensedAbbreviated
20 | narrow
21 | wide
22 | spellOut
23 | digits
24 |
25 | Titles
26 |
27 | 1 hr, 15 min
28 | 1h 15m
29 | 1hr 15min
30 | 1 hour, 15 minutes
31 | one hour, fifteen minutes
32 | 1:15
33 |
34 |
35 |
36 | Type
37 | PSToggleSwitchSpecifier
38 | Title
39 | Show Seconds
40 | Key
41 | showSeconds
42 | DefaultValue
43 |
44 |
45 |
46 | Type
47 | PSToggleSwitchSpecifier
48 | Title
49 | Glass Background
50 | Key
51 | glassBackground
52 | DefaultValue
53 |
54 |
55 |
56 | Type
57 | PSToggleSwitchSpecifier
58 | Title
59 | Reset Timer
60 | Key
61 | reset
62 | DefaultValue
63 |
64 |
65 |
66 | Type
67 | PSGroupSpecifier
68 | Title
69 | Timer Settings
70 |
71 |
72 | Type
73 | PSTextFieldSpecifier
74 | Title
75 | Background Task Interval
76 | Key
77 | backgroundTaskInterval
78 | DefaultValue
79 | 300
80 | KeyboardType
81 | NumberPad
82 |
83 |
84 | Type
85 | PSTextFieldSpecifier
86 | Title
87 | Background Grace Period
88 | Key
89 | backgroundGracePeriod
90 | DefaultValue
91 | 7200
92 | KeyboardType
93 | NumberPad
94 |
95 |
96 | Type
97 | PSTextFieldSpecifier
98 | Title
99 | Lock Grace Period
100 | Key
101 | lockGracePeriod
102 | DefaultValue
103 | 60
104 | KeyboardType
105 | NumberPad
106 |
107 |
108 | Type
109 | PSTextFieldSpecifier
110 | Title
111 | Max Suspending Clock Drift
112 | Key
113 | maxSuspendingClockDrift
114 | DefaultValue
115 | 10
116 | KeyboardType
117 | NumberPad
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/AwareTests/NotificationCenterTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Aware
4 |
5 | final class NotificationCenterTests: XCTestCase {
6 | let center = NotificationCenter.default
7 | let fooNotification = Notification.Name("fooNotification")
8 | let barNotification = Notification.Name("barNotification")
9 | let userInfo = ["message": "Hello, world!"]
10 |
11 | func testSendableObserver() {
12 | let expectation = expectation(description: "fooNotification")
13 |
14 | let observer = center.observe(for: fooNotification) { [userInfo] notification in
15 | XCTAssertEqual(notification.name.rawValue, "fooNotification")
16 | XCTAssertEqual(notification.userInfo as? [String: String], userInfo)
17 | expectation.fulfill()
18 | }
19 |
20 | center.post(name: fooNotification, object: nil, userInfo: userInfo)
21 | center.post(name: barNotification, object: nil, userInfo: userInfo)
22 | wait(for: [expectation], timeout: 1.0)
23 |
24 | observer.cancel()
25 | }
26 |
27 | func testMergeNotifications() {
28 | let fooExpectation = expectation(description: "fooNotification")
29 | let barExpectation = expectation(description: "barNotification")
30 |
31 | let consumerTask = Task { [center, fooNotification, barNotification] in
32 | for await notification in center.mergeNotifications(named: [fooNotification, barNotification]) {
33 | if notification.name.rawValue == "fooNotification" {
34 | fooExpectation.fulfill()
35 | }
36 | if notification.name.rawValue == "barNotification" {
37 | barExpectation.fulfill()
38 | }
39 | }
40 | }
41 |
42 | let producerTask = Task { [center, fooNotification, barNotification, userInfo] in
43 | try? await Task.sleep(for: .milliseconds(100))
44 | center.post(name: fooNotification, object: nil, userInfo: userInfo)
45 | center.post(name: barNotification, object: nil, userInfo: userInfo)
46 | }
47 |
48 | wait(for: [fooExpectation, barExpectation], timeout: 1.0)
49 | consumerTask.cancel()
50 | producerTask.cancel()
51 | }
52 |
53 | func testPostBeforeSubscriptionDropped() {
54 | let expectation = expectation(description: "fooNotification")
55 |
56 | for _ in 1 ... 5 {
57 | center.post(name: fooNotification, object: nil, userInfo: ["message": "Goodbye, world!"])
58 | }
59 |
60 | let consumerTask = Task { [center, fooNotification, userInfo] in
61 | for await notification in center.notifications(named: fooNotification) {
62 | XCTAssertEqual(notification.userInfo as? [String: String], userInfo)
63 | break
64 | }
65 | expectation.fulfill()
66 | }
67 |
68 | let producerTask = Task { [center, fooNotification, userInfo] in
69 | try? await Task.sleep(for: .milliseconds(100))
70 | center.post(name: fooNotification, object: nil, userInfo: userInfo)
71 | }
72 |
73 | wait(for: [expectation], timeout: 1.0)
74 | consumerTask.cancel()
75 | producerTask.cancel()
76 | }
77 |
78 | func testSingleBufferingPolicy() {
79 | let notifications = center.notifications(named: fooNotification)
80 |
81 | for i in 1 ... 10 {
82 | center.post(name: fooNotification, object: nil, userInfo: ["count": i])
83 | }
84 |
85 | let expectation = expectation(description: "fooNotification")
86 |
87 | let consumerTask = Task {
88 | let iterator = notifications.makeAsyncIterator()
89 |
90 | var total = 0
91 | for i in 4 ... 10 {
92 | let notification = await iterator.next()
93 | XCTAssertNotNil(notification)
94 | XCTAssertEqual(notification?.userInfo as? [String: Int], ["count": i])
95 | total += 1
96 | }
97 | XCTAssertEqual(total, 7)
98 |
99 | expectation.fulfill()
100 | }
101 |
102 | wait(for: [expectation], timeout: 1.0)
103 | consumerTask.cancel()
104 | }
105 |
106 | func testMultipleBufferingPolicy() {
107 | let notifications = center.mergeNotifications(named: [fooNotification])
108 |
109 | for i in 1 ... 10 {
110 | center.post(name: fooNotification, object: nil, userInfo: ["count": i])
111 | }
112 |
113 | let expectation = expectation(description: "fooNotification")
114 |
115 | let consumerTask = Task {
116 | var iterator = notifications.makeAsyncIterator()
117 |
118 | var total = 0
119 | for i in 4 ... 10 {
120 | let notification = await iterator.next()
121 | XCTAssertNotNil(notification)
122 | XCTAssertEqual(notification?.userInfo as? [String: Int], ["count": i])
123 | total += 1
124 | }
125 | XCTAssertEqual(total, 7)
126 |
127 | expectation.fulfill()
128 | }
129 |
130 | wait(for: [expectation], timeout: 1.0)
131 | consumerTask.cancel()
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Aware/macOS/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 4/22/24.
6 | //
7 |
8 | #if os(macOS)
9 |
10 | import OSLog
11 | import ServiceManagement
12 | import SwiftUI
13 |
14 | private nonisolated(unsafe) let logger = Logger(
15 | subsystem: "com.awaremac.Aware", category: "SettingsView"
16 | )
17 |
18 | struct SettingsView: View {
19 | @AppStorage("reset") private var resetTimer: Bool = false
20 | @AppStorage("userIdleSeconds") private var userIdleSeconds: Int = 120
21 | @AppStorage("formatStyle") private var timerFormatStyle: TimerFormatStyle.Style = .condensedAbbreviated
22 | @AppStorage("showSeconds") private var showSeconds: Bool = false
23 |
24 | @State private var lastLoginItemRegistration: Result?
25 |
26 | @State private var exportingLogs: Bool = false
27 | @State private var showExportErrored: Bool = false
28 |
29 | @State private var window: NSWindow?
30 | @State private var windowIsVisible: Bool = false
31 |
32 | var body: some View {
33 | Form {
34 | Section {
35 | Picker("Format Style:", selection: $timerFormatStyle) {
36 | ForEach(TimerFormatStyle.Style.allCases, id: \.self) { style in
37 | Text(style.exampleText)
38 | }
39 | }
40 |
41 | Toggle("Show Seconds", isOn: $showSeconds)
42 | }
43 |
44 | Spacer()
45 | .frame(width: 0, height: 0)
46 | .padding(.top)
47 |
48 | Section {
49 | LabeledContent("Reset after:") {
50 | TextField("Idle Seconds", value: $userIdleSeconds, format: .number)
51 | .multilineTextAlignment(.trailing)
52 | .labelsHidden()
53 | .frame(width: 50)
54 | Stepper("Idle Seconds", value: $userIdleSeconds, step: 30)
55 | .labelsHidden()
56 | Text("seconds of inactivity")
57 | .padding(.leading, 5)
58 | }
59 |
60 | Button("Reset Timer") {
61 | self.resetTimer = true
62 | }
63 | }
64 |
65 | Spacer()
66 | .frame(width: 0, height: 0)
67 | .padding(.top)
68 |
69 | Section {
70 | LabeledContent("Login Item:") {
71 | Toggle("Open at Login", isOn: openAtLogin)
72 | }
73 | }
74 |
75 | Spacer()
76 | .frame(width: 0, height: 0)
77 | .padding(.top)
78 |
79 | Section {
80 | Button(exportingLogs ? "Exporting Developer Logs..." : "Export Developer Logs") {
81 | self.exportingLogs = true
82 | Task(priority: .low) {
83 | do {
84 | let logURL = try await exportLogs()
85 | NSWorkspace.shared.activateFileViewerSelecting([logURL])
86 | self.showExportErrored = false
87 | } catch {
88 | self.showExportErrored = true
89 | }
90 | self.exportingLogs = false
91 | }
92 | }
93 | .disabled(exportingLogs)
94 | .alert(isPresented: $showExportErrored) {
95 | Alert(
96 | title: Text("Export Error"),
97 | message: Text("Couldn't export logs"),
98 | dismissButton: .default(Text("OK"))
99 | )
100 | }
101 | }
102 | }
103 | .padding()
104 | .frame(width: 350)
105 | .bindWindow($window)
106 | .onChange(of: windowIsVisible) { oldValue, newValue in
107 | logger.debug("Window visibility change: \(oldValue) -> \(newValue)")
108 |
109 | if oldValue == false && newValue == true {
110 | NSApp.activateAggressively()
111 | }
112 | }
113 | .task(id: window) {
114 | guard let window else {
115 | assertionFailure("no window is set")
116 | return
117 | }
118 |
119 | self.windowIsVisible = window.isVisible
120 |
121 | logger.debug("Starting to observe occlusion state changes for \(window)")
122 | let notifications = NotificationCenter.default.notifications(
123 | named: NSWindow.didChangeOcclusionStateNotification,
124 | object: window
125 | ).map { _ in () }
126 |
127 | for await _ in notifications {
128 | logger.debug("Window occlusion state changed for \(window): \(window.isVisible)")
129 | self.windowIsVisible = window.isVisible
130 | }
131 |
132 | logger.debug("Finished observing occlusion state changes for \(window)")
133 | }
134 | }
135 |
136 | var openAtLogin: Binding {
137 | .init {
138 | switch lastLoginItemRegistration {
139 | case let .success(enabled): return enabled
140 | default: return SMAppService.mainApp.status == .enabled
141 | }
142 | } set: { enabled in
143 | lastLoginItemRegistration = Result {
144 | if enabled {
145 | try SMAppService.mainApp.register()
146 | } else {
147 | try SMAppService.mainApp.unregister()
148 | }
149 | return SMAppService.mainApp.status == .enabled
150 | }
151 | }
152 | }
153 | }
154 |
155 | #Preview {
156 | SettingsView()
157 | }
158 |
159 | #endif
160 |
--------------------------------------------------------------------------------
/Aware/macOS/ActivityMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityMonitor.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/16/24.
6 | //
7 |
8 | #if os(macOS)
9 |
10 | import AppKit
11 | import OSLog
12 |
13 | private nonisolated(unsafe) let logger = Logger(
14 | subsystem: "com.awaremac.Aware", category: "ActivityMonitor"
15 | )
16 |
17 | struct ActivityMonitor {
18 | /// Initial timer state
19 | let initialState: TimerState
20 |
21 | struct Configuration: Equatable {
22 | /// The duration since the last user event to consider time idle.
23 | var userIdle: Duration
24 |
25 | /// The duration of idle timer tolerance
26 | var userIdleTolerance: Duration = .seconds(5)
27 | }
28 |
29 | let configuration: Configuration
30 |
31 | /// Subscribe to an async stream of the latest `TimerState` events.
32 | /// - Returns: An async sequence of `TimerState` values.
33 | func updates() -> AsyncStream> {
34 | AsyncStream(bufferingPolicy: .bufferingNewest(1)) { @MainActor yield in
35 | do {
36 | logger.log("Starting ActivityMonitor update task: \(initialState, privacy: .public)")
37 |
38 | var state = initialState {
39 | didSet {
40 | let newValue = state
41 | if oldValue != newValue {
42 | logger.log(
43 | "State changed from \(oldValue, privacy: .public) to \(newValue, privacy: .public)")
44 | yield(newValue)
45 | } else {
46 | logger.debug("No state change \(newValue, privacy: .public)")
47 | }
48 | }
49 | }
50 |
51 | async let notificationsTask: () = { @MainActor in
52 | for await name in NSWorkspace.shared.notificationCenter.mergeNotifications(
53 | named: sleepWakeNotifications
54 | ).map(\.name) {
55 | logger.log("Received \(name.rawValue, privacy: .public)")
56 |
57 | switch name {
58 | case NSWorkspace.willSleepNotification, NSWorkspace.screensDidSleepNotification,
59 | NSWorkspace.willPowerOffNotification:
60 | state.deactivate()
61 | case NSWorkspace.didWakeNotification, NSWorkspace.screensDidWakeNotification:
62 | state.restart()
63 | default:
64 | assertionFailure("unexpected notification: \(name.rawValue)")
65 | }
66 | }
67 | }()
68 |
69 | async let userDefaultsTask: () = { @MainActor in
70 | let store = UserDefaults.standard
71 | for await value in store.updates(forKeyPath: "reset", type: Bool.self, initial: true) {
72 | logger.debug("Received UserDefaults \"reset\" change")
73 | if value == true {
74 | state.restart()
75 | }
76 | if value != nil {
77 | logger.debug("Cleaning up \"reset\" key")
78 | store.removeObject(forKey: "reset")
79 | }
80 | }
81 | }()
82 |
83 | while !Task.isCancelled {
84 | let lastUserEvent = secondsSinceLastUserEvent()
85 | let idleRemaining = configuration.userIdle - lastUserEvent
86 | logger.debug("Last user event \(lastUserEvent, privacy: .public) ago")
87 |
88 | if idleRemaining <= .zero || isMainDisplayAsleep() {
89 | state.deactivate()
90 |
91 | logger.debug("Waiting for user activity event")
92 | let now: ContinuousClock.Instant = .now
93 | try await waitUntilNextUserActivityEvent()
94 | logger.debug("Received user activity event after \(.now - now, privacy: .public)")
95 | } else {
96 | state.activate()
97 |
98 | logger.debug("Sleeping for \(idleRemaining, privacy: .public)")
99 | let now: ContinuousClock.Instant = .now
100 | try await Task.sleep(for: idleRemaining, tolerance: configuration.userIdleTolerance)
101 | logger.debug("Slept for \(.now - now, privacy: .public)")
102 | }
103 | }
104 |
105 | await notificationsTask
106 | await userDefaultsTask
107 |
108 | assert(Task.isCancelled)
109 | try Task.checkCancellation()
110 |
111 | logger.log("Finished ActivityMonitor update task")
112 | } catch is CancellationError {
113 | logger.log("ActivityMonitor update task canceled")
114 | } catch {
115 | logger.error("ActivityMonitor update task canceled unexpectedly: \(error, privacy: .public)")
116 | }
117 | }
118 | }
119 | }
120 |
121 | private let sleepWakeNotifications = [
122 | NSWorkspace.willSleepNotification,
123 | NSWorkspace.didWakeNotification,
124 | NSWorkspace.screensDidSleepNotification,
125 | NSWorkspace.screensDidWakeNotification,
126 | NSWorkspace.willPowerOffNotification,
127 | ]
128 |
129 | private let userActivityEventMask: NSEvent.EventTypeMask = [
130 | .leftMouseDown,
131 | .rightMouseDown,
132 | .mouseMoved,
133 | .keyDown,
134 | .scrollWheel,
135 | ]
136 |
137 | private let userActivityEventTypes: [CGEventType] = [
138 | .leftMouseDown,
139 | .rightMouseDown,
140 | .mouseMoved,
141 | .keyDown,
142 | .scrollWheel,
143 | ]
144 |
145 | func waitUntilNextUserActivityEvent() async throws {
146 | for await _ in NSEvent.globalEvents(matching: userActivityEventMask) {
147 | return
148 | }
149 | }
150 |
151 | private func secondsSinceLastUserEvent() -> Duration {
152 | userActivityEventTypes.map { eventType in
153 | CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: eventType)
154 | }.min().map { ti in Duration(timeInterval: ti) } ?? .zero
155 | }
156 |
157 | private func isMainDisplayAsleep() -> Bool {
158 | CGDisplayIsAsleep(CGMainDisplayID()) == 1
159 | }
160 |
161 | #endif
162 |
--------------------------------------------------------------------------------
/Aware/Shared/TimerState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimerState.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/9/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TimerState: Sendable {
11 | let clock: C
12 |
13 | private var state: InternalState
14 |
15 | private enum InternalState: Hashable, Sendable {
16 | case idle
17 | case grace(start: C.Instant, expires: C.Instant)
18 | case active(start: C.Instant)
19 | }
20 |
21 | /// Initializes timer in idle state.
22 | /// - Parameter clock: A clock instance
23 | init(clock: C) {
24 | self.clock = clock
25 | state = .idle
26 | }
27 |
28 | /// Initializes timer in active state since the specified start time.
29 | /// - Parameters:
30 | /// - start: When the timer has started
31 | /// - clock: A clock instance
32 | init(since start: C.Instant, clock: C) {
33 | self.clock = clock
34 | assert(start <= clock.now, "start should be now or in the past")
35 | state = .active(start: start)
36 | }
37 |
38 | /// Initializes timer in active state until the specified expiration time.
39 | /// - Parameters:
40 | /// - start: When the timer has started
41 | /// - expires: When the timer should expire
42 | /// - clock: A clock instance
43 | init(since start: C.Instant, until expires: C.Instant, clock: C) {
44 | self.clock = clock
45 | assert(start <= clock.now, "start should be now or in the past")
46 | assert(expires > clock.now, "expires should be in the future")
47 | state = .grace(start: start, expires: expires)
48 | }
49 |
50 | /// Check if the timer is active.
51 | var isActive: Bool {
52 | switch state {
53 | case .idle:
54 | false
55 | case let .grace(_, expires):
56 | clock.now < expires
57 | case .active:
58 | true
59 | }
60 | }
61 |
62 | /// Check if the timer is idle.
63 | var isIdle: Bool {
64 | !isActive
65 | }
66 |
67 | /// Check timer has associated expiration, regardless of it being valid.
68 | var hasExpiration: Bool {
69 | if case .grace = state {
70 | true
71 | } else {
72 | false
73 | }
74 | }
75 |
76 | /// Get valid timer start instant. Return `nil` if idle or grace period has expired.
77 | var start: C.Instant? {
78 | switch state {
79 | case .idle:
80 | nil
81 | case let .grace(start, expires):
82 | clock.now < expires ? start : nil
83 | case let .active(start):
84 | start
85 | }
86 | }
87 |
88 | /// If timer has an expiration, return the instant.
89 | var expires: C.Instant? {
90 | switch state {
91 | case .idle:
92 | nil
93 | case let .grace(_, expires):
94 | clock.now < expires ? expires : nil
95 | case .active:
96 | nil
97 | }
98 | }
99 |
100 | /// Get duration the timer has been running for.
101 | /// - Parameter end: The current clock instant
102 | /// - Returns: the duration or zero if timer is idle
103 | func duration(to end: C.Instant) -> C.Duration {
104 | if let start {
105 | start.duration(to: end)
106 | } else {
107 | C.Duration.zero
108 | }
109 | }
110 |
111 | /// Regardless of state, deactivate the timer putting it in idle mode.
112 | mutating func deactivate() {
113 | state = .idle
114 | }
115 |
116 | /// Activate the timer if it's idle or expired. Otherwise, perserve the current start instant.
117 | mutating func activate() {
118 | switch state {
119 | case .idle:
120 | state = .active(start: clock.now)
121 | case let .grace(start, expires):
122 | let now = clock.now
123 | state = .active(start: now < expires ? start : now)
124 | case .active:
125 | ()
126 | }
127 | }
128 |
129 | /// Activates the timer state until the specified expiration time.
130 | ///
131 | /// - Parameters:
132 | /// - expires: The instant at which the timer state should expire.
133 | ///
134 | mutating func activate(until expires: C.Instant) {
135 | assert(expires > clock.now, "expires should be in the future")
136 | let now = clock.now
137 | switch state {
138 | case .idle:
139 | state = .grace(start: now, expires: expires)
140 | case let .grace(start, oldExpires):
141 | state = .grace(start: now < oldExpires ? start : now, expires: expires)
142 | case let .active(start):
143 | state = .grace(start: start, expires: expires)
144 | }
145 | }
146 |
147 | /// Activates the timer state for the specified duration.
148 | ///
149 | /// - Parameters:
150 | /// - duration: The duration for which the timer state should be active.
151 | ///
152 | mutating func activate(for duration: C.Duration) {
153 | assert(duration > .zero, "duration should be positive")
154 | activate(until: clock.now.advanced(by: duration))
155 | }
156 |
157 | /// Activates the timer state setting the start to now regardless of the current state.
158 | mutating func restart() {
159 | state = .active(start: clock.now)
160 | }
161 | }
162 |
163 | extension TimerState: Equatable {
164 | /// Returns a Boolean value indicating whether two values are equal.
165 | static func == (lhs: Self, rhs: Self) -> Bool {
166 | lhs.state == rhs.state
167 | }
168 | }
169 |
170 | extension TimerState: CustomStringConvertible where C.Duration == Swift.Duration {
171 | /// A textual representation of this timer state.
172 | var description: String {
173 | switch state {
174 | case .idle:
175 | return "idle"
176 |
177 | case let .grace(start, expires):
178 | let now = clock.now
179 | if now < expires {
180 | let startFormatted = start.duration(to: now).formatted(.time(pattern: .hourMinuteSecond))
181 | let expiresFormatted = now.duration(to: expires).formatted(
182 | .time(pattern: .hourMinuteSecond))
183 | return "active[\(startFormatted), expires in \(expiresFormatted)]"
184 | } else {
185 | return "idle[expired]"
186 | }
187 |
188 | case let .active(start):
189 | let now = clock.now
190 | let startFormatted = start.duration(to: now).formatted(.time(pattern: .hourMinuteSecond))
191 | return "active[\(startFormatted)]"
192 | }
193 | }
194 | }
195 |
196 | extension TimerState where C == UTCClock {
197 | init() {
198 | self.init(clock: UTCClock())
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Aware/visionOS/BackgroundTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackgroundTask.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 3/23/24.
6 | //
7 |
8 | #if canImport(BackgroundTasks)
9 |
10 | import BackgroundTasks
11 | import OSLog
12 | import SwiftUI
13 |
14 | private nonisolated(unsafe) let logger = Logger(
15 | subsystem: "com.awaremac.Aware", category: "BackgroundTask"
16 | )
17 |
18 | @available(macOS, unavailable)
19 | actor BackgroundTask {
20 | enum TaskRequestType {
21 | case appRefresh
22 | case processing(requiresExternalPower: Bool, requiresNetworkConnectivity: Bool)
23 | }
24 |
25 | struct ScheduledTask {
26 | let submittedAt: Date
27 | let earliestBeginAt: Date
28 | }
29 |
30 | struct RanTask {
31 | let task: ScheduledTask?
32 | let ranAt: Date
33 | }
34 |
35 | let identifier: String
36 | let notification: Notification.Name
37 | private let taskRequestType: TaskRequestType
38 |
39 | var scheduledTask: ScheduledTask?
40 | var lastRanTask: RanTask?
41 |
42 | static func appRefresh(_ identifier: String) -> BackgroundTask {
43 | BackgroundTask(identifier: identifier, taskRequestType: .appRefresh)
44 | }
45 |
46 | static func processing(
47 | _ identifier: String,
48 | requiresExternalPower: Bool = false,
49 | requiresNetworkConnectivity: Bool = false
50 | ) -> BackgroundTask {
51 | BackgroundTask(
52 | identifier: identifier,
53 | taskRequestType: .processing(
54 | requiresExternalPower: requiresExternalPower,
55 | requiresNetworkConnectivity: requiresNetworkConnectivity
56 | )
57 | )
58 | }
59 |
60 | init(identifier: String, taskRequestType: TaskRequestType) {
61 | self.identifier = identifier
62 | self.taskRequestType = taskRequestType
63 | notification = Notification.Name(identifier)
64 | }
65 |
66 | private var request: BGTaskRequest {
67 | switch taskRequestType {
68 | case .appRefresh:
69 | return BGAppRefreshTaskRequest(identifier: identifier)
70 | case let .processing(requiresExternalPower, requiresNetworkConnectivity):
71 | let request = BGProcessingTaskRequest(identifier: identifier)
72 | request.requiresExternalPower = requiresExternalPower
73 | request.requiresNetworkConnectivity = requiresNetworkConnectivity
74 | return request
75 | }
76 | }
77 |
78 | fileprivate func run() {
79 | let identifier = self.identifier
80 | logger.log("Starting background task: \(identifier, privacy: .public)")
81 |
82 | if let scheduledTask {
83 | let submittedAgo: Duration = UTCClock.Instant(scheduledTask.submittedAt).duration(to: .now)
84 | let earliestBeginAgo: Duration = UTCClock.Instant(scheduledTask.earliestBeginAt).duration(
85 | to: .now)
86 | logger.log(
87 | "Background submitted at \(scheduledTask.submittedAt, privacy: .public), \(submittedAgo, privacy: .public) ago"
88 | )
89 | logger.log(
90 | "Requested to run after \(scheduledTask.earliestBeginAt, privacy: .public), \(earliestBeginAgo, privacy: .public) after"
91 | )
92 | } else {
93 | logger.error(
94 | "Running background task, but no scheduled \(identifier, privacy: .public) task noted")
95 | }
96 | lastRanTask = RanTask(task: scheduledTask, ranAt: .now)
97 |
98 | let notification = Notification(name: self.notification, object: self)
99 | logger.log("Posting \(notification.name.rawValue, privacy: .public) notification")
100 | NotificationCenter.default.post(notification)
101 |
102 | logger.log("Finished background task: \(identifier, privacy: .public)")
103 | }
104 |
105 | func cancel() async {
106 | let pendingCount = await countPendingTaskRequests()
107 | guard pendingCount > 0 else {
108 | logger.debug("No scheduled tasks to cancel")
109 | return
110 | }
111 | assert(pendingCount <= 1, "more than one background task was scheduled")
112 |
113 | scheduledTask = nil
114 | let identifier = identifier
115 | logger.info("Canceling \(pendingCount) \(identifier, privacy: .public) task")
116 | BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier)
117 | }
118 |
119 | func reschedule(for beginDate: Date) async {
120 | let earliestBeginAt: Date = await earliestPendingTaskRequestBeginDate() ?? .distantPast
121 | guard beginDate > earliestBeginAt.addingTimeInterval(60) else {
122 | let identifier = identifier
123 | logger.debug("\(identifier, privacy: .public) task already scheduled for \(beginDate, privacy: .public)")
124 | return
125 | }
126 |
127 | await cancel()
128 | schedule(for: beginDate)
129 | }
130 |
131 | func schedule(for beginDate: Date) {
132 | let identifier = identifier
133 | let request = request
134 |
135 | request.earliestBeginDate = beginDate
136 |
137 | do {
138 | try BGTaskScheduler.shared.submit(request)
139 | scheduledTask = ScheduledTask(submittedAt: .now, earliestBeginAt: beginDate)
140 | logger.info("Scheduled \(identifier, privacy: .public) task after \(beginDate, privacy: .public)")
141 | } catch let error as BGTaskScheduler.Error {
142 | switch error.code {
143 | case .unavailable:
144 | #if !targetEnvironment(simulator)
145 | logger.info("App can’t schedule background work")
146 | #endif
147 | case .tooManyPendingTaskRequests:
148 | logger.error("Too many pending \(identifier, privacy: .public) tasks requested")
149 | case .notPermitted:
150 | logger.error("App isn’t permitted to launch \(identifier, privacy: .public) task")
151 | @unknown default:
152 | logger.error(
153 | "Unknown error scheduling \(identifier, privacy: .public) task: \(error, privacy: .public)")
154 | }
155 | } catch {
156 | logger.error("Unknown error scheduling \(identifier, privacy: .public) task: \(error, privacy: .public)")
157 | }
158 | }
159 |
160 | private nonisolated func countPendingTaskRequests() async -> Int {
161 | await BGTaskScheduler.shared.pendingTaskRequests().filter { request in
162 | request.identifier == self.identifier
163 | }.count
164 | }
165 |
166 | private nonisolated func earliestPendingTaskRequestBeginDate() async -> Date? {
167 | await BGTaskScheduler.shared.pendingTaskRequests().compactMap(\.earliestBeginDate).min()
168 | }
169 | }
170 |
171 | @available(macOS, unavailable)
172 | extension Scene {
173 | func backgroundTask(_ task: BackgroundTask) -> some Scene {
174 | backgroundTask(.appRefresh(task.identifier)) {
175 | await task.run()
176 | }
177 | }
178 | }
179 |
180 | #endif
181 |
--------------------------------------------------------------------------------
/Aware/visionOS/ActivityMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityMonitor.swift
3 | // Aware
4 | //
5 | // Created by Joshua Peek on 2/23/24.
6 | //
7 |
8 | #if os(visionOS)
9 |
10 | import OSLog
11 | import UIKit
12 |
13 | private nonisolated(unsafe) let logger = Logger(
14 | subsystem: "com.awaremac.Aware", category: "ActivityMonitor"
15 | )
16 |
17 | let fetchActivityMonitorTask: BackgroundTask = .appRefresh("fetchActivityMonitor")
18 | let processingActivityMonitorTask: BackgroundTask = .processing("processingActivityMonitor")
19 |
20 | struct ActivityMonitor {
21 | /// Initial timer state
22 | let initialState: TimerState
23 |
24 | struct Configuration: Equatable {
25 | /// The minimum number of seconds to schedule between background tasks.
26 | var backgroundTaskInterval: Duration
27 |
28 | /// The duration the app can be in the background and be considered active if it's opened again.
29 | var backgroundGracePeriod: Duration
30 |
31 | /// The duration after locking the device it can be considered active if it's unlocked again.
32 | var lockGracePeriod: Duration
33 |
34 | /// The max duration to allow the suspending clock to drift from the continuous clock.
35 | var maxSuspendingClockDrift: Duration
36 | }
37 |
38 | let configuration: Configuration
39 |
40 | /// Subscribe to an async stream of the latest `TimerState` events.
41 | /// - Returns: An async sequence of `TimerState` values.
42 | func updates() -> AsyncStream> {
43 | AsyncStream(bufferingPolicy: .bufferingNewest(1)) { @MainActor yield in
44 | do {
45 | logger.log("Starting ActivityMonitor update task: \(initialState)")
46 |
47 | var state = initialState {
48 | didSet {
49 | let newValue = state
50 | if oldValue != newValue {
51 | logger.log(
52 | "State changed from \(oldValue, privacy: .public) to \(newValue, privacy: .public)")
53 | yield(newValue)
54 | } else {
55 | logger.debug("No state change \(newValue, privacy: .public)")
56 | }
57 | }
58 | }
59 |
60 | let app = UIApplication.shared
61 |
62 | // Set initial state
63 | assert(app.applicationState != .background)
64 | assert(app.isProtectedDataAvailable)
65 | state.activate()
66 |
67 | let center = NotificationCenter.default
68 |
69 | async let driftTask: () = { @MainActor in
70 | while !Task.isCancelled {
71 | try await SuspendingClock().monitorDrift(threshold: configuration.maxSuspendingClockDrift)
72 | state.deactivate()
73 |
74 | if app.applicationState != .background {
75 | assert(app.isProtectedDataAvailable, "expected protected data to be available")
76 | state.activate()
77 | } else if app.isProtectedDataAvailable {
78 | state.activate(for: configuration.backgroundGracePeriod)
79 | }
80 | }
81 | }()
82 |
83 | async let userDefaultsTask: () = { @MainActor in
84 | let store = UserDefaults.standard
85 | for await value in store.updates(forKeyPath: "reset", type: Bool.self, initial: true) {
86 | logger.debug("Received UserDefaults \"reset\" change")
87 | if value == true {
88 | state.restart()
89 | }
90 | if value != nil {
91 | logger.debug("Cleaning up \"reset\" key")
92 | store.removeObject(forKey: "reset")
93 | }
94 | }
95 | }()
96 |
97 | let notificationNames = [
98 | UIApplication.didEnterBackgroundNotification,
99 | UIApplication.willEnterForegroundNotification,
100 | UIApplication.protectedDataDidBecomeAvailableNotification,
101 | UIApplication.protectedDataWillBecomeUnavailableNotification,
102 | fetchActivityMonitorTask.notification,
103 | processingActivityMonitorTask.notification,
104 | ]
105 |
106 | for await notificationName in center.mergeNotifications(named: notificationNames).map(\.name) {
107 | logger.log("Received \(notificationName.rawValue, privacy: .public)")
108 |
109 | let oldState = state
110 |
111 | switch notificationName {
112 | case UIApplication.didEnterBackgroundNotification:
113 | assert(app.applicationState == .background)
114 | assert(app.isProtectedDataAvailable)
115 | state.activate(for: configuration.backgroundGracePeriod)
116 |
117 | case UIApplication.willEnterForegroundNotification:
118 | assert(app.applicationState != .background)
119 | assert(app.isProtectedDataAvailable)
120 | state.activate()
121 |
122 | case UIApplication.protectedDataDidBecomeAvailableNotification:
123 | assert(app.applicationState == .background)
124 | assert(app.isProtectedDataAvailable)
125 | state.activate(for: configuration.backgroundGracePeriod)
126 |
127 | case UIApplication.protectedDataWillBecomeUnavailableNotification:
128 | assert(app.applicationState == .background)
129 | assert(app.isProtectedDataAvailable)
130 | state.activate(for: configuration.lockGracePeriod)
131 |
132 | case fetchActivityMonitorTask.notification,
133 | processingActivityMonitorTask.notification:
134 |
135 | if app.applicationState == .background {
136 | if app.isProtectedDataAvailable {
137 | // Running in background while device is unlocked
138 | state.activate(for: configuration.backgroundGracePeriod)
139 | } else {
140 | // Running in background while device is locked
141 | state.deactivate()
142 | }
143 | } else {
144 | // Active in foreground
145 | assert(app.isProtectedDataAvailable, "expected protected data to be available")
146 | assert(state.isActive, "expected to already be active")
147 | assert(!state.hasExpiration, "expected to not have expiration")
148 | state.activate()
149 | }
150 |
151 | default:
152 | assertionFailure("unexpected notification: \(notificationName.rawValue)")
153 | }
154 |
155 | // It would be nice to do this in the state didSet hook, but we need async
156 | await rescheduleBackgroundTasks(oldState: oldState, newState: state)
157 | }
158 |
159 | try await driftTask
160 | await userDefaultsTask
161 |
162 | assert(Task.isCancelled)
163 | try Task.checkCancellation()
164 |
165 | logger.log("Finished ActivityMonitor update task")
166 | } catch is CancellationError {
167 | logger.log("ActivityMonitor update task canceled")
168 | } catch {
169 | logger.error("ActivityMonitor update task canceled unexpectedly: \(error, privacy: .public)")
170 | }
171 | }
172 | }
173 |
174 | private func rescheduleBackgroundTasks(oldState: TimerState, newState: TimerState) async {
175 | if newState.hasExpiration {
176 | let taskBeginDate = UTCClock.Instant.now.advanced(by: configuration.backgroundTaskInterval).date
177 | await fetchActivityMonitorTask.reschedule(for: taskBeginDate)
178 | await processingActivityMonitorTask.reschedule(for: taskBeginDate)
179 | // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"fetchActivityMonitor"]
180 | // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"processingActivityMonitor"]
181 | } else if oldState.hasExpiration {
182 | await fetchActivityMonitorTask.cancel()
183 | await processingActivityMonitorTask.cancel()
184 | }
185 | }
186 | }
187 |
188 | #endif
189 |
--------------------------------------------------------------------------------
/AwareTests/TimerStateTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Aware
4 |
5 | final class PausedClock: @unchecked Sendable, Clock {
6 | var now: Instant
7 | init() { now = .now }
8 |
9 | typealias Instant = ContinuousClock.Instant
10 | var minimumResolution: Duration { ContinuousClock().minimumResolution }
11 | func sleep(until _: Instant, tolerance _: Duration?) async throws {}
12 |
13 | func advance(by duration: Duration) {
14 | now = now.advanced(by: duration)
15 | }
16 | }
17 |
18 | final class TimerStateTests: XCTestCase {
19 | let clock: PausedClock = .init()
20 |
21 | func testIsActive() {
22 | var timer: TimerState
23 |
24 | timer = TimerState(clock: clock)
25 | XCTAssertFalse(timer.isActive)
26 |
27 | let start = clock.now.advanced(by: .seconds(-30))
28 | timer = TimerState(since: start, clock: clock)
29 | XCTAssertTrue(timer.isActive)
30 |
31 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(150)), clock: clock)
32 | XCTAssertTrue(timer.isActive)
33 |
34 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(30)), clock: clock)
35 | clock.advance(by: .seconds(60))
36 | XCTAssertFalse(timer.isActive)
37 | }
38 |
39 | func testIsIdle() {
40 | var timer: TimerState
41 |
42 | timer = TimerState(clock: clock)
43 | XCTAssertTrue(timer.isIdle)
44 |
45 | let start = clock.now.advanced(by: .seconds(-30))
46 | timer = TimerState(since: start, clock: clock)
47 | XCTAssertFalse(timer.isIdle)
48 |
49 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(150)), clock: clock)
50 | XCTAssertFalse(timer.isIdle)
51 |
52 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(30)), clock: clock)
53 | clock.advance(by: .seconds(60))
54 | XCTAssertTrue(timer.isIdle)
55 | }
56 |
57 | func testStart() {
58 | var timer: TimerState
59 |
60 | timer = TimerState(clock: clock)
61 | XCTAssertNil(timer.start)
62 |
63 | let start = clock.now.advanced(by: .seconds(-30))
64 | timer = TimerState(since: start, clock: clock)
65 | XCTAssertEqual(timer.start, start)
66 |
67 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(150)), clock: clock)
68 | XCTAssertEqual(timer.start, start)
69 |
70 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(30)), clock: clock)
71 | clock.advance(by: .seconds(60))
72 | XCTAssertNil(timer.start)
73 | }
74 |
75 | func testExpires() {
76 | var timer: TimerState
77 |
78 | timer = TimerState(clock: clock)
79 | XCTAssertNil(timer.expires)
80 |
81 | let start = clock.now.advanced(by: .seconds(-30))
82 | timer = TimerState(since: start, clock: clock)
83 | XCTAssertNil(timer.expires)
84 |
85 | let expires = clock.now.advanced(by: .seconds(150))
86 | timer = TimerState(since: start, until: expires, clock: clock)
87 | XCTAssertEqual(timer.expires, expires)
88 |
89 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(30)), clock: clock)
90 | clock.advance(by: .seconds(60))
91 | XCTAssertNil(timer.expires)
92 | }
93 |
94 | func testDuration() {
95 | var timer: TimerState
96 |
97 | timer = TimerState(clock: clock)
98 | XCTAssertEqual(timer.duration(to: clock.now), .seconds(0))
99 |
100 | let start = clock.now.advanced(by: .seconds(-30))
101 | timer = TimerState(since: start, clock: clock)
102 | XCTAssertEqual(timer.duration(to: clock.now), .seconds(30))
103 |
104 | let expires = clock.now.advanced(by: .seconds(150))
105 | timer = TimerState(since: start, until: expires, clock: clock)
106 | XCTAssertEqual(timer.duration(to: clock.now), .seconds(30))
107 |
108 | timer = TimerState(since: start, until: clock.now.advanced(by: .seconds(30)), clock: clock)
109 | clock.advance(by: .seconds(60))
110 | XCTAssertEqual(timer.duration(to: clock.now), .seconds(0))
111 | }
112 |
113 | func testDeactivate() {
114 | var timer: TimerState
115 |
116 | timer = TimerState(clock: clock)
117 | timer.deactivate()
118 | XCTAssertEqual(String(describing: timer), "idle")
119 |
120 | timer = TimerState(since: clock.now.advanced(by: .seconds(-300)), clock: clock)
121 | timer.deactivate()
122 | XCTAssertEqual(String(describing: timer), "idle")
123 |
124 | timer = TimerState(
125 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
126 | clock: clock
127 | )
128 | timer.deactivate()
129 | XCTAssertEqual(String(describing: timer), "idle")
130 |
131 | timer = TimerState(
132 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
133 | clock: clock
134 | )
135 | clock.advance(by: .seconds(60))
136 | timer.deactivate()
137 | XCTAssertEqual(String(describing: timer), "idle")
138 | }
139 |
140 | func testActivate() {
141 | var timer: TimerState
142 |
143 | timer = TimerState(clock: clock)
144 | timer.activate()
145 | XCTAssertEqual(String(describing: timer), "active[0:00:00]")
146 |
147 | timer = TimerState(since: clock.now.advanced(by: .seconds(-300)), clock: clock)
148 | timer.activate()
149 | XCTAssertEqual(String(describing: timer), "active[0:05:00]")
150 |
151 | timer = TimerState(
152 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
153 | clock: clock
154 | )
155 | timer.activate()
156 | XCTAssertEqual(String(describing: timer), "active[0:05:00]")
157 |
158 | timer = TimerState(
159 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
160 | clock: clock
161 | )
162 | clock.advance(by: .seconds(60))
163 | timer.activate()
164 | XCTAssertEqual(String(describing: timer), "active[0:00:00]")
165 | }
166 |
167 | func testActivateUntil() {
168 | var timer: TimerState
169 |
170 | timer = TimerState(clock: clock)
171 | timer.activate(until: clock.now.advanced(by: .seconds(60)))
172 | XCTAssertEqual(String(describing: timer), "active[0:00:00, expires in 0:01:00]")
173 |
174 | timer = TimerState(since: clock.now.advanced(by: .seconds(-300)), clock: clock)
175 | timer.activate(until: clock.now.advanced(by: .seconds(60)))
176 | XCTAssertEqual(String(describing: timer), "active[0:05:00, expires in 0:01:00]")
177 |
178 | timer = TimerState(
179 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
180 | clock: clock
181 | )
182 | timer.activate(until: clock.now.advanced(by: .seconds(60)))
183 | XCTAssertEqual(String(describing: timer), "active[0:05:00, expires in 0:01:00]")
184 |
185 | timer = TimerState(
186 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
187 | clock: clock
188 | )
189 | clock.advance(by: .seconds(60))
190 | timer.activate(until: clock.now.advanced(by: .seconds(60)))
191 | XCTAssertEqual(String(describing: timer), "active[0:00:00, expires in 0:01:00]")
192 | }
193 |
194 | func testActivateFor() {
195 | var timer: TimerState
196 |
197 | timer = TimerState(clock: clock)
198 | timer.activate(for: .seconds(60))
199 | XCTAssertEqual(String(describing: timer), "active[0:00:00, expires in 0:01:00]")
200 |
201 | timer = TimerState(since: clock.now.advanced(by: .seconds(-300)), clock: clock)
202 | timer.activate(for: .seconds(60))
203 | XCTAssertEqual(String(describing: timer), "active[0:05:00, expires in 0:01:00]")
204 |
205 | timer = TimerState(
206 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
207 | clock: clock
208 | )
209 | timer.activate(for: .seconds(60))
210 | XCTAssertEqual(String(describing: timer), "active[0:05:00, expires in 0:01:00]")
211 |
212 | timer = TimerState(
213 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
214 | clock: clock
215 | )
216 | clock.advance(by: .seconds(60))
217 | timer.activate(for: .seconds(60))
218 | XCTAssertEqual(String(describing: timer), "active[0:00:00, expires in 0:01:00]")
219 | }
220 |
221 | func testEquatable() {
222 | XCTAssertEqual(TimerState(clock: clock), TimerState(clock: clock))
223 | XCTAssertEqual(
224 | TimerState(since: clock.now, clock: clock), TimerState(since: clock.now, clock: clock)
225 | )
226 | XCTAssertNotEqual(TimerState(since: clock.now, clock: clock), TimerState(clock: clock))
227 | XCTAssertNotEqual(
228 | TimerState(clock: clock),
229 | TimerState(since: clock.now.advanced(by: .seconds(-1)), clock: clock)
230 | )
231 | }
232 |
233 | func testCustomStringConvertible() {
234 | var timer: TimerState
235 |
236 | timer = TimerState(clock: clock)
237 | XCTAssertEqual(String(describing: timer), "idle")
238 |
239 | timer = TimerState(since: clock.now.advanced(by: .seconds(-5)), clock: clock)
240 | XCTAssertEqual(String(describing: timer), "active[0:00:05]")
241 |
242 | timer = TimerState(since: clock.now.advanced(by: .seconds(-300)), clock: clock)
243 | XCTAssertEqual(String(describing: timer), "active[0:05:00]")
244 |
245 | timer = TimerState(
246 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(150)),
247 | clock: clock
248 | )
249 | XCTAssertEqual(String(describing: timer), "active[0:05:00, expires in 0:02:30]")
250 |
251 | timer = TimerState(
252 | since: clock.now.advanced(by: .seconds(-300)), until: clock.now.advanced(by: .seconds(30)),
253 | clock: clock
254 | )
255 | clock.advance(by: .seconds(60))
256 | XCTAssertEqual(String(describing: timer), "idle[expired]")
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/AwareTests/TimerFormatStyleTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Aware
4 |
5 | final class TimerFormatStyleTests: XCTestCase {
6 | func testAbbreviatedWithoutSeconds() {
7 | let format = TimerFormatStyle(style: .abbreviated, showSeconds: false)
8 | XCTAssertEqual(format.format(.zero), "0 min")
9 |
10 | XCTAssertEqual(format.format(.seconds(1)), "0 min")
11 | XCTAssertEqual(format.format(.seconds(59)), "0 min")
12 | XCTAssertEqual(format.format(.minutes(1)), "1 min")
13 | XCTAssertEqual(format.format(.seconds(119)), "1 min")
14 | XCTAssertEqual(format.format(.seconds(61)), "1 min")
15 | XCTAssertEqual(format.format(.minutes(15)), "15 min")
16 | XCTAssertEqual(format.format(.minutes(59)), "59 min")
17 | XCTAssertEqual(format.format(.seconds(3599)), "59 min")
18 |
19 | XCTAssertEqual(format.format(.hours(1)), "1 hr")
20 | XCTAssertEqual(format.format(.seconds(3661)), "1 hr, 1 min")
21 | XCTAssertEqual(format.format(.seconds(4500)), "1 hr, 15 min")
22 | XCTAssertEqual(format.format(.seconds(7200)), "2 hr")
23 |
24 | XCTAssertEqual(format.format(.seconds(-1)), "0 min")
25 | XCTAssertEqual(format.format(.seconds(-90)), "0 min")
26 | XCTAssertEqual(format.format(.hours(-1)), "0 min")
27 |
28 | XCTAssertEqual(format.format(.seconds(Int.max)), "0 min")
29 | XCTAssertEqual(format.format(.seconds(Int.min)), "0 min")
30 | }
31 |
32 | func testAbbreviatedWithSeconds() {
33 | let format = TimerFormatStyle(style: .abbreviated, showSeconds: true)
34 | XCTAssertEqual(format.format(.zero), "0 sec")
35 |
36 | XCTAssertEqual(format.format(.seconds(1)), "1 sec")
37 | XCTAssertEqual(format.format(.seconds(59)), "59 sec")
38 |
39 | XCTAssertEqual(format.format(.minutes(1)), "1 min")
40 | XCTAssertEqual(format.format(.seconds(119)), "1 min, 59 sec")
41 | XCTAssertEqual(format.format(.seconds(61)), "1 min, 1 sec")
42 | XCTAssertEqual(format.format(.minutes(15)), "15 min")
43 | XCTAssertEqual(format.format(.minutes(59)), "59 min")
44 | XCTAssertEqual(format.format(.seconds(3599)), "59 min, 59 sec")
45 |
46 | XCTAssertEqual(format.format(.hours(1)), "1 hr")
47 | XCTAssertEqual(format.format(.seconds(3661)), "1 hr, 1 min, 1 sec")
48 | XCTAssertEqual(format.format(.seconds(4500)), "1 hr, 15 min")
49 | XCTAssertEqual(format.format(.seconds(7200)), "2 hr")
50 |
51 | XCTAssertEqual(format.format(.seconds(-1)), "0 sec")
52 | XCTAssertEqual(format.format(.seconds(-90)), "0 sec")
53 | XCTAssertEqual(format.format(.hours(-1)), "0 sec")
54 |
55 | XCTAssertEqual(format.format(.seconds(Int.max)), "0 sec")
56 | XCTAssertEqual(format.format(.seconds(Int.min)), "0 sec")
57 | }
58 |
59 | func testCondensedAbbreviatedWithoutSeconds() {
60 | let format = TimerFormatStyle(style: .condensedAbbreviated, showSeconds: false)
61 | XCTAssertEqual(format.format(.zero), "0m")
62 |
63 | XCTAssertEqual(format.format(.seconds(1)), "0m")
64 | XCTAssertEqual(format.format(.seconds(59)), "0m")
65 | XCTAssertEqual(format.format(.minutes(1)), "1m")
66 | XCTAssertEqual(format.format(.seconds(119)), "1m")
67 | XCTAssertEqual(format.format(.seconds(61)), "1m")
68 | XCTAssertEqual(format.format(.minutes(15)), "15m")
69 | XCTAssertEqual(format.format(.minutes(59)), "59m")
70 | XCTAssertEqual(format.format(.seconds(3599)), "59m")
71 |
72 | XCTAssertEqual(format.format(.hours(1)), "1h")
73 | XCTAssertEqual(format.format(.seconds(3661)), "1h 1m")
74 | XCTAssertEqual(format.format(.seconds(4500)), "1h 15m")
75 | XCTAssertEqual(format.format(.seconds(7200)), "2h")
76 |
77 | XCTAssertEqual(format.format(.seconds(-1)), "0m")
78 | XCTAssertEqual(format.format(.seconds(-90)), "0m")
79 | XCTAssertEqual(format.format(.hours(-1)), "0m")
80 |
81 | XCTAssertEqual(format.format(.seconds(Int.max)), "0m")
82 | XCTAssertEqual(format.format(.seconds(Int.min)), "0m")
83 | }
84 |
85 | func testCondensedAbbreviatedWithSeconds() {
86 | let format = TimerFormatStyle(style: .condensedAbbreviated, showSeconds: true)
87 | XCTAssertEqual(format.format(.zero), "0s")
88 |
89 | XCTAssertEqual(format.format(.seconds(1)), "1s")
90 | XCTAssertEqual(format.format(.seconds(59)), "59s")
91 |
92 | XCTAssertEqual(format.format(.minutes(1)), "1m")
93 | XCTAssertEqual(format.format(.seconds(119)), "1m 59s")
94 | XCTAssertEqual(format.format(.seconds(61)), "1m 1s")
95 | XCTAssertEqual(format.format(.minutes(15)), "15m")
96 | XCTAssertEqual(format.format(.minutes(59)), "59m")
97 | XCTAssertEqual(format.format(.seconds(3599)), "59m 59s")
98 |
99 | XCTAssertEqual(format.format(.hours(1)), "1h")
100 | XCTAssertEqual(format.format(.seconds(3661)), "1h 1m 1s")
101 | XCTAssertEqual(format.format(.seconds(4500)), "1h 15m")
102 | XCTAssertEqual(format.format(.seconds(7200)), "2h")
103 |
104 | XCTAssertEqual(format.format(.seconds(-1)), "0s")
105 | XCTAssertEqual(format.format(.seconds(-90)), "0s")
106 | XCTAssertEqual(format.format(.hours(-1)), "0s")
107 |
108 | XCTAssertEqual(format.format(.seconds(Int.max)), "0s")
109 | XCTAssertEqual(format.format(.seconds(Int.min)), "0s")
110 | }
111 |
112 | func testNarrowWithoutSeconds() {
113 | let format = TimerFormatStyle(style: .narrow, showSeconds: false)
114 | XCTAssertEqual(format.format(.zero), "0min")
115 |
116 | XCTAssertEqual(format.format(.seconds(1)), "0min")
117 | XCTAssertEqual(format.format(.seconds(59)), "0min")
118 | XCTAssertEqual(format.format(.minutes(1)), "1min")
119 | XCTAssertEqual(format.format(.seconds(119)), "1min")
120 | XCTAssertEqual(format.format(.seconds(61)), "1min")
121 | XCTAssertEqual(format.format(.minutes(15)), "15min")
122 | XCTAssertEqual(format.format(.minutes(59)), "59min")
123 | XCTAssertEqual(format.format(.seconds(3599)), "59min")
124 |
125 | XCTAssertEqual(format.format(.hours(1)), "1hr")
126 | XCTAssertEqual(format.format(.seconds(3661)), "1hr 1min")
127 | XCTAssertEqual(format.format(.seconds(4500)), "1hr 15min")
128 | XCTAssertEqual(format.format(.seconds(7200)), "2hr")
129 |
130 | XCTAssertEqual(format.format(.seconds(-1)), "0min")
131 | XCTAssertEqual(format.format(.seconds(-90)), "0min")
132 | XCTAssertEqual(format.format(.hours(-1)), "0min")
133 |
134 | XCTAssertEqual(format.format(.seconds(Int.max)), "0min")
135 | XCTAssertEqual(format.format(.seconds(Int.min)), "0min")
136 | }
137 |
138 | func testNarrowWithSeconds() {
139 | let format = TimerFormatStyle(style: .narrow, showSeconds: true)
140 | XCTAssertEqual(format.format(.zero), "0sec")
141 |
142 | XCTAssertEqual(format.format(.seconds(1)), "1sec")
143 | XCTAssertEqual(format.format(.seconds(59)), "59sec")
144 |
145 | XCTAssertEqual(format.format(.minutes(1)), "1min")
146 | XCTAssertEqual(format.format(.seconds(119)), "1min 59sec")
147 | XCTAssertEqual(format.format(.seconds(61)), "1min 1sec")
148 | XCTAssertEqual(format.format(.minutes(15)), "15min")
149 | XCTAssertEqual(format.format(.minutes(59)), "59min")
150 | XCTAssertEqual(format.format(.seconds(3599)), "59min 59sec")
151 |
152 | XCTAssertEqual(format.format(.hours(1)), "1hr")
153 | XCTAssertEqual(format.format(.seconds(3661)), "1hr 1min 1sec")
154 | XCTAssertEqual(format.format(.seconds(4500)), "1hr 15min")
155 | XCTAssertEqual(format.format(.seconds(7200)), "2hr")
156 |
157 | XCTAssertEqual(format.format(.seconds(-1)), "0sec")
158 | XCTAssertEqual(format.format(.seconds(-90)), "0sec")
159 | XCTAssertEqual(format.format(.hours(-1)), "0sec")
160 |
161 | XCTAssertEqual(format.format(.seconds(Int.max)), "0sec")
162 | XCTAssertEqual(format.format(.seconds(Int.min)), "0sec")
163 | }
164 |
165 | func testWideWithoutSeconds() {
166 | let format = TimerFormatStyle(style: .wide, showSeconds: false)
167 | XCTAssertEqual(format.format(.zero), "0 minutes")
168 |
169 | XCTAssertEqual(format.format(.seconds(1)), "0 minutes")
170 | XCTAssertEqual(format.format(.seconds(59)), "0 minutes")
171 | XCTAssertEqual(format.format(.minutes(1)), "1 minute")
172 | XCTAssertEqual(format.format(.seconds(119)), "1 minute")
173 | XCTAssertEqual(format.format(.seconds(61)), "1 minute")
174 | XCTAssertEqual(format.format(.minutes(15)), "15 minutes")
175 | XCTAssertEqual(format.format(.minutes(59)), "59 minutes")
176 | XCTAssertEqual(format.format(.seconds(3599)), "59 minutes")
177 |
178 | XCTAssertEqual(format.format(.hours(1)), "1 hour")
179 | XCTAssertEqual(format.format(.seconds(3661)), "1 hour, 1 minute")
180 | XCTAssertEqual(format.format(.seconds(4500)), "1 hour, 15 minutes")
181 | XCTAssertEqual(format.format(.seconds(7200)), "2 hours")
182 |
183 | XCTAssertEqual(format.format(.seconds(-1)), "0 minutes")
184 | XCTAssertEqual(format.format(.seconds(-90)), "0 minutes")
185 | XCTAssertEqual(format.format(.hours(-1)), "0 minutes")
186 |
187 | XCTAssertEqual(format.format(.seconds(Int.max)), "0 minutes")
188 | XCTAssertEqual(format.format(.seconds(Int.min)), "0 minutes")
189 | }
190 |
191 | func testWideWithSeconds() {
192 | let format = TimerFormatStyle(style: .wide, showSeconds: true)
193 | XCTAssertEqual(format.format(.zero), "0 seconds")
194 |
195 | XCTAssertEqual(format.format(.seconds(1)), "1 second")
196 | XCTAssertEqual(format.format(.seconds(59)), "59 seconds")
197 |
198 | XCTAssertEqual(format.format(.minutes(1)), "1 minute")
199 | XCTAssertEqual(format.format(.seconds(119)), "1 minute, 59 seconds")
200 | XCTAssertEqual(format.format(.seconds(61)), "1 minute, 1 second")
201 | XCTAssertEqual(format.format(.minutes(15)), "15 minutes")
202 | XCTAssertEqual(format.format(.minutes(59)), "59 minutes")
203 | XCTAssertEqual(format.format(.seconds(3599)), "59 minutes, 59 seconds")
204 |
205 | XCTAssertEqual(format.format(.hours(1)), "1 hour")
206 | XCTAssertEqual(format.format(.seconds(3661)), "1 hour, 1 minute, 1 second")
207 | XCTAssertEqual(format.format(.seconds(4500)), "1 hour, 15 minutes")
208 | XCTAssertEqual(format.format(.seconds(7200)), "2 hours")
209 |
210 | XCTAssertEqual(format.format(.seconds(-1)), "0 seconds")
211 | XCTAssertEqual(format.format(.seconds(-90)), "0 seconds")
212 | XCTAssertEqual(format.format(.hours(-1)), "0 seconds")
213 |
214 | XCTAssertEqual(format.format(.seconds(Int.max)), "0 seconds")
215 | XCTAssertEqual(format.format(.seconds(Int.min)), "0 seconds")
216 | }
217 |
218 | func testSpellOutWithoutSeconds() {
219 | let format = TimerFormatStyle(style: .spellOut, showSeconds: false)
220 | XCTAssertEqual(format.format(.zero), "zero minutes")
221 |
222 | XCTAssertEqual(format.format(.seconds(1)), "zero minutes")
223 | XCTAssertEqual(format.format(.seconds(59)), "zero minutes")
224 | XCTAssertEqual(format.format(.minutes(1)), "one minute")
225 | XCTAssertEqual(format.format(.seconds(119)), "one minute")
226 | XCTAssertEqual(format.format(.seconds(61)), "one minute")
227 | XCTAssertEqual(format.format(.minutes(15)), "fifteen minutes")
228 | XCTAssertEqual(format.format(.minutes(59)), "fifty-nine minutes")
229 | XCTAssertEqual(format.format(.seconds(3599)), "fifty-nine minutes")
230 |
231 | XCTAssertEqual(format.format(.hours(1)), "one hour")
232 | XCTAssertEqual(format.format(.seconds(3661)), "one hour, one minute")
233 | XCTAssertEqual(format.format(.seconds(4500)), "one hour, fifteen minutes")
234 | XCTAssertEqual(format.format(.seconds(7200)), "two hours")
235 |
236 | XCTAssertEqual(format.format(.seconds(-1)), "zero minutes")
237 | XCTAssertEqual(format.format(.seconds(-90)), "zero minutes")
238 | XCTAssertEqual(format.format(.hours(-1)), "zero minutes")
239 |
240 | XCTAssertEqual(format.format(.seconds(Int.max)), "zero minutes")
241 | XCTAssertEqual(format.format(.seconds(Int.min)), "zero minutes")
242 | }
243 |
244 | func testSpellOutWithSeconds() {
245 | let format = TimerFormatStyle(style: .spellOut, showSeconds: true)
246 | XCTAssertEqual(format.format(.zero), "zero seconds")
247 |
248 | XCTAssertEqual(format.format(.seconds(1)), "one second")
249 | XCTAssertEqual(format.format(.seconds(59)), "fifty-nine seconds")
250 |
251 | XCTAssertEqual(format.format(.minutes(1)), "one minute")
252 | XCTAssertEqual(format.format(.seconds(119)), "one minute, fifty-nine seconds")
253 | XCTAssertEqual(format.format(.seconds(61)), "one minute, one second")
254 | XCTAssertEqual(format.format(.minutes(15)), "fifteen minutes")
255 | XCTAssertEqual(format.format(.minutes(59)), "fifty-nine minutes")
256 | XCTAssertEqual(format.format(.seconds(3599)), "fifty-nine minutes, fifty-nine seconds")
257 |
258 | XCTAssertEqual(format.format(.hours(1)), "one hour")
259 | XCTAssertEqual(format.format(.seconds(3661)), "one hour, one minute, one second")
260 | XCTAssertEqual(format.format(.seconds(4500)), "one hour, fifteen minutes")
261 | XCTAssertEqual(format.format(.seconds(7200)), "two hours")
262 |
263 | XCTAssertEqual(format.format(.seconds(-1)), "zero seconds")
264 | XCTAssertEqual(format.format(.seconds(-90)), "zero seconds")
265 | XCTAssertEqual(format.format(.hours(-1)), "zero seconds")
266 |
267 | XCTAssertEqual(format.format(.seconds(Int.max)), "zero seconds")
268 | XCTAssertEqual(format.format(.seconds(Int.min)), "zero seconds")
269 | }
270 |
271 | func testDigitsWithoutSeconds() {
272 | let format = TimerFormatStyle(style: .digits, showSeconds: false)
273 | XCTAssertEqual(format.format(.zero), "0:00")
274 |
275 | XCTAssertEqual(format.format(.seconds(1)), "0:00")
276 | XCTAssertEqual(format.format(.seconds(59)), "0:00")
277 | XCTAssertEqual(format.format(.minutes(1)), "0:01")
278 | XCTAssertEqual(format.format(.seconds(119)), "0:01")
279 | XCTAssertEqual(format.format(.seconds(61)), "0:01")
280 | XCTAssertEqual(format.format(.minutes(15)), "0:15")
281 | XCTAssertEqual(format.format(.minutes(59)), "0:59")
282 | XCTAssertEqual(format.format(.seconds(3599)), "0:59")
283 |
284 | XCTAssertEqual(format.format(.hours(1)), "1:00")
285 | XCTAssertEqual(format.format(.seconds(3661)), "1:01")
286 | XCTAssertEqual(format.format(.seconds(4500)), "1:15")
287 | XCTAssertEqual(format.format(.seconds(7200)), "2:00")
288 |
289 | XCTAssertEqual(format.format(.seconds(-1)), "0:00")
290 | XCTAssertEqual(format.format(.seconds(-90)), "0:00")
291 | XCTAssertEqual(format.format(.hours(-1)), "0:00")
292 |
293 | XCTAssertEqual(format.format(.seconds(Int.max)), "2,562,047,788,015,215:30")
294 | XCTAssertEqual(format.format(.seconds(Int.min)), "0:00")
295 | }
296 |
297 | func testDigitsWithSeconds() {
298 | let format = TimerFormatStyle(style: .digits, showSeconds: true)
299 | XCTAssertEqual(format.format(.zero), "0:00:00")
300 |
301 | XCTAssertEqual(format.format(.seconds(1)), "0:00:01")
302 | XCTAssertEqual(format.format(.seconds(59)), "0:00:59")
303 |
304 | XCTAssertEqual(format.format(.minutes(1)), "0:01:00")
305 | XCTAssertEqual(format.format(.seconds(119)), "0:01:59")
306 | XCTAssertEqual(format.format(.seconds(61)), "0:01:01")
307 | XCTAssertEqual(format.format(.minutes(15)), "0:15:00")
308 | XCTAssertEqual(format.format(.minutes(59)), "0:59:00")
309 | XCTAssertEqual(format.format(.seconds(3599)), "0:59:59")
310 |
311 | XCTAssertEqual(format.format(.hours(1)), "1:00:00")
312 | XCTAssertEqual(format.format(.seconds(3661)), "1:01:01")
313 | XCTAssertEqual(format.format(.seconds(4500)), "1:15:00")
314 | XCTAssertEqual(format.format(.seconds(7200)), "2:00:00")
315 |
316 | XCTAssertEqual(format.format(.seconds(-1)), "0:00:00")
317 | XCTAssertEqual(format.format(.seconds(-90)), "0:00:00")
318 | XCTAssertEqual(format.format(.hours(-1)), "0:00:00")
319 |
320 | XCTAssertEqual(format.format(.seconds(Int.max)), "2,562,047,788,015,215:30:07")
321 | XCTAssertEqual(format.format(.seconds(Int.min)), "0:00:00")
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/Aware.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 030840142BD8498300C75EE3 /* UserDefaults+AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030840132BD8498300C75EE3 /* UserDefaults+AsyncSequence.swift */; };
11 | 03162DE12BB12F3B0004BFDE /* AsyncStream+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03162DE02BB12F3B0004BFDE /* AsyncStream+Extensions.swift */; };
12 | 03162DE32BB130880004BFDE /* AsyncStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03162DE22BB130880004BFDE /* AsyncStreamTests.swift */; };
13 | 032E60A82BD7243E008BB2B4 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 032E60A72BD7243E008BB2B4 /* Settings.bundle */; platformFilters = (xros, ); };
14 | 032E60AA2BD737A5008BB2B4 /* TimerFormatStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E60A92BD737A5008BB2B4 /* TimerFormatStyle.swift */; };
15 | 032E60AC2BD737EC008BB2B4 /* TimerFormatStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E60AB2BD737EC008BB2B4 /* TimerFormatStyleTests.swift */; };
16 | 032E60AE2BD74BEF008BB2B4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E60AD2BD74BEF008BB2B4 /* SettingsView.swift */; platformFilters = (macos, ); };
17 | 03409BC42B897F7C00EF8EE9 /* ActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03409BC32B897F7C00EF8EE9 /* ActivityMonitor.swift */; platformFilters = (xros, ); };
18 | 0341CB6A2B9C2CC800CC0C96 /* NSEvent+AsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB692B9C2CC800CC0C96 /* NSEvent+AsyncStream.swift */; platformFilters = (macos, ); };
19 | 0341CB712B9C3FCE00CC0C96 /* UTCClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB702B9C3FCE00CC0C96 /* UTCClock.swift */; };
20 | 0341CB732B9C456400CC0C96 /* UTCClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB722B9C456400CC0C96 /* UTCClockTests.swift */; };
21 | 0341CB752B9CDEAC00CC0C96 /* TimerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB742B9CDEAC00CC0C96 /* TimerState.swift */; };
22 | 0341CB772B9CDEC400CC0C96 /* TimerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB762B9CDEC400CC0C96 /* TimerStateTests.swift */; };
23 | 0341CB792B9D4CEB00CC0C96 /* TimerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341CB782B9D4CEB00CC0C96 /* TimerWindow.swift */; platformFilters = (xros, ); };
24 | 0347D5992BAF8B5400C5741E /* NotificationCenter+AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0347D5982BAF8B5400C5741E /* NotificationCenter+AsyncSequence.swift */; };
25 | 0347D59B2BAF8CB200C5741E /* NotificationCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0347D59A2BAF8CB200C5741E /* NotificationCenterTests.swift */; };
26 | 0347D59D2BAF9A3F00C5741E /* SuspendingClock+Drift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0347D59C2BAF9A3F00C5741E /* SuspendingClock+Drift.swift */; };
27 | 0347D59F2BAFB97300C5741E /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0347D59E2BAFB97300C5741E /* BackgroundTask.swift */; platformFilters = (xros, ); };
28 | 035820952BDB11880099E707 /* View+NSStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035820942BDB11880099E707 /* View+NSStatusItem.swift */; platformFilters = (macos, ); };
29 | 035820972BDB122B0099E707 /* View+NSWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035820962BDB122B0099E707 /* View+NSWindow.swift */; platformFilters = (macos, ); };
30 | 035820992BDB24EC0099E707 /* NSApplication+Activate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035820982BDB24EC0099E707 /* NSApplication+Activate.swift */; platformFilters = (macos, ); };
31 | 036569CE2B9A40C8003D3DCA /* MenuBarTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036569CD2B9A40C8003D3DCA /* MenuBarTimelineView.swift */; platformFilters = (macos, ); };
32 | 036DA9B52B7AF52E0066B4B2 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036DA9B42B7AF52E0066B4B2 /* App.swift */; };
33 | 036EBD1B1C1408C200121D0B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 036EBD1A1C1408C200121D0B /* Assets.xcassets */; };
34 | 037195362B804E4C00B807ED /* ActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037195352B804E4C00B807ED /* ActivityMonitor.swift */; platformFilters = (macos, ); };
35 | 0381B4992B808A5A002213F6 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381B4982B808A5A002213F6 /* MenuBar.swift */; platformFilters = (macos, ); };
36 | 03830C992BA6079D00532C40 /* NotificationCenter+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03830C982BA6079D00532C40 /* NotificationCenter+Observer.swift */; };
37 | 038858802BA54DBA003E287D /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0388587F2BA54DBA003E287D /* Duration+Extensions.swift */; };
38 | 0394D1752B845FB400FE7020 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0394D1742B845FB400FE7020 /* TimerView.swift */; platformFilters = (xros, ); };
39 | 0394D1772B84630E00FE7020 /* TimerTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0394D1762B84630E00FE7020 /* TimerTextView.swift */; platformFilters = (xros, ); };
40 | 03CC1F472BD8B241000BA17D /* LogExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1F462BD8B241000BA17D /* LogExport.swift */; };
41 | 03E00DD42BB0A82100A6C522 /* NotificationName+Nonisolated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E00DD32BB0A82100A6C522 /* NotificationName+Nonisolated.swift */; platformFilters = (xros, ); };
42 | 03F1C34E2B92AE6E0084572C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 03F1C34D2B92AE300084572C /* PrivacyInfo.xcprivacy */; };
43 | /* End PBXBuildFile section */
44 |
45 | /* Begin PBXContainerItemProxy section */
46 | 03F9E22B1C24CAD3001DBE86 /* PBXContainerItemProxy */ = {
47 | isa = PBXContainerItemProxy;
48 | containerPortal = 036EBD0D1C1408C200121D0B /* Project object */;
49 | proxyType = 1;
50 | remoteGlobalIDString = 036EBD141C1408C200121D0B;
51 | remoteInfo = Aware;
52 | };
53 | /* End PBXContainerItemProxy section */
54 |
55 | /* Begin PBXFileReference section */
56 | 030840132BD8498300C75EE3 /* UserDefaults+AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AsyncSequence.swift"; sourceTree = ""; };
57 | 03162DE02BB12F3B0004BFDE /* AsyncStream+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncStream+Extensions.swift"; sourceTree = ""; };
58 | 03162DE22BB130880004BFDE /* AsyncStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStreamTests.swift; sourceTree = ""; };
59 | 032E60A72BD7243E008BB2B4 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; };
60 | 032E60A92BD737A5008BB2B4 /* TimerFormatStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerFormatStyle.swift; sourceTree = ""; };
61 | 032E60AB2BD737EC008BB2B4 /* TimerFormatStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerFormatStyleTests.swift; sourceTree = ""; };
62 | 032E60AD2BD74BEF008BB2B4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
63 | 03409BC32B897F7C00EF8EE9 /* ActivityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityMonitor.swift; sourceTree = ""; };
64 | 0341CB692B9C2CC800CC0C96 /* NSEvent+AsyncStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSEvent+AsyncStream.swift"; sourceTree = ""; };
65 | 0341CB702B9C3FCE00CC0C96 /* UTCClock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTCClock.swift; sourceTree = ""; };
66 | 0341CB722B9C456400CC0C96 /* UTCClockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTCClockTests.swift; sourceTree = ""; };
67 | 0341CB742B9CDEAC00CC0C96 /* TimerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerState.swift; sourceTree = ""; };
68 | 0341CB762B9CDEC400CC0C96 /* TimerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerStateTests.swift; sourceTree = ""; };
69 | 0341CB782B9D4CEB00CC0C96 /* TimerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerWindow.swift; sourceTree = ""; };
70 | 0347D5982BAF8B5400C5741E /* NotificationCenter+AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+AsyncSequence.swift"; sourceTree = ""; };
71 | 0347D59A2BAF8CB200C5741E /* NotificationCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterTests.swift; sourceTree = ""; };
72 | 0347D59C2BAF9A3F00C5741E /* SuspendingClock+Drift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuspendingClock+Drift.swift"; sourceTree = ""; };
73 | 0347D59E2BAFB97300C5741E /* BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTask.swift; sourceTree = ""; };
74 | 035820942BDB11880099E707 /* View+NSStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NSStatusItem.swift"; sourceTree = ""; };
75 | 035820962BDB122B0099E707 /* View+NSWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NSWindow.swift"; sourceTree = ""; };
76 | 035820982BDB24EC0099E707 /* NSApplication+Activate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Activate.swift"; sourceTree = ""; };
77 | 036569CD2B9A40C8003D3DCA /* MenuBarTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarTimelineView.swift; sourceTree = ""; };
78 | 036DA9B42B7AF52E0066B4B2 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
79 | 036EBD151C1408C200121D0B /* Aware.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Aware.app; sourceTree = BUILT_PRODUCTS_DIR; };
80 | 036EBD1A1C1408C200121D0B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
81 | 037195352B804E4C00B807ED /* ActivityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityMonitor.swift; sourceTree = ""; };
82 | 0381B4982B808A5A002213F6 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; };
83 | 0381B49A2B808FF4002213F6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
84 | 03830C982BA6079D00532C40 /* NotificationCenter+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Observer.swift"; sourceTree = ""; };
85 | 0388587F2BA54DBA003E287D /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = ""; };
86 | 038D0B381C4DDD5600040C44 /* Aware.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Aware.entitlements; sourceTree = ""; };
87 | 0394D1742B845FB400FE7020 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; };
88 | 0394D1762B84630E00FE7020 /* TimerTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerTextView.swift; sourceTree = ""; };
89 | 03CC1F462BD8B241000BA17D /* LogExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogExport.swift; sourceTree = ""; };
90 | 03E00DD32BB0A82100A6C522 /* NotificationName+Nonisolated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Nonisolated.swift"; sourceTree = ""; };
91 | 03F1C34D2B92AE300084572C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
92 | 03F9E2261C24CAD3001DBE86 /* AwareTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AwareTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
93 | /* End PBXFileReference section */
94 |
95 | /* Begin PBXFrameworksBuildPhase section */
96 | 036EBD121C1408C200121D0B /* Frameworks */ = {
97 | isa = PBXFrameworksBuildPhase;
98 | buildActionMask = 2147483647;
99 | files = (
100 | );
101 | runOnlyForDeploymentPostprocessing = 0;
102 | };
103 | 03F9E2231C24CAD3001DBE86 /* Frameworks */ = {
104 | isa = PBXFrameworksBuildPhase;
105 | buildActionMask = 2147483647;
106 | files = (
107 | );
108 | runOnlyForDeploymentPostprocessing = 0;
109 | };
110 | /* End PBXFrameworksBuildPhase section */
111 |
112 | /* Begin PBXGroup section */
113 | 0341CB6F2B9C35EE00CC0C96 /* Shared */ = {
114 | isa = PBXGroup;
115 | children = (
116 | 03162DE02BB12F3B0004BFDE /* AsyncStream+Extensions.swift */,
117 | 0388587F2BA54DBA003E287D /* Duration+Extensions.swift */,
118 | 03CC1F462BD8B241000BA17D /* LogExport.swift */,
119 | 0347D5982BAF8B5400C5741E /* NotificationCenter+AsyncSequence.swift */,
120 | 03830C982BA6079D00532C40 /* NotificationCenter+Observer.swift */,
121 | 0347D59C2BAF9A3F00C5741E /* SuspendingClock+Drift.swift */,
122 | 032E60A92BD737A5008BB2B4 /* TimerFormatStyle.swift */,
123 | 0341CB742B9CDEAC00CC0C96 /* TimerState.swift */,
124 | 030840132BD8498300C75EE3 /* UserDefaults+AsyncSequence.swift */,
125 | 0341CB702B9C3FCE00CC0C96 /* UTCClock.swift */,
126 | );
127 | path = Shared;
128 | sourceTree = "";
129 | };
130 | 036EBD0C1C1408C200121D0B = {
131 | isa = PBXGroup;
132 | children = (
133 | 036EBD171C1408C200121D0B /* Aware */,
134 | 03F9E2271C24CAD3001DBE86 /* AwareTests */,
135 | 036EBD161C1408C200121D0B /* Products */,
136 | );
137 | sourceTree = "";
138 | };
139 | 036EBD161C1408C200121D0B /* Products */ = {
140 | isa = PBXGroup;
141 | children = (
142 | 036EBD151C1408C200121D0B /* Aware.app */,
143 | 03F9E2261C24CAD3001DBE86 /* AwareTests.xctest */,
144 | );
145 | name = Products;
146 | sourceTree = "";
147 | };
148 | 036EBD171C1408C200121D0B /* Aware */ = {
149 | isa = PBXGroup;
150 | children = (
151 | 036DA9B42B7AF52E0066B4B2 /* App.swift */,
152 | 0341CB6F2B9C35EE00CC0C96 /* Shared */,
153 | 0381B49C2B8095FB002213F6 /* macOS */,
154 | 0381B49D2B809661002213F6 /* visionOS */,
155 | 036EBD1A1C1408C200121D0B /* Assets.xcassets */,
156 | 038D0B381C4DDD5600040C44 /* Aware.entitlements */,
157 | 03F1C34D2B92AE300084572C /* PrivacyInfo.xcprivacy */,
158 | 0381B49A2B808FF4002213F6 /* Info.plist */,
159 | );
160 | path = Aware;
161 | sourceTree = "";
162 | };
163 | 0381B49C2B8095FB002213F6 /* macOS */ = {
164 | isa = PBXGroup;
165 | children = (
166 | 037195352B804E4C00B807ED /* ActivityMonitor.swift */,
167 | 0381B4982B808A5A002213F6 /* MenuBar.swift */,
168 | 036569CD2B9A40C8003D3DCA /* MenuBarTimelineView.swift */,
169 | 035820982BDB24EC0099E707 /* NSApplication+Activate.swift */,
170 | 0341CB692B9C2CC800CC0C96 /* NSEvent+AsyncStream.swift */,
171 | 032E60AD2BD74BEF008BB2B4 /* SettingsView.swift */,
172 | 035820942BDB11880099E707 /* View+NSStatusItem.swift */,
173 | 035820962BDB122B0099E707 /* View+NSWindow.swift */,
174 | );
175 | path = macOS;
176 | sourceTree = "";
177 | };
178 | 0381B49D2B809661002213F6 /* visionOS */ = {
179 | isa = PBXGroup;
180 | children = (
181 | 03409BC32B897F7C00EF8EE9 /* ActivityMonitor.swift */,
182 | 0347D59E2BAFB97300C5741E /* BackgroundTask.swift */,
183 | 03E00DD32BB0A82100A6C522 /* NotificationName+Nonisolated.swift */,
184 | 0394D1762B84630E00FE7020 /* TimerTextView.swift */,
185 | 0394D1742B845FB400FE7020 /* TimerView.swift */,
186 | 0341CB782B9D4CEB00CC0C96 /* TimerWindow.swift */,
187 | 032E60A72BD7243E008BB2B4 /* Settings.bundle */,
188 | );
189 | path = visionOS;
190 | sourceTree = "";
191 | };
192 | 03F9E2271C24CAD3001DBE86 /* AwareTests */ = {
193 | isa = PBXGroup;
194 | children = (
195 | 03162DE22BB130880004BFDE /* AsyncStreamTests.swift */,
196 | 0347D59A2BAF8CB200C5741E /* NotificationCenterTests.swift */,
197 | 032E60AB2BD737EC008BB2B4 /* TimerFormatStyleTests.swift */,
198 | 0341CB762B9CDEC400CC0C96 /* TimerStateTests.swift */,
199 | 0341CB722B9C456400CC0C96 /* UTCClockTests.swift */,
200 | );
201 | path = AwareTests;
202 | sourceTree = "";
203 | };
204 | /* End PBXGroup section */
205 |
206 | /* Begin PBXNativeTarget section */
207 | 036EBD141C1408C200121D0B /* Aware */ = {
208 | isa = PBXNativeTarget;
209 | buildConfigurationList = 036EBD221C1408C200121D0B /* Build configuration list for PBXNativeTarget "Aware" */;
210 | buildPhases = (
211 | 036EBD111C1408C200121D0B /* Sources */,
212 | 036EBD121C1408C200121D0B /* Frameworks */,
213 | 036EBD131C1408C200121D0B /* Resources */,
214 | );
215 | buildRules = (
216 | );
217 | dependencies = (
218 | );
219 | name = Aware;
220 | productName = Aware;
221 | productReference = 036EBD151C1408C200121D0B /* Aware.app */;
222 | productType = "com.apple.product-type.application";
223 | };
224 | 03F9E2251C24CAD3001DBE86 /* AwareTests */ = {
225 | isa = PBXNativeTarget;
226 | buildConfigurationList = 03F9E22F1C24CAD3001DBE86 /* Build configuration list for PBXNativeTarget "AwareTests" */;
227 | buildPhases = (
228 | 03F9E2221C24CAD3001DBE86 /* Sources */,
229 | 03F9E2231C24CAD3001DBE86 /* Frameworks */,
230 | 03F9E2241C24CAD3001DBE86 /* Resources */,
231 | );
232 | buildRules = (
233 | );
234 | dependencies = (
235 | 03F9E22C1C24CAD3001DBE86 /* PBXTargetDependency */,
236 | );
237 | name = AwareTests;
238 | productName = AwareTests;
239 | productReference = 03F9E2261C24CAD3001DBE86 /* AwareTests.xctest */;
240 | productType = "com.apple.product-type.bundle.unit-test";
241 | };
242 | /* End PBXNativeTarget section */
243 |
244 | /* Begin PBXProject section */
245 | 036EBD0D1C1408C200121D0B /* Project object */ = {
246 | isa = PBXProject;
247 | attributes = {
248 | BuildIndependentTargetsInParallel = YES;
249 | LastSwiftUpdateCheck = 0720;
250 | LastUpgradeCheck = 1520;
251 | TargetAttributes = {
252 | 036EBD141C1408C200121D0B = {
253 | CreatedOnToolsVersion = 7.1.1;
254 | DevelopmentTeam = 5SW9VUVYKC;
255 | LastSwiftMigration = 1020;
256 | ProvisioningStyle = Automatic;
257 | SystemCapabilities = {
258 | com.apple.Sandbox = {
259 | enabled = 1;
260 | };
261 | };
262 | };
263 | 03F9E2251C24CAD3001DBE86 = {
264 | CreatedOnToolsVersion = 7.2;
265 | DevelopmentTeam = 5SW9VUVYKC;
266 | LastSwiftMigration = 1020;
267 | ProvisioningStyle = Automatic;
268 | TestTargetID = 036EBD141C1408C200121D0B;
269 | };
270 | };
271 | };
272 | buildConfigurationList = 036EBD101C1408C200121D0B /* Build configuration list for PBXProject "Aware" */;
273 | compatibilityVersion = "Xcode 3.2";
274 | developmentRegion = en;
275 | hasScannedForEncodings = 0;
276 | knownRegions = (
277 | en,
278 | Base,
279 | );
280 | mainGroup = 036EBD0C1C1408C200121D0B;
281 | productRefGroup = 036EBD161C1408C200121D0B /* Products */;
282 | projectDirPath = "";
283 | projectRoot = "";
284 | targets = (
285 | 036EBD141C1408C200121D0B /* Aware */,
286 | 03F9E2251C24CAD3001DBE86 /* AwareTests */,
287 | );
288 | };
289 | /* End PBXProject section */
290 |
291 | /* Begin PBXResourcesBuildPhase section */
292 | 036EBD131C1408C200121D0B /* Resources */ = {
293 | isa = PBXResourcesBuildPhase;
294 | buildActionMask = 2147483647;
295 | files = (
296 | 036EBD1B1C1408C200121D0B /* Assets.xcassets in Resources */,
297 | 03F1C34E2B92AE6E0084572C /* PrivacyInfo.xcprivacy in Resources */,
298 | 032E60A82BD7243E008BB2B4 /* Settings.bundle in Resources */,
299 | );
300 | runOnlyForDeploymentPostprocessing = 0;
301 | };
302 | 03F9E2241C24CAD3001DBE86 /* Resources */ = {
303 | isa = PBXResourcesBuildPhase;
304 | buildActionMask = 2147483647;
305 | files = (
306 | );
307 | runOnlyForDeploymentPostprocessing = 0;
308 | };
309 | /* End PBXResourcesBuildPhase section */
310 |
311 | /* Begin PBXSourcesBuildPhase section */
312 | 036EBD111C1408C200121D0B /* Sources */ = {
313 | isa = PBXSourcesBuildPhase;
314 | buildActionMask = 2147483647;
315 | files = (
316 | 036DA9B52B7AF52E0066B4B2 /* App.swift in Sources */,
317 | 03162DE12BB12F3B0004BFDE /* AsyncStream+Extensions.swift in Sources */,
318 | 038858802BA54DBA003E287D /* Duration+Extensions.swift in Sources */,
319 | 03CC1F472BD8B241000BA17D /* LogExport.swift in Sources */,
320 | 0347D5992BAF8B5400C5741E /* NotificationCenter+AsyncSequence.swift in Sources */,
321 | 03830C992BA6079D00532C40 /* NotificationCenter+Observer.swift in Sources */,
322 | 0347D59D2BAF9A3F00C5741E /* SuspendingClock+Drift.swift in Sources */,
323 | 032E60AA2BD737A5008BB2B4 /* TimerFormatStyle.swift in Sources */,
324 | 0341CB752B9CDEAC00CC0C96 /* TimerState.swift in Sources */,
325 | 030840142BD8498300C75EE3 /* UserDefaults+AsyncSequence.swift in Sources */,
326 | 0341CB712B9C3FCE00CC0C96 /* UTCClock.swift in Sources */,
327 | 037195362B804E4C00B807ED /* ActivityMonitor.swift in Sources */,
328 | 0381B4992B808A5A002213F6 /* MenuBar.swift in Sources */,
329 | 036569CE2B9A40C8003D3DCA /* MenuBarTimelineView.swift in Sources */,
330 | 035820992BDB24EC0099E707 /* NSApplication+Activate.swift in Sources */,
331 | 0341CB6A2B9C2CC800CC0C96 /* NSEvent+AsyncStream.swift in Sources */,
332 | 032E60AE2BD74BEF008BB2B4 /* SettingsView.swift in Sources */,
333 | 035820952BDB11880099E707 /* View+NSStatusItem.swift in Sources */,
334 | 035820972BDB122B0099E707 /* View+NSWindow.swift in Sources */,
335 | 03409BC42B897F7C00EF8EE9 /* ActivityMonitor.swift in Sources */,
336 | 0347D59F2BAFB97300C5741E /* BackgroundTask.swift in Sources */,
337 | 03E00DD42BB0A82100A6C522 /* NotificationName+Nonisolated.swift in Sources */,
338 | 0394D1772B84630E00FE7020 /* TimerTextView.swift in Sources */,
339 | 0394D1752B845FB400FE7020 /* TimerView.swift in Sources */,
340 | 0341CB792B9D4CEB00CC0C96 /* TimerWindow.swift in Sources */,
341 | );
342 | runOnlyForDeploymentPostprocessing = 0;
343 | };
344 | 03F9E2221C24CAD3001DBE86 /* Sources */ = {
345 | isa = PBXSourcesBuildPhase;
346 | buildActionMask = 2147483647;
347 | files = (
348 | 03162DE32BB130880004BFDE /* AsyncStreamTests.swift in Sources */,
349 | 0347D59B2BAF8CB200C5741E /* NotificationCenterTests.swift in Sources */,
350 | 032E60AC2BD737EC008BB2B4 /* TimerFormatStyleTests.swift in Sources */,
351 | 0341CB772B9CDEC400CC0C96 /* TimerStateTests.swift in Sources */,
352 | 0341CB732B9C456400CC0C96 /* UTCClockTests.swift in Sources */,
353 | );
354 | runOnlyForDeploymentPostprocessing = 0;
355 | };
356 | /* End PBXSourcesBuildPhase section */
357 |
358 | /* Begin PBXTargetDependency section */
359 | 03F9E22C1C24CAD3001DBE86 /* PBXTargetDependency */ = {
360 | isa = PBXTargetDependency;
361 | target = 036EBD141C1408C200121D0B /* Aware */;
362 | targetProxy = 03F9E22B1C24CAD3001DBE86 /* PBXContainerItemProxy */;
363 | };
364 | /* End PBXTargetDependency section */
365 |
366 | /* Begin XCBuildConfiguration section */
367 | 036EBD201C1408C200121D0B /* Debug */ = {
368 | isa = XCBuildConfiguration;
369 | buildSettings = {
370 | ALWAYS_SEARCH_USER_PATHS = NO;
371 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
372 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
374 | CLANG_CXX_LIBRARY = "libc++";
375 | CLANG_ENABLE_MODULES = YES;
376 | CLANG_ENABLE_OBJC_ARC = YES;
377 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
378 | CLANG_WARN_BOOL_CONVERSION = YES;
379 | CLANG_WARN_COMMA = YES;
380 | CLANG_WARN_CONSTANT_CONVERSION = YES;
381 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
382 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
383 | CLANG_WARN_EMPTY_BODY = YES;
384 | CLANG_WARN_ENUM_CONVERSION = YES;
385 | CLANG_WARN_INFINITE_RECURSION = YES;
386 | CLANG_WARN_INT_CONVERSION = YES;
387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
388 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
389 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
391 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
392 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
393 | CLANG_WARN_STRICT_PROTOTYPES = YES;
394 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
395 | CLANG_WARN_UNREACHABLE_CODE = YES;
396 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
397 | CODE_SIGN_IDENTITY = "-";
398 | COPY_PHASE_STRIP = NO;
399 | DEAD_CODE_STRIPPING = YES;
400 | DEBUG_INFORMATION_FORMAT = dwarf;
401 | ENABLE_HARDENED_RUNTIME = YES;
402 | ENABLE_STRICT_OBJC_MSGSEND = YES;
403 | ENABLE_TESTABILITY = YES;
404 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
405 | GCC_C_LANGUAGE_STANDARD = gnu99;
406 | GCC_DYNAMIC_NO_PIC = NO;
407 | GCC_NO_COMMON_BLOCKS = YES;
408 | GCC_OPTIMIZATION_LEVEL = 0;
409 | GCC_PREPROCESSOR_DEFINITIONS = (
410 | "DEBUG=1",
411 | "$(inherited)",
412 | );
413 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
414 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
415 | GCC_WARN_UNDECLARED_SELECTOR = YES;
416 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
417 | GCC_WARN_UNUSED_FUNCTION = YES;
418 | GCC_WARN_UNUSED_VARIABLE = YES;
419 | MACOSX_DEPLOYMENT_TARGET = 14.4;
420 | MTL_ENABLE_DEBUG_INFO = YES;
421 | ONLY_ACTIVE_ARCH = YES;
422 | SDKROOT = macosx;
423 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
424 | SWIFT_STRICT_CONCURRENCY = complete;
425 | XROS_DEPLOYMENT_TARGET = 1.1;
426 | };
427 | name = Debug;
428 | };
429 | 036EBD211C1408C200121D0B /* Release */ = {
430 | isa = XCBuildConfiguration;
431 | buildSettings = {
432 | ALWAYS_SEARCH_USER_PATHS = NO;
433 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
434 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
436 | CLANG_CXX_LIBRARY = "libc++";
437 | CLANG_ENABLE_MODULES = YES;
438 | CLANG_ENABLE_OBJC_ARC = YES;
439 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
440 | CLANG_WARN_BOOL_CONVERSION = YES;
441 | CLANG_WARN_COMMA = YES;
442 | CLANG_WARN_CONSTANT_CONVERSION = YES;
443 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
445 | CLANG_WARN_EMPTY_BODY = YES;
446 | CLANG_WARN_ENUM_CONVERSION = YES;
447 | CLANG_WARN_INFINITE_RECURSION = YES;
448 | CLANG_WARN_INT_CONVERSION = YES;
449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
450 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
451 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
453 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
455 | CLANG_WARN_STRICT_PROTOTYPES = YES;
456 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
457 | CLANG_WARN_UNREACHABLE_CODE = YES;
458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
459 | CODE_SIGN_IDENTITY = "-";
460 | COPY_PHASE_STRIP = NO;
461 | DEAD_CODE_STRIPPING = YES;
462 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
463 | ENABLE_HARDENED_RUNTIME = YES;
464 | ENABLE_NS_ASSERTIONS = NO;
465 | ENABLE_STRICT_OBJC_MSGSEND = YES;
466 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
467 | GCC_C_LANGUAGE_STANDARD = gnu99;
468 | GCC_NO_COMMON_BLOCKS = YES;
469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
471 | GCC_WARN_UNDECLARED_SELECTOR = YES;
472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
473 | GCC_WARN_UNUSED_FUNCTION = YES;
474 | GCC_WARN_UNUSED_VARIABLE = YES;
475 | MACOSX_DEPLOYMENT_TARGET = 14.4;
476 | MTL_ENABLE_DEBUG_INFO = NO;
477 | SDKROOT = macosx;
478 | SWIFT_STRICT_CONCURRENCY = complete;
479 | XROS_DEPLOYMENT_TARGET = 1.1;
480 | };
481 | name = Release;
482 | };
483 | 036EBD231C1408C200121D0B /* Debug */ = {
484 | isa = XCBuildConfiguration;
485 | buildSettings = {
486 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
487 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO;
488 | CODE_SIGN_ENTITLEMENTS = Aware/Aware.entitlements;
489 | CODE_SIGN_IDENTITY = "Apple Development";
490 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
491 | CODE_SIGN_STYLE = Automatic;
492 | COMBINE_HIDPI_IMAGES = YES;
493 | CURRENT_PROJECT_VERSION = 1;
494 | DEAD_CODE_STRIPPING = YES;
495 | DEVELOPMENT_TEAM = 5SW9VUVYKC;
496 | ENABLE_HARDENED_RUNTIME = YES;
497 | GENERATE_INFOPLIST_FILE = YES;
498 | INFOPLIST_FILE = Aware/Info.plist;
499 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
500 | INFOPLIST_KEY_LSUIElement = YES;
501 | LD_RUNPATH_SEARCH_PATHS = (
502 | "$(inherited)",
503 | "@executable_path/../Frameworks",
504 | );
505 | MARKETING_VERSION = 1.2.0;
506 | PRODUCT_BUNDLE_IDENTIFIER = com.awaremac.Aware;
507 | PRODUCT_NAME = "$(TARGET_NAME)";
508 | PROVISIONING_PROFILE_SPECIFIER = "";
509 | SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
510 | SUPPORTS_MACCATALYST = NO;
511 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
512 | SWIFT_VERSION = 5.0;
513 | TARGETED_DEVICE_FAMILY = 7;
514 | };
515 | name = Debug;
516 | };
517 | 036EBD241C1408C200121D0B /* Release */ = {
518 | isa = XCBuildConfiguration;
519 | buildSettings = {
520 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
521 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO;
522 | CODE_SIGN_ENTITLEMENTS = Aware/Aware.entitlements;
523 | CODE_SIGN_IDENTITY = "Apple Development";
524 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
525 | CODE_SIGN_STYLE = Automatic;
526 | COMBINE_HIDPI_IMAGES = YES;
527 | CURRENT_PROJECT_VERSION = 1;
528 | DEAD_CODE_STRIPPING = YES;
529 | DEVELOPMENT_TEAM = 5SW9VUVYKC;
530 | ENABLE_HARDENED_RUNTIME = YES;
531 | GENERATE_INFOPLIST_FILE = YES;
532 | INFOPLIST_FILE = Aware/Info.plist;
533 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
534 | INFOPLIST_KEY_LSUIElement = YES;
535 | LD_RUNPATH_SEARCH_PATHS = (
536 | "$(inherited)",
537 | "@executable_path/../Frameworks",
538 | );
539 | MARKETING_VERSION = 1.2.0;
540 | PRODUCT_BUNDLE_IDENTIFIER = com.awaremac.Aware;
541 | PRODUCT_NAME = "$(TARGET_NAME)";
542 | PROVISIONING_PROFILE_SPECIFIER = "";
543 | SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
544 | SUPPORTS_MACCATALYST = NO;
545 | SWIFT_COMPILATION_MODE = wholemodule;
546 | SWIFT_OPTIMIZATION_LEVEL = "-O";
547 | SWIFT_VERSION = 5.0;
548 | TARGETED_DEVICE_FAMILY = 7;
549 | };
550 | name = Release;
551 | };
552 | 03F9E22D1C24CAD3001DBE86 /* Debug */ = {
553 | isa = XCBuildConfiguration;
554 | buildSettings = {
555 | BUNDLE_LOADER = "$(TEST_HOST)";
556 | CODE_SIGN_IDENTITY = "Apple Development";
557 | CODE_SIGN_STYLE = Automatic;
558 | COMBINE_HIDPI_IMAGES = YES;
559 | DEAD_CODE_STRIPPING = YES;
560 | DEVELOPMENT_TEAM = 5SW9VUVYKC;
561 | GENERATE_INFOPLIST_FILE = YES;
562 | LD_RUNPATH_SEARCH_PATHS = (
563 | "$(inherited)",
564 | "@executable_path/../Frameworks",
565 | "@loader_path/../Frameworks",
566 | );
567 | PRODUCT_BUNDLE_IDENTIFIER = com.awaremac.AwareTests;
568 | PRODUCT_NAME = "$(TARGET_NAME)";
569 | PROVISIONING_PROFILE_SPECIFIER = "";
570 | SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
571 | SUPPORTS_MACCATALYST = NO;
572 | SWIFT_VERSION = 5.0;
573 | TARGETED_DEVICE_FAMILY = 7;
574 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aware.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aware";
575 | };
576 | name = Debug;
577 | };
578 | 03F9E22E1C24CAD3001DBE86 /* Release */ = {
579 | isa = XCBuildConfiguration;
580 | buildSettings = {
581 | BUNDLE_LOADER = "$(TEST_HOST)";
582 | CODE_SIGN_IDENTITY = "Apple Development";
583 | CODE_SIGN_STYLE = Automatic;
584 | COMBINE_HIDPI_IMAGES = YES;
585 | DEAD_CODE_STRIPPING = YES;
586 | DEVELOPMENT_TEAM = 5SW9VUVYKC;
587 | GENERATE_INFOPLIST_FILE = YES;
588 | LD_RUNPATH_SEARCH_PATHS = (
589 | "$(inherited)",
590 | "@executable_path/../Frameworks",
591 | "@loader_path/../Frameworks",
592 | );
593 | PRODUCT_BUNDLE_IDENTIFIER = com.awaremac.AwareTests;
594 | PRODUCT_NAME = "$(TARGET_NAME)";
595 | PROVISIONING_PROFILE_SPECIFIER = "";
596 | SUPPORTED_PLATFORMS = "macosx xros xrsimulator";
597 | SUPPORTS_MACCATALYST = NO;
598 | SWIFT_COMPILATION_MODE = wholemodule;
599 | SWIFT_OPTIMIZATION_LEVEL = "-O";
600 | SWIFT_VERSION = 5.0;
601 | TARGETED_DEVICE_FAMILY = 7;
602 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aware.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aware";
603 | };
604 | name = Release;
605 | };
606 | /* End XCBuildConfiguration section */
607 |
608 | /* Begin XCConfigurationList section */
609 | 036EBD101C1408C200121D0B /* Build configuration list for PBXProject "Aware" */ = {
610 | isa = XCConfigurationList;
611 | buildConfigurations = (
612 | 036EBD201C1408C200121D0B /* Debug */,
613 | 036EBD211C1408C200121D0B /* Release */,
614 | );
615 | defaultConfigurationIsVisible = 0;
616 | defaultConfigurationName = Release;
617 | };
618 | 036EBD221C1408C200121D0B /* Build configuration list for PBXNativeTarget "Aware" */ = {
619 | isa = XCConfigurationList;
620 | buildConfigurations = (
621 | 036EBD231C1408C200121D0B /* Debug */,
622 | 036EBD241C1408C200121D0B /* Release */,
623 | );
624 | defaultConfigurationIsVisible = 0;
625 | defaultConfigurationName = Release;
626 | };
627 | 03F9E22F1C24CAD3001DBE86 /* Build configuration list for PBXNativeTarget "AwareTests" */ = {
628 | isa = XCConfigurationList;
629 | buildConfigurations = (
630 | 03F9E22D1C24CAD3001DBE86 /* Debug */,
631 | 03F9E22E1C24CAD3001DBE86 /* Release */,
632 | );
633 | defaultConfigurationIsVisible = 0;
634 | defaultConfigurationName = Release;
635 | };
636 | /* End XCConfigurationList section */
637 | };
638 | rootObject = 036EBD0D1C1408C200121D0B /* Project object */;
639 | }
640 |
--------------------------------------------------------------------------------