You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
4.9 KiB
146 lines
4.9 KiB
-- Terminal helpers layered on top of the native LazyVim/Snacks terminal. |
|
-- |
|
-- Native behaviour is kept intact and untouched: |
|
-- <C-/> / <C-_> toggle a bottom terminal at the project root (supports a |
|
-- count, e.g. 2<C-/> opens a *separate* terminal #2) |
|
-- <leader>fT terminal in the current working directory |
|
-- |
|
-- This module adds: |
|
-- <Tab> open/focus a *dedicated* floating terminal (close it with <C-/>) |
|
-- <leader>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 <Tab> floating terminal. Tracked as an upvalue so <Tab> always re-focuses |
|
-- the *same* float regardless of which directory the editor is in. |
|
---@type snacks.win? |
|
local float |
|
|
|
--- Open or focus the <Tab> floating terminal, or hide it if already focused. |
|
-- NOTE: currently unused; <Tab> calls open_float() instead so shell |
|
-- tab-completion keeps working while the terminal is open. Kept around for a |
|
-- future explicit open/close toggle. |
|
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 <Tab> 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: <Tab> 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", "<Tab>", function() |
|
if float and float:win_valid() then |
|
float:hide() |
|
end |
|
end, { buffer = float.buf, silent = true, desc = "Hide floating terminal" }) |
|
end |
|
end |
|
|
|
--- Open or focus the <Tab> floating terminal. This never hides the window, so |
|
--- once the float is open <Tab> falls through to the shell and stays available |
|
--- for tab-completion. Close the floating terminal with <C-/> while it is |
|
--- visible (handled by Snacks' native terminal keymap). |
|
function M.open_float() |
|
if float and float:buf_valid() then |
|
if not float:win_valid() then |
|
float:show() |
|
end |
|
if float:win_valid() then |
|
vim.api.nvim_set_current_win(float.win) |
|
end |
|
vim.cmd("startinsert") |
|
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 <Tab> no matter the context. |
|
float = Snacks.terminal(nil, { |
|
count = 99, |
|
win = { position = "float", border = "rounded" }, |
|
}) |
|
end |
|
|
|
--- List every live Snacks terminal in a picker. Restore/focus one with <CR>, |
|
--- kill one with <d>. |
|
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
|
|
|