30 Commits

Author SHA1 Message Date
82779cc299 refactor: backup 2023-09-08 22:44:23 +08:00
eb68b8bb95 refactor: rewrite setup and conf module 2023-07-24 22:28:09 +08:00
073e8667b2 refactor: redirect Trans.conf 2023-07-13 10:15:38 +08:00
c2e56f7769 chore: backup progress, begin to rewrite version 3 2023-07-10 21:21:04 +08:00
5845a40d94 chore: add MIT License 2023-05-14 10:53:39 +08:00
f17171d28b fix: query strategy 2023-05-13 20:04:19 +08:00
ab212fefe1 chore: fix check health termux binary dependencies error 2023-05-13 20:03:57 +08:00
586cd6ff08 fix: system checker mistake 2023-05-13 19:36:40 +08:00
b5e626a88c Merge pull request #34 from Xiao-M0/v2
feat: add win wsl, remove python and nodejs dependencies
2023-05-13 19:07:36 +08:00
e993dc12be fix: get_select get_lines 2023-05-13 18:16:28 +08:00
871cc1e48a refactor: wsl 2023-05-13 18:13:32 +08:00
6e02f3b1ba fix: update exchange_map 2023-05-12 20:39:46 +08:00
377c84c146 fix: double quote error 2023-05-12 20:27:39 +08:00
ba08913b1e feat: get_lines 2023-04-25 13:48:52 +08:00
854bce7b5a chore: remove useless code 2023-04-24 20:58:56 +08:00
a48caf51e1 remove: node 2023-04-24 19:57:33 +08:00
24a838646d feat: add win wsl 2023-04-24 18:48:36 +08:00
156e03306a Merge pull request #33 from Xiao-M0/v2
fix: win separator
2023-04-24 13:04:43 +08:00
0a1b6cf742 feat: say.py 2023-04-24 13:02:48 +08:00
758b9c0b33 fix: win separator 2023-04-24 12:42:59 +08:00
3faab735fb chore: make hover window focusable and update docs 2023-04-16 16:38:39 +08:00
335c5079cc docs: update README 2023-04-08 21:46:29 +08:00
38b8e20729 chore: close window and buffer more safely 2023-04-07 19:05:24 +08:00
fcde85544a feat: use a non-invasive keymap define method 2023-04-06 13:38:19 +08:00
2ae2effecc chore: more spinner style support 2023-04-04 16:19:06 +08:00
da6e717f2c feat: use a non-invasive keymap define method 2023-04-02 18:02:21 +08:00
576f1eb66a feat: add termux tts support 2023-04-02 13:34:54 +08:00
9357574b5c chore: sync 2023-04-01 09:59:09 +08:00
987e1a3341 test: add simple window unit test 2023-03-31 12:51:11 +08:00
b27bc4ed26 fix: add util unit test and fix util.center mistake 2023-03-31 01:04:36 +08:00
36 changed files with 1560 additions and 824 deletions

21
LICENCE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Zoran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

250
README.md
View File

