Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,9 @@ local defaults = {
create = "terminal", ---@type "terminal"|"window"|"split"
split = {
vertical = true, -- vertical or horizontal split
before = false, -- place the split before the current pane (left when vertical, above when horizontal)
size = 0.5, -- size of the split (0-1 for percentage)
close_on_exit = false, -- close the split when Neovim exits
},
},
--- Actual cli tool config is loaded from the runtime path `sk/cli/{tool}.lua` and merged with the config below.
Expand Down
34 changes: 25 additions & 9 deletions lua/sidekick/cli/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ end
---@overload fun(name: string)
function M.show(opts)
opts = filter_opts(opts)
State.with(function() end, {
State.with(function(state)
if not state.terminal and state.session and not state.session:is_open() then
state.session:show()
end
end, {
all = opts.all,
attach = true,
filter = opts.filter,
Expand All @@ -100,14 +104,22 @@ end
function M.toggle(opts)
opts = filter_opts(opts)
State.with(function(state, attached)
if not state.terminal then
if state.terminal then
if not attached then
state.terminal:toggle()
end
if state.terminal:is_open() and opts.focus ~= false then
state.terminal:focus()
end
return
end
if not attached then
state.terminal:toggle()
end
if state.terminal:is_open() and opts.focus ~= false then
state.terminal:focus()
if state.session then
local open = state.session:is_open()
if open and not attached then
state.session:hide()
elseif not open then
state.session:show()
end
end
end, {
attach = true,
Expand Down Expand Up @@ -142,10 +154,14 @@ end
function M.hide(opts)
opts = filter_opts(opts)
State.with(function(state)
return state.terminal and state.terminal:hide()
if state.terminal then
return state.terminal:hide()
elseif state.session and state.session:is_open() then
return state.session:hide()
end
end, {
all = opts.all,
filter = Util.merge(opts.filter, { terminal = true }),
filter = opts.filter,
})
end

Expand Down
16 changes: 16 additions & 0 deletions lua/sidekick/cli/session/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ function B:attach() end
--- Detach from an existing session
function B:detach() end

--- Whether the session is currently visible.
--- Backends without a visibility concept are always considered open.
---@return boolean
function B:is_open()
return true
end

--- Hide the session from view without stopping it (optional hook)
function B:hide() end

--- Stop the session and remove its pane/window (optional hook)
function B:close() end

--- Bring a hidden session back into view (optional hook)
function B:show() end

--- Start a new session
--- If the backend returns a Cmd, a new terminal session will be spawned
---@return sidekick.cli.terminal.Cmd?
Expand Down
84 changes: 84 additions & 0 deletions lua/sidekick/cli/session/tmux.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,98 @@ function M:start()
elseif Config.cli.mux.create == "split" then
local cmd = { "tmux", "split-window", "-dP", "-c", self.cwd, "-F", PANE_FORMAT }
cmd[#cmd + 1] = Config.cli.mux.split.vertical and "-h" or "-v"
if Config.cli.mux.split.before then
cmd[#cmd + 1] = "-b"
end
local size = Config.cli.mux.split.size
vim.list_extend(cmd, { "-l", tostring(size <= 1 and ((size * 100) .. "%") or size) })
self:add_cmd(cmd)
self:spawn(cmd)
if Config.cli.mux.split.close_on_exit then
self:close_on_exit()
end
Util.info(("Started **%s** in a new tmux split"):format(self.tool.name))
end
end

--- Whether the split pane currently lives in Neovim's tmux window.
--- Hidden panes are broken out into a separate (background) window.
---@return boolean
function M:is_open()
if Config.cli.mux.create ~= "split" then
return true
end
local pane = self:pane_id()
local nvim_pane = vim.env.TMUX_PANE
if not pane or not nvim_pane then
return true
end
local windows = {} ---@type table<string, string>
local lines = Util.exec({ "tmux", "list-panes", "-a", "-F", "#{pane_id} #{window_id}" }, { notify = false })
for _, line in ipairs(lines or {}) do
local p, w = line:match("^(%%%d+) (@%d+)$")
if p then
windows[p] = w
end
end
return windows[pane] ~= nil and windows[pane] == windows[nvim_pane]
end

--- Hide the split by breaking its pane into a detached background window.
function M:hide()
if Config.cli.mux.create ~= "split" then
return
end
local pane = self:pane_id()
if pane then
Util.exec({ "tmux", "break-pane", "-d", "-s", pane }, { notify = false })
end
end

--- Show the split by joining its pane back next to Neovim.
function M:show()
if Config.cli.mux.create ~= "split" then
return
end
local pane = self:pane_id()
local nvim_pane = vim.env.TMUX_PANE
if not pane or not nvim_pane then
return
end
local cmd = { "tmux", "join-pane", "-s", pane, "-t", nvim_pane }
cmd[#cmd + 1] = Config.cli.mux.split.vertical and "-h" or "-v"
if Config.cli.mux.split.before then
cmd[#cmd + 1] = "-b"
end
local size = Config.cli.mux.split.size
vim.list_extend(cmd, { "-l", tostring(size <= 1 and ((size * 100) .. "%") or size) })
Util.exec(cmd, { notify = false })
-- keep focus on Neovim, matching the detached (`-d`) behavior of a fresh split
Util.exec({ "tmux", "select-pane", "-t", nvim_pane }, { notify = false })
end

--- Close the split/window by killing its tmux pane.
function M:close()
local pane = self.tmux_pane_id
if pane then
Util.exec({ "tmux", "kill-pane", "-t", pane }, { notify = false })
end
end

--- Kill the tmux pane when Neovim exits.
function M:close_on_exit()
local pane_id = self.tmux_pane_id
if not pane_id then
return
end
vim.api.nvim_create_autocmd("VimLeavePre", {
once = true,
callback = function()
pcall(Util.exec, { "tmux", "kill-pane", "-t", pane_id }, { notify = false })
end,
})
end

--- Execute the given tmux command and update the session info,
--- based on the first pane returned.
---@param cmd string[]
Expand Down
1 change: 1 addition & 0 deletions lua/sidekick/cli/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function M.detach(state)
if state.terminal then
state.terminal:close()
else
state.session:close()
Session.detach(state.session)
Util.info("Detached from `" .. state.tool.name .. "`")
end
Expand Down
2 changes: 2 additions & 0 deletions lua/sidekick/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ local defaults = {
create = "terminal", ---@type "terminal"|"window"|"split"
split = {
vertical = true, -- vertical or horizontal split
before = false, -- place the split before the current pane (left when vertical, above when horizontal)
size = 0.5, -- size of the split (0-1 for percentage)
close_on_exit = false, -- close the split when Neovim exits
},
-- max lines to capture when dumping a multiplexer pane for scrollback support
-- more lines means slower loading of the scrollback
Expand Down