9 changed files with 233 additions and 177 deletions
@ -0,0 +1,118 @@ |
|||||||
|
-- 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> a *dedicated* floating terminal (stable toggle, one-key close) |
||||||
|
-- <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> is a real |
||||||
|
-- open/close toggle 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. |
||||||
|
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 |
||||||
|
|
||||||
|
--- 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 |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
return { |
||||||
|
{ |
||||||
|
"MeanderingProgrammer/render-markdown.nvim", |
||||||
|
opts = { |
||||||
|
enabled = false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
-- Terminal UX layered on Snacks (replaces toggleterm). |
||||||
|
-- Helpers live in lua/config/terminal.lua; keys are declared here so lazy.nvim |
||||||
|
-- registers the which-key labels with the rest of the snacks spec. |
||||||
|
return { |
||||||
|
"folke/snacks.nvim", |
||||||
|
keys = { |
||||||
|
{ |
||||||
|
"<Tab>", |
||||||
|
function() |
||||||
|
require("config.terminal").toggle_float() |
||||||
|
end, |
||||||
|
desc = "Terminal (float)", |
||||||
|
mode = "n", |
||||||
|
}, |
||||||
|
{ |
||||||
|
"<leader>ft", |
||||||
|
function() |
||||||
|
require("config.terminal").picker() |
||||||
|
end, |
||||||
|
desc = "Terminals (list/restore)", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
Loading…
Reference in new issue