@ -2,51 +2,48 @@
<!--toc:start-->
- [Trans.nvim](#transnvim) - [注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我](#注意-当前分支目前没有发布-readmemd-的描述并不准确-遇到问题请切换到-main分支或者联系我)
- [Trans.nvim](#transnvim)
- [特点](#特点)
- [屏幕截图](#屏幕截图)
- [演示](#演示)
- [离线查询](#离线查询)
- [\*在线查询\*\* (有道)](#在线查询-有道)
- [在线查询演示 (有道)](#在线查询演示-有道)
- [主题](#主题)
- [安装](#安装)
- [配置](#配置)
- [快捷键绑定](#快捷键绑定)
- [高亮组](#高亮组)
- [声明](#声明)
- [感谢](#感谢)
- [贡献](#贡献)
- [从 v1 (main)分支迁移](#从-v1-main分支迁移)
- [待办 (画大饼)](#待办-画大饼)
- [项目情况](#项目情况)
<!--toc:end-->
### 注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我
> **插件默认词库的路径为插件目录**
例如: `lazy` 用户应该在 `$HOME/.local/share/nvim/lazy/Trans.nvim`
## 特点
- 使用纯 lua 编写, 速度极快
> `Lazy.nvim`的记录: <font color="#0099FF">`➜  Trans.nvim 0.82ms`</font>
- **可以定义快捷键读英文单词**
> 见 wiki
- `使用纯 lua 编写`
- 大部分功能可以自定义:
- 高亮
- 悬浮大小
- 排版顺序
- 弹窗大小
- `舒服窗口动画`
- etc (更多可以查看[配置](#配置))
- **完全离线** 的单词翻译体验 (可能后面会支持在线翻译)
- 🔍 高亮
- 👀 悬浮大小
- 📜 排版顺序
- 💬 弹窗大小
- 🎉 舒服窗口动画
- 更多可以查看[配置](#配置)
- `离线``在线`翻译的支持
- 支持显示:
- 柯林斯星级
- 牛津 3000 词汇
- 中文翻译
- 英文翻译 (不是英译中, 而是用英文解释)
- 词根
- 🌟 柯林斯星级
- 📚 牛津 3000 词汇
- 🇨🇳 中文翻译
- 🇺🇸 英文翻译 (不是英译中, 而是用英文解释)
- 🌱 词根
- etc
- 舒服的排版和`动画`
- 支持`平滑动画`
- 支持 `normal``visual`模式
> <font color='#FF9900'>不支持 visual-block mode</font>
- 本地词库单词量: `430w`
@ -61,13 +58,13 @@
https://user-images.githubusercontent.com/107862700/226175984-1a95bea7-8d66-450e-87e1-ba9c91c37ab8.mp4
### 在线查询 (有道)
### 在线查询演示 (有道)
https://user-images.githubusercontent.com/107862700/226176106-c2962dd3-d66c-499c-b44a-1f471b79fe38.mp4
**使用在线查询需要配置相应的 app_id 和 app_passwd**
在线查询配置见: [wiki](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE#%E5%9C%A8%E7%BA%BF%E6%9F%A5%E8%AF%A2%E9%85%8D%E7%BD%AE)
配置说明见: [wiki](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE#%E5%9C%A8%E7%BA%BF%E6%9F%A5%E8%AF%A2%E9%85%8D%E7%BD%AE)
### 主题
@ -139,6 +136,7 @@ use {
```lua
{
"JuanZoran/Trans.nvim",
build = function () require'Trans'.install() end,
keys = {
-- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' },
@ -157,40 +155,32 @@ use {
<font color="#FF9900">**注意事项**: </font>
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: 1.2G
如果下载出现问题, 正常是会自动下载
- 安装后如果不能正常运行, 请尝试检查一下问题:
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: **1.2G**
- 本机是否已经安装了 `sqlite3`
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
- 安装后如果不能正常运行, 清尝试运行 `checkhealth Trans`
> **尝试运行 `checkhealth Trans`**
- **`auto_play`** 的使用:
- **`auto_play`** 使用步骤:
- `Linux` 需要安装`festival`
> linux 只需要安装`festival`
> sudo apt-get install festival festvox-kallpc16k
> **_如果你想要设置音色发音可以访问:_** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
> 可以选择英音、美音、男声、女声
> `sudo apt-get install festival festvox-kallpc16k`
> 其他操作系统
**如果你想要设置音色,发音可以访问:** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
可以选择英音、美音、男声、女声
- 需要确保安装`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `Termux` 需要安装`termux-api`
- `Mac` 使用系统的`say`命令
- `Windows` 使用 `nodejs`的 say 模块, 如果你有更好的方案欢迎提供 PR
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9+`版本有效
@ -244,12 +234,16 @@ use {
## 配置
详细见**wiki**: [配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
详细见**wiki**: [基本配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
<details>
<summary>默认配置</summary>
```lua
require 'Trans'.setup {
default_conf = {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
debug = true,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula
strategy = {
@ -294,11 +288,13 @@ require 'Trans'.setup {
split_width = 60,
padding = 10, -- padding for hover window width
keymaps = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- INFO : No default keymaps anymore, please set it yourself
-- pageup = '<C-u>',
-- pagedown = '<C-d>',
-- pin = '<leader>[',
-- close = '<leader>]',
-- toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
@ -349,6 +345,8 @@ require 'Trans'.setup {
}
```
</details>
## 快捷键绑定
**示例:**
@ -361,66 +359,90 @@ vim.keymap.set({'n', 'x'}, 'mm', '<Cmd>Translate<CR>')
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词
```
## 高亮组
> 默认定义
**窗口快捷键**
```lua
{
TransWord = {
fg = '#7ee787',
bold = true,
require 'Trans'.setup {
frontend = {
hover = {
keymaps = {
-- pageup = 'whatever you want',
-- pagedown = 'whatever you want',
-- pin = 'whatever you want',
-- close = 'whatever you want',
-- toggle_entry = 'whatever you want',
},
},
TransPhonetic = {
link = 'Linenr'
},
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
},
TransTitleRound = {
fg = '#75beff',
},
TransTag = {
-- fg = '#e5c07b',
link = '@tag'
},
TransExchange = {
link = 'TransTag',
},
TransPos = {
link = 'TransTag',
},
TransTranslation = {
link = 'TransWord',
},
TransDefinition = {
link = 'Moremsg',
},
TransWin = {
link = 'Normal',
},
TransBorder = {
fg = '#89B4FA',
},
TransCollins = {
fg = '#faf743',
bold = true,
},
TransFailed = {
fg = '#7aa89f',
},
TransWaitting = {
link = 'MoreMsg'
},
TransWeb = {
-- TODO :
link = 'MoreMsg',
}
},
}
}
```
> 当窗口没有打开的时候, key 会被使用`vim.api.nvim_feedkey`来执行
## 高亮组
所有主题可见 `lua/Trans/style/theme.lua`
<details>
<summary>默认主题</summary>
```lua
TransWord = {
fg = '#7ee787',
bold = true,
}
TransPhonetic = {
link = 'Linenr'
}
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
}
TransTitleRound = {
fg = '#75beff',
}
TransTag = {
-- fg = '#e5c07b',
link = '@tag'
}
TransExchange = {
link = 'TransTag',
}
TransPos = {
link = 'TransTag',
}
TransTranslation = {
link = 'TransWord',
}
TransDefinition = {
link = 'Moremsg',
}
TransWin = {
link = 'Normal',
}
TransBorder = {
fg = '#89B4FA',
}
TransCollins = {
fg = '#faf743',
bold = true,
}
TransFailed = {
fg = '#7aa89f',
}
TransWaitting = {
link = 'MoreMsg'
}
TransWeb = {
link = 'MoreMsg',
}
```
</details>
## 声明
- 本插件词典基于[ECDICT](https://github.com/skywind3000/ECDICT)
@ -436,6 +458,10 @@ vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或
> 更新比较频繁, 文档先鸽了
> 如果你想要参加这个项目, 可以提 issue, 我会把文档补齐
## 从 v1 (main)分支迁移
见[wiki](https://github.com/JuanZoran/Trans.nvim/wiki/%E4%BB%8E(v1)main%E5%88%86%E6%94%AF%E8%BF%81%E7%A7%BB)
## 待办 (画大饼)
- [x] 快捷键定义

View File

@ -15,4 +15,9 @@
- [ ] Unlimit width for sentence
已知问题:
1. 缓存了的单词, 无法使用toggle_entry 进入页面
2. 加载配置需要输入所有表的key
- default_strategy can't deal with table correctly

View File

@ -1,18 +1,13 @@
---@class Baidu: TransOnlineBackend
---@field uri string api uri
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
---@class Baidu: TransBackendOnline
---@field conf { app_id: string, app_passwd: string }
local M = {
uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate',
salt = tostring(math.random(bit.lshift(1, 15))),
name = 'baidu',
name_zh = '百度',
method = 'get',
name = 'baidu',
display_text = '百度',
uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate',
salt = tostring(math.random(bit.lshift(1, 15))),
method = 'get',
}
local Trans = require 'Trans'
---@class BaiduQuery
@ -27,14 +22,17 @@ local Trans = require 'Trans'
---@param data TransData
---@return BaiduQuery
function M.get_query(data)
local tmp = M.app_id .. data.str .. M.salt .. M.app_passwd
local m_conf = M.conf
assert(m_conf, 'Load Baidu config failed')
local tmp = m_conf.app_id .. data.str .. M.salt .. m_conf.app_passwd
local sign = Trans.util.md5.sumhexa(tmp)
return {
q = data.str,
from = data.from,
to = data.to,
appid = M.app_id,
appid = m_conf.app_id,
salt = M.salt,
sign = sign,
}
@ -57,14 +55,31 @@ function M.formatter(body, data)
}
end
---@class TransBackend
---@field baidu Baidu
return M
-- -- NOTE :free tts:
-- -- https://zj.v.api.aa1.cn/api/baidu-01/?msg=我爱你&choose=0&su=100&yd=5
-- -- 选择转音频的人物女生1 输入0 女生2输入5男生1 输入1男生2 输入2男生3 输入3
-- NOTE :free tts:
-- https://zj.v.api.aa1.cn/api/baidu-01/?msg=我爱你&choose=0&su=100&yd=5
-- 选择转音频的人物女生1 输入0 女生2输入5男生1 输入1男生2 输入2男生3 输入3
-- {
-- body = '{"from":"en","to":"zh","trans_result":[{"src":"require","dst":"\\u8981\\u6c42"}]}',
-- exit = 0,

View File

@ -1,4 +1,191 @@
local Trans = require 'Trans'
local Trans = require 'Trans'
if false then
-- local dict = db:open(Trans.conf.dir .. Trans.separator .. 'ultimate.db')
local db = require 'sqlite.db'
local conf = Trans.loader.conf
local dict = db:open(conf.dict)
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
if db:isopen() then db:close() end
end,
})
---@class TransOfflineBackend
local M = {
name = 'offline',
name_zh = '本地',
no_wait = true,
}
---@param data any
function M.query(data)
if data.is_word == false or data.from == 'zh' then
return
end
local res = dict:select(conf.db_name, {
where = { word = data.str },
keys = M.query_field,
limit = 1,
})[1]
data.result.offline = res and M.formatter(res) or false
end
-- this is a awesome plugin
M.query_field = {
'word',
'phonetic',
'definition',
'translation',
'pos',
'collins',
'oxford',
'tag',
'exchange',
}
local function exist(str)
return str and str ~= ''
end
---@type (fun(res):any)[]
local formatter = {
title = function(res)
local title = {
word = res.word,
oxford = res.oxford,
collins = res.collins,
phonetic = res.phonetic,
}
res.word = nil
res.oxford = nil
res.collins = nil
res.phonetic = nil
return title
end,
tag = function(res)
if not exist(res.tag) then
return
end
local tag_map = {
zk = '中考',
gk = '高考',
ky = '考研',
gre = 'gre ',
cet4 = '四级',
cet6 = '六级',
ielts = '雅思',
toefl = '托福',
}
local tag = {}
for i, _tag in ipairs(vim.split(res.tag, ' ', { plain = true })) do
tag[i] = tag_map[_tag]
end
return tag
end,
exchange = function(res)
if not exist(res.exchange) then
return
end
local exchange_map = {
['0'] = '原型 ',
['1'] = '类别 ',
['p'] = '过去式 ',
['r'] = '比较级 ',
['t'] = '最高级 ',
['b'] = '比较级 ',
['z'] = '最高级 ',
['s'] = '复数 ',
['d'] = '过去分词 ',
['i'] = '现在分词 ',
['3'] = '第三人称单数',
['f'] = '第三人称单数',
}
local exchange = {}
for _, _exchange in ipairs(vim.split(res.exchange, '/', { plain = true })) do
exchange[exchange_map[_exchange:sub(1, 1)]] = _exchange:sub(3)
end
return exchange
end,
pos = function(res)
if not exist(res.pos) then
return
end
local pos_map = {
a = '代词pron ',
c = '连接词conj ',
i = '介词prep ',
j = '形容词adj ',
m = '数词num ',
n = '名词n ',
p = '代词pron ',
r = '副词adv ',
u = '感叹词int ',
v = '动词v ',
x = '否定标记not ',
t = '不定式标记infm ',
d = '限定词determiner ',
}
local pos = {}
for _, _pos in ipairs(vim.split(res.pos, '/', { plain = true })) do
pos[pos_map[_pos:sub(1, 1)]] = ('%2s%%'):format(_pos:sub(3))
end
return pos
end,
translation = function(res)
if not exist(res.translation) then
return
end
local translation = {}
for i, _translation in ipairs(vim.split(res.translation, '\n', { plain = true })) do
translation[i] = _translation
end
return translation
end,
definition = function(res)
if not exist(res.definition) then
return
end
local definition = {}
for i, _definition in ipairs(vim.split(res.definition, '\n', { plain = true })) do
-- -- TODO :判断是否需要分割空格
definition[i] = _definition:gsub('^%s+', '', 1)
end
return definition
end,
}
---Formater for TransResul
---@param res TransResult
---@return TransResult
function M.formatter(res)
for field, func in pairs(formatter) do
res[field] = func(res)
end
return res
end
---@class TransBackends
---@field offline TransOfflineBackend
return {
name = 'offline',
name_zh = '本地',
no_wait = true,
}
end
local db = require 'sqlite.db'
local path = Trans.conf.dir .. Trans.separator .. 'ultimate.db'
@ -18,8 +205,6 @@ local M = {
}
---@param data any
---@return any
---@overload fun(TransData): TransResult
function M.query(data)
if data.is_word == false or data.from == 'zh' then
return
@ -99,6 +284,8 @@ local formatter = {
['p'] = '过去式 ',
['r'] = '比较级 ',
['t'] = '最高级 ',
['b'] = '比较级 ',
['z'] = '最高级 ',
['s'] = '复数 ',
['d'] = '过去分词 ',
['i'] = '现在分词 ',
@ -176,6 +363,4 @@ function M.formatter(res)
return res
end
---@class TransBackends
---@field offline TransOfflineBackend
return M

View File

@ -1,15 +1,14 @@
---@class Youdao: TransOnlineBackend
---@class Youdao: TransBackendOnline
---@field uri string api uri
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
---@field conf { app_id: string, app_passwd: string }
local M = {
uri = 'https://openapi.youdao.com/api',
salt = tostring(math.random(bit.lshift(1, 15))),
name = 'youdao',
name_zh = '有道',
method = 'get',
uri = 'https://openapi.youdao.com/api',
name = 'youdao',
display_text = '有道',
method = 'get',
salt = tostring(math.random(bit.lshift(1, 15))),
}
---@class YoudaoQuery
@ -26,7 +25,7 @@ local M = {
---@return YoudaoQuery
function M.get_query(data)
local str = data.str
local app_id = M.app_id
local m_conf = M.conf
local salt = M.salt
local curtime = tostring(os.time())
@ -38,7 +37,7 @@ function M.get_query(data)
-- sign=sha256(应用ID+input+salt+curtime+应用密钥) 一二三四五六七八九十
local hash = app_id .. input .. salt .. curtime .. M.app_passwd
local hash = m_conf.app_id .. input .. salt .. curtime .. m_conf.app_passwd
local sign = vim.fn.sha256(hash)
@ -47,7 +46,7 @@ function M.get_query(data)
to = data.from == 'zh' and 'en' or 'zh-CHS',
from = 'auto',
signType = 'v3',
appKey = app_id,
appKey = m_conf.app_id,
salt = M.salt,
curtime = curtime,
sign = sign,
@ -160,6 +159,26 @@ end
---@class TransBackend
---@field youdao Youdao
return M
-- INFO :Query Result Example
-- {

View File

@ -0,0 +1,8 @@
local Trans = require'Trans'
---@class Trans
local M = {}
return M

View File

@ -1,27 +1,68 @@
local Trans = require 'Trans'
---@class TransBackend
---@field no_wait? boolean whether need to wait for the result
---@field all_name string[] @all backend name
---@field name string @backend name
---@field name_zh string @backend name in Chinese
---@field name string
---@field display_text string?
---@field conf table? @User specific config
---@class TransOnlineBackend: TransBackend
---@class TransBackendOnline: TransBackend
---@field uri string @request uri
---@field method 'get' | 'post' @request method
---@field formatter fun(body: table, data: TransData): TransResult|false|nil @formatter
---@field get_query fun(data: TransData): table<string, string> @get query
---@field header? table<string, string> | fun(data: TransData): table<string, string> @request header
---@field debug? fun(body: table?) @debug
---@field formatter fun(body: table, data: TransData): TransResult|false|nil transform response body to TransResult
---@field get_query fun(data: TransData): table<string, any> @get query table
---@field error_message? fun(errorCode) @get error message
-- -@field header table<string, string>|fun(data: TransData): table<string, string> @request header
local conf = Trans.conf
--- INFO :Parse online engine keys config file
local path = conf.dir .. '/Trans.json'
---@class TransBackendOffline: TransBackend
---@field query fun(data: TransData)
---@class TransBackendCore
local M = {
---@type table<string, TransBackend> backendname -> backend source
sources = {},
}
local m_util = {}
-- INFO :Template method for online query
---@param data TransData @data
---@param backend TransBackendOnline @backend
function M.do_query(data, backend)
local name = backend.name
local formatter = backend.formatter
-- local header = type(backend.header) == 'function' and backend.header(data) or backend.header
local function handle(output)
local status, body = pcall(vim.json.decode, output.body)
if not status or not body then
data.result[name] = false
return
end
data.result[name] = formatter(body, data)
end
Trans.curl[backend.method](backend.uri, {
query = backend.get_query(data),
callback = handle,
--- FIXME :
header = header,
})
end
-- TODO :Implement all of utility functions
M.util = m_util
-- INFO :Parse configuration file
local path = Trans.conf.dir .. '/Trans.json'
local file = io.open(path, 'r')
local user_conf = {}
if file then
local content = file:read '*a'
@ -29,33 +70,16 @@ if file then
file:close()
end
local all_name = {}
for _, config in ipairs(user_conf) do
if not config.disable then
all_name[#all_name + 1] = config.name
user_conf[config.name] = config
end
end
---@class TransBackends
---@field all_name string[] all backend names
-- WARNING : [Breaking change] 'Trans.json' should use json object instead of array
---@class Trans
---@field backend TransBackends
return setmetatable({
all_name = all_name,
}, {
---@field backend TransBackendCore
return setmetatable(M, {
__index = function(self, name)
---@type TransBackend
local backend = require('Trans.backend.' .. name)
backend.conf = user_conf[name]
for key, value in pairs(user_conf[name] or {}) do
backend[key] = value
end
self[name] = backend
self.sources[name] = backend
return backend
end,
})

View File

@ -2,7 +2,7 @@ local api, fn = vim.api, vim.fn
---@class TransBuffer
---@field bufnr integer buffer handle
---@field [number] string|TransNode|TransNode[] buffer[line] content
---@field [integer] string|TransNode|TransNode[] buffer[line] content
local buffer = {}
-- INFO : corountine can't invoke C function
@ -37,7 +37,7 @@ end
---Destory buffer
function buffer:destroy()
api.nvim_buf_delete(self.bufnr, { force = true })
pcall(api.nvim_buf_delete, self.bufnr, { force = true })
end
---Set buffer load keymap
@ -91,11 +91,7 @@ end
---@return integer
function buffer:line_count()
local line_count = api.nvim_buf_line_count(self.bufnr)
if line_count == 1 and self[1] == '' then
return 0
end
return line_count
return line_count == 1 and self[1] == '' and 0 or line_count
end
---Set line content
@ -151,11 +147,11 @@ buffer.__index = function(self, key)
end
end
buffer.__newindex = function(self, key, nodes)
buffer.__newindex = function(self, key, values)
if type(key) == 'number' then
self:setline(nodes, key)
self:setline(values, key)
else
rawset(self, key, nodes)
rawset(self, key, values)
end
end

View File

@ -1,137 +0,0 @@
---@class Trans
---@field conf TransConf
---@class TransConf
return {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
debug = true,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula
strategy = {
---@type { frontend:string, backend:string | string[] } fallback strategy for mode
default = {
frontend = 'hover',
backend = '*',
},
},
---@type table frontend options
frontend = {
---@class TransFrontendOpts
---@field keymaps table<string, string>
default = {
query = 'fallback',
border = 'rounded',
title = vim.fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil, -- need nvim-0.9+
auto_play = true,
---@type {open: string | boolean, close: string | boolean, interval: integer} Hover Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000,
},
---@class TransHoverOpts : TransFrontendOpts
hover = {
---@type integer Max Width of Hover Window
width = 37,
---@type integer Max Height of Hover Window
height = 27,
---@type string -- see: /lua/Trans/style/spinner
spinner = 'dots',
---@type string
fallback_message = '{{notfound}} 翻译超时或没有找到相关的翻译',
auto_resize = true,
split_width = 60,
padding = 10, -- padding for hover window width
keymaps = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
---@type table<string, string[]> order to display translate result
order = {
default = {
'str',
'translation',
'definition',
},
offline = {
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
},
},
icon = {
-- or use emoji
list = '', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟠| 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟦
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = '', --❔ | ❓ | ❗ | ❕|
yes = '', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = '',
pos = '',
exchange = '',
definition = '󰗊',
translation = '󰊿',
},
},
},
}
-- TODO :
-- float = {
-- width = 0.8,
-- height = 0.8,
-- border = 'rounded',
-- keymap = {
-- quit = 'q',
-- },
-- animation = {
-- open = 'fold',
-- close = 'fold',
-- interval = 10,
-- },
-- tag = {
-- wait = '#519aba',
-- fail = '#e46876',
-- success = '#10b981',
-- },
-- },
-- local title = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}

View File

@ -1,53 +1,39 @@
local Trans = require 'Trans'
---@class TransData
---@class TransData: TransDataOption
---@field mode string @The mode of the str
---@field from string @Source language type
---@field to string @Target language type
---@field is_word boolean @Is the str a word
---@field str string @The original string
---@field mode string @The mode of the str
---@field result table<string, TransResult|nil|false> @The result of the translation
---@field frontend TransFrontend
---@field is_word? boolean @Is the str a word
---@field trace table<string, string> debug message
---@field backends table<string, TransBackend>
---@field backends TransBackend[]
local M = {}
M.__index = M
---TransData constructor
---@param opts table
---@param opts TransDataOption
---@return TransData
function M.new(opts)
local mode = opts.mode
local str = opts.str
---@cast opts TransData
local mode = opts.mode
opts.result = {}
opts.trace = {}
local strategy = Trans.conf.strategy[mode]
local data = setmetatable({
str = str,
mode = mode,
result = {},
trace = {},
}, M)
data.frontend = Trans.frontend[strategy.frontend].new()
data.backends = {}
for i, name in ipairs(strategy.backend) do
data.backends[i] = Trans.backend[name]
end
---@cast opts TransData
setmetatable(opts, M)
if Trans.util.is_English(str) then
data.from = 'en'
data.to = 'zh'
else
data.from = 'zh'
data.to = 'en'
end
data.is_word = Trans.util.is_word(str)
-- NOTE : whether should we use the default strategy
opts.frontend = Trans.frontend[strategy.frontend].new()
opts.backends = {}
return data
return opts
end
---@class TransResult
@ -67,12 +53,8 @@ end
---@return string? backend.name
function M:get_available_result()
local result = self.result
if result['offline'] then return result['offline'], 'offline' end
for _, backend in ipairs(self.backends) do
if result[backend.name] then
---@diagnostic disable-next-line: return-type-mismatch
return result[backend.name], backend.name
end
end

8
lua/Trans/core/debug.lua Normal file
View File

@ -0,0 +1,8 @@
---@class Trans
---@field debug fun(message: string, level: number?)
return function (message, level)
level = level or vim.log.levels.INFO
-- TODO : custom messaage filter
vim.notify(message, level)
end

View File

@ -1,32 +1,65 @@
local Trans = require 'Trans'
local conf = Trans.conf
local frontend_opts = conf.frontend
local Trans = require 'Trans'
---@class TransFrontendOpts
local default_frontend = {
auto_play = true,
query = 'fallback',
border = 'rounded',
title = vim.fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil, -- need nvim-0.9+
---@type {open: string | boolean, close: string | boolean, interval: integer} Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000, -- only for online backend query
}
if Trans.conf.frontend.default then
default_frontend = vim.tbl_extend('force', default_frontend, Trans.conf.frontend.default)
end
local function empty_method(method)
return function() error('Method [' .. method .. '] not implemented') end
end
---@class TransFrontend
---@field opts TransFrontendOpts
---@field opts? TransFrontendOpts options which user can set
---@field get_active_instance fun():TransFrontend?
---@field process fun(self: TransFrontend, data: TransData)
---@field wait fun(self: TransFrontend): fun(backend: TransBackend): boolean Update wait status
---@field execute fun(action: string) @Execute action for frontend instance
---@field fallback fun() @Fallback method when no result
---@field setup? fun() @Setup method for frontend [optional] **NOTE: This method will be called when frontend is first used**
local M = {
---@type fun() @Fallback method when no result
fallback = empty_method 'fallback',
---@type fun(self: TransFrontend, data: TransData) @render backend result
process = empty_method 'process',
}
---@class Trans
---@field frontend TransFrontend
return setmetatable({}, {
return setmetatable(M, {
__index = function(self, name)
local opts = vim.tbl_extend('keep', frontend_opts[name] or {}, frontend_opts.default)
---@type TransFrontend
local frontend = require('Trans.frontend.' .. name)
frontend.opts = opts
self[name] = frontend
frontend.opts =
vim.tbl_extend('force', frontend.opts or {}, default_frontend, Trans.conf.frontend[name])
if frontend.setup then
frontend.setup()
end
rawset(self, name, frontend)
return frontend
end,
})

19
lua/Trans/core/helper.lua Normal file
View File

@ -0,0 +1,19 @@
local fn, api = vim.fn, vim.api
local Trans = require 'Trans'
---@class TransHelper for Trans module dev
local M = {}
---Get abs_path of file
---@param path string[]
---@param is_dir boolean? [default]: false
---@return string @Generated path
function M.relative_path(path, is_dir)
return Trans.plugin_dir .. table.concat(path, Trans.separator) .. (is_dir and Trans.separator or '')
end
return M

7
lua/Trans/core/init.lua Normal file
View File

@ -0,0 +1,7 @@
-- Return Core module
local M = {}
return M

View File

@ -40,7 +40,6 @@ return function()
vim.notify(debug_message, vim.log.ERROR)
end
Trans.curl.get(uri, {
output = zip,
callback = handle,
@ -51,7 +50,7 @@ return function()
vim.notify(message, vim.log.levels.INFO)
-- INFO : Install tts dependencies
if fn.has 'linux' == 0 and fn.has 'mac' == 0 then
if Trans.system == 'win' then
os.execute 'cd ./tts && npm install'
end
end

View File

@ -1,44 +1,31 @@
---@class Trans
local Trans = require 'Trans'
local function set_strategy_opts(conf)
local all_backends = Trans.backend.all_name
local g_strategy = conf.strategy
---@alias TransMode 'visual' 'input'
local default_strategy = {
frontend = 'hover',
backend = {
'offline',
-- 'youdao',
-- 'baidu',
},
}
local function parse_backend(backend)
if type(backend) == 'string' then
return backend == '*' and all_backends or { backend }
end
Trans.conf = {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [@see lua/Trans/style/theme.lua]
theme = 'default',
---@type table<TransMode, { frontend:string, backend:string | string[] }> fallback strategy for mode
-- input = {
-- visual = {
-- ...
strategy = vim.defaulttable(function()
return setmetatable({}, default_strategy)
end),
frontend = {},
}
return backend
end
local default_strategy = g_strategy.default
default_strategy.backend = parse_backend(default_strategy.backend)
default_strategy.__index = default_strategy
g_strategy.default = nil
setmetatable(g_strategy, {
__index = function()
return default_strategy
end,
})
for _, strategy in pairs(g_strategy) do
strategy.backend = parse_backend(strategy.backend)
setmetatable(strategy, default_strategy)
end
end
local function define_highlights(conf)
local set_hl = vim.api.nvim_set_hl
local highlights = Trans.style.theme[conf.theme]
for hl, opt in pairs(highlights) do
set_hl(0, hl, opt)
end
end
---@class Trans
@ -48,8 +35,12 @@ return function(opts)
Trans.conf = vim.tbl_deep_extend('force', Trans.conf, opts)
end
local conf = Trans.conf
conf.dir = vim.fn.expand(conf.dir)
conf.dir = vim.fn.expand(conf.dir)
set_strategy_opts(conf)
define_highlights(conf)
-- INFO : set highlight
local set_hl = vim.api.nvim_set_hl
local highlights = Trans.style.theme[conf.theme]
for hl, opt in pairs(highlights) do
set_hl(0, hl, opt)
end
end

View File

@ -1,94 +1,45 @@
local Trans = require 'Trans'
local util = Trans.util
local function init_opts(opts)
local function process(opts)
opts = opts or {}
opts.mode = opts.mode or vim.fn.mode()
opts.str = util.get_str(opts.mode)
return opts
end
local str = Trans.util.get_str(opts.mode)
opts.str = str
---To Do Online Query
---@param data TransData @data
---@param backend TransOnlineBackend @backend
local function do_query(data, backend)
-- TODO : template method for online query
local name = backend.name
local uri = backend.uri
local method = backend.method
local formatter = backend.formatter
local query = backend.get_query(data)
local header = type(backend.header) == 'function' and backend.header(data) or backend.header
local function handle(output)
local status, body = pcall(vim.json.decode, output.body)
if not status or not body then
if not Trans.conf.debug then
backend.debug(body)
data.trace[name] = output
end
data.result[name] = false
return
end
-- vim.print(data.result[name])
data.result[name] = formatter(body, data)
if not str or str == '' then
Trans.debug 'No string to translate'
return
end
Trans.curl[method](uri, {
query = query,
callback = handle,
header = header,
})
-- Hook ?
end
---@type table<string, fun(data: TransData):boolean>
local strategy = {
fallback = function(data)
local result = data.result
Trans.backend.offline.query(data)
if result.offline then return true end
local update = data.frontend:wait()
for _, backend in ipairs(data.backends) do
do_query(data, backend)
local name = backend.name
---@cast backend TransBackend
while result[name] == nil do
if not update(backend) then break end
end
if result[name] then return true end
if opts.from == nil and opts.to == nil then
-- INFO : Default support [zh -> en] or [en -> zh]
if Trans.util.is_english(str) then
opts.from = 'en'
opts.to = 'zh'
else
opts.from = 'zh'
opts.to = 'en'
end
end
assert(opts.from and opts.to, 'opts.from and opts.to must be set at the same time')
return false
end,
--- TODO :More Strategys
}
-- HACK : Core process logic
local function process(opts)
opts = init_opts(opts)
local str = opts.str
if not str or str == '' then return end
opts.is_word = opts.is_word or Trans.util.is_word(str)
-- Find in cache
if Trans.cache[str] then
local data = Trans.cache[str]
data.frontend:process(data)
return
return data.frontend:process(data)
end
-- Create new data
local data = Trans.data.new(opts)
if strategy[data.frontend.opts.query](data) then
if Trans.strategy[data.frontend.opts.query](data) then
Trans.cache[data.str] = data
data.frontend:process(data)
else
@ -97,8 +48,17 @@ local function process(opts)
end
---@class TransDataOption
---@field mode string?
---@field frontend string?
---@field from string? @Source language type
---@field to string? @Target language type
---@field is_word? boolean @Is the str a word
--- NOTE : Use coroutine to stop and resume the process (for animation)
---@class Trans
---@field translate fun(opts: { frontend: string?, mode: string?}?) Translate string core function
return function(opts)
coroutine.wrap(process)(opts)
end
---@field translate fun(opts: TransDataOption?) Translate string core function
return function(...) coroutine.wrap(process)(...) end

View File

@ -3,7 +3,6 @@ local fn, api = vim.fn, vim.api
---@class TransUtil
local M = require 'Trans'.metatable 'util'
---Get selected text
---@return string
function M.get_select()
@ -32,7 +31,27 @@ function M.get_select()
local e = #lines
lines[1] = lines[1]:sub(s_col)
lines[e] = line:sub(1, e_col)
return table.concat(lines)
return table.concat(lines, ' ')
end
end
---Get selected text
---@return string
function M.get_lines()
local _start = vim.fn.getpos 'v'
local _end = vim.fn.getpos '.'
if _start[2] > _end[2] then
_start, _end = _end, _start
end
local s_row, e_row = _start[2], _end[2]
if s_row == e_row then
return vim.fn.getline(s_row)
else
local lines = vim.fn.getline(s_row, e_row)
return table.concat(lines, ' ')
end
end
@ -52,7 +71,8 @@ function M.get_str(mode)
return fn.input '需要翻译的字符串: '
end,
V = function()
print 'TODO'
api.nvim_input '<Esc>'
return M.get_lines()
end,
})[mode]():match '^%s*(.-)%s*$'
end
@ -60,6 +80,7 @@ end
---Puase coroutine for {ms} milliseconds
---@param ms integer
function M.pause(ms)
assert(ms)
local co = coroutine.running()
vim.defer_fn(function()
coroutine.resume(co)
@ -70,7 +91,7 @@ end
---Detect whether the string is English
---@param str string
---@return boolean
function M.is_English(str)
function M.is_english(str)
local char = { str:byte(1, -1) }
for i = 1, #str do
if char[i] > 128 then
@ -114,7 +135,6 @@ function M.center(node, win_width)
end
local str = node[1]
win_width = str:width()
local space = math.max(0, math.floor((win_width - str:width()) / 2))
node[1] = string.rep(' ', space) .. str
return node
@ -174,7 +194,7 @@ end
---@param str string
---@return boolean
function M.is_word(str)
return str:match '%w+' == str
return str:find '%W' == nil
end
---@param list any[]
@ -204,6 +224,13 @@ function M.list_fields(list, field)
return ret
end
-- function M.checker(method, ...)
-- -- TODO :Use function programming to simplify the code
-- local params = { ... }
-- end
---@class Trans
---@field util TransUtil
return M

View File

@ -124,7 +124,8 @@ function window:try_close()
}
end
api.nvim_win_close(self.winid, true)
pcall(api.nvim_win_close, self.winid, true)
end
---Set window local highlight group
@ -161,21 +162,7 @@ window.__index = window
---@alias WindowOpts
---|{style: string, border: string, focusable: boolean, noautocmd?: boolean, relative: string, width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string}
---@class TransWindowOpts
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
style = 'minimal',
border = 'rounded',
focusable = false,
noautocmd = true,
},
}
---|{style: string, border: string, focusable: boolean, noautocmd?: boolean, relative: 'mouse'|'cursor'|'editor'|'win', width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string}
---@class TransWindow
@ -197,11 +184,34 @@ local default_opts = {
-- relative = relative,
-- }
---@class TransWindowOpts
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
-- INFO : ensured options
-- col
-- row
-- width
-- height
-- relative
-- zindex
style = 'minimal',
border = 'rounded',
focusable = true,
noautocmd = true,
},
animation = nil, ---@type table? Hover Window Animation
buffer = nil, ---@type TransBuffer attached buffer object
}
---Create new window
---@param opts TransWindowOpts window config
---@return TransWindow
function window.new(opts)
opts = vim.tbl_deep_extend('keep', opts, default_opts)
opts.animation = opts.animation or { interval = 12 }
local win = setmetatable(opts, window)
---@cast win TransWindow

View File

@ -121,3 +121,34 @@ return M
-- iciba = 'iciba',
-- offline = '本地',
-- }
-- TODO :
-- float = {
-- width = 0.8,
-- height = 0.8,
-- border = 'rounded',
-- keymap = {
-- quit = 'q',
-- },
-- animation = {
-- open = 'fold',
-- close = 'fold',
-- interval = 10,
-- },
-- tag = {
-- wait = '#519aba',
-- fail = '#e46876',
-- success = '#10b981',
-- },
-- },
-- local title = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}

View File

@ -5,9 +5,11 @@ local strategy = {
pageup = function(hover)
hover.buffer:normal 'gg'
end,
pagedown = function(hover)
hover.buffer:normal 'G'
end,
pin = function(hover)
if hover.pin then
return
@ -27,9 +29,11 @@ local strategy = {
window:set('wrap', true)
end,
close = function(hover)
hover:destroy()
end,
toggle_entry = function(hover)
if api.nvim_get_current_win() ~= hover.window.winid then
api.nvim_set_current_win(hover.window.winid)
@ -48,6 +52,12 @@ local strategy = {
---@class TransHover
---@field execute fun(hover: TransHover, action: string)
return function(hover, action)
-- TODO :
strategy[action](hover)
return strategy[action](hover)
end
-- This function will be called within coroutine, so we can't use __call
-- return setmetatable(strategy, {
-- __call = function(_, hover, action)
-- return strategy[action](hover)
-- end,
-- })

View File

@ -1,5 +1,6 @@
---@type Trans
local Trans = require 'Trans'
local util = Trans.util
-- FIXME :Adjust Window Size
@ -9,25 +10,93 @@ local Trans = require 'Trans'
---@field window TransWindow @hover window
---@field queue TransHover[] @hover queue for all hover instances
---@field destroy_funcs fun(hover:TransHover)[] @functions to be executed when hover window is closed
---@field opts TransHoverOpts @options for hover window
---@field opts TransHoverOpts @hover window options
---@field pin boolean @whether hover window is pinned
local M = Trans.metatable('frontend.hover', {
ns = vim.api.nvim_create_namespace 'TransHoverWin',
queue = {},
---@class TransHoverOpts: TransFrontendOpts
opts = {
---@type integer Max Width of Hover Window
width = 37,
---@type integer Max Height of Hover Window
height = 27,
---@type string -- see: /lua/Trans/style/spinner
spinner = 'dots',
---@type string
fallback_message = '{{notfound}} {{error_message}}',
auto_resize = true,
split_width = 60,
padding = 10, -- padding for hover window width
keymaps = {
-- pageup = '<C-u>',
-- pagedown = '<C-d>',
-- pin = '<leader>[',
-- close = '<leader>]',
-- toggle_entry = '<leader>;',
},
---@type string[] auto close events
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
---@type table<string, string[]> order to display translate result
order = {
default = {
'str',
'translation',
'definition',
},
offline = {
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
},
},
icon = {
-- or use emoji
list = '', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟠| 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟦
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = '', --❔ | ❓ | ❗ | ❕|
yes = '', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = '',
pos = '',
exchange = '',
definition = '󰗊',
translation = '󰊿',
},
},
})
M.__index = M
--[[
Set up function which will be invoked when this module is loaded
Because the options are not loaded yet when this module is loaded
--]]
function M.setup()
local set = vim.keymap.set
for action, key in pairs(M.opts.keymaps) do
set('n', key, function()
local instance = M.get_active_instance()
if instance then
coroutine.wrap(instance.execute)(instance, action)
else
return key
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key, true, true, true), 'n', false)
end
end)
end
@ -88,17 +157,20 @@ end
function M:init_window(opts)
opts = opts or {}
local m_opts = self.opts
---@format disable-next
local option = {
buffer = self.buffer,
animation = m_opts.animation,
win_opts = {
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
relative = opts.relative or 'cursor',
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
title = m_opts.title,
title_pos = m_opts.title and 'center' or nil,
title_pos = m_opts.title and 'center',
zindex = 100,
},
}
@ -118,7 +190,6 @@ end
---Get Check function for waiting
---@return fun(backend: TransBackend): boolean
function M:wait()
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
local pause = util.pause
@ -126,7 +197,7 @@ function M:wait()
local spinner = Trans.style.spinner[opts.spinner]
local times = opts.width - spinner[1]:width()
local size = #spinner
local interval = math.floor(opts.timeout / opts.width)
local interval = math.floor(opts.timeout / times)
self:init_window {
height = 2,
@ -139,7 +210,7 @@ function M:wait()
local it = util.node.item
return function(backend)
cur = cur + 1
buffer[1] = pr(backend.name_zh)
buffer[1] = pr(backend.display_text)
buffer[2] = it { spinner[cur % size + 1] .. (cell):rep(cur), 'TransWaitting' }
pause(interval)
return cur < times
@ -154,7 +225,7 @@ function M:fallback()
local buffer = self.buffer
buffer:wipe()
buffer[1] = Trans.util.center(fallback_msg, opts.width)
buffer[1] = util.center(fallback_msg, opts.width)
buffer:add_highlight(1, 'TransFailed')
if not self.window then
self:init_window {
@ -168,7 +239,7 @@ end
---Defer function when process done
function M:defer()
Trans.util.main_loop(function()
util.main_loop(function()
self.window:set('wrap', true)
self.buffer:set('modifiable', false)
@ -204,7 +275,6 @@ function M:process(data)
return
end
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
@ -227,11 +297,11 @@ function M:process(data)
local width =
valid and
(opts.auto_resize and
math.max(
math.min(opts.width, util.display_width(lines) + opts.padding),
math.min(data.str:width(), opts.split_width)
)
or opts.width)
math.max(
math.min(opts.width, util.display_width(lines) + opts.padding),
math.min(data.str:width(), opts.split_width)
)
or opts.width)
or math.min(opts.width, util.display_width(lines) + opts.padding)
local height = math.min(opts.height, util.display_height(lines, width))

View File

@ -1,15 +1,15 @@
local Trans = require 'Trans'
local health, fn = vim.health, vim.fn
local ok = health.report_ok
local warn = health.report_warn
local error = health.report_error
local ok = health.ok or health.report_ok
local warn = health.warn or health.report_warn
local error = health.error or health.report_error
local has = fn.has
local executable = fn.executable
local function check_neovim_version()
if has 'nvim-0.9' == 1 then
ok [[You have [neovim-nightly] ]]
ok [[You have [neovim-0.9] ]]
else
warn [[Trans Title requires Neovim 0.9 or newer
See neovim-nightly: [https://github.com/neovim/neovim/releases/tag/nightly]
@ -38,13 +38,13 @@ local function check_binary_dependencies()
'sqlite3',
}
if has 'linux' == 1 then
binary_dependencies[3] = 'festival'
elseif has 'mac' == 1 then
binary_dependencies[3] = 'say'
else
binary_dependencies[3] = 'node'
end
binary_dependencies[3] = ({
win = 'node',
mac = 'say',
linux = 'festival',
termux = 'termux-tts-speak',
})[Trans.system]
for _, dep in ipairs(binary_dependencies) do
if executable(dep) == 1 then

View File

@ -5,41 +5,48 @@
local function metatable(folder_name, origin)
return setmetatable(origin or {}, {
__index = function(tbl, key)
local status, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key))
if status then
tbl[key] = result
local found, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key))
if found then
rawset(tbl, key, result)
return result
end
end,
})
end
---@class string
---@field width function @Get string display width
---@field play function @Use tts to play string
local sep = vim.loop.os_uname().sysname == 'Windows' and '\\' or '/'
local uname = vim.loop.os_uname().sysname
local system =
uname == 'Darwin' and 'mac' or
uname == 'Windows_NT' and 'win' or
uname == 'Linux' and (vim.fn.executable 'termux-api-start' == 1 and 'termux' or 'linux') or
error 'Unknown System, Please Report Issue'
local separator = system == 'win' and '\\\\' or '/'
---@class Trans
---@field style table @Style module
---@field cache table<string, TransData> @Cache for translated data object
---@field plugin_dir string @Plugin directory
---@field separator string @Path separator
---@field system 'mac'|'win'|'termux'|'linux' @Path separator
---@field strategy table<string, fun(data: TransData):boolean> Translate string core function
local M = metatable('core', {
cache = {},
style = metatable 'style',
plugin_dir = debug.getinfo(1, 'S').source:sub(2):match('(.-)lua' .. sep .. 'Trans'),
separator = sep,
strategy = metatable 'strategy',
separator = separator,
system = system,
plugin_dir = debug.getinfo(1, 'S').source:sub(2):match('(.-)lua' .. separator .. 'Trans'),
})
M.metatable = metatable
---Get abs_path of file
---@param path string[]
---@param is_dir boolean?
---@return string
function M.relative_path(path, is_dir)
return M.plugin_dir .. table.concat(path, sep) .. (is_dir and sep or '')
end
return M

View File

@ -0,0 +1,27 @@
---Fallback query strategy
---@param data TransData
---@return boolean @true if query success
return function(data)
local result = data.result
local update
for _, backend in ipairs(data.backends) do
local name = backend.name
if backend.no_wait then
---@cast backend TransOfflineBackend
backend.query(data)
else
---@cast backend TransOnlineBackend
require 'Trans'.backend.do_query(data, backend)
update = update or data.frontend:wait()
while result[name] == nil and update(backend) do
end
end
---@cast backend TransBackend
if result[name] then return true end
end
return false
end

View File

@ -29,248 +29,461 @@
-- SOFTWARE.
return {
dots = {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_negative = { -- dots2
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_snake = { -- dots3
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_footsteps = { -- dots10
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
},
dots_hop = { -- dots11
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
},
line = {
"-",
"\\",
"|",
"/",
'-',
'\\',
'|',
'/',
},
pipe = {
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_ellipsis = { -- simpleDots
". ",
".. ",
"...",
" ",
'. ',
'.. ',
'...',
' ',
},
dots_scrolling = { -- simpleDotsScrolling
". ",
".. ",
"...",
" ..",
" .",
" ",
'. ',
'.. ',
'...',
' ..',
' .',
' ',
},
star = {
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
},
flip = {
"_",
"_",
"_",
"-",
"`",
"`",
'_',
'_',
'_',
'-',
'`',
'`',
"'",
"´",
"-",
"_",
"_",
"_",
'´',
'-',
'_',
'_',
'_',
},
hamburger = {
"",
"",
"",
'',
'',
'',
},
grow_vertical = { -- growVertical
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
grow_horizontal = { -- growHorizontal
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
noise = {
"",
"",
"",
'',
'',
'',
},
dots_bounce = { -- bounce
"",
"",
"",
"",
'',
'',
'',
'',
},
triangle = {
"",
"",
"",
"",
'',
'',
'',
'',
},
arc = {
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
},
circle = {
"",
"",
"",
'',
'',
'',
},
square_corners = { -- squareCorners
"",
"",
"",
"",
'',
'',
'',
'',
},
circle_quarters = { -- circleQuarters
"",
"",
"",
"",
'',
'',
'',
'',
},
circle_halves = { -- circleHalves
"",
"",
"",
"",
'',
'',
'',
'',
},
dots_toggle = { -- toggle
"",
"",
'',
'',
},
box_toggle = { -- toggle2
"",
"",
'',
'',
},
arrow = {
"",
"",
"",
"",
"",
"",
"",
"",
'',
'',
'',
'',
'',
'',
'',
'',
},
clock = {
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
'🕛 ',
'🕐 ',
'🕑 ',
'🕒 ',
'🕓 ',
'🕔 ',
'🕕 ',
'🕖 ',
'🕗 ',
'🕘 ',
'🕙 ',
'🕚 ',
},
earth = {
"🌍 ",
"🌎 ",
"🌏 ",
'🌍 ',
'🌎 ',
'🌏 ',
},
moon = {
"🌑 ",
"🌒 ",
"🌓 ",
"🌔 ",
"🌕 ",
"🌖 ",
"🌗 ",
"🌘 ",
'🌑 ',
'🌒 ',
'🌓 ',
'🌔 ',
'🌕 ',
'🌖 ',
'🌗 ',
'🌘 ',
},
dots_pulse = { -- point
"∙∙∙",
"●∙∙",
"∙●∙",
"∙∙●",
"∙∙∙",
'∙∙∙',
'●∙∙',
'∙●∙',
'∙∙●',
'∙∙∙',
},
fistBump = {
'🤜    🤛 ',
'🤜    🤛 ',
'🤜    🤛 ',
' 🤜  🤛  ',
'  🤜🤛   ',
' 🤜✨🤛   ',
'🤜 ✨ 🤛  ',
},
monkey = {
'🙈 ',
'🙈 ',
'🙉 ',
'🙊 '
},
soccerHeader = {
' 🧑⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
},
weather = {
'☀️ ',
'☀️ ',
'☀️ ',
'🌤 ',
'⛅️ ',
'🌥 ',
'☁️ ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'',
'🌨 ',
'🌧 ',
'🌨 ',
'☁️ ',
'🌥 ',
'⛅️ ',
'🌤 ',
'☀️ ',
'☀️ ',
},
speaker = {
'🔈 ',
'🔉 ',
'🔊 ',
'🔉 ',
},
smiley = {
'😄 ',
'😝 ',
},
toggle = {
'',
''
},
toggle10 = {
'',
'',
''
},
toggle11 = {
'',
''
},
toggle12 = {
'',
''
},
toggle13 = {
'=',
'*',
'-'
},
toggle2 = {
'',
''
},
toggle3 = {
'',
''
},
toggle4 = {
'',
'',
'',
''
},
toggle5 = {
'',
''
},
toggle6 = {
'',
''
},
toggle7 = {
'',
'⦿'
},
toggle8 = {
'',
''
},
toggle9 = {
'',
''
},
star2 = {
'+',
'x',
'*'
},
orangeBluePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 ',
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 ',
},
orangePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 '
},
mindblown = {
'😐 ',
'😐 ',
'😮 ',
'😮 ',
'😦 ',
'😦 ',
'😧 ',
'😧 ',
'🤯 ',
'💥 ',
'',
'  ',
'  ',
'  ',
},
hearts = {
'💛 ',
'💙 ',
'💜 ',
'💚 ',
'❤️ '
},
fingerDance = {
'🤘 ',
'🤟 ',
'🖖 ',
'',
'🤚 ',
'👆 '
},
christmas = {
'🌲',
'🎄'
},
circleHalves = {
'',
'',
'',
''
},
bouncingBall = {
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'( ●)',
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'(● )',
},
bluePulse = {
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 '
},
betaWave = {
'ρββββββ',
'βρβββββ',
'ββρββββ',
'βββρβββ',
'ββββρββ',
'βββββρβ',
'ββββββρ',
},
}

View File

@ -4,62 +4,63 @@ local util = require 'Trans'.util
---@field [1] string text to be rendered
---@field render fun(self: TransNode, buffer: TransBuffer, line: number, col: number) render the node
local item = (function()
---@class TransItem : TransNode
local mt = {
---@param self TransItem
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
if self[2] then
buffer:add_highlight(line, self[2], col, col + #self[1])
end
end,
}
mt.__index = mt
---@class TransItem : TransNode
local item_meta = {
---@param self TransItem
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
if self[2] then
buffer:add_highlight(line, self[2], col, col + #self[1])
end
end,
}
---Basic item node
---@param tuple {[1]: string, [2]: string?}
---@return TransItem
return function(tuple)
return setmetatable(tuple, mt)
end
end)()
---@class TransText : TransNode
---@field step string
---@field nodes TransNode[]
local text_meta = {
---@param self TransText
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
local nodes = self.nodes
local step = self.step
local len = step and #step or 0
local text = (function()
---@class TransText : TransNode
---@field step string
---@field nodes TransNode[]
for _, node in ipairs(nodes) do
node:render(buffer, line, col)
col = col + #node[1] + len
end
end,
}
local mt = {
---@param self TransText
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
local nodes = self.nodes
local step = self.step
local len = step and #step or 0
item_meta.__index = item_meta
text_meta.__index = text_meta
for _, node in ipairs(nodes) do
node:render(buffer, line, col)
col = col + #node[1] + len
end
end,
}
mt.__index = mt
---Basic item node
---@param tuple {[1]: string, [2]: string?}
---@return TransItem
local function item(tuple)
return setmetatable(tuple, item_meta)
end
---@param nodes {[number]: TransNode, step: string?}
---@return TransText
local function text(nodes)
return setmetatable({
[1] = table.concat(util.list_fields(nodes, 1), nodes.step),
step = nodes.step,
nodes = nodes,
}, text_meta)
end
---@param nodes {[number]: TransNode, step: string?}
---@return TransText
return function(nodes)
return setmetatable({
[1] = table.concat(util.list_fields(nodes, 1), nodes.step),
step = nodes.step,
nodes = nodes,
}, mt)
end
end)()
---@param args {[number]: TransNode, width: integer, spin: string?}
@ -85,7 +86,6 @@ end
---@class TransUtil
---@field node TransNodes
---@class TransNodes
return {
item = item,

View File

@ -6,6 +6,7 @@ _G.api = vim.api
_G.fn = vim.fn
_G.mock = require 'luassert.mock'
_G.stub = require 'luassert.stub'
string.width = api.nvim_strwidth
---@param func fun(buffer: TransBuffer)
---@return fun()

View File

@ -1,9 +1,92 @@
---@diagnostic disable: param-type-mismatch
require 'test.setup'
local util = Trans.util
describe('util.display_height', with_buffer(function(buffer)
--- TODO :
describe('util.display_height', function()
it('can calculate the height of lines when window with wrap option', function()
local lines = {
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
}
assert.are.equal(#lines, util.display_height(lines, 10))
assert.are.equal(#lines, util.display_height(lines, 11))
assert.are.equal(2 * #lines, util.display_height(lines, 9))
-- Unicode width test
local u_lines = {
'12345678👍', -- 10
'あうえお', -- 8
'𠮷野い𠮷家野家家', -- 16
'👍👍👍お家', -- 10
}
assert.are.equal(4, util.display_height(u_lines, 20))
assert.are.equal(4, util.display_height(u_lines, 16))
assert.are.equal(5, util.display_height(u_lines, 10))
assert.are.equal(7, util.display_height(u_lines, 8))
assert.are.equal(9, util.display_height(u_lines, 7))
end)
end))
end)
describe('util.display_width', function()
it('can calculate the max width of lines', function()
local lines = {
'1234567890',
'123456789',
'12345678',
'1234567',
'123456',
'12345',
'1234',
'123',
'12',
'1',
}
assert.are.equal(10, util.display_width(lines))
-- Unicode width test
local u_lines = {
'12345678👍', -- 10
'あうえお', -- 8
'𠮷野い𠮷家野家家', -- 16
'👍👍👍お家', -- 10
}
assert.are.equal(16, util.display_width(u_lines))
end)
end)
describe('util.center', function()
it('will return the node if its width more than width', function()
local node = i { '1234567890' }
assert.are.same(node, util.center(node, 9))
end)
it('will auto padding space', function()
local node = i { '1234567890' }
assert.are.same(i { (' '):rep(2) .. '1234567890' }, util.center(node, 15))
end)
end)
describe('util.is_word', function()
it('can detect word', function()
for test, value in pairs {
['あうえお'] = false,
['hello'] = true,
[' hello'] = false,
['hello world'] = false,
['test_cool'] = false,
} do
assert.are.same(util.is_word(test), value)
end
end)
end)

100
lua/test/window_spec.lua Normal file
View File

@ -0,0 +1,100 @@
require 'test.setup'
describe('window', with_buffer(function(buffer)
local window
before_each(function()
buffer:wipe()
window = Trans.window.new {
buffer = buffer,
win_opts = {
col = 1,
row = 1,
width = 1,
height = 1,
relative = 'editor',
},
}
window:set('wrap', false)
end)
after_each(function()
window:try_close()
end)
it('can work well when no pass animation table', function()
window:open()
assert.is_true(api.nvim_win_is_valid(window.winid))
end)
describe('smooth_expand', function()
it('can work well when no pass animation table', function()
for field, values in pairs {
width = {
10,
6,
8,
5,
},
height = {
10,
6,
3,
},
} do
for _, value in ipairs(values) do
window:smooth_expand { field = field, to = value }
assert.are.same(value, window[field](window))
end
end
end)
it("don't change window wrap option", function()
window:smooth_expand { field = 'width', to = 10 }
assert.is_false(window:option 'wrap')
window:set('wrap', true)
window:smooth_expand { field = 'width', to = 10 }
assert.is_true(window:option 'wrap')
window:smooth_expand { field = 'height', to = 10 }
assert.is_true(window:option 'wrap')
end)
end)
it("resize() don't change window wrap option", function()
window:resize { width = 10, height = 10 }
assert.is_false(window:option 'wrap')
window:set('wrap', true)
window:resize { width = 5, height = 5 }
assert.is_true(window:option 'wrap')
end)
it('adjust_height() can auto adjust window height to buffer display height', function()
for idx, content in ipairs {
'cool',
'co10',
'家👍',
'👍ol',
'cあl',
'家野',
} do
buffer[idx] = content
end
local max_height = vim.o.lines - 2
for width, expect in ipairs {
[2] = 12,
[3] = 12,
[4] = 6,
[5] = 6,
} do
window:smooth_expand { field = 'width', to = width }
window:adjust_height()
assert.are.same(math.min(expect, max_height), window:height())
end
end)
end))

View File

@ -1,4 +1,4 @@
.PHONE: test
test:
nvim --headless --noplugin -u script/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'
nvim --headless --noplugin -u scripts/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'

View File

@ -1,34 +1,39 @@
local api, fn = vim.api, vim.fn
--- INFO :Define plugin command
local Trans = require("Trans")
local Trans = require 'Trans'
local command = api.nvim_create_user_command
command("Translate", function()
Trans.translate()
end, { desc = " Translate cursor word" })
command('Translate', function() Trans.translate() end,
{ desc = ' Translate cursor word' })
command("TranslateInput", function()
Trans.translate({ mode = 'i' })
end, { desc = " Translate input word" })
command('TranslateInput', function() Trans.translate { mode = 'i' } end,
{ desc = ' Translate input word' })
command("TransPlay", function()
command('TransPlay', function()
local util = Trans.util
local str = util.get_str(vim.fn.mode())
if str and str ~= "" and util.is_English(str) then
if str and str ~= '' and util.is_english(str) then
str:play()
end
end, { desc = " Auto play" })
end, { desc = ' Auto play' })
string.width = api.nvim_strwidth
local system = Trans.system
local f =
fn.has('linux') == 1 and ([[echo %q | festival --tts]])
or fn.has('mac') == 1 and ([[say %q]])
or 'node' .. Trans.relative_path { 'tts', 'say.js' } .. ' %q'
(vim.fn.has 'wsl' == 1 or system == 'win') and
'powershell.exe -Command "Add-Type -AssemblyName System.speech;(New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak(\\\"%s\\\")"' or
system == 'mac' and 'say %q' or
system == 'termux' and 'termux-tts-speak %q' or
system == 'linux' and 'echo %q | festival --tts' or
error 'Unsupported system'
string.play = function(self)
fn.jobstart(f:format(self))
---@diagnostic disable-next-line: param-type-mismatch
local s = string.gsub(self, '\"', ' ')
fn.jobstart(f:format(s))
end

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"say": "^0.16.0"
}
}

View File

@ -1,4 +0,0 @@
const say = require('say')
// console.log(word)
say.speak(process.argv.slice(2))