这应该是连载得最近的一次,书接上回:《或许可以用 TypeScript 编写 hammerspoon》(也就是下面那篇)

这里只会描述通过 TypeScript 实现的过程

以下内容可能产生不适(因为hammerspoond.ts 全是 interface 一把梭,编码不好看)

TL;DR

  • 创建界面
  • 实现剪贴板读取
  • 存储数据
  • 绑定快捷键

创建界面

hammerspoon有很多种交互接口,其中chooser就是某小黑帽那种对话式弹窗,用这个挺合适的

// choice 就是当你对着选项按下 enter 之后,这个被选择对象的内容
const completionFn = choice => {
  // 一般来说这个判断不可省略,这样可以方便排除取消的情况
  if (choice) {}
}
const chooser = hs.chooser.new(completionFn)

这样chooser就是一个实例,可以使用相应的方法,例如显示或隐藏

chooser.show()
chooser.hide()

剪贴板操作

关于剪贴板的操作都已经封装在hs.pasteboard这个模块中,通过两个函数获取到我们对于剪贴板历史比较常用的两种内容类型

pasteboard.readString() // 读取最后一次剪贴板的文本
pasteboard.readImage() // 读取最后一次剪贴板的图片数据

如何得知我的剪贴板已经有新内容了?社区基本上的方案都是通过对比剪贴次数来判定更新的,如下

pasteboard.changeCount()

所以操作大概是:使用定时器,在若干时间后检查一次次数,如发生改变即更新剪贴板历史

const clipboard = new Clipboard()
// 我选择 1s 检查一次
export const clipWatcher = hs.timer.new(1, () => {
  const now = hs.pasteboard.changeCount()
  if (now !== preCount) {
    pcall(clipboard.save.bind(clipboard))
    preCount = now
  }
})
clipWatcher.start()

操作数据

识别数据

只要出现对比差异,就可以执行保存操作

日常使用中一般会复制到文本和图像(截图),先做到如何区分来源类型

通过苹果开发者文档关于 UTI,可以得到大概文本就是public.plain-text,图像就是public.{pic format}

我截图是png的,舍远求近直接只识别我自己使用的两种格式:public.png, public.utf8-plain-text

save() {
  const types = hs.pasteboard.contentTypes<ModelChoice['type']>()

  for (const type of types) {
    if (isImgType(type)) {
      this.saveImage(type)
    } else if (isTextType(type)) {
      this.saveText(type)
    }
  }
}

保存数据

对应的,当知道数据来源是什么类型之后就可以相应操作

保存我采用了sqlite,因为 hammerspoon 带了数据库操作模块hs.sqlite3。主要原因:

  • timer可能会崩溃导致不会继续捕获,重启服务数据丢失
  • 数据库查询比较快
  • 数据库我还另有其用,不亏

这部分直接看 github

启用

绑定快捷键

hammerspoon 的快捷键模块hs.hotkey,可以将快捷键绑定到具体操作上

hs.hotkey.bind(clipboardConf.hotkey[0], clipboardConf.hotkey[1], () => {
  clipboard.show()
})

一套组合键,chooser 就可以显示了

加载内容

一般来说,在显示对话框时再去加载数据可以保证数据是新的,所以使用chooser.choices(choices)加载数据,再chooser.show()展示

this.chooser!.choices(choices)
this.chooser!.show()

至于获取数据的形式,就是需要查询数据库,还是查询文件,还是另有其他方式而已

参考连接

《如何使用 Hammerspoon 实现剪贴板历史》 —— Ahonn
Uniform Type Identifier Concepts