输入法的奇妙冒险: zsh 斗士

01 Dec 2024 4274 words 15 minutes BY-SA 4.0
develop ime

书接上回。我们实现了一个 python 输入法。虽然很酷,但用处不大。 考虑到图形化用户界面下的输入法在第一篇文章中已经提过了。 我们来想一想在命令行场景下输入法会用在哪些地方:

  1. 在编辑器中打字
  2. 文件管理,比如把一个文件重命名为“学号+姓名+工程伦理大作业.pdf”

我们先解决第二个需求:

zsh

和之前一样,一共分为两个部分:

  1. 用 C 语言实现一个 zsh 模块。
  2. 基于此模块实现一个 zsh 插件,定义切换输入法状态的快捷键

模块

如果因为性能等因素,要自己写 zsh 模块来调用,也是比较方便的。Zsh 的源码中 Src/Modules 是模块目录,里边有一个实例模块 example(example.c 和 example.mdd 文件)。可以参考代码编写自己的模块,难度并不是很大。

18_Zsh-开发指南(第十八篇-更多内置模块的用法)

“难度并不是很大”,信你个糟老头子……

和用 C 语言开发 python 模块不同,网上的教程近乎乏善可陈。甚至有些问题笔者得求助 zsh 邮件列表上的开发人员才得以解决。

熟悉构建系统的朋友都知道,构建目标的类型通常有:

模块实际上是一种特殊的动态链接库。

一般脚本语言会为模块开发提供一个特殊的头文件:

这些头文件暴露了一些 API 。模块不同于一般动态链接库的地方在于这些 API 的函数是没有被链接上其他动态链接库的“悬空”状态。即不可以直接被 dlopen()

构建模块需要支持构建二进制模块的适用于该脚本语言的构建系统。该构建系统通常会调用另一个支持构建二进制文件的 C/C++ 构建系统。譬如:

是的! zsh 目前只有 autotools 。关于 autotools 的性能语法有多懒笔者就不解释了啊。

先上代码: zsh-rime

真正的 C 代码在 rime.c 。 我们实现一个 rime 的内置命令(就像 cd 一样):

% rime
rime init [arguments...]
rime createSession [session_id]
rime destroySession $session_id
rime getCurrentSchema $session_id [schema_id]
rime getSchemaList [schema_list]
rime selectSchema $session_id $schema_id
rime processKey $session_id $keycode $mask
rime getContext $session_id [context_composition] [context_menu] [context_menu_candidates_text] [context_menu_candidates_comment]
rime getCommit $session_id [commit]
rime commitComposition $session_id
rime clearComposition $session_id

难点有 2 个:

% read
123
% echo $REPLY
123

其余就和开发一般脚本语言的模块的方法完全一致。

插件

zsh 的插件开发有一个标准

除此之外我们需要:

自动构建

检测模块是否存在,不存在运行: ./configure && make

配置

zstyle -s context_name option_name option_value 然后利用 $option_value 配置该插件

补全

参考 zsh-completions 的文档

ANSI escape code

termcap 和 terminfo 提供了所有按键和对应 ANSI escape code 的映射。 不同计算机上使用的映射不太一样。有的是 termcap ,有的是 terminfo 。

例如在 zsh 中,可以

zmodload zsh/termcap
print -l ${(kv)termcap} | cat -v
zmodload zsh/terminfo
print -l ${(kv)terminfo} | cat -v

一些按键的名字对应如下:

按键 termcap terminfo
Up ku kcuu1
Down kd kcud1
Left kl kcub1
Right kr kcuf1
Delete kD kdch1
Insert kI kich1
PageUp kP kpp
PageDown kN knp
Home kh khome
End @7 kend
F1 k1 kf1
F10 k; kf10
F11 F1 kf11
F20 FA kf20
F63 Fr kf63

例如,如果想要知道向上键对应的 ANSI escape code ,可以:

% echo ${terminfo[kcuu1]} | cat -v
^[OA
% echo ${termcap[ku]} | cat -v
^[OA

可能会是 ^[[A ,取决于计算机。比如我的台式机就是。

这里的 ^[ 就是 \x1b ESC 。计算机所有的不可打印字符都通过该字符对应的 ASCII 码逻辑或 0x40 再取余来表示。所以 \x00-\x1f 就是 ^@-^_\x7f 就是 ^? 。 不知道的请自觉翻阅 man 7 ascii

另一个方法就是使用现成的软件啦:

$ showkey -a

Press any keys - Ctrl-D will terminate this program

^[[A     27 0033 0x1b
         91 0133 0x5b
         65 0101 0x41

检测按键

一个最简单的例子:

rime-ime() {
  self-insert() {
    LBUFFER+="$KEYS"
    zle -M '  hello'
  }

  zle -N self-insert
  zle -A rime-ime save-rime-ime
  zle -A accept-line rime-ime

  bindkey -N rime main

  zle recursive-edit -K rime

  bindkey -D rime
  zle -M ''
  integer stat=$?

  zle -A .self-insert self-insert
  zle -A save-rime-ime rime-ime
  zle -D save-rime-ime

  unfunction self-insert

  (( stat )) && zle send-break

  return $stat
}

bindkey "^^" rime-ime

按下快捷键 Control + 6, 之后无论按下什么键,都会在光标下方显示 hello 。

hello

我们需要做的,仅仅是模仿上一篇文章把 hello 替换成输入法的菜单,用输入法选中的汉字修改 $LBUFFER

集成

可以在 powerlevel10k 的提示符中显示 rime 的输入方案

其他 shell

除了 zsh, 其他软件有可能实现这种命令行输入法吗?

shell:

终端分屏器:

终端模拟器:

不管怎样,笔者选择 zsh 来验证命令行输入法可行性,考虑到此前从未有人提出过命令行输入法的概念,也算是笔者的贡献(以下省略 500 字自吹自擂)。

https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png