From 6733231418b5bc5e11c29dcbb417a43d4d737060 Mon Sep 17 00:00:00 2001 From: WesLin Date: Tue, 11 Aug 2026 02:20:00 +0800 Subject: [PATCH 1/5] Polish release documentation and CI --- .github/workflows/ci.yml | 43 +++++++++++++++++++++++++ README.md | 68 +++++++++++++++++++++++++--------------- Scripts/build-dmgs.sh | 6 ++++ 3 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8acdb80 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-and-build: + runs-on: macos-15 + timeout-minutes: 20 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Run tests + run: ./Scripts/run-tests.sh + + - name: Build universal app + run: ./Scripts/build-app.sh + + - name: Verify bundled architectures and signature + run: | + lipo -verify_arch arm64 x86_64 dist/CleanMyScreen.app/Contents/MacOS/CleanMyScreen + codesign --verify --deep --strict dist/CleanMyScreen.app + + - name: Build and verify DMGs + run: ./Scripts/build-dmgs.sh + + - name: Upload unsigned build artifacts + uses: actions/upload-artifact@v4 + with: + name: CleanMyScreen-${{ github.sha }} + path: | + dist/releases/*.dmg + dist/releases/SHA256SUMS.txt + if-no-files-found: error + retention-days: 14 diff --git a/README.md b/README.md index 2c367df..4fbe062 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,52 @@ # CleanMyScreen -CleanMyScreen is a native macOS utility built with SwiftUI, AppKit, Core Graphics, and IOKit. The first release keeps every mode fully available without accounts, payments, trials, subscriptions, telemetry, or network access. +

+ CleanMyScreen sea-otter icon +

-The app icon uses a sea-otter mascot in a forest-green palette. The mascot represents the cleaning workflow without implying that CleanMyScreen is a security boundary. +CleanMyScreen is a free, open-source macOS utility for temporarily blocking accidental keyboard and pointer input while you clean a display, play a video for a child, or let a pet sit near your Mac. -## Downloads +It is built natively with SwiftUI, AppKit, Core Graphics, and IOKit. There are no accounts, payments, trials, subscriptions, telemetry, or network access. -Release images are written to `dist/releases/` by `./Scripts/build-dmgs.sh`: +[Download the latest release](https://github.com/opensource-works/CleanMyMac/releases/latest) · [View the source](https://github.com/opensource-works/CleanMyMac) · [MIT License](LICENSE) + +## Modes + +| Mode | What stays visible | What is locked | +| --- | --- | --- | +| **Cleaning** | Pure-black overlays on one or every display | Keyboard and pointer input | +| **Pet / Kid** | Your current video, webpage, or application | Keyboard and trackpad/pointer input | +| **Selective** | Your current desktop | Keyboard, built-in trackpad, compatible external devices, or any combination | + +Every session includes a three-second countdown, an optional automatic timeout, and a three-second Escape-key emergency unlock gesture. Ending a session restores overlays, the cursor, seized devices, and every brightness value the app was able to change. + +![Cleaning mode](Design/implementation-cleaning-final.png) + +

+ Pet and Kid mode + Selective mode +

+ +## Download and install + +Choose the DMG that matches your Mac: - `CleanMyScreen-arm64.dmg` for Apple Silicon Macs (M1, M2, M3, M4 and newer) - `CleanMyScreen-x86_64.dmg` for Intel Macs -These local builds are ad-hoc signed. On first launch, macOS may require opening the app from Finder and confirming it in Privacy & Security. Input Monitoring is required only when a session locks input; the app provides a button that opens the exact System Settings pane. +Open the DMG and drag CleanMyScreen into Applications. The current public builds are ad-hoc signed rather than notarized with a paid Apple Developer ID. On first launch, macOS may require you to open the app from Finder and confirm it in **System Settings → Privacy & Security**. -## Modes +Published SHA-256 hashes are available in `SHA256SUMS.txt` on the release page. + +## macOS permissions -- **Cleaning** covers one or every display in pure black, attempts to maximize supported display brightness, and temporarily blocks keyboard and pointer input. -- **Pet / Kid** leaves the current video or webpage visible while temporarily blocking accidental input. -- **Selective** can lock the keyboard, the built-in trackpad, compatible external input devices, or a combination. +macOS requires **Input Monitoring** before an app can observe and suppress input sent to other applications. CleanMyScreen asks for it only when a lock session needs it. If macOS no longer repeats its permission prompt, the app offers a button that opens the exact Input Monitoring pane. On configurations where the active event filter remains unavailable, it also links directly to Accessibility as a compatibility fallback. -Every session has a three-second countdown, a three-second Escape-key emergency unlock gesture, and an optional automatic timeout. Ending a session restores overlays, the cursor, seized devices, and any brightness values the app was able to change. +The app does not record, store, transmit, or inspect keystroke contents. -## Build and run +## Build from source -The repository can be built with Apple's Swift command-line tools: +Apple's Swift command-line tools are enough for local development: ```sh ./Scripts/run-tests.sh @@ -32,25 +55,20 @@ The repository can be built with Apple's Swift command-line tools: open dist/CleanMyScreen.app ``` -`run-tests.sh` uses Swift Testing's bundled framework because Command Line Tools alone do not include the `xctest` launcher. With full Xcode installed, `swift test` can be used directly. - -The build script produces a universal Apple Silicon + Intel application bundle and applies an ad-hoc local signature. +`build-app.sh` produces a universal Apple Silicon + Intel application and applies an ad-hoc local signature. `build-dmgs.sh` creates separate architecture-specific disk images, verifies them, and writes their SHA-256 hashes. -`build-dmgs.sh` thins that universal bundle into separate architecture-specific DMGs and verifies each disk image checksum. - -Installing full Xcode is only required for an Xcode project workflow, Developer ID signing, notarization, or App Store distribution. The current brightness implementation uses a private CoreDisplay fallback, so an App Store build must replace or disable that capability before submission; the local/Developer ID build is the intended first distribution path. - -## macOS system access +`run-tests.sh` uses Swift Testing's bundled framework because Command Line Tools alone do not include the `xctest` launcher. With full Xcode installed, `swift test` can be used directly. -macOS requires **Input Monitoring** approval before an app can observe and suppress input sent to other applications. CleanMyScreen requests that system control only when a lock session needs it. If macOS no longer repeats its own permission prompt, the app presents a button that opens the exact Input Monitoring pane. On configurations where an active event filter is still denied, the same flow links directly to Accessibility as a compatibility fallback. These are operating-system requirements, not account or product restrictions. The app does not record, store, transmit, or inspect keystroke contents. +Full Xcode is only required for an Xcode project workflow, Developer ID signing, notarization, or App Store distribution. The current brightness implementation uses a private CoreDisplay fallback, so an App Store build must replace or disable that capability before submission. Local and Developer ID distribution are the intended paths for this release. ## Hardware limits -- Brightness control is best-effort because macOS has no single public brightness API for every built-in and external display. Unsupported displays stay at their current brightness. -- Device-specific trackpad and external-device locking depends on what the connected HID hardware and macOS allow the app to seize. If the device cannot be safely isolated, the app reports the limitation and leaves input available. -- On a desktop Mac with no built-in keyboard, one external keyboard is deliberately kept available for the emergency unlock gesture. Devices connected after a session starts may remain available until that session is restarted. +- Brightness control is best-effort because macOS has no single public brightness API for every built-in and external display. Unsupported displays remain at their current brightness. +- Device-specific trackpad and external-device locking depends on what the connected HID hardware and macOS allow the app to seize. If a device cannot be safely isolated, the app reports the limitation and leaves that input available. +- On a desktop Mac with no built-in keyboard, one external keyboard is deliberately kept available for the emergency unlock gesture. +- Devices connected after a session starts may remain available until that session is restarted. - CleanMyScreen is an accidental-input guard, not a login lock, parental-control boundary, or security product. Power, Touch ID, force quit, and other system-reserved operations remain available. ## License -CleanMyScreen is released under the MIT License. See [LICENSE](LICENSE). +CleanMyScreen is released under the [MIT License](LICENSE). diff --git a/Scripts/build-dmgs.sh b/Scripts/build-dmgs.sh index 25d71fe..c5cb600 100755 --- a/Scripts/build-dmgs.sh +++ b/Scripts/build-dmgs.sh @@ -36,3 +36,9 @@ for architecture in arm64 x86_64; do hdiutil verify "$output_path" >/dev/null echo "$output_path" done + +( + cd "$OUTPUT_ROOT" + shasum -a 256 CleanMyScreen-arm64.dmg CleanMyScreen-x86_64.dmg > SHA256SUMS.txt +) +echo "$OUTPUT_ROOT/SHA256SUMS.txt" From 9248a5a6b76341a539de20ae3d127da9c02573bf Mon Sep 17 00:00:00 2001 From: WesLin Date: Tue, 11 Aug 2026 02:24:30 +0800 Subject: [PATCH 2/5] Support Swift 6.1 CI runners --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index db3e4f9..bd29752 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.2 +// swift-tools-version: 6.1 import PackageDescription From 3de83837b4849115c673eb2840722cc2216d9341 Mon Sep 17 00:00:00 2001 From: WesLin Date: Tue, 11 Aug 2026 02:31:39 +0800 Subject: [PATCH 3/5] Make release build compatible with Swift 6.1 --- Package.swift | 5 ++++- Scripts/build-app.sh | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index bd29752..c78b033 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,10 @@ let package = Package( targets: [ .target( name: "CleanMyScreenKit", - path: "Sources/CleanMyScreenKit" + path: "Sources/CleanMyScreenKit", + swiftSettings: [ + .enableExperimentalFeature("IsolatedDeinit") + ] ), .executableTarget( name: "CleanMyScreen", diff --git a/Scripts/build-app.sh b/Scripts/build-app.sh index 0896aa3..017ce69 100755 --- a/Scripts/build-app.sh +++ b/Scripts/build-app.sh @@ -44,6 +44,10 @@ fi mkdir -p "$OUTPUT_ROOT" ditto --noextattr --noqtn "$STAGED_APP" "$APP_ROOT" xattr -cr "$APP_ROOT" +# File Provider folders can immediately reapply bundle-level metadata after a +# copy. Remove those two attributes explicitly before the final verification. +xattr -d com.apple.FinderInfo "$APP_ROOT" 2>/dev/null || true +xattr -d 'com.apple.fileprovider.fpfs#P' "$APP_ROOT" 2>/dev/null || true codesign --verify --deep --strict "$APP_ROOT" echo "$APP_ROOT" From 7995ab9a605e7f8e568efc8b762453ec1e707c74 Mon Sep 17 00:00:00 2001 From: WesLin Date: Tue, 11 Aug 2026 02:43:24 +0800 Subject: [PATCH 4/5] Avoid experimental isolated deinitializers --- Package.swift | 5 +--- .../Services/DisplayOverlayController.swift | 16 ------------- .../Services/SystemBrightnessController.swift | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/Package.swift b/Package.swift index c78b033..bd29752 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,7 @@ let package = Package( targets: [ .target( name: "CleanMyScreenKit", - path: "Sources/CleanMyScreenKit", - swiftSettings: [ - .enableExperimentalFeature("IsolatedDeinit") - ] + path: "Sources/CleanMyScreenKit" ), .executableTarget( name: "CleanMyScreen", diff --git a/Sources/CleanMyScreenKit/Services/DisplayOverlayController.swift b/Sources/CleanMyScreenKit/Services/DisplayOverlayController.swift index 8c2402d..949efc1 100644 --- a/Sources/CleanMyScreenKit/Services/DisplayOverlayController.swift +++ b/Sources/CleanMyScreenKit/Services/DisplayOverlayController.swift @@ -32,22 +32,6 @@ public final class DisplayOverlayController: NSObject, OverlayControlling { ) } - isolated deinit { - hintFadeTimer?.invalidate() - hudDismissTimer?.invalidate() - hudCloseTimer?.invalidate() - closeCleaningWindows() - dismissTransientHUD() - - if cursorIsHidden { - NSCursor.unhide() - } - - // Removing the selector observer can retain a temporary reference to - // self, so keep it as the final operation in an isolated deinitializer. - NotificationCenter.default.removeObserver(self) - } - public func showCleaningOverlay(onAllDisplays: Bool, unlockHint: String) { dismissTransientHUD() closeCleaningWindows() diff --git a/Sources/CleanMyScreenKit/Services/SystemBrightnessController.swift b/Sources/CleanMyScreenKit/Services/SystemBrightnessController.swift index 8867b78..b144588 100644 --- a/Sources/CleanMyScreenKit/Services/SystemBrightnessController.swift +++ b/Sources/CleanMyScreenKit/Services/SystemBrightnessController.swift @@ -2,6 +2,18 @@ import CoreGraphics import Darwin import Foundation +private final class DynamicFrameworkHandle: @unchecked Sendable { + let pointer: UnsafeMutableRawPointer + + init(_ pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + dlclose(pointer) + } +} + /// Best-effort software brightness control for displays supported by /// CoreDisplay. Unsupported displays are deliberately ignored. @MainActor @@ -15,7 +27,7 @@ public final class SystemBrightnessController: BrightnessControlling { Float ) -> Int32 - private var frameworkHandle: UnsafeMutableRawPointer? + private var frameworkHandle: DynamicFrameworkHandle? private var getBrightnessFunction: GetBrightnessFunction? private var setBrightnessFunction: SetBrightnessFunction? private var originalBrightness: [CGDirectDisplayID: Float] = [:] @@ -48,7 +60,7 @@ public final class SystemBrightnessController: BrightnessControlling { return } - frameworkHandle = handle + frameworkHandle = DynamicFrameworkHandle(handle) getBrightnessFunction = unsafeBitCast( getSymbol, to: GetBrightnessFunction.self @@ -59,13 +71,6 @@ public final class SystemBrightnessController: BrightnessControlling { ) } - isolated deinit { - restore() - if let frameworkHandle { - dlclose(frameworkHandle) - } - } - public func maximizeSupportedDisplays() { guard let getBrightnessFunction, let setBrightnessFunction else { return } From fb048ec8043eeb4cb89828c24f91c59c8cabdce8 Mon Sep 17 00:00:00 2001 From: WesLin Date: Tue, 11 Aug 2026 02:49:04 +0800 Subject: [PATCH 5/5] Fix universal binary verification --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8acdb80..1ac97f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Verify bundled architectures and signature run: | - lipo -verify_arch arm64 x86_64 dist/CleanMyScreen.app/Contents/MacOS/CleanMyScreen + lipo dist/CleanMyScreen.app/Contents/MacOS/CleanMyScreen -verify_arch arm64 x86_64 codesign --verify --deep --strict dist/CleanMyScreen.app - name: Build and verify DMGs