分类目录归档:代码

Hammerspoon : macOS 界的瑞士军刀

相信很多朋友在自己的电脑里面都有特殊化定制。这可能也是为什么很多人都非常不喜欢配置新电脑或者重装系统。

譬如我自己,有一波应用用来提升我的电脑使用体验,如下是不完全统计:

应用用途
SizeUp可以通过快捷键将窗口吸附在屏幕的上下左右以及死角。对于大屏幕非常有用,左侧文档,右侧代码
Pap.er可以自动定时将美丽的风景画设置成我的壁纸
Stretchly定时遮挡屏幕以强制休息
Lexico.com牛津英英词典官方网站
Cheatsheet用来查看当前应用的所有快捷键
New Terminal Here在当前目录打开终端
我的常用工具列表

这些工具在提升我的用机舒适度的同时,也在我造成了一些困扰,尤其重装系统和换电脑的时候。有时候是因为软件开始收费了,我需要寻找一个免费的替代品;有时候是因为停止维护很久不再支持最新系统了,我需要寻找一个新的替代品。直到有段时间,Mac 似乎都和我有仇一般,半年更换了三台电脑。忍无可忍之下,我开始寻找一个能够替代这些小工具的方法,终于被我找到了一个 MacOS 上的瑞士军刀: Hammerspoon

类似的工具其实也不少,但是 Hammerspoon 在定制化能力和易用性上找到了一个平衡点。用户只需要稍微了解一下 Lua 基础就能很快上手利用 Hammerspoon 通俗易懂的 API 和完整的 Spoon 库实现一些定制化功能。我在毫无 Lua 基础的情况下,很快通过阅读文档完成了几个功能的定制。

我们可以用官方提供的 Spoon 来定制,以窗口控制为例

-- 我们可以通过引入 WinWin 这个 Spoon 来轻松实现 hs.loadSpoon("WinWin") -- 判断 WinWin 是否正常载入,并根据窗口控制效果配置热键 if spoon.WinWin then -- 文本提示能完美支持符号字符,这对后面设置热键列表非常有用,后面会说 -- Side hs.hotkey.bind({"cmd", "alt", "ctrl"}, "left", "Window ⬅", function() spoon.WinWin:moveAndResize("halfleft") end) hs.hotkey.bind({"cmd", "alt", "ctrl"}, "right", "Window ➡", function() spoon.WinWin:moveAndResize("halfright") end) hs.hotkey.bind({"cmd", "alt", "ctrl"}, "up", "Window ⬆", function() spoon.WinWin:moveAndResize("halfup") end) hs.hotkey.bind({"cmd", "alt", "ctrl"}, "down", "Window ⬇", function() spoon.WinWin:moveAndResize("halfdown") end) -- Corner hs.hotkey.bind({"shift", "alt", "ctrl"}, "left", "Window ↖", function() spoon.WinWin:moveAndResize("cornerNW") end) hs.hotkey.bind({"shift", "alt", "ctrl"}, "right", "Window ↘", function() spoon.WinWin:moveAndResize("cornerSE") end) hs.hotkey.bind({"shift", "alt", "ctrl"}, "up", "Window ↗", function() spoon.WinWin:moveAndResize("cornerNE") end) hs.hotkey.bind({"shift", "alt", "ctrl"}, "down", "Window ↙", function() spoon.WinWin:moveAndResize("cornerSW") end) -- Stretch hs.hotkey.bind({"cmd", "alt", "ctrl"}, "C", "Window Center", function() spoon.WinWin:moveAndResize("center") end) hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", "Window ↕↔", function() spoon.WinWin:moveAndResize("maximize") end) -- Screen hs.hotkey.bind({"alt", "ctrl"}, "right", "Window ➡ 🖥", function() spoon.WinWin:moveToScreen("next") end) -- Other hs.hotkey.bind({"cmd", "alt", "ctrl"}, "/", "Window Undo", function() spoon.WinWin:undo() end) end
Code language: Lua (lua)

是不是很简单?只需要定义几个热键,选择一下需要的控制效果就 OK 了。

我们也可以利用 Hammerspoon 的 API 自定义 Spoon ,以 BreakTime 为例

为了实现这个,我们需要定时使用一个页面或者图片遮挡整个屏幕以提示电脑前的家伙应该短暂休息一下了。

首先,我们可以轻松通过 hs.timer 开启一个定时器:

obj.Timer = hs.timer.new(60, refresh) obj.Timer:start()
Code language: Lua (lua)

然后,我们可以通过 hs.webview 创建一个遮挡这个屏幕的网页,页面中展示一个半透明的图片以达到透明效果

function makeBrowserOfBreakTime () local screen = require"hs.screen" local webview = require"hs.webview" local mainScreenFrame = screen:primaryScreen():frame() browserFrame = { x = mainScreenFrame.x, y = mainScreenFrame.y, h = mainScreenFrame.h, w = mainScreenFrame.w } local options = { developerExtrasEnabled = true, } -- local browser = webview.new(browserFrame, options):windowStyle(1+2+4+8) local browser = webview.new(browserFrame, options):windowStyle(1+2+128) :closeOnEscape(true) :deleteOnClose(true) :bringToFront(true) :allowTextEntry(true) :transparent(true) return browser end
Code language: Lua (lua)

接下来,我们就可以通过定时器来创建 browser:show() 和销毁 browser:delete() 页面来实现遮挡效果。

