ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Lunar 5.9.5 Alpha 1 解析:让 Shortcuts 显示器布局在主屏非内建屏时正常工作,并新增“Set Screen as Main“快捷指令

Lunar 5.9.5 Alpha 1 解析:让 Shortcuts 显示器布局在主屏非内建屏时正常工作,并新增“Set Screen as Main“快捷指令
  • 桌面应用

【免费下载链接】Lunar

Intelligent adaptive brightness for your external monitors

项目地址:https://gitcode.com/gh_mirrors/lu/Lunar
点击查看免费下载

导读

本文基于 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 }

关键点解读:

  1. 幂等保护:若目标屏幕已经是DC.mainDisplay,直接返回成功,不会做无谓的重新配置。

  2. Display.configure事务化提交:这是 Lunar 封装好的 CoreGraphics 配置入口,定义在 Lunar/Data/Display.swift(约 L4483-L4505):

    • 先CGBeginDisplayConfiguration开启配置会话;
    • 执行闭包内的CGConfigureDisplayOrigin布局修改;
    • 闭包返回true则CGCompleteDisplayConfiguration(..., .permanently)永久生效,否则CGCancelDisplayConfiguration回滚。

    这种方式保证"主屏切换 + 旧主屏移位"是一整套原子操作,避免出现半个屏幕布局变更的状态。

  3. 坐标搬移技巧:当新主屏已位于原点时,先把旧主屏(内建屏)从 (0,0) 平移到左侧(x 取负),再把目标屏坐标原点设为 (0,0)。由于 macOS 约定"原点所在屏幕即主屏",这一步实质上是完成了主屏的切换。

  4. 主屏判定来源:DC.mainDisplay定义在 Lunar/Utils/DisplayController.swift(约 L984-L986):

    var mainDisplay: Display? { activeDisplayList.first(where: { CGDisplayIsMain($0.id) == 1 }) }

    即通过系统 APICGDisplayIsMain实时判定当前主屏,与用户看到的"显示器排列窗口"中菜单栏所在屏一致。

布局快捷指令:完整阵容与修复要点

可用的布局动作

根据源码,5.9.5 Alpha 1 时代的布局(Arrangement)动作包括:

快捷指令动作源码类型适用场景
Set Screen as MainMakeMonitorMainIntent任意屏幕设为主屏
Arrange 2 Screens VerticallyVerticalMonitorLayoutIntent两块屏上下排列(MacBook + 1 块外接屏)
Arrange 2 Screens HorizontallyHorizontalMonitorLayoutIntent两块屏左右排列(2 块外接屏)
Arrange 3 Screens in a 2-above-1 configurationTwoAboveOneMonitorLayoutIntent三块屏"上二下一"
Arrange 4 Screens in a 3-above-1 configurationThreeAboveOneMonitorLayoutIntent四块屏"上三下一"
Arrange 3 Screens HorizontallyHorizontalMonitorThreeLayoutIntent三块屏一字横排
Fix monitor arrangementFixMonitorArrangementIntent修复 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 块及以上外接屏的物理顺序被系统反复打乱"的问题。工作流程为:

  1. 在每块外接屏上通过display.showArrangementOSD(id:number:)显示一个编号(OSD);
  2. 用户按从左到右的顺序在键盘上依次输入各屏显示的编号;
  3. 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

项目地址:https://gitcode.com/gh_mirrors/lu/Lunar
点击查看免费下载

相关推荐

上一篇:InterviewGuide 力扣刷题笔记:434. 字符串中的单词数 —— C++ 双解法与 stringstream 流式分割剖析
下一篇:Untrunc终极指南:三步修复损坏MP4视频的免费开源神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进