Tinycast 窗口布局(Window Layouts)完全指南:一次快捷键恢复跨屏桌面布局
2026/9/19 18:23:57 网站建设 项目流程

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 LayoutCreate 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 中的 ▶ 按钮。

执行过程分三步:

  1. 已打开的窗口一次性移动归位——所有已存在窗口在一个步骤内同时就位;
  2. 未运行的应用被打开,窗口出现后立即放置——Tinycast 为每个应用最多等待 10 秒;
  3. 标记为 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跳过(因为打开同参数只会得到一个窗口);
  • 显示器不在线、几何不可解、目标重复三种情况分别以displayDisconnectedunresolvableGeometryduplicateTarget记录为Skip(预期行为而非错误),并汇总成 HUD 提示文案,例如 "1 display not connected" 或 "2 entries skipped"。

几何换算:比例、网格与偏移如何落到屏幕上

WindowLayoutGeometry(WindowLayoutGeometry.swift)是布局的纯函数几何层,完全在 AX 空间(AX space)内运算:

  • box:条目比例所基于的"盒子",即屏幕可用区域(visible frame)扣除间距后的矩形;
  • resolve:把条目换算成最终 frame。顺序是先锚点放置、再施加偏移、最后钳制回 box 并取整——偏移是用户意图,钳制只是安全网;
  • describeresolve的逆运算,用于捕获窗口。它先把 frame 钳到 box 内再算比例,保证存储的比例永远可被resolve复现;残差偏移保持精确,居中锚点在奇数自由空间上会落到半点,此处刻意不做舍入以免把窗口挪动 1 pt。

锚点网格由WindowLayoutAnchor(WindowLayoutAnchor.swift)定义:top-lefttoptop-rightleftcenterrightbottom-leftbottombottom-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 形式存入UserDefaultswindowLayouts键(WindowLayoutStore.swift),加载时先清洗再持久化,坏记录被清理而不是丢弃;
  • 校验规则:名称不能为空、不能重复(大小写不敏感)、不能包含空字符,且至少需要一个应用条目,否则抛出对应的WindowLayoutValidationError(WindowLayout.swift)。

布局与备份

布局及其快捷键会随备份迁移。在 备份文档 的分类表中,窗口布局属于Settings & Shortcuts类别(与快捷键、自定义命令、quicklinks、收藏、别名和偏好设置同组),随.tinycast备份文件导出与导入。

导入到另一台 Mac 时,该机器不存在的显示器对应的条目会被跳过,与"缺失显示器不猜测"的规则一致。

测试与质量保障

窗口布局的纯模型、几何、计划与存储逻辑都有独立的契约测试覆盖,见 window-layout-test.swift。测试重点包括:

  • 9 个锚点的网格与双向映射(Anchor.namedplacement互逆);
  • resolvedescribe的往返一致性(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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询