function refresh() obj.curTime = obj.curTime + 1 if obj.curTime > obj.microbreakInterval then obj.curMicrobreakCount = obj.curMicrobreakCount + 1 if obj.curMicrobreakCount > obj.microbreakCount then hs.alert.show(obj.breakTime .. " minute break starts") local browser = makeBrowserOfBreakTime(); browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.breakTime * 60 - 1)):show() hs.timer.doAfter(obj.breakTime * 60, function() if browser ~= nil then browser:delete(); end end) obj.curMicrobreakCount = 0 else hs.alert.show(obj.microbreakTime .. " second microbreak starts") local browser = makeBrowserOfBreakTime(); browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.microbreakTime - 1)):show() hs.timer.doAfter(obj.microbreakTime, function() if browser ~= nil then browser:delete(); end end) end obj.curTime = obj.curTime - obj.microbreakInterval end end
Code language: Lua (lua)

最后,我们就可以像前面调用 WinWin 一样载入 BreakTime 这个自定义 spoon 并启动就可以了。

我们还可以定义任务栏菜单和提升一下用户体验

以 BreakTime 为例,我们可以将下一个休息时间显示出来

BreakTime 菜单提示
BreakTime 菜单提示

不光如此,我们还可以把所有在 Hammerspoon 定义的热键都展示出来。

所有热键的菜单提示
所有热键的菜单提示

由于他对符号字符支持的很完美,所以你在菜单里面可以设置很多有意思的提示,是不是很赞?😄

obj.menubar:setTitle("⌨️") obj.menubar:setTooltip("Hot Key Info") local hotkeys = hs.hotkey.getHotkeys() local menuItem = {} for key, value in pairs(hotkeys) do local item = { title = value["msg"] } table.insert(menuItem, item) end obj.menubar:setMenu(menuItem)
Code language: Lua (lua)

除以上这些,Hammerspoon 还有很多东西值得探索。各位如果有兴趣的话,可以用我的定制随便试试。我也在不断摸索完善我自己的定制。

其实不光这些小工具,每次重新安装我的常用 App,例如 VS Code、Plex、Sublime Text、VIM 等等,也是个很痛苦的事情。我现在通过维护一套 homebrew 列表来快速完成大多数的 App 安装。😂

ImgCache 0.2.1 发布

我终于给 ImgCache 进行了一次版本更新。当时我承诺会在下一个版本完成对 HTTPS 图片链接的支持,没想到一晃就是十二年。我还在开博日志中吐槽了一下自己。现在我终于信守承诺,在新版本中搞定了 😂。Better late than never!

时隔 12 年发布 ImgCache 0.2.1
时隔 12 年发布 ImgCache 0.2.1

当然除了支持了 HTTPS 图片链接的缓存,也升级了 Snoopy 类,更新了代码格式使得更符合代码规范和 README,还修复了两个有意思的小 bug。

第一个 bug 是对 SVG 图片后缀名的错误提取。这几天在开发 HTTPS 支持功能的时候,我用 PNG 测试,功能完全正常。但是我偶然使用了一个 SVG 图片链接来作为测试,发现缓存的图片无法在前端展示,看了下源代码缓存的图片文件后缀名不是 .svg 而是 .svg+xml 。

坦白说,我已经完全想不起来,在这套十二年代的远古代码里面我是通过什么方式来确定缓存图片文件后缀名的。简单翻了下代码,原来是用图片链接请求 HTTP 头字段中的 Content-Type 去判断。通常图片链接的 HTTP 头字段中都包含类似于 Content-Type: image/png 的信息来定义 MIME 类型,我们只要通过 / 前部分来判断是否是图片,后半部分来判断图片类型直接得到后缀名就可以了。本来插件也基本上就是自用,所以当时估计就随便测试了常用格式,发现一切 OK,就发布版本了。

但是很奇怪的是,SVG 的 Content-Type 并不是 image/svg 而是 image/svg+xml,所以当我把提取出 svg+xml 作为后缀名的时候,前端展示就出现了问题。

根据 Wikipedia 中,SVG 中的矢量图信息是以 XML 格式保存的,所以本质上 SVG 是一个 XML 文件。这样子一切就说得通了。

Scalable Vector Graphics (SVG) is an XML-based vector image format for two-dimensional graphics with support for interactivity and animation. The SVG specification is an open standard developed by the World Wide Web Consortium (W3C) since 1999.

SVG images are defined in a vector graphics format and stored in XML text files. SVG images can thus be scaled in size without loss of quality, and SVG files can be searched, indexed, scripted, and compressed. The XML text files can be created and edited with text editors or vector graphics editors, and are rendered by the most-used web browsers.

https://en.wikipedia.org/wiki/Scalable_Vector_Graphics

于是我不得不对 SVG 的 Content-Type 做一个预处理后再作为后缀名来使用。其实存在这个问题的还不止 SVG,打算在后续版本中处理掉。

第二个 bug 是因为 WordPress 编辑器生成的 IMG 标签是不关闭的,且最后的属性和标签末尾之间缺少空格,如下:

<meta http-equiv="content-type" content="text/html; charset=utf-8"><img src="http://nginx.org/nginx.png">
Code language: HTML, XML (xml)

而当时的我,认为所有的 HTML 标签都应该是关闭了,且最后的属性和标签结束应该是存在空格的,如下:

<meta http-equiv="content-type" content="text/html; charset=utf-8"><img src="http://nginx.org/nginx.png" /> <img src="http://nginx.org/nginx.png" ></img>
Code language: HTML, XML (xml)

基于这个认知,之前版本中提取图片 URL 的正则鲁棒性不够,所以无法成功从这个 WordPress 生成的 IMG 标签中提取出正确的图片 URL,使得插件认为图片链接都是无效的,就停止了缓存。😅

关于这个版本的故事就先扯的这里。至于下个版本,我可能会加上对缓存图片过期时间的自定义能力。由于这个插件当时主要是为了用来缓存 Feedburner 订阅数之类的图片,更新频率比较高,所以我把过期时间固定在了 1 小时,即 3600 秒。现在想想,过期时间能自定义肯定是更合理一点。

好了,下个版本见吧。