突然发现五个月前自己写过一篇 fcitx5-lua 的教程,刚好拿来水一篇

fcitx5 终于加上了 lua 支持,可以使用 lua 编写一些简单的插件。但是文档一如既往地非常少,查阅了大量资料并翻看源码以后总算弄懂了插件的编写步骤,记录一下以免忘记,顺便分享给同样需要的人。

fcitx5-lua 支持两种插件的加载方式,以及对应的两种风格的 API。但实际上两者可以混用,哪个方便用哪个。

GooglePinyin 风格

顾名思义,这是继承自 GooglePinyin 的插件 API 风格,可惜 GooglePinyin 现在已经没在维护了。

创建插件

~/.local/share/fcitx5/lua/imeapi/extension 下放置你的插件(单个文件)即可,非常简单。

API

GooglePinyin 的 API 位于 ime 模块中,ime 模块已经为你预先导入了,不需要手动导入。

为完整的 API 列表可以在 https://fcitx.github.io/fcitx5-lua/modules/ime.html 查看,大部分 API 一眼就能看出是干啥的,这里只列出部分常用并且从名字上看不出作用的。

register_command

用于注册命令扩展,可以在“快速输入”模式中输入特定的命令时触发指定的函数。

什么是“快速输入”模式?按下 ; fcitx5 就会进入快速输入模式,你可以在这个模式下使用自定义的特殊词组。比如颜文字、emoji、特殊字符。触发词不要求是合法拼音,如默认的快速输入就提供了 “orz” -> “_(:з」∠)_ “、”smile”-> “☺” 这样的映射,可以方便自己输入各种特殊字符。普通词库是无法做到这一点的,因为普通词库要求触发词必须是有效拼音。

Tips: 使用 Ctrl+Shift+U 可以启用 Unicode 输入功能,可以方便地输入包括 emoji 在内的各种 Unicode 字符。

基本格式:register_command (command_name, lua_function_name, description, [leading, help])

  • command_name:命令名称,即触发词
  • lua_function_name:被触发的 lua 函数
  • description: 命令的描述
  • leading: 选择结果的快捷键,默认为 “digit” 表示用数字键选择,也可以用 “alpha” 表示用字母,或者 “none” 禁用快捷键
  • help:更详细的命令帮助,不过目前似乎尚未实装

下面提供了一个例子,注册了一个 cl 命令用于进行简单的数学计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function calc(input)
-- input 不包含命令,比如输入 cl5+5,input 就为 5+5,不会包含 cl 前缀。
local func = loadstring(string.format("return %s", input))
if func == nil then
return "-- 表达式不正确 --"
end
local ret = func()
if ret == math.huge or ret ~= ret then
return "-- 计算错误 --"
end
-- 返回计算结果
return ret
end

-- 此处 leading 使用了 alpha,因为输入的表达式中会含有数字,如果使用数字作为快捷键可能会导致冲突,所以只能使用字母选择
ime.register_command("cl", "calc", "数学计算", "alpha", "进行简单的数学计算机")

register_trigger

用于注册整合扩展。整合扩展和命令扩展的区别在于,它不要求固定的命令前缀,可以使用通配符来匹配符合一定格式的命令或者候选,而且也不需要进入快速输入模式。

基本格式:register_trigger (lua_function_name, description, input_trigger_strings, candidate_trigger_strings)

  • lua_function_name:被触发的函数
  • description:描述
  • input_trigger_strings:当用户输入匹配这个的时候就触发函数,可以在字符串前后包含“*”表示匹配任意字符
  • candidate_trigger_strings:当候选中存在匹配这个的项时触发函数,同样可以在前后包含“*”

看这个例子就懂了,以下是一个为 fcitx5 添加输入“时间戳”三个字时自动在候选列表中添加当前时间戳的功能的例子。

1
2
3
4
5
6
-- 此处并不需要用户输入
function timestamp(_input)
return os.time(os.date("!*t"))
end
-- 当候选中存在“时间戳”三个字时,调用我们的函数
ime.register_trigger("timestamp", "UNIX 时间戳", { }, { "时间戳" })

register_converter

注册一个转换器扩展,用于在最终结果上屏之前对结果进行处理

比 如 以 下 函 数 可 以 为 你 的 输 入 自 动 加 入 空 格 , 就 像 我 这 个 样 子

1
2
3
4
5
6
7
8
9
ime.register_converter("konge", "使用空格分词")

function konge(str)
local tmp = string.gsub(str, utf8.charpattern, "%1 ")
if tmp ~= nil then
str = tmp
end
return str
end

fcitx 插件风格

相比 GooglePinyin 风格其实功能上差不多,一基本上都是一一对应的,除了 watchEvent 以外。

创建插件

这个相比 GooglePinyin 就复杂了点。假设我们的插件叫 testlua,那么就需要在 ~/.local/share/fcitx5/addon 文件夹下创建一个 testlua.conf 文件,填入插件的基本信息,模板如下:

1
2
3
4
5
6
7
8
9
10
11
[Addon]
Name=Test Lua
Comment=Test Lua
Category=Module
Type=Lua
OnDemand=False
Configurable=False
Library=test.lua

[Addon/Dependencies]
0=luaaddonloader

然后在 ~/.local/share/fcitx5/lua/testlua/test.lua 中放置插件源码。

API

这里只讲解 watchEvent,因为其他 API 要么一眼看得出用法要么和 GooglePinyin API 的功能一样,完整列表可以到 https://fcitx.github.io/fcitx5-lua/modules/fcitx.html 查看。

这个 API 需要使用 local fcitx = require("fcitx") 手动引入。

watchEvent

顾名思义,这个函数可以监控指定的事件,完整列表如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
local EventType = {
ContextCreated = 0x0001000 | 0x1,
ContextDestroyed = 0x0001000 | 0x2,
FocusOut = 0x0001000 | 0x3,
FocusIn = 0x0001000 | 0x4,
KeyEvent = 0x0001000 | 0x5,
SurroundingTextUpdated = 0x0001000 | 0x7,
CursorRectChanged = 0x0001000 | 0x9,
InputMethodActivated = 0x0001000 | 0xA,
InputMethodDeactivated = 0x0001000 | 0xB,
CommitString = 0x0002000 | 0x2,
UpdatePreedit = 0x0002000 | 0x4,
}

大部分事件都是我加上去的,但是很多事件我也不知道它们的含义具体是什么

记住 CommitString 是内容上屏、KeyEvent 是按键事件、UpdatePreedit 是输入框内容更新就差不多了。

以下是使用 Ctrl + Shift + Space 开关空格模式的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
local fcitx = require("fcitx")

-- 注册我们的事件监听器以及转换器
fcitx.watchEvent(fcitx.EventType.KeyEvent, "key_event")
fcitx.addConverter("konge")

-- 用于判断转换器是否需要开启
local enable = false

function key_event(sym, state, release)
-- Ctrl + Shift + Space
if state == fcitx.KeyState.Ctrl_Shift and sym == 32 and not release then
enable = not enable
if enable then
io.popen("notify-send '空格模式开启'")
else
io.popen("notify-send '空格模式关闭'")
end
print(string.format("change state of konge: %s", enable))
end
return false
end

function konge(str)
print(string.format("call konge: %s", enable))
if enable then
local tmp = string.gsub(str, utf8.charpattern, "%1 ")
if tmp ~= nil then
str = tmp
end
end
return str
end