Vim里使用真正的可自定义对话框
作者:韦易笑
你有没有试过在Vim里搭一个表单?不是那种简单的单行提示,而是一个真正的有文本输入框,单选按钮,复选框,下拉列表和确认按钮,全部集中在一个窗口里的表单?
如果你试过,你应该知道有多痛苦.
原方式有多难受
Vim本地给你的工具,只有input()做单行输入,和inputlist()做列表选择,就这些了.
想要问用户要一个项名?简单:
cpp
let name = input("Project name: ")
再加一个邮箱:
cpp
let name = input("Project name: ")
let email = input("Email: ")
两次阻塞式的提示.用户填完项名按回车,然后再填邮箱.填错了项名?对不起,回不去.没有可视化布局,没法同时看到两个字段.
再加个语言选择:
cpp
let name = input("Project name: ")
let email = input("Email: ")
let lang = inputlist(["1. Python", "2. Go", "3. Rust"])
三个字段,三次独立的交互.这不是表单,这是审讯.
用户选完语言后想改项名?从头来过.
如果还需要一个"是否初化git仓库"的复选框呢?Vim没有inputcheck()该东西,你只能再搞一个input("初化git?(y/n):"),然后自己解析答案.
这样,不能扩展.
更好的方式:vimquickuiDialog
vimquickui是一个为Vim和NeoVim设计的TUI组件库,提供菜单,列表框,文本框等控件,全部用纯VimScript实现,不依赖任何外部工具.
在1.5.0版本中,它新增了一套数据驱动的对话框系统:按一组字典声明控件,quickui在一个弹窗中渲染它们;
用户完成操作后,按一个字典返回给你所有值.
不需要+神算,不需要Lua,不需要外部依赖,纯VimScript搞定.
安装
用vimplug:
cpp
Plug skywind3000/vimquickui"
或用Vim内置包管理:
cpp
cd ~/.vim/pack/vendor/start && git clone https://github.com/skywind3000/vim-quickui
可选设置统一边框:
cpp
let g:quickui_border_style = 2
好了,没有构建步骤,没有依赖.
你的第一个对话框
来简单设置对话框,如下放置进一个函数里:
cpp
function! MySettings()
let items = [
\ {'type': 'label', 'text': 'Settings:'},
\ {'type': 'input', 'name': 'name', 'prompt': 'Name:',
\ 'value': 'test'},
\ {'type': 'radio', 'name': 'choice', 'prompt': 'Pick:',
\ 'items': ['A', 'B', 'C']},
\ {'type': 'check', 'name': 'flag',
\ 'text': 'Enable Feature'},
\ {'type': 'button', 'name': 'confirm',
\ 'items': [' &OK ', ' &Cancel ']},
\ ]
let result = quickui#dialog#open(items, {'title': 'Settings'})
echo result
endfunc
执行:callMySettings()效果如下:略.
一个真正的对话框,在Vim里,带多个控件.
逐行解释一下:
1,label,顶部的静态文本,不可得焦
2,input,带提示标签和默认值的文本输入框
3,radio,单选按钮组,只能选一个
4,check,复选框,可切换开关
5,button,底部的按钮行
可用制表符和ShiftTab在控件间切换焦点,在输入框里直接输入,按空格切换复选框或选择单选项,按回车或点击按钮确认.
在结果字典里返回所有的值.
怎么退出?
对话框关闭后,你需要知道两件事:用户是确认了还是取消了?如果确认了,是按了按钮还是在输入框里按了回车?
返回值有两个关键字段:
1,button_index,按了哪个按钮(0开始),取消时为-1.
2,button,按钮的名字,如果是在非按钮上按回车或取消则为"".
这是在每个对话框里都会用到的判断模式:
cpp
let r = quickui#dialog#open(items, opts)
if r.button_index == -1
" 用户按了 ESC,CtrlC 或`关闭按钮`"
echo "Cancelled"
elseif r.button == ""
" 用户在输入框/单选/复选上按了回车"
echo "Confirmed (Enter): name=" . r.name
else
" 用户点击了按钮"
echo "Button pressed: " . r.button . " #" . r.button_index
endif
几个要点:
1,button_index从0开始.第一个按钮返回0,第二个返回1,等.
2,用按钮区分回车和按钮点击.当button_index为0时,检查r.button:如果为"",说明是在非按钮上按了回车;如果非空,说明是点击了第一个按钮.
3,取消时仍返回值.即使按了ESC,r.name等字段仍包含用户在取消前输入的内容.下次重新打开对话框时可恢复状态.
一般,只需要这样判断:
cpp
let r = quickui#dialog#open(items, opts)
if r.button_index >= 0 && r.button != ""
" 用户点击了按钮,处理`返回值`"
echo "Name: " . r.name
endif
或如果有好和取消两个按钮:
cpp
" " &OK " 是按钮 0," &Cancel " 是按钮 1"
if r.button_index == 0 && r.button != ""
echo "Accepted: " . r.name
endif
一个实战示例
一个更贴近真实插件的:一个包含所有控件类型的"新建项":
cpp
function! NewProject()
let items = [
\ {'type': 'label', 'text': 'Create New Project:'},
\ {'type': 'input', 'name': 'project_name', 'prompt': 'Project:'},
\ {'type': 'input', 'name': 'email', 'prompt': 'Email:'},
\ {'type': 'dropdown', 'name': 'language', 'prompt': 'Language:',
\ 'items': ['Python', 'JavaScript', 'Go', 'Rust', 'C++'],
\ 'value': 0},
\ {'type': 'dropdown', 'name': 'build', 'prompt': 'Build:',
\ 'items': ['Make', 'CMake', 'Cargo', 'npm', 'pip'],
\ 'value': 0},
\ {'type': 'radio', 'name': 'license', 'prompt': 'License:',
\ 'items': ['&MIT', '&Apache', '&GPL', '&Proprietary'],
\ 'value': 0},
\ {'type': 'check', 'name': 'git_init',
\ 'text': 'Initialize git repo', 'value': 1},
\ {'type': 'check', 'name': 'ci',
\ 'text': 'Add CI config'},
\ {'type': 'button', 'name': 'confirm',
\ 'items': [' &Create ', ' Cancel ']},
\ ]
let opts = {'title': 'New Project', 'w': 50, 'focus': 'project_name'}
let result = quickui#dialog#open(items, opts)
" 检查用户是否点击了 "Create" 按钮(按钮 0)"
if result.button_index == 0 && result.button != ''
let languages = ['Python', 'JavaScript', 'Go', 'Rust', 'C++']
let builds = ['Make', 'CMake', 'Cargo', 'npm', 'pip']
echo 'Project: ' . result.project_name
echo 'Email: ' . result.email
echo 'Language: ' . languages[result.language]
echo 'Build: ' . builds[result.build]
echo 'License: ' . result.license
echo 'Git: ' . (result.git_init ? 'yes' : 'no')
echo 'CI: ' . (result.ci ? 'yes' : 'no')
else
echo 'Cancelled'
endif
endfunc
效果截图:略.
该例展示了几个要点:
1,下拉列表控件(dropdown)按一个折叠的选择框显示.按回车或空格弹出选项列表供选择.返回值是0开始的索引,需要自己映射回文本.
2,分隔线(separator)在复选框和按钮间画一条水平线,替代了控件间的默认空行,保持布局整洁.
3,opts.focus把初始焦点设置到project_name输入框,用户打开对话框就能立刻开始输入.
4,提示文本自动对齐.注意Project:,Email:,Language:,Build:和License:这些标签都是左对齐的,它们后面的控件开始位置在同一列.
5,QuickUI会自动计算最长的提示文本,补齐其余的.
6,热键标记.按钮,单选和复选文本中的&标记了热键符.比如&创建,&Create让C变成热键,在对话框的任何地方(不在输入框中时)按C就可触发该按钮.
单选组的&MIT,&Apache同理.
使用技巧
分享几个我在开发对话过程中总结的经验:
1,先设好opts.w.如果不设宽度,QuickUI会自动计算.对简单对话框没问题,但对有多个字段,显式设置一个宽度(比如50)可让布局更一致.
2,用"值"设默认值.每个控件都支持值字段.输入框接收串,单选/下拉/复选框接收数字.预填好默认值可让用户少打几个字.
3,复选框不需要提示标签.和输入框,单选不同,复选框的文本自身就是标签,不加提示看着更自然.如果想让它和其他有提示的控件对齐,也可加"提示"字段.
4,给按钮行命名.如果只有一行按钮,默认名字"按钮"就够了.但如果有两行按钮(比如"Apply/Reset"和"OK/Cancel"),要给它们不同名,这样才能区分用户点的是哪一行.
更多进阶特性
这里只覆盖了基础用法.对话框系统还有更多能力:
1,输入历史,输入框可历史字段在多次调用间共享历史记录,按Ctrl+Up/Ctrl+Down浏览
2,垂直单选,选项文本较长时,单选组会自动切换为垂直布局
3,表单验证,opts.validator设置一个回调函数,在对话框关闭前验证字段值.
4,支持鼠标,点击任何控件即可得焦,切换或激活.
5,自定义颜色和边框,配合你的Vim配色方案
完整参考可查看vimquickui仓库中的DialogGuide.
不只是对话框
vimquickui不仅是对话框.它还提供:
1,顶部菜单栏,屏幕顶部的下拉菜单,Borland/TurboC++风格
2,右键菜单,光标附近弹的环境菜单
3,列表框,带搜索功能的可滚动栏表
4,文本框,在弹窗中显示文本
5,预览窗口,在光标附近预览文件内容
6,输入框,简单的单行输入(比完整对话框更轻量)
7,终端,在弹窗中运行壳命令
全部是纯VimScript实现,全部同时支持Vim和NeoVim.