Dec 22, 2022
使用Hammerspoon实现NVIM的输入法自动切换
背景
之前花了一段时间搭建好了Neovim的配置,并使用Neovide作为GUI进行使用,大部分情况下使用起来是非常舒服的,但是中文的切换是一件让人有点不爽的事情,很多时候,想第一时间按下代码,却发现使用的是中文输入,这个时候还需要手动的切换输入法为英文输入状态。虽然是一个小小的操作,但是在很多时候确实极致体验上的一块巨大的绊脚石。
于是上Google搜了一番是否有一些现成的方案,相信肯定不是我一个人有这种不优雅的体验。先说说具体的诉求,我想在除了插入模式之外的模式,保持英文输入状态,仅在插入模式下,可以手动切换中英文。按照这个思路搜了一圈,果然在很早之前就有人提供了一系列的解决方案,主要是两个层级,一是从输入法级别去解决,二是从VIM本身去解决。
- 输入法上解决的思路就是,切换输入法。我目前使用的是搜狗输入法,其内置的高级功能最多支持打开某个App时,将输入法状态切换为英文输入,无法响应VIM本身的模式切换。所以搜狗输入法本身来说是无法满足这个需求,社区提供的方案是使用RIME开源输入法,对应Mac平台的实现是鼠须管,于是我第一时间就按照配置弄出来了一个版本试试,用了几个星期后,发现这个输入法想要得心应手,还需要比较多的配置和折腾,但就我目前这个需求来说,它没有很好的满足,至少在使用期间感觉不太好用。
- 从VIM本身解决的思路就是使用Plugin,也确实有很多大佬写了一些Plugin去解决这个,不过我没有具体的尝试,先去Github上看了下对应仓库及
issue
,发现很多都没有怎么维护了,而且从issue
的情况来看,似乎没有很完美的解决这个诉求(如Ctrl-c
切换模式、输入法切换慢、切换到其他App后再回来输入法的状态不对之类的)。所以这个方案直接就放弃了。
正好前几天因为要解决电脑上双屏工作时,App应用切换屏幕的需求,在这个需求中了解到了一款极具效率化的工具Hammerspoon,这个工具使用Lua进行脚本编写,提供与MacOS交互的相关API。所以顿时脑子里就生成了一个想法,如果可以监听到我使用的App及相关按键,并可以控制输入法的切换,那这一套流程正好就能解决我的需求。
几个核心的点:监听键盘输入事件,监听App焦点(当前是哪个App处于获取焦点的状态–活跃
),具备切换输入法的能力。顺着这个思路去搜索了下Hammerspoon的API文档和一些使用的教程,发现虽然之前没有人实现过我这个需求,但是几个核心的点都是有使用的场景,也就是说整个操作的上,是可行的。
准备工作
说下我的相关环境:
- 系统: macOS Monterey 12.5
- Neovim: 0.8.1
- Hammerspoon: Version 0.9.97 (6267)
- LuaJIT: 2.1.0-beta3
- Neovide: 0.10.3
- 输入法: 搜狗输入法
Hammerspoon直接从官网下载安装,然后点击顶部的图标后,点击Open Config
即可打开对应的配置目录,其在磁盘的位置是~/.hammerspoon
。
开发
我的的配置结构如下:
1 | hammerspoon/ |
在init.lua
中声明相关的Module,主要的逻辑都写在了neovim.lua
文件中。
App的监听
因为不想对其他的App的使用产生影响,所以需要监听到使用Neovide时,才开启相关的逻辑,这个功能是通过hs.window
订阅窗口聚焦事件来实现的:
1 | window.filter.default:subscribe(window.filter.windowFocused, function(_, appName) |
这样就能实现仅当使用Neovide时,才开始我们相关的逻辑。
输入法的切换
我们最终的目的是通过一系列的逻辑实现自动切换输入法,所以这一步很重要(要是这个实现不了,前面的逻辑都是白搭了)。Hammerspoon中也提供了API让我们可以很轻松的实现输入法的切换:hs.keycodes.currentSourceID("com.sougou.inputmethod.sogou.pinyin")
,如果使用的不是搜狗,这里Google就可以搜索到查看当前系统上所装应用的ID,对应的替换这里就可以了。这样能直接切换到对应的输入法,对我而言,主要是英文和搜狗的切换,Mac自带的英文输入就可以满足,其ID是com.apple.keylayout.ABC
。
这里可能有一个疑问,搜狗本身就具备中英文的输入,为啥不直接切换搜狗的中文状态呢,其实要这样做,也不是不可以,不过我自己感觉,上诉的方案基本满足了我的使用;另一个原因是,Hammerspoon所交互的都是系统的API,是没法直接和App本身打交道的(无法去获取App的状态),所以如果需要知道搜狗输入法目前的输入状态是英文还是中文,就需要开机就开启一个事件监听,来监听CapsLock和Shift键的点击状态,以此来存储输入法的状态,这种方案我感觉不是很舒服,同时社区有人基于这个方案做的显示输入法的状态,是有一定的Bug的,就是这个监听可能会死掉,我自己也不喜欢后台有一个这样的服务一直运行着。
那么把这个方案的代码落实一下就如下了:
1 | local sougouId = "com.sougou.inputmethod.sogou.pinyin" |
Tip:这里有一点需要注意,因为我除了聊天之外,其余App大部分输入都需要的是英文状态,所以我把搜狗输入法的默认状态设置为英文,这样输入体验是很丝滑衔接的。
键盘事件的监听
键盘事件需要监听的内容有两种,一是普通的输入,即a,b,c,1,2,3
这些键,二是修饰键(ModifyKey),即Control、Command、Shift、Option这些键,这在Hammerspoon中是两种不同的事件监听,分别对应的是hs.eventtap.event.types.keyDown
和hs.eventtap.event.types.flagsChanged
,这两种监听的伪代码如下:
1 | eventtap.new({eventtap.event.types.keyDown}, function(e) |
到这里,整个功能的大概结构就完成了。接下来就是针对VIM中的模式切换写具体的逻辑了
实现自适应的输入法切换
在VIM中,进入插入模式的方式,主要有以下几种
- 普通模式下通过
i
键进入插入模式; - 普通模式下通过
a、A、I、o、O
等进入插入模式; - 普通模式下通过
r、R
进行单字符和多字符替换(这种情况仍然是普通模式,但是可能需要替换的字符为中文); - 可视模式下通过
c
键进入插入模式,这里的可视模式包括了块可视(Ctrl-v
进入Block Visual Mode)和行可视(Shift-v
进入Line Visual Mode);
这样看,所涉及的模式主要是三个NOR、VIS、INS
,做好定义
1 | NOR = "Normal Mode" |
这四种方式,前三个可以归为一类,所以可以定义一个数组insertKeys = {"i", "I", "a", "A", "o", "O", "r", "R"}
;
整个逻辑的分支有三个:
- 切换到普通模式,这个时候需要将输入法设置为英文;
- 普通模式下切换到插入模式,将输入法设置为搜狗;
- 可视模式下切换到插入模式,将输入法设置为搜狗;
- 这一点是针对我自己的,我配置了快捷键实现在插入模式下保存并返回普通模式,所以有插入模式下通过快捷键切换到普通模式,将输入法设置为英文;
结合前面监听修饰键,需要将修饰键暂存起来,用于判断是点击了那些组合键,所以用一个变量lastModify
来保存比普通键先按下的修饰键。
再在普通键的监听中根据上面的思路写逻辑,代码如下:
1 | if pressKey == ECS then |
到这里,整个功能基本算完成了。最后再收一下尾巴,想想前面Plugin遇到的问题,如果我中途切换到其他App,切换了输入法,那回到Neovide中,状态不就不对了么,比如我处于普通模式,应该是abc
,切换到其他App修改为搜狗中文输入后,在Neovide还是至少需要按一下Shift
来切换。所以回到最开始监听App切换那儿,做一个状态的判断:
1 | if curMode ~= INS then |
非输入模式下,就切换abc
。这样当你再回到Neovide时,就可以丝滑的进行操作,而不用担心当前输入法的状态了。