-- Terminal helpers layered on top of the native LazyVim/Snacks terminal. -- -- Native behaviour is kept intact and untouched: -- / toggle a bottom terminal at the project root (supports a -- count, e.g. 2 opens a *separate* terminal #2) -- fT terminal in the current working directory -- -- This module adds: -- a *dedicated* floating terminal (stable toggle, one-key close) -- ft a picker that lists every live terminal so any contextual -- terminal can be focused or restored even after its window was -- closed/hidden. local M = {} -- The floating terminal. Tracked as an upvalue so is a real -- open/close toggle regardless of which directory the editor is in. ---@type snacks.win? local float --- Open or focus the floating terminal, or hide it if already focused. function M.toggle_float() -- Reuse the existing instance: hide if visible, re-show if hidden. if float and float:buf_valid() then if float:win_valid() then float:hide() else float:show() vim.cmd("startinsert") end return end -- First open: a high fixed count guarantees this terminal never collides with -- the count-keyed bottom terminals (1, 2, 3 ...), so it is always the *same* -- float for no matter the context. float = Snacks.terminal(nil, { count = 99, win = { position = "float", border = "rounded" }, }) -- Close it with a single keystroke from inside the shell: hides the -- window (buffer-local, so it only affects this floating terminal and never -- the bottom split, where shell tab-completion must keep working). if float and float.buf then vim.keymap.set("t", "", function() if float and float:win_valid() then float:hide() end end, { buffer = float.buf, silent = true, desc = "Hide floating terminal" }) end end --- List every live Snacks terminal in a picker. Restore/focus one with , --- kill one with . function M.picker() local terms = Snacks.terminal.list() if #terms == 0 then Snacks.notify.info("No open terminals") return end local items = {} ---@type snacks.picker.finder.Item[] for _, term in ipairs(terms) do local meta = vim.b[term.buf] and vim.b[term.buf].snacks_terminal or {} items[#items + 1] = { term = term, buf = term.buf, id = meta.id, cwd = meta.cwd or "?", cmd = meta.cmd or vim.o.shell, visible = term:win_valid(), text = ("#%s %s %s"):format(meta.id or "?", meta.cwd or "?", meta.cmd or vim.o.shell), } end Snacks.picker({ title = "Terminals", items = items, format = function(item) local ret = {} ---@type snacks.picker.Highlight[] ret[#ret + 1] = { item.visible and "● " or "○ ", item.visible and "SnacksPickerSpecial" or "SnacksPickerComment", } ret[#ret + 1] = { ("#%-2s"):format(item.id or "?"), "Number" } ret[#ret + 1] = { " " .. vim.fn.fnamemodify(item.cwd, ":~"), "SnacksPickerPath" } ret[#ret + 1] = { " " .. vim.fn.fnamemodify(item.cmd, ":t"), "SnacksPickerComment" } return ret end, confirm = function(picker, item) picker:close() local term = item.term if not term:buf_valid() then Snacks.notify.warn("Terminal no longer exists") return end if term:win_valid() then vim.api.nvim_set_current_win(term.win) else term:show() vim.cmd("startinsert") end end, win = { input = { keys = { ["d"] = "term_delete" } } }, actions = { term_delete = function(picker, item) local term = item.term picker:close() if term and term:buf_valid() then term:close() pcall(vim.api.nvim_buf_delete, term.buf, { force = true }) end end, }, }) end return M