-- ~/.config/vis/fzf.lua -- Pickers: fuzzy file finder, live ripgrep, terminal file manager. -- Loaded from visrc.lua. Each function is self-contained on purpose. local M = {} -- height of the drop-down fzf panel at the top ('100%' = full screen) M.height = '50%' -- --------------------------------------------------------------------------- -- files: fuzzy-find a file, or create one from the query -- enter open the highlighted file (or create the query if nothing matches) -- ctrl-n new file named after the query, e.g. src/util/new.odin -- --------------------------------------------------------------------------- function M.files() -- file list: fd, fdfind (Debian/Ubuntu), or plain find local list = "find . -type f -not -path '*/.git/*' | sed 's|^\\./||'" for _, name in ipairs({ 'fd', 'fdfind' }) do local ok = os.execute('command -v ' .. name .. ' >/dev/null 2>&1') if ok == true or ok == 0 then list = name .. ' --type f --hidden --exclude .git' break end end -- preview: bat, batcat (Debian/Ubuntu), or head local preview = 'head -n 500 {}' for _, name in ipairs({ 'bat', 'batcat' }) do local ok = os.execute('command -v ' .. name .. ' >/dev/null 2>&1') if ok == true or ok == 0 then preview = name .. ' --color=always --style=numbers --line-range=:500 {}' break end end local cmd = list .. ' | fzf' .. ' --height=' .. M.height .. ' --layout=reverse --border' .. " --prompt 'files> '" .. ' --print-query --expect=ctrl-n' -- line 1: query, line 2: key .. " --header 'enter: open ยท ctrl-n: new file from query'" .. " --preview '" .. preview .. "'" .. " --preview-window 'right:60%:border-left'" .. " --bind 'ctrl-/:toggle-preview'" .. " --bind 'shift-up:preview-up,shift-down:preview-down'" -- fzf's --height mode draws from the cursor down: start at the top-left local tty = io.open('/dev/tty', 'w') if tty then tty:write('\27[H') tty:close() end local handle = io.popen(cmd) if not handle then vis:info('fzf: failed to start') return end local out = {} for line in handle:lines() do table.insert(out, line) end local _, _, code = handle:close() -- back onto the alternate screen (so quitting vis restores the -- terminal), then repaint vis tty = io.open('/dev/tty', 'w') if tty then tty:write('\27[?1049h') tty:close() end vis:redraw() if code == 130 then return end -- Esc / Ctrl-C local query, key, choice = out[1], out[2], out[3] local is_new = key == 'ctrl-n' or not choice or choice == '' local path = is_new and query or choice if not path or path == '' then return end -- open in the current window, unless it's already the current file local win = vis.win local current = win and win.file and win.file.path local same = current ~= nil and (current == path or current:sub(-#path - 1) == '/' .. path) if not same then if win and win.file and win.file.modified then vis:info('Buffer has unsaved changes โ€” :w first (or use :e! manually)') return end vis:command(string.format("e '%s'", path)) end if is_new then vis:info('New file: ' .. path .. ' (written to disk on :w)') end end -- --------------------------------------------------------------------------- -- grep: live ripgrep search across the project, jump to the match -- --------------------------------------------------------------------------- function M.grep() local ok = os.execute('command -v rg >/dev/null 2>&1') if not (ok == true or ok == 0) then vis:info('ripgrep (rg) not found on PATH') return end -- preview: bat/batcat with the match highlighted, or cat -n -- {1} = path, {2} = line number (fields split on ':') local preview = 'cat -n {1}' for _, name in ipairs({ 'bat', 'batcat' }) do local found = os.execute('command -v ' .. name .. ' >/dev/null 2>&1') if found == true or found == 0 then preview = name .. ' --color=always --style=numbers --highlight-line {2} {1}' break end end -- don't run rg on an empty query (it would match every line) local reload = '[ -n {q} ] && rg --column --line-number --no-heading' .. ' --color=always --smart-case --hidden --glob "!.git" -- {q} || true' -- empty stdin: results only appear once you start typing local cmd = ': | fzf' .. ' --height=' .. M.height .. ' --layout=reverse --border' .. " --prompt 'rg> '" .. ' --ansi --disabled' -- let rg do the matching, not fzf .. ' --delimiter :' .. " --bind 'change:reload:" .. reload .. "'" .. " --preview '" .. preview .. "'" .. " --preview-window 'right:60%:border-left:+{2}/2'" -- centre match .. " --bind 'ctrl-/:toggle-preview'" .. " --bind 'shift-up:preview-up,shift-down:preview-down'" -- fzf's --height mode draws from the cursor down: start at the top-left local tty = io.open('/dev/tty', 'w') if tty then tty:write('\27[H') tty:close() end local handle = io.popen(cmd) if not handle then vis:info('fzf: failed to start') return end local choice = handle:read('*l') handle:close() -- back onto the alternate screen, then repaint vis tty = io.open('/dev/tty', 'w') if tty then tty:write('\27[?1049h') tty:close() end vis:redraw() if not choice or choice == '' then return end choice = choice:gsub('\27%[[%d;]*m', '') -- strip any colour codes local path, line, col = choice:match('^(.-):(%d+):(%d+):') if not path then return end -- open in the current window, unless it's already the current file local win = vis.win local current = win and win.file and win.file.path local same = current ~= nil and (current == path or current:sub(-#path - 1) == '/' .. path) if not same then if win and win.file and win.file.modified then vis:info('Buffer has unsaved changes โ€” :w first (or use :e! manually)') return end vis:command(string.format("e '%s'", path)) end vis.win.selection:to(tonumber(line), tonumber(col)) end -- --------------------------------------------------------------------------- -- fm: browse/create/rename/delete in a terminal file manager, then open the -- file you pick. Uses the first one found: yazi, lf, nnn, ranger -- --------------------------------------------------------------------------- function M.fm() local tmp = os.tmpname() local managers = { { 'yazi', "yazi --chooser-file='%s'" }, { 'lf', "lf -selection-path '%s'" }, { 'nnn', "nnn -p '%s'" }, { 'ranger', "ranger --choosefile='%s'" }, } local cmd for _, m in ipairs(managers) do local ok = os.execute('command -v ' .. m[1] .. ' >/dev/null 2>&1') if ok == true or ok == 0 then cmd = string.format(m[2], tmp) break end end if not cmd then os.remove(tmp) vis:info('No file manager found (install yazi, lf, nnn or ranger)') return end os.execute(cmd .. ' /dev/tty 2>&1') -- back onto the alternate screen, then repaint vis local tty = io.open('/dev/tty', 'w') if tty then tty:write('\27[?1049h') tty:close() end vis:redraw() local f = io.open(tmp, 'r') local path = f and f:read('*l') if f then f:close() end os.remove(tmp) if not path or path == '' then return end -- open in the current window, unless it's already the current file local win = vis.win local current = win and win.file and win.file.path if current ~= path then if win and win.file and win.file.modified then vis:info('Buffer has unsaved changes โ€” :w first (or use :e! manually)') return end vis:command(string.format("e '%s'", path)) end end -- --------------------------------------------------------------------------- -- create missing parent directories on save, so a new file from ctrl-n like -- src/util/new.odin can be written even if src/util/ doesn't exist yet -- --------------------------------------------------------------------------- vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file, path) -- luacheck: no unused args local dir = path and path:match('^(.*)/[^/]+$') if dir and dir ~= '' then os.execute("mkdir -p '" .. dir:gsub("'", "'\\''") .. "'") end return true end) return M