Mac mini Mode:让 MacBook 合盖也不睡
· 约 3 分钟读完
手边有台 MacBook,想把它放在家里当 Mac mini 用:跑点服务、当个无头主机,盖子合上塞进角落就行。问题是 MacBook 一合盖就睡,常见的办法都有点别扭:
- 接一个 HDMI 假负载,骗系统说有外接显示器;
- 开着终端跑
caffeinate,关掉窗口就失效; sudo pmset disablesleep 1,有效,但得记着关,不然哪天塞进包里它还醒着,一路发热。
我想要的其实很简单:插着电就别睡,拔了电就正常睡。于是写了 Mac mini Mode。

它做了什么
- 插电自动开启:接上电源适配器后不再睡眠,合盖、不接显示器也照常运行。
- 拔电自动关闭:切到电池马上恢复正常睡眠;如果这时已经合盖,直接让它睡下去。
- 菜单栏三档:自动、始终开启、始终关闭,当前状态一眼能看到。
安装包签了名、过了 Apple 公证,支持 macOS 13 及以上,Apple 芯片和 Intel 都能用。直接下载,双击装完菜单栏就多一个图标,不用重启。
怎么做的
整个项目三百多行 Swift,分成两个进程:
- 守护进程
macmini-moded:以 root 身份作为 LaunchDaemon 常驻,真正去改系统的睡眠设置。 - 菜单栏程序:普通用户权限,只负责显示状态和让你切换模式。
盯着电源变化
核心就是监听电源来源的变化。IOKit 提供了 IOPSNotificationCreateRunLoopSource,电源一变就回调:
guard let runLoopSource = IOPSNotificationCreateRunLoopSource({ _ in sync() }, nil)?.takeRetainedValue() else {
exit(1)
}
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .defaultMode)判断当前是不是插着电:
func onACPower() -> Bool {
let info = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let type = IOPSGetProvidingPowerSourceType(info).takeUnretainedValue() as String
return type == kIOPSACPowerValue
}以系统里的真实值为准
sync() 每次都去读 IOPMrootDomain 上的 SleepDisabled,跟期望值不一致才调用 pmset -a disablesleep 1/0。这样即使别的工具或者手动敲的 pmset 改掉了设置,下一轮对账也能纠正回来。
另外还有个每分钟一次的定时器兜底。万一漏掉一次通知,比如睡眠期间拔了电,醒来后最多一分钟就能对上。
拔电后补一脚
这里有个坑:把 disablesleep 改回 0,已经合上盖的机器并不会自己睡下去。如果不处理,拔电塞进包里,它还是醒着的。
所以关掉 Mac mini 模式两秒后,守护进程会再看一眼:
guard !wantsMacMiniMode(),
rootDomainFlag("AppleClamshellState"),
rootDomainFlag("AppleClamshellCausesSleep") else { return }
pmset(["sleepnow"])AppleClamshellState 表示盖子是合着的,AppleClamshellCausesSleep 是内核自己的判断:接着外接显示器时它是 false,这时就不去打扰。两个都满足才 sleepnow。
两个进程怎么说话
菜单栏程序没有 root 权限,不能直接改睡眠设置。它和守护进程之间用的是 Darwin 通知(notify.h),非常轻:
- 菜单栏发
com.macminimode.set-auto/set-on/set-off; - 守护进程收到后切换模式,把当前模式写进
com.macminimode.state通知的 state 值里; - 菜单栏用
notify_get_state读出来更新界面。
不需要 XPC,也不需要开端口。模式会存到 /var/db/macmini-mode,重启后保留。
退出时恢复原样
守护进程收到 SIGTERM 或 SIGINT 时,会先执行 pmset -a disablesleep 0 再退出。卸载或者进程被停掉,机器都会回到正常的睡眠行为,不会留下一个「永远不睡」的状态。
最后
这是一个很小的工具,但解决的是每天都会碰到的事。代码在 GitHub 上,MIT 协议,有问题欢迎提 issue。