Mac 多屏幕窗口管理神器

使用多个显示器可以避免在任务窗口或应用之间来回切换,从而提高效率。然而随着工作空间的扩大,同时也带来了其它问题,包括:

  1. 使用鼠标的效率进一步降低;
  2. 活动窗口过多容易失去焦点;
  3. 更多时候是以上两点同时发生:找半天不知道鼠标指针在哪,多屏幕之间拖动、管理窗口变得更加困难。

不管你是不是 VIM 党,不停地在鼠标与键盘之间切换绝对是一件分心且低效的事,很多“懒人”宁可花时间记住数量庞大的组合快捷键(当然如果记不住也没关系,有一款作弊神器可以帮助你快速查看当前应用的快捷键:Cheatsheet),也不愿让自己的右手离开键盘。

虽然为了避免使用鼠标你甚至可以给 Chrome 装上 VIM 映射的插件,但总有些时候不得不依赖鼠标完成一些精确的点击、拖动等操作,然而根据心理物理学中著名的费茨定律(Fitt's law),鼠标移动时间与目标距离成正比,与目标大小成反比:

也就是说随着工作空间的扩大,使用鼠标的效率将会越来越低。除了鼠标的问题之外,当同时有多个应用窗口在你面前打开的时候,即使想要通过组合快捷键操作,也容易搞混当前捕获焦点的应用是哪个。当然就算有多个窗口重叠出现,你可以通过查看左上角工具栏所显示的应用名称来确定当前焦点,不过费茨定律对于视觉搜索也是同样适用的,对于连眼睛都懒得抬起来的人来说宁可通过 Command+Tab 尝试切换来找到当前焦点的应用。

不过这个问题可以通过另外一款作弊神器来解决:HazeOver

它可以让当前出于焦点的窗口正常显示,而其它所有窗口都蒙上一层半透明的黑纱:

macOS 提供了一个有趣的小功能,只要快速晃动鼠标就可以让指针放大,从而让你快速定位自己的鼠标。然而这点微小的工作远远无法弥补它在窗口管理上的不足。与 Windows 上贴边停靠、甩一甩甩掉其它窗口的功能相比,macOS 窗口左上角红黄绿的三个小圆点几乎毫无存在感,抛开面积太小不易点击不说,绿色的放大按钮只能进入或退出全屏模式,虽然后来加入了长按进入双全屏模式,也基本上是个鸡肋:在小屏幕笔记本上没什么用,有扩展屏的时候更没必要用。于是乎在苹果强大的生态号召力之下出现了许多第三方解决方案,但是我的需求很简单,我觉得将一块屏幕划分成皿、田之类的格局没有任何必要,我只需要最简单的功能:

  1. 可以最大化但不是全屏;
  2. 可以占据左半边或右半边;
  3. 可以在不同显示器之间快速移动。

前两条根本就是 Windows 的基本功能,我尝试了一些窗口管理应用之后,最终选择可以通过代码精确配置的 Hammerspoon,与一般的工具不同,首先它是开源的,其次使用 Lua 脚本作为配置文件。

我的配置文件在 这里 Gist-hammerspoon.init.lua,保存到本地~/.hammerspoon/init.lua,然后 Reload Config 即可。Hammerspoon 还提供一个 Console 界面,可以方便调试:

配置文件说明

-- 一般组合键为 Shift + Command + ?
local hyper = {'shift', 'cmd'}

-- 最大化窗口
-- 快捷键为 Shift + Command + ↑
hs.hotkey.bind(hyper, 'up', function()
    hs.grid.maximizeWindow()
end)

-- 让窗口占据左半边(Windows 下面的向左贴边停靠)
-- 快捷键为 Shift + Command + ←
hs.hotkey.bind(hyper, "Left", function()
  local win = hs.window.focusedWindow()
    local f = win:frame()
    local screen = win:screen()
    local max = screen:frame()

    f.x = max.x
    f.y = max.y
    f.w = max.w / 2
    f.h = max.h
    win:setFrame(f)
end)

-- 向右停靠类似

-- 将当前窗口移动到第 n 个屏幕
-- 并最大化窗口
-- 快捷键为 Ctrl + Command + 屏幕数字
local hyper2 = {'ctrl', 'cmd'}
moveto = function(win, n)
  local screens = hs.screen.allScreens()
  if n > #screens then
    hs.alert.show("No enough screens " .. #screens)
  else
    local toWin = hs.screen.allScreens()[n]:name()
    hs.alert.show("Move " .. win:application():name() .. " to " .. toWin)
    hs.layout.apply({{nil, win:title(), toWin, hs.layout.maximized, nil, nil}})
  end
end

hs.hotkey.bind(hyper2, "1", function()
  local win = hs.window.focusedWindow()
  moveto(win, 1)
end)
hs.hotkey.bind(hyper2, "2", function()
  local win = hs.window.focusedWindow()
  moveto(win, 2)
end)
hs.hotkey.bind(hyper2, "3", function()
  local win = hs.window.focusedWindow()
  moveto(win, 3)
end)