Tinycast 窗口布局(Window Layouts)完全指南:一次快捷键恢复跨屏桌面布局
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
窗口布局(Window Layout)是 Tinycast 保存的一组"屏幕摆放方案":哪些应用、以什么尺寸、放在什么位置、位于哪台显示器上。执行一次布局,Tinycast 会自动打开尚未运行的应用,并把每个窗口一次性归位。本文基于仓库文档 window-layouts.md 与 WindowManagement 模块源码,系统讲解布局的创建、编辑、执行与管理,并深入数据模型与匹配算法,帮助你理解布局背后"一次归位、绝不猜错"的实现原理。
布局与 Window Management 的关系
窗口布局不是独立功能,而是 Window Management 的组成部分。它复用了 Window Management 的三个基础设施,不需要额外安装或授权:
- 切换开关:
Settings → Window Management中的 "Enable window management" 开关。该功能默认关闭;关闭状态下启动器中不会出现任何窗口命令,录制的快捷键也不会移动任何窗口; - 辅助功能授权:布局复用剪贴板粘贴等操作已有的 Accessibility 权限,无需申请新权限(详见 permissions 文档);
- 窗口间距设置:布局的 "Use preferred gap"(使用首选间距)选项引用 Window Management 的 "Gap between windows" 设置(0 到 64 点,步进 2,默认 0),效果与平铺命令一致。
因此,在开始使用布局前,请先在 Window Management 设置页打开主开关并确保已授予辅助功能权限。
创建布局
在Settings → Window Management → Window Layouts中,有两种创建方式:
- New Layout(新建布局):从空白开始,逐个添加应用条目;
- Create Layout from Current Windows(从当前窗口创建布局):捕获你当前打开的全部窗口,自动生成条目。
这两种操作同时也以启动器命令的形式存在:Create Window Layout与Create Layout from Current Windows。
捕获动作本身不会自动保存。捕获完成后会直接打开编辑器,让你检查捕获结果、删除不需要的窗口、并为其命名。当从启动器执行捕获时,捕获前你正在使用的那个窗口会被自动标记为Bring to front(置顶)。
从源码看,捕获逻辑位于 WindowLayoutRunner.swift 的captureCurrentWindows():它通过WindowInventory.snapshot(positionableOnly: true)获取当前所有可定位窗口,用WindowPlacementEngine.screen(containing:)找到每个窗口所在显示器,再调用WindowLayoutGeometry.entry(...)把窗口的 frame 反向换算成"宽高比例 + 3×3 锚点 + 偏移"的布局条目;当前聚焦窗口(focused window)的条目会被记录为frontmostEntryID。
编辑器字段
编辑器左侧按显示器分页预览每个屏幕及绘制其上的窗口,右侧编辑当前选中的窗口。各字段含义如下:
| 字段 | 作用 |
|---|---|
| App | 该窗口所属的应用 |
| Argument | 可选。要打开的文件、文件夹、网址或 quicklink |
| Bring to front | 布局执行完毕后将该窗口置于最前。每个布局只能有一个 |
| Size | 宽和高,以显示器尺寸的比例表示 |
| Position | 位置,落在 3×3 网格上 |
| Offset | 从该位置出发按点(points)进行的微调 |
| Use preferred gap | 按 Window Management 的间距设置内缩窗口,与平铺命令一致 |
预览会随输入实时移动。Save 的快捷键是⌘return,因为普通return归属于当前正在输入的文本框。
布局会记住它当初构建时所针对的显示器,即使这些显示器当前被拔掉,编辑器依然保留它们的标签页——你可以在笔记本上编辑一套"办公室桌面"的布局。
数据模型:一个条目到底保存了什么
WindowLayoutEntry(WindowLayout.swift)是布局的最小单元,包含:bundleID(应用标识)、可选的argument、目标显示器display、宽高比例widthFraction/heightFraction、锚点anchor与偏移offset。
几个值得注意的工程细节:
- 比例永远是比例:
fractionRange被约束在 0...1,且WindowLayoutGeometry负责兜底 1 pt 的最小边长(minimumLength: CGFloat = 1),保证永远不会写入面积为零的窗口 frame; - 偏移有上限:
offsetLimit为 10,000 点,真实微调永远不会被裁剪,而异常存储值会被钳制; - 宽容的导入策略:
sanitized()采用"钳制而非拒绝"——一个损坏的导入最多丢失微调值,不会丢掉整个布局。非法的 bundleID、空 UUID 才会被剔除; - 编码兼容:
CodingKeys手写,新增字段后旧布局与旧备份仍可读取,缺失字段回退到默认值。
布局本身(WindowLayout)还带有一个frontmostEntryID,它用ID 而非布尔标记来指明"执行后置顶的窗口"——这从结构上保证了每个布局至多只有一个置顶窗口。
执行布局
执行布局有三种方式:从启动器运行、使用该布局自己的全局快捷键、或点击 Settings 中的 ▶ 按钮。
执行过程分三步:
- 已打开的窗口一次性移动归位——所有已存在窗口在一个步骤内同时就位;
- 未运行的应用被打开,窗口出现后立即放置——Tinycast 为每个应用最多等待 10 秒;
- 标记为 Bring to front 的窗口在全部完成后获得焦点——如果在等待期间你切换到了其他应用,焦点会留在你切换的地方,不会被强行抢走。
让执行可预测的四条规则
- 现有窗口匹配最近的槽位:如果桌面已经排好,执行就是"无操作",且窗口绝不会跨显示器乱跳;
- 带 argument 的条目总是打开新窗口:这是让多数应用产生第二个窗口唯一可靠的办法;
- 缺失的显示器被跳过,绝不猜测:显示器被拔掉时,属于它的窗口保持原样,不会堆到笔记本屏幕上;
- 尺寸是屏幕比例:改变分辨率后布局依然适配。
此外要特别留意:Restore Window 不会撤销布局。它只把窗口恢复到"上一条窗口命令之前"的位置,而不是"布局执行之前"的位置。
运行器实现:一次通过的调度
执行逻辑在 WindowLayoutRunner.swift 的run(_:gap:)。其核心思想是先规划、后执行:先通过WindowLayoutPlan.make(...)生成一份纯数据计划(plan),再按计划写入 AX 层,全程不夹杂即时决策。
关键实现参数:
- 10 秒启动截止(
launchDeadline = Duration.seconds(10)):足够冷启动的应用完成绘制,又不会被卡住的应用拖住整个流程; - 200 ms 轮询间隔(
pollInterval = Duration.milliseconds(200)):每个待处理应用每 tick 一次 AX 往返,刻意不做逐帧轮询; - 单次抑制动画:每个应用在放置其多个窗口时只做一次
suppressEnhancedUserInterface(WindowLayoutRunner.swift),而非逐窗口重复; - 聚焦保护:
userSwitchedApps检测等待期间用户是否切换了应用(WindowLayoutRunner.swift),如果用户已经主动离开,布局不会把焦点抢回来; - 权限前置检查:执行入口先调用
Permissions.ensureAccessibility(),权限缺失时返回isBlockedOnPermission并终止,这是唯一需要用户主动处理的失败情形。
匹配算法:就近原则
WindowLayoutPlan.make(WindowLayoutPlan.swift)负责把布局条目映射到真实窗口:
- 同一应用的现有窗口按阅读顺序(先按 Y 再按 X,即自上而下、从左到右)排序,保证库存顺序不会改变计划结果;
- 无 argument 的条目从可用窗口里挑选中心点距离目标 frame 最近的一个(
nearest(to:among:),WindowLayoutPlan.swift),所以已排好的桌面执行后就是"原地不动"; - 带 argument 的条目必然走
launch路径;若同一应用 + 同一 argument 出现两次,第二次被标记为duplicateTarget跳过(因为打开同参数只会得到一个窗口); - 显示器不在线、几何不可解、目标重复三种情况分别以
displayDisconnected、unresolvableGeometry、duplicateTarget记录为Skip(预期行为而非错误),并汇总成 HUD 提示文案,例如 "1 display not connected" 或 "2 entries skipped"。
几何换算:比例、网格与偏移如何落到屏幕上
WindowLayoutGeometry(WindowLayoutGeometry.swift)是布局的纯函数几何层,完全在 AX 空间(AX space)内运算:
- box:条目比例所基于的"盒子",即屏幕可用区域(visible frame)扣除间距后的矩形;
- resolve:把条目换算成最终 frame。顺序是先锚点放置、再施加偏移、最后钳制回 box 并取整——偏移是用户意图,钳制只是安全网;
- describe:
resolve的逆运算,用于捕获窗口。它先把 frame 钳到 box 内再算比例,保证存储的比例永远可被resolve复现;残差偏移保持精确,居中锚点在奇数自由空间上会落到半点,此处刻意不做舍入以免把窗口挪动 1 pt。
锚点网格由WindowLayoutAnchor(WindowLayoutAnchor.swift)定义:top-left、top、top-right、left、center、right、bottom-left、bottom、bottom-right共 9 个位置,raw value 全部显式拼写,重命名枚举 case 不会破坏已存储的数据。注意在 AX 坐标系中+Y 向下,因此.min表示顶部。
显示器身份由WindowLayoutDisplay(WindowLayoutDisplay.swift)表示:uuid来自CGDisplayCreateUUIDFromDisplayID(由服务层字符串化),可在重启和重新插拔后保持稳定;name在创建时取自NSScreen.localizedName,这样即使显示器当前不在线,也能在跳过的提示中说出自己的名字。
管理布局
Settings 中每个布局都有:快捷键录制器、启动器显示复选框,以及运行 / 编辑 / 复制 / 删除按钮。
- Show layouts in launcher(默认开启):一次把全部布局和两个布局命令移入/移出启动器搜索,不影响其他窗口命令;
- 复制(duplicate):会生成全新 ID 的副本。副本命名自动规避冲突——"Office" → "Office Copy" → "Office Copy 2"(见 WindowLayoutStore.swift),且新 ID 使副本不会继承原布局的快捷键;置顶标记按位置跟随其条目(因为条目 ID 全部焕新),见 WindowLayoutStore.swift;
- 存储:布局库以 JSON 形式存入
UserDefaults的windowLayouts键(WindowLayoutStore.swift),加载时先清洗再持久化,坏记录被清理而不是丢弃; - 校验规则:名称不能为空、不能重复(大小写不敏感)、不能包含空字符,且至少需要一个应用条目,否则抛出对应的
WindowLayoutValidationError(WindowLayout.swift)。
布局与备份
布局及其快捷键会随备份迁移。在 备份文档 的分类表中,窗口布局属于Settings & Shortcuts类别(与快捷键、自定义命令、quicklinks、收藏、别名和偏好设置同组),随.tinycast备份文件导出与导入。
导入到另一台 Mac 时,该机器不存在的显示器对应的条目会被跳过,与"缺失显示器不猜测"的规则一致。
测试与质量保障
窗口布局的纯模型、几何、计划与存储逻辑都有独立的契约测试覆盖,见 window-layout-test.swift。测试重点包括:
- 9 个锚点的网格与双向映射(
Anchor.named与placement互逆); resolve与describe的往返一致性(resolve(describe(frame)) == frame);- 比例钳制、1 pt 最小边长、偏移钳制与超大间距的边界行为;
- 条目 ID 的命名空间前缀
window-layout:的往返与拒识; - 布局排序(按名称大小写不敏感)与摘要文案("1 window"、"2 windows · 2 displays")。
这些测试把"捕获什么就能放回什么""已排好的桌面执行后不动"等核心承诺固定为可回归验证的契约,与 WindowManagement 的其余部分一起保障布局功能的稳定性。
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考