···1# artio.nvim
23-A minimal, nature-infused file picker for Neovim using the new extui window.
4Inspired by forest spirits and the calm intuition of hunting, Artio helps you gently select files without the weight of heavy fuzzy-finder dependencies.
56
···910Requires Neovim `>= 0.12`
1112-- Lightweight picker window built on Neovim's extui
13- Prompt + list UI components - minimal and focused
14- Fuzzy filtering using matchfuzzy (built-in)
15- Icon support for common filetypes through [mini.icons](https://github.com/echasnovski/mini.nvim) _(optional)_
16- No heavy dependencies - pure Lua
1718-### extui
1920-artio requires the extui to be enabled.
2122an example of how to set this up is:
2324```lua
25-require("vim._extui").enable({ enable = true, msg = {
26 target = "msg",
27} })
28```
···1# artio.nvim
23+A minimal, nature-infused file picker for Neovim using ui2.
4Inspired by forest spirits and the calm intuition of hunting, Artio helps you gently select files without the weight of heavy fuzzy-finder dependencies.
56
···910Requires Neovim `>= 0.12`
1112+- Lightweight picker window built on Neovim's ui2
13- Prompt + list UI components - minimal and focused
14- Fuzzy filtering using matchfuzzy (built-in)
15- Icon support for common filetypes through [mini.icons](https://github.com/echasnovski/mini.nvim) _(optional)_
16- No heavy dependencies - pure Lua
1718+### ui2
1920+artio requires ui2 to be enabled.
2122an example of how to set this up is:
2324```lua
25+require("vim._core.ui2").enable({ enable = true, msg = {
26 target = "msg",
27} })
28```
+5-6
doc/artio.txt
···1-*artio.txt* a minimal, nature-infused file picker for neovim using the new
2- extui window
34==============================================================================
5···78Requires Neovim `>= 0.12`
910-- Lightweight picker window built on Neovim's extui
11- Prompt + list UI components - minimal and focused
12- Fuzzy filtering using matchfuzzy (built-in)
13- Optional icon support for common filetypes through mini.icons
14 (https://github.com/echasnovski/mini.nvim)
15- No heavy dependencies - pure Lua
1617-EXTUI
1819-artio requires the extui to be enabled.
2021an example of how to set this up is:
2223>lua
24- require("vim._extui").enable({ enable = true, msg = {
25 target = "msg",
26 } })
27<
···1+*artio.txt* a minimal, nature-infused file picker for neovim using ui2
023==============================================================================
4···67Requires Neovim `>= 0.12`
89+- Lightweight picker window built on Neovim's ui2
10- Prompt + list UI components - minimal and focused
11- Fuzzy filtering using matchfuzzy (built-in)
12- Optional icon support for common filetypes through mini.icons
13 (https://github.com/echasnovski/mini.nvim)
14- No heavy dependencies - pure Lua
1516+UI2
1718+artio requires ui2 to be enabled.
1920an example of how to set this up is:
2122>lua
23+ require("vim._core.ui2").enable({ enable = true, msg = {
24 target = "msg",
25 } })
26<
+3-3
lua/artio/builtins.lua
···90 props.grepprg = props.grepprg or vim.o.grepprg
9192 local base_dir = vim.fn.getcwd(0)
93- local ext = require("vim._extui.shared")
94 local grepcmd = utils.make_cmd(props.grepprg, {
95 cwd = base_dir,
96 })
···105106 local lines = grepcmd(input)
107108- vim.fn.setloclist(ext.wins.cmd, {}, " ", {
109 title = "grep[" .. input .. "]",
110 lines = lines,
111 efm = vim.o.grepformat,
···113 })
114115 return vim
116- .iter(ipairs(vim.fn.getloclist(ext.wins.cmd)))
117 :map(function(i, locitem)
118 local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr))
119 return {
···90 props.grepprg = props.grepprg or vim.o.grepprg
9192 local base_dir = vim.fn.getcwd(0)
93+ local ui2 = require("vim._core.ui2")
94 local grepcmd = utils.make_cmd(props.grepprg, {
95 cwd = base_dir,
96 })
···105106 local lines = grepcmd(input)
107108+ vim.fn.setloclist(ui2.wins.cmd, {}, " ", {
109 title = "grep[" .. input .. "]",
110 lines = lines,
111 efm = vim.o.grepformat,
···113 })
114115 return vim
116+ .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd)))
117 :map(function(i, locitem)
118 local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr))
119 return {
+2-2
lua/artio/health.lua
···7 vim.health.error("artio.nvim not loaded")
8 end
910- if not vim.tbl_get(require("vim._extui.shared") or {}, "cfg", "enable") then
11- vim.health.error("extui not enabled")
12 end
1314 if _G["MiniIcons"] then
···7 vim.health.error("artio.nvim not loaded")
8 end
910+ if not vim.tbl_get(require("vim._core.ui2") or {}, "cfg", "enable") then
11+ vim.health.error("ui2 not enabled")
12 end
1314 if _G["MiniIcons"] then
+5-5
lua/artio/picker.lua
···169end
170171function Picker:initkeymaps()
172- local ext = require("vim._extui.shared")
173174 ---@type vim.keymap.set.Opts
175- local opts = { buffer = ext.bufs.cmd }
176177 if self.actions then
178 vim.iter(pairs(self.actions)):each(function(k, v)
···187end
188189function Picker:delkeymaps()
190- local ext = require("vim._extui.shared")
191192- local keymaps = vim.api.nvim_buf_get_keymap(ext.bufs.cmd, "i")
193194 vim.iter(ipairs(keymaps)):each(function(_, v)
195 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then
196- vim.api.nvim_buf_del_keymap(ext.bufs.cmd, "i", v.lhs)
197 end
198 end)
199end
···169end
170171function Picker:initkeymaps()
172+ local ui2 = require("vim._core.ui2")
173174 ---@type vim.keymap.set.Opts
175+ local opts = { buffer = ui2.bufs.cmd }
176177 if self.actions then
178 vim.iter(pairs(self.actions)):each(function(k, v)
···187end
188189function Picker:delkeymaps()
190+ local ui2 = require("vim._core.ui2")
191192+ local keymaps = vim.api.nvim_buf_get_keymap(ui2.bufs.cmd, "i")
193194 vim.iter(ipairs(keymaps)):each(function(_, v)
195 if v.lhs:match("^<Plug>(artio-action-") or (v.rhs and v.rhs:match("^<Plug>(artio-action-")) then
196+ vim.api.nvim_buf_del_keymap(ui2.bufs.cmd, "i", v.lhs)
197 end
198 end)
199end
+35-35
lua/artio/view.lua
···1-local cmdline = require("vim._extui.cmdline")
2-local ext = require("vim._extui.shared")
34local _log = {}
5local _loglevel = vim.log.levels.ERROR
···38---@param hide boolean Whether to hide or show the window.
39---@param height integer (Text)height of the cmdline window.
40local function win_config(win, hide, height)
41- if ext.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then
42 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil })
43 elseif vim.api.nvim_win_get_height(win) ~= height then
44 vim.api.nvim_win_set_height(win, height)
···49 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function()
50 vim.o.cmdheight = height
51 end)
52- ext.msg.set_pos()
53 end
54end
55···92--- gets updated after changedtick event
93local last_draw_tick = 0
94local function get_changedtick()
95- return vim.api.nvim_buf_get_changedtick(ext.bufs.cmd)
96end
9798local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset.
···119120 self:promptpos()
121 self:setlines(promptidx, promptidx + 1, lines)
122- vim.fn.prompt_setprompt(ext.bufs.cmd, promptstr)
123 vim.schedule(function()
124- local ok, result = pcall(vim.api.nvim_buf_set_mark, ext.bufs.cmd, ":", promptidx + 1, promptlen, {})
125 if not ok then
126 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result))
127 return
···141function View:show(content, pos, firstc, prompt, indent, level, hl_id)
142 cmdline.level, cmdline.indent = level, indent
143 if cmdline.highlighter and cmdline.highlighter.active then
144- cmdline.highlighter.active[ext.bufs.cmd] = nil
145 end
146- if ext.msg.cmd.msg_row ~= -1 then
147- ext.msg.msg_clear()
148 end
149- ext.msg.virt.last = { {}, {}, {}, {} }
150151 self:clear()
152 prompthl_id = hl_id
···169170---@param predicted? integer The predicted height of the cmdline window
171function View:updatewinheight(predicted)
172- local height = math.max(1, predicted or vim.api.nvim_win_text_height(ext.wins.cmd, {}).all)
173 height = math.min(height, self.win.height)
174- win_config(ext.wins.cmd, false, height)
175end
176177function View:saveview()
···211 self.opts[level] = self.opts[level] or {}
212 local props = {
213 scope = level == "g" and "global" or "local",
214- buf = level == "buf" and ext.bufs.cmd or nil,
215- win = level == "win" and ext.wins.cmd or nil,
216 }
217218 for name, value in pairs(o) do
···248 _log = nil
249 _log = {}
250251- ext.check_targets()
252253 vim.schedule(function()
254 self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true })
···286287 vim.api.nvim_create_autocmd("TextChangedI", {
288 group = self.augroup,
289- buffer = ext.bufs.cmd,
290 callback = function()
291 self:update()
292 end,
···294295 vim.api.nvim_create_autocmd("CursorMovedI", {
296 group = self.augroup,
297- buffer = ext.bufs.cmd,
298 callback = function()
299 self:updatecursor()
300 end,
···312 self:trigger_show()
313314 vim._with({ noautocmd = true }, function()
315- vim.api.nvim_set_current_win(ext.wins.cmd)
316 end)
317318 self:setopts()
···325326 -- trigger after registering events
327 vim.schedule(function()
328- vim._with({ win = ext.wins.cmd, wo = { eventignorewin = "" } }, function()
329 vim.api.nvim_exec_autocmds("WinEnter", {})
330 end)
331 end)
···338 self:closepreview()
339 vim.schedule(function()
340 pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
341- pcall(vim.api.nvim_buf_detach, ext.bufs.cmd)
342343 vim.cmd.stopinsert()
344···364end
365366function View:hide()
367- vim.fn.clearmatches(ext.wins.cmd) -- Clear matchparen highlights.
368- vim.api.nvim_win_set_cursor(ext.wins.cmd, { 1, 0 })
369- vim.api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {})
370371 local clear = vim.schedule_wrap(function(was_prompt)
372 -- Avoid clearing prompt window when it is re-entered before the next event
373 -- loop iteration. E.g. when a non-choice confirm button is pressed.
374 if was_prompt and not cmdline.prompt then
375 pcall(function()
376- vim.api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {})
377- vim.api.nvim_buf_set_lines(ext.bufs.dialog, 0, -1, false, {})
378- vim.api.nvim_win_set_config(ext.wins.dialog, { hide = true })
379- vim.on_key(nil, ext.msg.dialog_on_key)
380 end)
381 end
382 -- Messages emitted as a result of a typed command are treated specially:
···389 clear(cmdline.prompt)
390391 cmdline.prompt, cmdline.level = false, 0
392- win_config(ext.wins.cmd, true, ext.cmdheight)
393end
394395function View:trigger_show()
···441 self:promptpos()
442443 if not pos or pos < 0 then
444- local cursorpos = vim.api.nvim_win_get_cursor(ext.wins.cmd)
445 pos = cursorpos[2] - promptlen
446 end
447···459 curpos[1], curpos[2] = promptidx + 1, promptlen + pos
460461 vim._with({ noautocmd = true }, function()
462- local ok, _ = pcall(vim.api.nvim_win_set_cursor, ext.wins.cmd, curpos)
463 if not ok then
464 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2]))
465 end
···482 -- update winheight to prevent wrong scroll when increasing from 1
483 local diff = #lines - (posend - posstart)
484 if diff ~= 0 then
485- local height = vim.api.nvim_win_text_height(ext.wins.cmd, {}).all
486 local predicted = height + diff
487 self:updatewinheight(predicted)
488 end
489490 before_draw_tick = get_changedtick()
491- vim.api.nvim_buf_set_lines(ext.bufs.cmd, posstart, posend, false, lines)
492 last_draw_tick = get_changedtick()
493end
494···511function View:mark(id, line, col, opts)
512 if id and self.marks[id] then
513 vim._with({ noautocmd = true }, function()
514- vim.api.nvim_buf_del_extmark(ext.bufs.cmd, view_ns, self.marks[id])
515 end)
516 self.marks[id] = nil
517 end
···521522 local ok, result
523 vim._with({ noautocmd = true }, function()
524- ok, result = pcall(vim.api.nvim_buf_set_extmark, ext.bufs.cmd, view_ns, line, col, opts)
525 end)
526 if not ok then
527 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))
···1+local cmdline = require("vim._core.ui2.cmdline")
2+local ui2 = require("vim._core.ui2")
34local _log = {}
5local _loglevel = vim.log.levels.ERROR
···38---@param hide boolean Whether to hide or show the window.
39---@param height integer (Text)height of the cmdline window.
40local function win_config(win, hide, height)
41+ if ui2.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then
42 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil })
43 elseif vim.api.nvim_win_get_height(win) ~= height then
44 vim.api.nvim_win_set_height(win, height)
···49 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function()
50 vim.o.cmdheight = height
51 end)
52+ ui2.msg.set_pos()
53 end
54end
55···92--- gets updated after changedtick event
93local last_draw_tick = 0
94local function get_changedtick()
95+ return vim.api.nvim_buf_get_changedtick(ui2.bufs.cmd)
96end
9798local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset.
···119120 self:promptpos()
121 self:setlines(promptidx, promptidx + 1, lines)
122+ vim.fn.prompt_setprompt(ui2.bufs.cmd, promptstr)
123 vim.schedule(function()
124+ local ok, result = pcall(vim.api.nvim_buf_set_mark, ui2.bufs.cmd, ":", promptidx + 1, promptlen, {})
125 if not ok then
126 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result))
127 return
···141function View:show(content, pos, firstc, prompt, indent, level, hl_id)
142 cmdline.level, cmdline.indent = level, indent
143 if cmdline.highlighter and cmdline.highlighter.active then
144+ cmdline.highlighter.active[ui2.bufs.cmd] = nil
145 end
146+ if ui2.msg.cmd.msg_row ~= -1 then
147+ ui2.msg.msg_clear()
148 end
149+ ui2.msg.virt.last = { {}, {}, {}, {} }
150151 self:clear()
152 prompthl_id = hl_id
···169170---@param predicted? integer The predicted height of the cmdline window
171function View:updatewinheight(predicted)
172+ local height = math.max(1, predicted or vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all)
173 height = math.min(height, self.win.height)
174+ win_config(ui2.wins.cmd, false, height)
175end
176177function View:saveview()
···211 self.opts[level] = self.opts[level] or {}
212 local props = {
213 scope = level == "g" and "global" or "local",
214+ buf = level == "buf" and ui2.bufs.cmd or nil,
215+ win = level == "win" and ui2.wins.cmd or nil,
216 }
217218 for name, value in pairs(o) do
···248 _log = nil
249 _log = {}
250251+ ui2.check_targets()
252253 vim.schedule(function()
254 self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true })
···286287 vim.api.nvim_create_autocmd("TextChangedI", {
288 group = self.augroup,
289+ buffer = ui2.bufs.cmd,
290 callback = function()
291 self:update()
292 end,
···294295 vim.api.nvim_create_autocmd("CursorMovedI", {
296 group = self.augroup,
297+ buffer = ui2.bufs.cmd,
298 callback = function()
299 self:updatecursor()
300 end,
···312 self:trigger_show()
313314 vim._with({ noautocmd = true }, function()
315+ vim.api.nvim_set_current_win(ui2.wins.cmd)
316 end)
317318 self:setopts()
···325326 -- trigger after registering events
327 vim.schedule(function()
328+ vim._with({ win = ui2.wins.cmd, wo = { eventignorewin = "" } }, function()
329 vim.api.nvim_exec_autocmds("WinEnter", {})
330 end)
331 end)
···338 self:closepreview()
339 vim.schedule(function()
340 pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
341+ pcall(vim.api.nvim_buf_detach, ui2.bufs.cmd)
342343 vim.cmd.stopinsert()
344···364end
365366function View:hide()
367+ vim.fn.clearmatches(ui2.wins.cmd) -- Clear matchparen highlights.
368+ vim.api.nvim_win_set_cursor(ui2.wins.cmd, { 1, 0 })
369+ vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
370371 local clear = vim.schedule_wrap(function(was_prompt)
372 -- Avoid clearing prompt window when it is re-entered before the next event
373 -- loop iteration. E.g. when a non-choice confirm button is pressed.
374 if was_prompt and not cmdline.prompt then
375 pcall(function()
376+ vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
377+ vim.api.nvim_buf_set_lines(ui2.bufs.dialog, 0, -1, false, {})
378+ vim.api.nvim_win_set_config(ui2.wins.dialog, { hide = true })
379+ vim.on_key(nil, ui2.msg.dialog_on_key)
380 end)
381 end
382 -- Messages emitted as a result of a typed command are treated specially:
···389 clear(cmdline.prompt)
390391 cmdline.prompt, cmdline.level = false, 0
392+ win_config(ui2.wins.cmd, true, ui2.cmdheight)
393end
394395function View:trigger_show()
···441 self:promptpos()
442443 if not pos or pos < 0 then
444+ local cursorpos = vim.api.nvim_win_get_cursor(ui2.wins.cmd)
445 pos = cursorpos[2] - promptlen
446 end
447···459 curpos[1], curpos[2] = promptidx + 1, promptlen + pos
460461 vim._with({ noautocmd = true }, function()
462+ local ok, _ = pcall(vim.api.nvim_win_set_cursor, ui2.wins.cmd, curpos)
463 if not ok then
464 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2]))
465 end
···482 -- update winheight to prevent wrong scroll when increasing from 1
483 local diff = #lines - (posend - posstart)
484 if diff ~= 0 then
485+ local height = vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all
486 local predicted = height + diff
487 self:updatewinheight(predicted)
488 end
489490 before_draw_tick = get_changedtick()
491+ vim.api.nvim_buf_set_lines(ui2.bufs.cmd, posstart, posend, false, lines)
492 last_draw_tick = get_changedtick()
493end
494···511function View:mark(id, line, col, opts)
512 if id and self.marks[id] then
513 vim._with({ noautocmd = true }, function()
514+ vim.api.nvim_buf_del_extmark(ui2.bufs.cmd, view_ns, self.marks[id])
515 end)
516 self.marks[id] = nil
517 end
···521522 local ok, result
523 vim._with({ noautocmd = true }, function()
524+ ok, result = pcall(vim.api.nvim_buf_set_extmark, ui2.bufs.cmd, view_ns, line, col, opts)
525 end)
526 if not ok then
527 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))