Files
rime_wanxiang/lua/super_tips.lua

280 lines
8.5 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 万象家族lua,超级提示,表情\化学式\方程式\简码等等直接上屏,不占用候选位置
-- 采用leveldb数据库,支持大数据遍历,支持多种类型混合,多种拼音编码混合,维护简单
-- 支持候选匹配和编码匹配两种,候选支持方向键高亮遍历
-- https://github.com/amzxyz/rime_wanxiang
-- - lua_processor@*super_tips
-- key_binder/tips_key: "slash" # 上屏按键配置
-- tips/disabled_types: [] # 禁用的 tips 类型
local wanxiang = require("wanxiang")
local bit = require("lib/bit")
local userdb = require("lib/userdb")
local tips_db = userdb.LevelDb("lua/tips")
-- 获取文件内容哈希值,使用 FNV-1a 哈希算法
local function calculate_file_hash(filepath)
local file = io.open(filepath, "rb")
if not file then return nil end
-- FNV-1a 哈希参数32位
local FNV_OFFSET_BASIS = 0x811C9DC5
local FNV_PRIME = 0x01000193
local hash = FNV_OFFSET_BASIS
while true do
local chunk = file:read(4096)
if not chunk then break end
for i = 1, #chunk do
local byte = string.byte(chunk, i)
hash = bit.bxor(hash, byte)
hash = (hash * FNV_PRIME) % 0x100000000
hash = bit.band(hash, 0xFFFFFFFF)
end
end
file:close()
return string.format("%08x", hash)
end
local tips = {}
---@type "pending" | "initialing" | "done"
tips.status = "pending"
---@type table<string, boolean>
tips.disabled_types = {}
tips.preset_file_path = wanxiang.get_filename_with_fallback("lua/data/tips_show.txt")
tips.user_override_path = rime_api.get_user_data_dir() .. "/lua/data/tips_user.txt"
local META_KEY = {
version = "wanxiang_version",
user_file_hash = "user_tips_file_hash",
disabled_types = "disabled_types",
}
---@param tip string
function tips.is_disabled(tip)
local type = tip:match("^(..-):")
or tip:match("^(..-)")
if not type then return false end
return tips.disabled_types[type] == true
end
function tips.init_db_from_file(path)
local file = io.open(path, "r")
if not file then return end
for line in file:lines() do
local value, key = line:match("([^\t]+)\t([^\t]+)")
if key and value
and not tips.is_disabled(value)
then
tips_db:update(key, value)
end
end
file:close()
end
function tips.ensure_dir_exist(dir)
-- 获取系统路径分隔符
local sep = package.config:sub(1, 1)
dir = dir:gsub([["]], [[\"]]) -- 处理双引号
if sep == "/" then
local cmd = 'mkdir -p "' .. dir .. '" 2>/dev/null'
os.execute(cmd)
end
end
---@param config Config
function tips.init(config)
if tips.status ~= "pending" then return end
local dist = rime_api.get_distribution_code_name() or ""
local user_lua_dir = rime_api.get_user_data_dir() .. "/lua"
if dist ~= "hamster" and dist ~= "hamster3" and dist ~= "Weasel" then
tips.ensure_dir_exist(user_lua_dir)
tips.ensure_dir_exist(user_lua_dir .. "/tips")
end
-- 读取配置
local disabled_types_list = config:get_list("tips/disabled_types")
if disabled_types_list then
for i = 1, disabled_types_list.size do
local item = disabled_types_list:get_value_at(i - 1)
if item and #item.value > 0 then
tips.disabled_types[item.value] = true
end
end
end
-- 检查是否需要重建数据库
tips_db:open()
local needs_rebuild = false
-- 检查 1: 万象版本号
if tips_db:meta_fetch(META_KEY.version) ~= wanxiang.version then
needs_rebuild = true
end
-- 检查 2: 用户文件哈希 (仅在版本号相同时检查)
local user_file_hash = calculate_file_hash(tips.user_override_path) or ""
if not needs_rebuild
and (tips_db:meta_fetch(META_KEY.user_file_hash) or "") ~= user_file_hash
then
needs_rebuild = true
end
-- 检查 3: 禁用类型 (仅在前两者都相同时检查)
local disabled_keys = {}
for k, _ in pairs(tips.disabled_types) do
table.insert(disabled_keys, k)
end
table.sort(disabled_keys) -- 排序以确保顺序一致
local disabled_types_str = table.concat(disabled_keys, ",")
if not needs_rebuild
and (tips_db:meta_fetch(META_KEY.disabled_types) or "") ~= disabled_types_str
then
needs_rebuild = true
end
-- 如果需要,则执行重建
if needs_rebuild then
tips_db:empty()
tips.init_db_from_file(tips.preset_file_path)
tips.init_db_from_file(tips.user_override_path)
-- 重建成功后,再更新所有元数据,确保操作的原子性
tips_db:meta_update(META_KEY.version, wanxiang.version)
tips_db:meta_update(META_KEY.user_file_hash, user_file_hash)
tips_db:meta_update(META_KEY.disabled_types, disabled_types_str)
end
-- 关闭并以只读模式重新打开
tips_db:close()
tips_db:open_read_only()
end
---从数据库中查询 tips
---@param keys string | string[] 接受一个字符串或一个字符串数组作为键,使用数组时会挨个查询,直到获得有效值
---@return string | nil
function tips.get_tip(keys)
-- 输入归一化:如果输入是 string将其包装成单元素的 table
if type(keys) == 'string' then
keys = { keys }
end
for _, key in ipairs(keys) do
if key and key ~= "" then
local tip = tips_db:fetch(key)
if tip and #tip > 0 then
return tip
end
end
end
return nil
end
---@class Env
---@field current_tip string | nil 当前 tips 值
---@field last_prompt string 最后一次设置的 prompt 值
---@field tips_update_connection Connection
---tips prompt 处理
---@param context Context
---@param env Env
local function update_tips_prompt(context, env)
env.current_tip = nil
local is_tips_enabled = context:get_option("super_tips")
if not is_tips_enabled then return end
local segment = context.composition:back()
if not segment then return end
local cand = context:get_selected_candidate() or {}
if segment.selected_index == 0 then
env.current_tip = tips.get_tip({ context.input, cand.text })
else
env.current_tip = tips.get_tip(cand.text)
end
if env.current_tip ~= nil and env.current_tip ~= "" then
-- 有 tips 则直接设置 prompt
segment.prompt = "" .. env.current_tip .. ""
env.last_prompt = segment.prompt
elseif segment.prompt ~= "" and env.last_prompt == segment.prompt then
-- 没有 tips且当前 prompt 不为空,且是由 super_tips 设置的,则重置
segment.prompt = ""
env.last_prompt = segment.prompt
end
end
local P = {}
-- Processor按键触发上屏 (S)
---@param env Env
function P.init(env)
local config = env.engine.schema.config
tips.init(config)
P.tips_key = config:get_string("key_binder/tips_key")
-- 注册 tips 查找监听器
local context = env.engine.context
env.tips_update_connection = context.update_notifier:connect(
function(context)
update_tips_prompt(context, env)
end
)
end
function P.fini(env)
-- 清理连接
if env.tips_update_connection then
env.tips_update_connection:disconnect()
env.tips_update_connection = nil
end
end
---@param key KeyEvent
---@param env Env
---@return ProcessResult
function P.func(key, env)
local context = env.engine.context
local is_tips_enabled = context:get_option("super_tips")
if not is_tips_enabled then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 以下处理 tips 上屏逻辑
if not P.tips_key -- 未设置上屏键
or P.tips_key ~= key:repr() -- 或者当前按下的不是上屏键
or wanxiang.is_function_mode_active(context) -- 或者是功能模式不用上屏
or not env.current_tip or env.current_tip == "" -- 或匹配的 tips 为空/空字符串
then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
---@type string 从 tips 内容中获取上屏文本
local commit_txt = env.current_tip:match("%s*(.*)%s*") -- 优先匹配常规的全角冒号
or env.current_tip:match(":%s*(.*)%s*") -- 没有匹配则回落到半角冒号
if commit_txt and #commit_txt > 0 then
env.engine:commit_text(commit_txt)
context:clear()
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return P