- 桌面应用
【免费下载链接】Lunar
Intelligent adaptive brightness for your external monitors
导读
本文基于 Lunar 开源仓库的发布说明 ReleaseNotes/5.9.5a1.md,聚焦 5.9.5 Alpha 1 引入的两项关键改进:一是修复了"当主显示器不是 Mac 内建屏(built-in display)时,Shortcuts 中的显示器布局(arrangement/layout)快捷指令失效"的问题;二是新增了"Set screen as main"(设为屏幕为主屏)快捷指令。读完本文,你将理解 Lunar 如何通过 macOS App Intents 与 CoreGraphics 显示配置 API 实现多屏布局自动化,以及如何在合盖外接屏场景下正确使用这些快捷指令。
背景:为什么布局快捷指令此前会失效
在 macOS 的 Shortcuts 生态中,Lunar 通过 LunarShortcuts/LunarShortcuts.swift 提供了一整套基于 AppIntents 的显示控制动作。其中"Arrangement(布局)"类别的动作负责把多块屏幕摆放成指定几何关系,例如两块屏幕垂直/水平排列、三块屏幕"上二下一"等。
在这些布局动作的实现里,一个隐含前提是:主显示器(main display)的坐标原点位于 (0, 0)。此前 Lunar 的布局算法默认以"内建屏即主屏"为前提来计算其他屏幕的偏移量;但当用户外接显示器、合上 MacBook 盖子(clamshell mode)后,主屏变成外接屏,原有坐标假设失效,布局动作就会把屏幕摆放到错误的位置甚至无法生效。5.9.5 Alpha 1 的这条改进,正是针对该场景的修复。
新增快捷指令:"Set Screen as Main"
动作定义与使用方式
在 5.9.5 Alpha 1 中新增的"Set Screen as Main"快捷指令,源码实现在 LunarShortcuts/LunarShortcuts.swift 的MakeMonitorMainIntent(约 L2997-L3040):
- 动作标题:
Set Screen as Main - 所属分类:
Arrangement - 参数:
Screen:目标屏幕,使用ScreenQuery(single: true, noDefault: true)提供候选,即要求用户明确选择一块屏幕,不提供默认值;Skip action if screen is missing:当所选屏幕不存在时跳过该动作(避免快捷指令报错中断)。
使用场景非常直接:在多屏环境中,通过该动作把任意一块外接屏设为主屏(Main Display),替代系统"显示器设置"里的拖拽操作,从而让菜单栏、桌面图标与焦点窗口出现在目标屏幕上。
源码级实现原理
MakeMonitorMainIntent.perform()的核心逻辑如下:
guard let mainDisplay = DC.mainDisplay, let display = screen.dynamicDisplay, mainDisplay.id != display.id else { return .result() } Display.configure { config in if let mainDisplay = DC.mainDisplay, let mainDisplayBounds = mainDisplay.nsScreen?.bounds, mainDisplayBounds.origin == .zero, let displayBounds = display.nsScreen?.bounds { CGConfigureDisplayOrigin(config, mainDisplay.id, -mainDisplayBounds.width.intround.i32, -displayBounds.origin.y.intround.i32) } return CGConfigureDisplayOrigin(config, display.id.u32, 0, 0) == .success }关键点解读:
幂等保护:若目标屏幕已经是
DC.mainDisplay,直接返回成功,不会做无谓的重新配置。Display.configure事务化提交:这是 Lunar 封装好的 CoreGraphics 配置入口,定义在 Lunar/Data/Display.swift(约 L4483-L4505):- 先
CGBeginDisplayConfiguration开启配置会话; - 执行闭包内的
CGConfigureDisplayOrigin布局修改; - 闭包返回
true则CGCompleteDisplayConfiguration(..., .permanently)永久生效,否则CGCancelDisplayConfiguration回滚。
这种方式保证"主屏切换 + 旧主屏移位"是一整套原子操作,避免出现半个屏幕布局变更的状态。
- 先
坐标搬移技巧:当新主屏已位于原点时,先把旧主屏(内建屏)从 (0,0) 平移到左侧(x 取负),再把目标屏坐标原点设为 (0,0)。由于 macOS 约定"原点所在屏幕即主屏",这一步实质上是完成了主屏的切换。
主屏判定来源:
DC.mainDisplay定义在 Lunar/Utils/DisplayController.swift(约 L984-L986):var mainDisplay: Display? { activeDisplayList.first(where: { CGDisplayIsMain($0.id) == 1 }) }即通过系统 API
CGDisplayIsMain实时判定当前主屏,与用户看到的"显示器排列窗口"中菜单栏所在屏一致。
布局快捷指令:完整阵容与修复要点
可用的布局动作
根据源码,5.9.5 Alpha 1 时代的布局(Arrangement)动作包括:
| 快捷指令动作 | 源码类型 | 适用场景 |
|---|---|---|
| Set Screen as Main | MakeMonitorMainIntent | 任意屏幕设为主屏 |
| Arrange 2 Screens Vertically | VerticalMonitorLayoutIntent | 两块屏上下排列(MacBook + 1 块外接屏) |
| Arrange 2 Screens Horizontally | HorizontalMonitorLayoutIntent | 两块屏左右排列(2 块外接屏) |
| Arrange 3 Screens in a 2-above-1 configuration | TwoAboveOneMonitorLayoutIntent | 三块屏"上二下一" |
| Arrange 4 Screens in a 3-above-1 configuration | ThreeAboveOneMonitorLayoutIntent | 四块屏"上三下一" |
| Arrange 3 Screens Horizontally | HorizontalMonitorThreeLayoutIntent | 三块屏一字横排 |
| Fix monitor arrangement | FixMonitorArrangementIntent | 修复 3 块以上外接屏被系统打乱顺序的问题 |
修复思路:不再假设内建屏是主屏
本次修复的关键改动,是让所有布局动作在计算屏幕坐标时动态检测哪块屏是主屏,而不是默认"内建屏即主屏"。以两块屏垂直排列为例(VerticalMonitorLayoutIntent.perform()):
let topScreenBounds = CGDisplayBounds(display1.id) let bottomScreenBounds = CGDisplayBounds(display2.id) if CGDisplayIsMain(display2.id) != 0 { // 底部屏是主屏:顶部屏摆放到底部屏正上方,水平居中 let topScreenX = bottomScreenBounds.midX - topScreenBounds.width / 2 let topScreenY = bottomScreenBounds.minY - topScreenBounds.height CGConfigureDisplayOrigin(config, display1.id, Int32(topScreenX.rounded()), Int32(topScreenY.rounded())) } else { // 顶部屏是主屏:底部屏摆放到顶部屏正下方,水平居中 let bottomScreenX = topScreenBounds.midX - bottomScreenBounds.width / 2 let bottomScreenY = topScreenBounds.maxY CGConfigureDisplayOrigin(config, display2.id, Int32(bottomScreenX.rounded()), Int32(bottomScreenY.rounded())) }可以看到:算法先通过CGDisplayIsMain判断哪块屏占据主屏位,然后固定主屏不动、移动另一块屏,从而在"主屏是外接屏"的合盖场景下也能得到正确的物理位置。2 屏横排、2-above-1、3-above-1、3 屏横排等动作在 LunarShortcuts/LunarShortcuts.swift(约 L3095-L3380)中均采用同样的分支策略,分别针对主屏在左、在右、在下等不同情况计算偏移。
屏幕筛选参数
布局动作的参数统一使用ScreenQuery(single: true, noDefault: true),且都要求所选屏幕互不相同(例如垂直布局中display1 != display2,3-above-1 布局中校验四个屏幕 ID 的去重集合大小为 4),否则抛出"Two/Three/Four different screens are needed to perform arrangement."的错误提示。
ScreenQuery本身是完整的EntityPropertyQuery实现(约 L204-L508),支持按名称、序列号、是否外接、是否内建、亮度、对比度等属性做谓词过滤与排序,并内置DisplayFilter动态筛选(如main、cursor、builtin、sidecar等,定义见 Lunar/Data/CLI.swift 的DisplayFilter枚举,约 L111-L120)。这意味着用户既可以在快捷指令里选具体某块屏,也可以选择"当前主屏""光标所在屏"这类动态目标。
配套能力:Fix monitor arrangement
与本次改进同属 Arrangement 分类的还有"Fix monitor arrangement"动作(FixMonitorArrangementIntent,约 L3383-L3458),它解决的是"3 块及以上外接屏的物理顺序被系统反复打乱"的问题。工作流程为:
- 在每块外接屏上通过
display.showArrangementOSD(id:number:)显示一个编号(OSD); - 用户按从左到右的顺序在键盘上依次输入各屏显示的编号;
- Lunar 依据输入顺序重新排列屏幕;按
Esc取消,按Enter部分应用已选顺序。
它有两个参数:
Only for monitors with same UUID(默认开启):在 Apple Silicon 上通过DDC.DCP(displayID:)读取各屏 EDID UUID,仅对出现重复 UUID 的屏幕进行重排,避免误动正常布局;Wait for operation to finish(默认关闭):开启后动作会等待重排完成后才返回,方便串联后续动作。
该动作同样依赖本次改进后的主屏无关坐标计算,可在外接屏为主屏的环境下使用。
小结
Lunar 5.9.5 Alpha 1 的两个改动相互配合,构成了完整的多屏布局自动化能力:
- "Set screen as Main"让你可以用快捷指令显式指定主屏,解决合盖/外接场景下的主屏归属问题;
- 全部Arrangement 布局动作改为动态检测主屏、以主屏为锚点计算偏移,使"主屏不是内建屏"时布局依然精确生效。
如果想进一步研究实现细节,建议阅读以下源码:
- 快捷指令实体与布局动作:LunarShortcuts/LunarShortcuts.swift
- 显示配置事务封装
Display.configure:Lunar/Data/Display.swift - 主屏/外接屏判定逻辑
mainDisplay与externalActiveDisplays:Lunar/Utils/DisplayController.swift - 动态屏幕筛选过滤器
DisplayFilter:Lunar/Data/CLI.swift - 对应正式版发布说明:ReleaseNotes/5.9.5.md
- 桌面应用
【免费下载链接】Lunar
Intelligent adaptive brightness for your external monitors
相关推荐
Lunar 5.6.7a1 技术解析:亮度键新增「主显示器 / 非主显示器」目标与非 DDC 亮度锁定修复
Lunar 5.6.7a1 技术解析:亮度键新增「主显示器 / 非主显示器」目标与非 DDC 亮度锁定修复 本篇基于仓库中 ReleaseNotes/5.6.7
桌面应用Lunar 6.4.4 高级 Gamma 快捷指令:用 Shortcuts 为任意显示器打造自定义色彩配置
Lunar 6.4.4 高级 Gamma 快捷指令:用 Shortcuts 为任意显示器打造自定义色彩配置 本文围绕 Lunar 6.4.4 引入的 Advan
桌面应用Qtile 屏幕配置完全指南:Screen、Bar、Widget 与多显示器布局实战
Qtile 屏幕配置完全指南:Screen、Bar、Widget 与多显示器布局实战 导读 本文围绕 Qtile 配置体系中与物理显示器直接相关的 screen
桌面应用操作系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考