menu

动机

我不是键盘狂热者,只是个人是比较偏向于全键盘操作,至于鼠标键盘之间的效率问题,某些方面讲也是仁者见仁,智者见智,推荐一下这篇文章: 使用鼠标可以提升编程效率吗? | NIL。我认为键盘操作比起鼠标操作更能容忍高延迟的屏幕显示,也认可光标到达目标控件前的移动距离确实低效。不过,即便如今键盘操作能覆盖我八九成的工作场景,鼠标操作仍作为一种手段的方法而时常故意为之。

对我而言最大的困扰其实是键鼠的切换成本。外接键盘鼠标的话,一次挪动手掌是需要肘与肩这样的大关节参与,而日常使用中整个操作序列难免要散布着几个鼠标操作,非常难受。特别是 macOS 下,默认的应用/窗口切换行为有时还是很让人迷惑,难以避免要用光标辅助一下。

当然做到全键盘操作,像聚焦(spotlight)、Alfred 这样工具是必不可少,我认为有点复杂度的生产工具都需要提供功能模糊搜索(fuzzy searching)甚至 transient.el 之类的功能。但像这样的工具配置还有快捷键相关的就不在这文章讨论之类,这篇文章主要分享我如何用键盘覆盖鼠标大部分场景下导航操作(Navigation)的经验。

应用内导航

一般化

/data/BC/FE8B2E-F75F-4495-8F9E-6678AE9E146F/vimac_example.png

很大一部分 GUI Apps 对全键盘使用并不友好,快捷键时常只能覆盖部分场景,更别谈记忆快捷键的成本了,记住的只能是日常通用操作或天天使用的生产力工具,当然像 cheatsheet 之类的辅助工具可能有所帮助。但更好的是一套一般化的导航方案,比如说遍历窗口的所有可交互元素,并为其编号,通过键盘输入记号与其交互。

vimac 就是类似的应用。它通过无障碍 API (Accessibility API),去遍历当前窗口的元素 1,采用 DFS 遍历为控件编上快捷键。触发的时候把光标移动到控件位置,并执行相应操作。

除了左键单击外还支持以下按键行为 2:

  • 右键,Shift
  • 双击,Command
  • 移动光标,Option

vimac 称之为 Hint 模式,默认按住空格键可以触发。另外 Hint 模式还遍历了以下区域的控件:

  • 通知中心(notificationcenterui)
  • 右侧菜单(extrasMenuBar)
  • App 菜单(menuBar)

另外,名字有 vi, 自然少不了 hjkl。vimac 称之为 Scroll 模式。

Scroll 模式同样会遍历当前窗口的所有子控件,并找到所有如下元素的可滚动控件3

  • AXScrollArea

然后将焦点定位到第一个 ScrollArea 4,可按 ⇥(Tab) 能切换到不同 ScrollArea

滚动是通过模拟鼠标滚轮实现的,比如 gg, G 滚动到顶部/底部,就是执行滚动 Int16.max 个像素。

let event = CGEvent.init(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: yAxis, wheel2: xAxis, wheel3: 0)!

vimac 的关键功能是通过无障碍 API 实现的,vimac 只能在支持无障碍 API 的应用上生效,好在原生控件是默认支持的,大部分场景下都能起作用。一些 App 需要特殊支持:

Chrome

浏览器需要单独拎出来说一说,重点当然是推荐 vimium 这扩展。vimac 相当于简化版的 vimium. vimium 的交互对象不限于控件(超链接),还能触达文本,可惜对文本的操作只有复制如果能支持右键菜单那就更好了。总之能熟悉 vi 的话用起来应该是得心应手的,按 ? 也能看到所有操作指南。

vimium 基本上能覆盖我大部分需要使用鼠标的场景。特别需要提一下的是 macOS 上 Chrome 的一个坑,当 ⌘+l 将焦点定位到地址栏后,竟没有快捷键让焦点回到页面。这个问题曾折腾了我不少时间,最终找到除了用鼠标点一下外的几种花式办法:

  1. tab 一次次切换焦点,直到页面
  2. 地址栏执行 javascript:
  3. ⌘+f 搜索任意字符(但大概率会重置当前视口)
  4. ⌘+⌥+↑

第四点对应的快捷键是, ⌘ Command + ⌥ Option + / 。其可以在地址栏、标签栏、书签、页面间切换焦点,虽然这个按键组合不太友好,但相比其他办法还是可以接受的。这是最近我整理资料才发现的,相当于 Windows/Linux 下的 F6/Ctrl+F6, 应该是后面版本才更新的。

不过我养成用 vimium omnibar 代替地址栏操作的习惯后,就摆脱这困扰了。相信定位到地址栏的目的 80% 是复制当前地址,可以直接用 yy 搞定。如果需要编辑地址则 ge 搞定。

vimium 覆盖不了的场景,频率最高的就是对于非 iframe 的其他可滚动元素,无法避免用鼠标点一下获得焦点,hjkl 才能对该滚动区域生效。至今还不能完美解决5,还是比较难受的,具体见 Make it possible to focus a scrollable div without using the mouse · Issue #425

另外,对于一些页面如果有快捷键冲突需要禁用 vimium, 比如 gmail, youtube… 那么 vimac 也是能派上用场的。

vimium 还有不少代替品,比如 Surfingkeys. 如果要更能折腾的还有 Nyxt 浏览器 或者 EAF 可选。

窗口导航

macOS 上的窗口导航可是个大坑。如果把 ⌘-⇥(Tab) 当成窗口切换去用,肯定会遇到不少难以理解的迷惑行为。比如 WeChat 常常是唤不起窗口的。实际上 ⌘-⇥ 切换的是应用而不是窗口,而切换应用时部分状态的窗口是不会被带起的。

