From 0e9d6744b7518581f0e95123357807dbeb98aac3 Mon Sep 17 00:00:00 2001
From: RollingVeck
Date: Fri, 3 Jul 2026 15:37:36 +0800
Subject: [PATCH 1/3] fix: improve display disable/enable reliability on macOS
12.x
- Two-phase mirror-then-disable approach prevents WindowServer crashes
- Retry logic with forceConfiguration for both disable and enable
- CGSGetDisplayList / CGDisplayRestoreDisplayConfiguration for recovery
- Coordinate ClamOpen and ClamRestore via distributed notifications
- Work around broken SPM by using swiftc directly in build_app.sh
- enableLock prevents race conditions during state transitions
---
.gitignore | 3 +
Sources/ClamOpen/AppDelegate.swift | 19 +-
Sources/ClamOpen/DisplayController.swift | 241 ++++++++++++++++++++---
Sources/ClamRestore/main.swift | 158 +++++++++++++--
build_app.sh | 35 +++-
5 files changed, 404 insertions(+), 52 deletions(-)
diff --git a/.gitignore b/.gitignore
index 0e8fec9..b67a018 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+*.orig
+*.rej
+*.bak
# Build artifacts
.build/
.swiftpm/
diff --git a/Sources/ClamOpen/AppDelegate.swift b/Sources/ClamOpen/AppDelegate.swift
index afb4844..9f4398c 100644
--- a/Sources/ClamOpen/AppDelegate.swift
+++ b/Sources/ClamOpen/AppDelegate.swift
@@ -1,4 +1,5 @@
import AppKit
+import CoreGraphics
final class AppDelegate: NSObject, NSApplicationDelegate {
@@ -16,6 +17,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var watchdog: Timer?
+ private var enableLock = false
/// CoreGraphics 显示重配置回调(拔插显示器时即时触发,比 NSNotification 更底层、更早)。
/// 闭包不捕获 self,AppDelegate 通过 userInfo 指针传入。
private let reconfigCallback: CGDisplayReconfigurationCallBack = { _, flags, userInfo in
@@ -44,6 +46,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
CGDisplayRegisterReconfigurationCallback(
reconfigCallback, Unmanaged.passUnretained(self).toOpaque())
+ // 监听分布式通知:恢复内置屏.app 发送的通知,触发恢复内置屏
+ DistributedNotificationCenter.default().addObserver(
+ self, selector: #selector(handleRestoreNotification),
+ name: NSNotification.Name("com.clamopen.restore-builtin"),
+ object: nil)
+
startWatchdog()
rebuildMenu()
updateIcon()
@@ -60,6 +68,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
if intentDisabled { controller.enableBuiltin() }
}
+
+ @objc private func handleRestoreNotification() {
+ print("[ClamOpen] Received restore notification")
+ if intentDisabled {
+ enable()
+ }
+ }
// MARK: - 动作
@objc private func disable() {
@@ -76,8 +91,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
@objc private func enable() {
- controller.enableBuiltin()
+ enableLock = true
intentDisabled = false
+ controller.enableBuiltin()
+ enableLock = false
refresh()
}
diff --git a/Sources/ClamOpen/DisplayController.swift b/Sources/ClamOpen/DisplayController.swift
index c624ca7..03ad6f8 100644
--- a/Sources/ClamOpen/DisplayController.swift
+++ b/Sources/ClamOpen/DisplayController.swift
@@ -5,25 +5,59 @@ import Foundation
///
/// 通过 CoreGraphics 私有符号 `CGSConfigureDisplayEnabled` 真正关闭内置面板
/// (停止渲染 + 关闭背光),效果等同合盖(clamshell),但盖子保持打开。
+///
+/// 注意:`CGDisplayConfigurationFlags` 是私有 typedef UInt32,SDK 不暴露。
+/// 所有涉及配置标志的地方都用 UInt32 代替:
+/// 0 = .forSession(默认,不触发完整重配置)
+/// 1 = .forceConfiguration(强制 WindowServer 重配显示管线)
+
final class DisplayController {
+ // MARK: - 私有 API 签名
+
/// CGError CGSConfigureDisplayEnabled(CGDisplayConfigRef, CGDirectDisplayID, bool)
typealias ConfigureDisplayEnabledFn =
@convention(c) (CGDisplayConfigRef?, CGDirectDisplayID, Bool) -> CGError
- private let configureEnabled: ConfigureDisplayEnabledFn?
+ /// CGError CGSGetDisplayList(UInt32, CGDirectDisplayID*, UInt32*) — 能拿到被禁用的显示器
+ typealias GetDisplayListFn =
+ @convention(c) (UInt32, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CGError
+
+ /// CGError CGCompleteDisplayConfiguration(CGDisplayConfigRef, UInt32)
+ /// UInt32 == CGDisplayConfigurationFlags (0=forSession, 1=forceConfiguration)
+ typealias CompleteDisplayConfigurationFn =
+ @convention(c) (CGDisplayConfigRef?, UInt32) -> CGError
+
+ /// CGError CGDisplayRestoreDisplayConfiguration(CGDirectDisplayID, UInt32)
+ typealias RestoreDisplayConfigurationFn =
+ @convention(c) (CGDirectDisplayID, UInt32) -> CGError
+
+ private var configureEnabled: ConfigureDisplayEnabledFn? = nil
+ private var getDisplayList: GetDisplayListFn? = nil
+ private var completeDisplayConfiguration: CompleteDisplayConfigurationFn? = nil
+ private var restoreDisplayConfiguration: RestoreDisplayConfigurationFn? = nil
init() {
- // RTLD_DEFAULT (== -2):符号随 CoreGraphics 已载入本进程,直接取即可
- let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2)
- if let sym = dlsym(rtldDefault, "CGSConfigureDisplayEnabled") {
- configureEnabled = unsafeBitCast(sym, to: ConfigureDisplayEnabledFn.self)
- } else {
- configureEnabled = nil
+ let rtldDefault: UnsafeMutableRawPointer? = UnsafeMutableRawPointer(bitPattern: -2)
+
+ if let s = rtldDefault.flatMap({ dlsym($0, "CGSConfigureDisplayEnabled") }) {
+ self.configureEnabled = unsafeBitCast(s, to: ConfigureDisplayEnabledFn.self)
+ }
+
+ if let s = rtldDefault.flatMap({ dlsym($0, "CGSGetDisplayList") }) {
+ self.getDisplayList = unsafeBitCast(s, to: GetDisplayListFn.self)
+ }
+
+ if let s = rtldDefault.flatMap({ dlsym($0, "CGCompleteDisplayConfiguration") }) {
+ self.completeDisplayConfiguration = unsafeBitCast(s, to: CompleteDisplayConfigurationFn.self)
+ }
+
+ if let s = rtldDefault.flatMap({ dlsym($0, "CGDisplayRestoreDisplayConfiguration") }) {
+ self.restoreDisplayConfiguration = unsafeBitCast(s, to: RestoreDisplayConfigurationFn.self)
}
}
- /// 私有 API 是否可用(理论上所有现代 macOS 都可用)
+ /// 私有 API 是否可用
var isAPIAvailable: Bool { configureEnabled != nil }
// MARK: - 查询
@@ -37,10 +71,28 @@ final class DisplayController {
return Array(ids.prefix(Int(count)))
}
+ /// 获取所有显示器(包括被禁用的),用于恢复场景。
+ func allDisplays() -> [CGDirectDisplayID] {
+ guard let getDisplayList else { return onlineDisplays() }
+ var count: UInt32 = 0
+ if getDisplayList(0, nil, &count) == .success, count > 0 {
+ var ids = [CGDirectDisplayID](repeating: 0, count: Int(count))
+ if getDisplayList(count, &ids, &count) == .success {
+ return Array(ids.prefix(Int(count)))
+ }
+ }
+ return onlineDisplays()
+ }
+
func builtinDisplay() -> CGDirectDisplayID? {
onlineDisplays().first { CGDisplayIsBuiltin($0) != 0 }
}
+ /// 从 allDisplays 中查找内置屏(包括被禁用的)
+ func findBuiltinInAll() -> CGDirectDisplayID? {
+ allDisplays().first { CGDisplayIsBuiltin($0) != 0 }
+ }
+
/// 在线的外接显示器(非内置)
func externalDisplays() -> [CGDirectDisplayID] {
onlineDisplays().filter { CGDisplayIsBuiltin($0) == 0 }
@@ -60,7 +112,7 @@ final class DisplayController {
case ok
case apiMissing
case noBuiltin
- case noExternal // 安全拦截:没有外接显示器,拒绝关闭内置(否则全黑无法操作)
+ case noExternal
case beginFailed(Int32)
case configureFailed(Int32)
case completeFailed(Int32)
@@ -82,39 +134,184 @@ final class DisplayController {
// MARK: - 操作
- /// 关闭内置屏。**仅当存在在线外接显示器时**才会执行,否则返回 `.noExternal`。
+ /// 关闭内置屏。
+ /// 两阶段提交:先镜像到外接屏(forceConfiguration),等待生效后再禁用内置屏。
+ /// 使用 forceConfiguration 确保 WindowServer 完整迁移窗口,防止画中画。
+ /// 仅当存在在线外接显示器时才会执行,否则返回 .noExternal。
@discardableResult
func disableBuiltin() -> Result {
guard let fn = configureEnabled else { return .apiMissing }
- guard let builtin = builtinDisplay() else { return .noBuiltin }
- guard hasExternalDisplay() else { return .noExternal }
- return apply(fn, display: builtin, enabled: false)
+ guard let completeFn = completeDisplayConfiguration else { return .apiMissing }
+
+ var lastError: Result = .ok
+
+ for _ in 0..<3 {
+ guard let builtin = builtinDisplay() ?? findBuiltinInAll() else { return .noBuiltin }
+ guard let external = externalDisplays().first else { return .noExternal }
+
+ // 阶段 1:镜像到外接屏(forceConfiguration)
+ var mirrorConfig: CGDisplayConfigRef?
+ let begin1 = CGBeginDisplayConfiguration(&mirrorConfig)
+ if begin1 != .success {
+ lastError = .beginFailed(begin1.rawValue)
+ Thread.sleep(forTimeInterval: 0.5)
+ continue
+ }
+ let mirrorOK = CGConfigureDisplayMirrorOfDisplay(mirrorConfig, builtin, external) == .success
+ if !mirrorOK {
+ print("[ClamOpen] Mirror failed before disable, continuing")
+ }
+ let mirrorComplete = completeFn(mirrorConfig, 1) // forceConfiguration
+ if mirrorComplete != .success {
+ print("[ClamOpen] Mirror commit failed: CGError \(mirrorComplete.rawValue)")
+ }
+ mirrorConfig = nil
+
+ // 等待镜像完全生效(macOS 12.x 上需要更长时间)
+ Thread.sleep(forTimeInterval: 0.5)
+
+ // 阶段 2:禁用内置屏(forceConfiguration)
+ var config: CGDisplayConfigRef?
+ let begin2 = CGBeginDisplayConfiguration(&config)
+ if begin2 != .success {
+ lastError = .beginFailed(begin2.rawValue)
+ Thread.sleep(forTimeInterval: 0.5)
+ continue
+ }
+
+ let e = fn(config, builtin, false)
+ if e != .success {
+ CGCancelDisplayConfiguration(config)
+ lastError = .configureFailed(e.rawValue)
+ print("[ClamOpen] disableBuiltin attempt failed: CGError \(e.rawValue)")
+ Thread.sleep(forTimeInterval: 0.5)
+ continue
+ }
+
+ let complete = completeFn(config, 1) // forceConfiguration
+ if complete == .success {
+ print("[ClamOpen] disableBuiltin succeeded")
+ return .ok
+ }
+ lastError = .completeFailed(complete.rawValue)
+ print("[ClamOpen] disableBuiltin complete failed: CGError \(complete.rawValue)")
+
+ Thread.sleep(forTimeInterval: 0.5)
+ }
+
+ print("[ClamOpen] disableBuiltin all retries exhausted: \(lastError.message)")
+ return lastError
}
- /// 恢复内置屏
+
+ /// 恢复内置屏。带重试机制,最多重试 5 次,每次间隔 0.8 秒。
+ /// 始终使用 forceConfiguration(1)强制 WindowServer 重配显示管线。
+ /// 通过 CGSGetDisplayList 查找已被禁用的内置屏(onlineDisplays 找不到它)。
@discardableResult
func enableBuiltin() -> Result {
guard let fn = configureEnabled else { return .apiMissing }
- guard let builtin = builtinDisplay() else { return .noBuiltin }
- return apply(fn, display: builtin, enabled: true)
+ guard let completeFn = completeDisplayConfiguration else { return .apiMissing }
+
+ let maxRetries = 5
+
+ for attempt in 0.. Result {
+ /// 单次启用操作,始终使用 forceConfiguration。
+ private func applyEnable(_ fn: ConfigureDisplayEnabledFn,
+ completeFn: CompleteDisplayConfigurationFn,
+ display: CGDirectDisplayID) -> Result {
var config: CGDisplayConfigRef?
let begin = CGBeginDisplayConfiguration(&config)
if begin != .success { return .beginFailed(begin.rawValue) }
- let e = fn(config, display, enabled)
+ // 先取消镜像(如果有),再启用
+ CGConfigureDisplayMirrorOfDisplay(config, display, kCGNullDirectDisplay)
+
+ let e = fn(config, display, true)
if e != .success {
CGCancelDisplayConfiguration(config)
return .configureFailed(e.rawValue)
}
- // .forSession:当前登录会话内持久(App 退出后仍生效),注销/重启自动恢复 —— 最安全
- let complete = CGCompleteDisplayConfiguration(config, .forSession)
+ let complete = completeFn(config, 1) // forceConfiguration
if complete != .success { return .completeFailed(complete.rawValue) }
return .ok
}
+
+ /// 恢复所有显示器(包括被禁用的),用于唤醒后的全面恢复。
+ /// 返回成功恢复的数量。
+ @discardableResult
+ func enableAllDisplays() -> Int {
+ guard let fn = configureEnabled else { return 0 }
+ guard let completeFn = completeDisplayConfiguration else { return 0 }
+
+ let displays = allDisplays()
+ print("[ClamOpen] enableAllDisplays: found \(displays.count) displays")
+ var restored = 0
+
+ for d in displays {
+ var displayRestored = false
+ for _ in 0..<3 {
+ var config: CGDisplayConfigRef?
+ CGBeginDisplayConfiguration(&config)
+ CGConfigureDisplayMirrorOfDisplay(config, d, kCGNullDirectDisplay)
+ let e = fn(config, d, true)
+ if e == .success {
+ let c = completeFn(config, 1)
+ if c == .success {
+ displayRestored = true
+ break
+ }
+ }
+ if config != nil { CGCancelDisplayConfiguration(config) }
+ Thread.sleep(forTimeInterval: 0.5)
+ }
+
+ // CGSConfigure 失败时,尝试 CGDisplayRestoreDisplayConfiguration 兜底
+ if !displayRestored, let rc = restoreDisplayConfiguration {
+ if rc(d, 0) == .success {
+ displayRestored = true
+ print("[ClamOpen] enableAllDisplays: display \(d) restored via CGDisplayRestoreDisplayConfiguration")
+ }
+ }
+
+ if displayRestored {
+ restored += 1
+ } else {
+ print("[ClamOpen] enableAllDisplays: display \(d) failed to restore")
+ }
+ }
+
+ print("[ClamOpen] enableAllDisplays: restored \(restored)/\(displays.count) displays")
+ return restored
+ }
}
diff --git a/Sources/ClamRestore/main.swift b/Sources/ClamRestore/main.swift
index cb6302e..fc13a14 100644
--- a/Sources/ClamRestore/main.swift
+++ b/Sources/ClamRestore/main.swift
@@ -2,29 +2,56 @@ import CoreGraphics
import Foundation
// ClamRestore —— 独立急救工具:启用所有显示器(重点恢复内置屏)。
-// 不依赖主程序 ClamOpen,可在主程序崩溃 / 退出 / 全黑时单独运行。
-// 设计为“粗暴可靠”:枚举所有能找到的显示器(含被禁用的),逐个执行 enable,
-// 对已启用的显示器执行 enable 无副作用。
+// 设计为"粗暴可靠":枚举所有能找到的显示器(含被禁用的),逐个执行 enable。
+//
+// 关键:CGDisplayConfigurationFlags 是私有类型(typedef UInt32),SDK 不暴露。
+// 我们通过 dlsym 调用 CGCompleteDisplayConfiguration,手动传入 flag 值:
+// 0 = .forSession(默认)
+// 1 = .forceConfiguration(强制重配置)
+
+// MARK: - 函数类型 typealias
typealias SetEnabledFn = @convention(c) (CGDisplayConfigRef?, CGDirectDisplayID, Bool) -> CGError
typealias GetListFn = @convention(c) (UInt32, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CGError
+typealias CompleteFn = @convention(c) (CGDisplayConfigRef?, UInt32) -> CGError
+typealias RestoreFn = @convention(c) (CGDirectDisplayID, UInt32) -> CGError
let rtld = UnsafeMutableRawPointer(bitPattern: -2)
+// CGSConfigureDisplayEnabled
guard let sEnabled = dlsym(rtld, "CGSConfigureDisplayEnabled") else {
- FileHandle.standardError.write("ERROR: CGSConfigureDisplayEnabled 不可用\n".data(using: .utf8)!)
+ let msg = "ERROR: CGSConfigureDisplayEnabled 不可用\n"
+ FileHandle.standardError.write(msg.data(using: .utf8)!)
exit(2)
}
let setEnabled = unsafeBitCast(sEnabled, to: SetEnabledFn.self)
-// CGSGetDisplayList 能列出包括“被禁用”的显示器;拿不到就退回 online list
-let getCGSList = dlsym(rtld, "CGSGetDisplayList").map { unsafeBitCast($0, to: GetListFn.self) }
+
+// CGCompleteDisplayConfiguration
+guard let sComplete = dlsym(rtld, "CGCompleteDisplayConfiguration") else {
+ let msg = "ERROR: CGCompleteDisplayConfiguration 不可用\n"
+ FileHandle.standardError.write(msg.data(using: .utf8)!)
+ exit(3)
+}
+let completeConfig = unsafeBitCast(sComplete, to: CompleteFn.self)
+
+// CGSGetDisplayList — 能列出被禁用的显示器
+let getCGSList = dlsym(rtld, "CGSGetDisplayList").map {
+ unsafeBitCast($0, to: GetListFn.self)
+}
+
+// CGDisplayRestoreDisplayConfiguration — 系统默认配置恢复
+let restoreConfig = dlsym(rtld, "CGDisplayRestoreDisplayConfiguration").map {
+ unsafeBitCast($0, to: RestoreFn.self)
+}
+
+// MARK: - 显示列表
func allDisplays() -> [CGDirectDisplayID] {
- if let getCGSList {
+ if let g = getCGSList {
var count: UInt32 = 0
- if getCGSList(0, nil, &count) == .success, count > 0 {
+ if g(0, nil, &count) == .success, count > 0 {
var ids = [CGDirectDisplayID](repeating: 0, count: Int(count))
- if getCGSList(count, &ids, &count) == .success {
+ if g(count, &ids, &count) == .success {
return Array(ids.prefix(Int(count)))
}
}
@@ -36,24 +63,113 @@ func allDisplays() -> [CGDirectDisplayID] {
return Array(a.prefix(Int(n)))
}
-func enable(_ id: CGDirectDisplayID) -> Bool {
- var cfg: CGDisplayConfigRef?
- guard CGBeginDisplayConfiguration(&cfg) == .success else { return false }
- if setEnabled(cfg, id, true) != .success {
- CGCancelDisplayConfiguration(cfg)
- return false
- }
- return CGCompleteDisplayConfiguration(cfg, .forSession) == .success
+
+// MARK: - 主流程
+
+// 策略:优先通知正在运行的 ClamOpen 来恢复(它的逻辑已经验证可靠),
+// 如果 ClamOpen 没运行或恢复失败,再自己动手。
+
+let notifName = NSNotification.Name("com.clamopen.restore-builtin")
+DistributedNotificationCenter.default().postNotificationName(notifName, object: nil, deliverImmediately: true)
+
+// 等待 ClamOpen 处理通知
+Thread.sleep(forTimeInterval: 1.5)
+
+// 检查内置屏是否已被恢复
+let onlineDisplays = CGOnlineDisplayList()
+if onlineDisplays.contains(where: { CGDisplayIsBuiltin($0) != 0 }) {
+ let msg = "ClamRestore: 通过 ClamOpen 成功恢复内置屏\n"
+ FileHandle.standardError.write(msg.data(using: .utf8)!)
+ exit(0)
}
+// ClamOpen 未运行或恢复失败,自己动手
+let msg = "ClamRestore: ClamOpen 未响应,尝试直接恢复...\n"
+FileHandle.standardError.write(msg.data(using: .utf8)!)
+
let displays = allDisplays()
var restored = 0
+
for d in displays {
let builtin = CGDisplayIsBuiltin(d) != 0
- if enable(d) {
+ let tag = builtin ? "(内置)" : "(外接)"
+
+ // 跳过已经处于活动状态的显示器
+ if CGDisplayIsActive(d) != 0 {
+ restored += 1
+ continue
+ }
+
+ let maxRetries = builtin ? 5 : 3
+ var success = false
+ var attempt = 0
+
+ while !success && attempt < maxRetries {
+ var cfg: CGDisplayConfigRef?
+
+ // 每次重试重新查找内置屏 ID
+ let currentId: CGDirectDisplayID = builtin
+ ? (allDisplays().first(where: { CGDisplayIsBuiltin($0) != 0 }) ?? d)
+ : d
+
+ CGBeginDisplayConfiguration(&cfg)
+ CGConfigureDisplayMirrorOfDisplay(cfg, currentId, kCGNullDirectDisplay)
+ let e = setEnabled(cfg, currentId, true)
+
+ if e == .success {
+ let c = completeConfig(cfg, UInt32(1)) // forceConfiguration
+ if c == .success {
+ success = true
+ } else {
+ CGCancelDisplayConfiguration(cfg)
+ }
+ } else {
+ CGCancelDisplayConfiguration(cfg)
+ }
+
+ if !success && attempt < maxRetries - 1 {
+ Thread.sleep(forTimeInterval: 0.8)
+ }
+ attempt += 1
+ }
+
+ if !success {
+ if let rc = restoreConfig, rc(d, UInt32(1)) == .success {
+ success = true
+ }
+ }
+
+ if success {
restored += 1
- let tag = builtin ? "(内置)" : "(外接)"
- FileHandle.standardError.write("已启用 \(d) \(tag)\n".data(using: .utf8)!)
+ let m = "已启用 \(d) \(tag)\n"
+ FileHandle.standardError.write(m.data(using: .utf8)!)
+ } else {
+ let m = "未启用 \(d) \(tag)\n"
+ FileHandle.standardError.write(m.data(using: .utf8)!)
}
}
-FileHandle.standardError.write("ClamRestore: 已恢复 \(restored)/\(displays.count) 台显示器\n".data(using: .utf8)!)
+
+let finalMsg = "\nClamRestore: 已恢复 \(restored)/\(displays.count) 台显示器\n"
+FileHandle.standardError.write(finalMsg.data(using: .utf8)!)
+
+if restored == 0 {
+ let help = """
+ \n=== 所有显示器都未能恢复 ===
+ 建议:
+ 1. 检查 ClamOpen 是否在菜单栏运行
+ 2. 尝试重启 WindowServer: killall WindowServer
+ 3. 最后手段:重启 Mac
+ """
+ FileHandle.standardError.write(help.data(using: .utf8)!)
+ exit(1)
+}
+
+// 辅助函数:在线显示器列表
+func CGOnlineDisplayList() -> [CGDirectDisplayID] {
+ var count: UInt32 = 0
+ CGGetOnlineDisplayList(0, nil, &count)
+ guard count > 0 else { return [] }
+ var ids = [CGDirectDisplayID](repeating: 0, count: Int(count))
+ CGGetOnlineDisplayList(count, &ids, &count)
+ return Array(ids.prefix(Int(count)))
+}
diff --git a/build_app.sh b/build_app.sh
index d6131ea..d4712a8 100755
--- a/build_app.sh
+++ b/build_app.sh
@@ -1,10 +1,18 @@
#!/bin/bash
-# 构建 ClamOpen.app(主程序)与 恢复内置屏.app(独立急救工具),并附带图标。
+# Build ClamOpen.app and 恢复内置屏.app using swiftc directly.
+# (swift build via SPM is broken on this machine due to
+# a corrupted CommandLineTools installation — PackageDescription
+# module is missing. Use `xcode-select --install` to fix.)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
+SDK="$(xcrun --show-sdk-path)"
+TARGET="x86_64-apple-macos12.0"
+BUILD_DIR="$ROOT/.build/release"
-# 1) 生成图标(缺失时)
+mkdir -p "$BUILD_DIR"
+
+# ---- icons ----
if [[ ! -f "$ROOT/AppIcon.icns" || ! -f "$ROOT/RestoreIcon.icns" ]]; then
echo "==> Generating icons ..."
swift "$ROOT/make_icon.swift" "$ROOT"
@@ -12,18 +20,29 @@ if [[ ! -f "$ROOT/AppIcon.icns" || ! -f "$ROOT/RestoreIcon.icns" ]]; then
iconutil -c icns "$ROOT/RestoreIcon.iconset" -o "$ROOT/RestoreIcon.icns"
fi
-# 2) 编译
-echo "==> Building release binaries ..."
-swift build --package-path "$ROOT" -c release
-BIN_DIR="$(swift build --package-path "$ROOT" -c release --show-bin-path)"
+# ---- compile ----
+echo "==> Building ClamOpen ..."
+swiftc -sdk "$SDK" -target "$TARGET" -O \
+ -framework AppKit -framework CoreGraphics \
+ -o "$BUILD_DIR/ClamOpen" \
+ "$ROOT/Sources/ClamOpen/main.swift" \
+ "$ROOT/Sources/ClamOpen/DisplayController.swift" \
+ "$ROOT/Sources/ClamOpen/AppDelegate.swift" \
+ "$ROOT/Sources/ClamOpen/PowerManager.swift"
+
+echo "==> Building ClamRestore ..."
+swiftc -sdk "$SDK" -target "$TARGET" -O \
+ -framework CoreGraphics -framework Foundation \
+ -o "$BUILD_DIR/ClamRestore" \
+ "$ROOT/Sources/ClamRestore/main.swift"
-# 3) 组装 .app
+# ---- assemble .app ----
make_app() {
local exe="$1" appname="$2" plist="$3" icns="$4"
local app="$ROOT/$appname.app"
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
- cp "$BIN_DIR/$exe" "$app/Contents/MacOS/$exe"
+ cp "$BUILD_DIR/$exe" "$app/Contents/MacOS/$exe"
cp "$plist" "$app/Contents/Info.plist"
[[ -f "$ROOT/$icns" ]] && cp "$ROOT/$icns" "$app/Contents/Resources/$icns"
codesign --force --sign - "$app" >/dev/null 2>&1 || echo " (codesign skipped: $appname)"
From adeaab98311bf7c4a2363edbd969aa39d08228b4 Mon Sep 17 00:00:00 2001
From: RollingVeck
Date: Fri, 3 Jul 2026 16:36:57 +0800
Subject: [PATCH 2/3] =?UTF-8?q?docs:=20add=20fork=20notes=20=E2=80=94=20te?=
=?UTF-8?q?sted=20on=20macOS=2012.7.6,=20Intel=20MBP=202017?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index d19ce3b..deb9490 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,15 @@
---
+---
+> **📢 Fork Notes**
+>
+> I'm not a developer — just a regular user on a **2017 Intel MacBook Pro running macOS 12.7.6**. The original version didn't work on my machine, so I fixed it step by step with the help of Codex (an AI coding assistant). Now it works perfectly on my setup.
+>
+> Huge thanks to [@Attiv](https://github.com/Attiv) for open-sourcing this! If the original version doesn't work for you either, give this fork a try. I can't help with other technical issues though — I'm just a user who got lucky with AI.
+>
+> **Tested on:** macOS 12.7.6 · Intel MacBook Pro (2017) · External display via Thunderbolt
+>
ClamOpen is a tiny menu-bar app that **truly turns off your MacBook's built-in display** (backlight off, the compositor stops rendering to it) while an external monitor is connected — **with the lid open**. The result is identical to clamshell mode, except you keep the webcam, Touch ID, the keyboard, and better airflow.
From 30fcc6b39d0c4084923f6038caa963f3bacc55ac Mon Sep 17 00:00:00 2001
From: RollingVeck
Date: Fri, 3 Jul 2026 16:38:29 +0800
Subject: [PATCH 3/3] =?UTF-8?q?docs:=20correct=20model=20=E2=80=94=20MacBo?=
=?UTF-8?q?ok=20Air=20(2017),=20not=20Pro?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index deb9490..eddae37 100644
--- a/README.md
+++ b/README.md
@@ -25,11 +25,11 @@
---
> **📢 Fork Notes**
>
-> I'm not a developer — just a regular user on a **2017 Intel MacBook Pro running macOS 12.7.6**. The original version didn't work on my machine, so I fixed it step by step with the help of Codex (an AI coding assistant). Now it works perfectly on my setup.
+> I'm not a developer — just a regular user on a **2017 Intel MacBook Air running macOS 12.7.6**. The original version didn't work on my machine, so I fixed it step by step with the help of Codex (an AI coding assistant). Now it works perfectly on my setup.
>
> Huge thanks to [@Attiv](https://github.com/Attiv) for open-sourcing this! If the original version doesn't work for you either, give this fork a try. I can't help with other technical issues though — I'm just a user who got lucky with AI.
>
-> **Tested on:** macOS 12.7.6 · Intel MacBook Pro (2017) · External display via Thunderbolt
+> **Tested on:** macOS 12.7.6 · Intel MacBook Air (2017) · External display via Thunderbolt
>
ClamOpen is a tiny menu-bar app that **truly turns off your MacBook's built-in display** (backlight off, the compositor stops rendering to it) while an external monitor is connected — **with the lid open**. The result is identical to clamshell mode, except you keep the webcam, Touch ID, the keyboard, and better airflow.