-- 万象家族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 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