这个坑的一大因素就是用户需要面对的窗口(或应用)状态繁多:

  • 正常
  • 全屏
  • 隐藏( C-h 隐藏当前应用的所有窗口)
  • 最小化
  • 无窗口

切换应用时,能带起的是正常状态、全屏状态和隐藏状态下的窗口。但不一致的是 ⌘-` 只能循环切换正常状态下的窗口,或在切换界面按下 进入 App Exposé 也只能看到正常状态下的窗口。如果应用没有窗口或只有最小化的窗口也是能被切换,但切换后没有任何反应,也十分迷惑。

但当使用场景进入到多屏幕多空间,同时应用拥有多个全屏窗口或者混合全屏窗口和普通窗口。切换行为才是最让人迷惑的,我到现在也没搞懂它的逻辑,反正我在屏幕 A 的应用 A 的普通窗口 A 切换同屏幕的其他应用窗口后,就再也无法通过 ⌘-⇥ 回到这个窗口,焦点会落在屏幕 C 的应用 A 的全屏窗口上。

在这里我强烈推荐使用 AltTab 代替 ⌘-⇥,AltTab 的切换对象就是窗口,非常直观。同时支持所有状态的窗口,包括无窗口的应用,切换的行为相当于在 Dock 栏点击应用图标,会重新打开被关闭的窗口。对各种窗口状态与屏幕、空间都整理的比较清晰,可以微调过滤不要的窗口状态,同时在能在切换器界面显示窗口的状态与所在的空间。这个可能意义不大,但仍要截个图说明一下,因为我一开始也搞不清楚这些小图标的意思。

/data/49/F4E68C-2BB7-4E9D-ABB6-5EB033FA1EB9/2022-06-15_01-39-03_screenshot.png

建议将 Shortcut 2 设置为 Active app 并绑定给 ⌘-` 。全面替换掉系统的默认行为 ,用了一段时间基本没有什么不适,可能有一些 Bug 但利大于弊。

另外 lwouis/alt-tab-macos 也是一个相当活跃的项目,作者还在一直保持更新。

屏幕间导航

屏幕间导航并不是指切换显示设置里的主屏幕,不过说起这个主屏幕主要是要提一下另外一个迷惑行为,一般认为 Dock 栏和 ⌘-⇥(Tab) 选择器是出现在主屏幕。实际上 Dock 栏和⌘-⇥选择器出现的屏幕是,主屏幕沿着 Dock 栏方位继续延伸直到到达整体屏幕空间的边缘,这时所在的屏幕才是 Dock 栏和⌘-⇥选择器出现的屏幕,更像是真正的主屏幕,这个屏幕在 AltTab 称之为 Screen including menubar。 虽然有点迷惑,但仔细想想也是合理的,毕竟 Dock 是需要边缘激活的。

重新回到主题上,那屏幕间导航是什么意思?控制屏幕焦点的主体不就是眼睛吗,切换屏幕不就是转动一下眼球或脖子的功夫?话是这样说没错,但是我认为 macOS 是有一个当前全屏的概念的,这个屏幕并不是拥有当前输入焦点的窗口所在的屏幕,AltTab 称之为 Active screen,而是当前鼠标指针所在的屏幕,AltTab 称之 Screen including mouse,姑且称为鼠标屏幕

为什么需要定义这样一个概念,很遗憾这是 Mission Control 生效的屏幕,同时也是唤起 Spotlight 的屏幕。为了通过快捷键C-→/C-←切换期望的屏幕的空间(Space)6,需要先切换当前的鼠标屏幕。屏幕间导航也就是指这个。

所以几年前我写了一个小工具 hopscotch, 来实现通过快捷键切换鼠标屏幕。更早之前用的是 @virushuoCatchMouse 来解决这个这个痛点,不过这个工具只能通过编号切换显示器,对于这种场景还是有点别扭。更符合直觉的上一个或下一个循环切换,就趁着 swiftui 刚出来,用它撸了个这个小工具,边学边做,做的比较粗糙,也就自己用而已。

/data/F9/6361A8-E745-465B-B407-E0415FCA1E3F/2022-07-10_14-22-37_screen_jumper_intro.gif

最近写这文章,就顺便把 AltTab 的活跃屏幕(Active scree)也搬运过来,实现快捷键将鼠标屏幕切换到活跃屏幕。这样,通过 ⌘-⇥ 切换键盘输入焦点后,就能一个快捷键把 Mission Control 的焦点也带过来,十分符合操作逻辑。只可惜找不到不用无障碍 API 获取其它应用窗口的坐标和尺寸的办法,所以要使用这个功能只能赋予 Accessibility 权限。

用了 AltTab 后,其实 hopscotch 大部分作用场景是能被 AltTab 替代。主要区别是 Mission Control 是基于空间切换的,而 AltTab 是基于窗口切换的,目的都是为了切换到目标窗口,显然 AltTab 更为直接,但俗话说技多不压身啊,多个提供一种方法也没什么不好的。

Footnotes

1vimac/TraverseGenericElementService.swift#L40

2vimac/HintModeController.swift#L32

3vimac/QueryScrollAreasService#L35

4vimac/ScrollModeActiveViewController#L37

5 其实代码是有对滚动元素进行判断的,见 vimium/link_hints.js#973, vimium/scroller.js#L103

6 macOS 上的空间相当于虚拟桌面的意思,见在 Mac 上的多个空间中工作

上一篇:谈谈比例代表制
keyboard_arrow_up