You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
7.9 KiB
Lua
271 lines
7.9 KiB
Lua
-- Tmux interaction module for nvim-claude
|
|
local M = {}
|
|
local utils = require('nvim-claude.utils')
|
|
|
|
M.config = {}
|
|
|
|
function M.setup(config)
|
|
M.config = config or {}
|
|
end
|
|
|
|
-- Find Claude pane by checking running command
|
|
function M.find_claude_pane()
|
|
-- Get list of panes with their PIDs and current command
|
|
local cmd = "tmux list-panes -F '#{pane_id}:#{pane_pid}:#{pane_title}:#{pane_current_command}'"
|
|
local result = utils.exec(cmd)
|
|
|
|
if result and result ~= '' then
|
|
-- Check each pane
|
|
for line in result:gmatch('[^\n]+') do
|
|
local pane_id, pane_pid, pane_title, pane_cmd = line:match('^([^:]+):([^:]+):([^:]*):(.*)$')
|
|
if pane_id and pane_pid then
|
|
-- Check by title first (our created panes)
|
|
if pane_title and pane_title == (M.config.pane_title or 'claude-chat') then
|
|
return pane_id
|
|
end
|
|
|
|
-- Check if title contains Claude Code indicators (Anthropic symbol, "claude", "Claude")
|
|
if pane_title and (pane_title:match('✳') or pane_title:match('[Cc]laude')) then
|
|
return pane_id
|
|
end
|
|
|
|
-- Check if current command is claude-related
|
|
if pane_cmd and pane_cmd:match('claude') then
|
|
return pane_id
|
|
end
|
|
|
|
-- Check if any child process of this pane is running claude or claude-code
|
|
-- This handles cases where claude is run under a shell
|
|
local check_cmd = string.format(
|
|
"ps -ef | awk '$3 == %s' | grep -c -E '(claude|claude-code)' 2>/dev/null",
|
|
pane_pid
|
|
)
|
|
local count_result = utils.exec(check_cmd)
|
|
if count_result and tonumber(count_result) and tonumber(count_result) > 0 then
|
|
return pane_id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Create new tmux pane for Claude (or return existing)
|
|
function M.create_pane(command)
|
|
local existing = M.find_claude_pane()
|
|
if existing then
|
|
-- Just select the existing pane, don't create a new one
|
|
local _, err = utils.exec('tmux select-pane -t ' .. existing)
|
|
if err then
|
|
-- Pane might have been closed, continue to create new one
|
|
vim.notify('Claude pane no longer exists, creating new one', vim.log.levels.INFO)
|
|
else
|
|
return existing
|
|
end
|
|
end
|
|
|
|
-- Build size option safely
|
|
local size_opt = ''
|
|
if M.config.split_size and tonumber(M.config.split_size) then
|
|
local size = tonumber(M.config.split_size)
|
|
if utils.tmux_supports_length_percent() then
|
|
size_opt = '-l ' .. tostring(size) .. '%'
|
|
else
|
|
size_opt = '-p ' .. tostring(size)
|
|
end
|
|
end
|
|
|
|
local split_cmd = M.config.split_direction == 'v' and 'split-window -v' or 'split-window -h'
|
|
|
|
-- Build command parts to avoid double spaces
|
|
local parts = { 'tmux', split_cmd }
|
|
if size_opt ~= '' then table.insert(parts, size_opt) end
|
|
table.insert(parts, '-P')
|
|
local cmd = table.concat(parts, ' ')
|
|
if command then
|
|
cmd = cmd .. " '" .. command .. "'"
|
|
end
|
|
|
|
local result, err = utils.exec(cmd)
|
|
if err or not result or result == '' or result:match('error') then
|
|
vim.notify('nvim-claude: tmux split failed: ' .. (err or result or 'unknown'), vim.log.levels.ERROR)
|
|
return nil
|
|
end
|
|
|
|
local pane_id = result:gsub('\n', '')
|
|
utils.exec(string.format("tmux select-pane -t %s -T '%s'", pane_id, M.config.pane_title or 'claude-chat'))
|
|
|
|
-- Launch command in the new pane if provided
|
|
if command and command ~= '' then
|
|
M.send_to_pane(pane_id, command)
|
|
end
|
|
|
|
return pane_id
|
|
end
|
|
|
|
-- Send keys to a pane (single line with Enter)
|
|
function M.send_to_pane(pane_id, text)
|
|
if not pane_id then return false end
|
|
|
|
-- Escape single quotes in text
|
|
text = text:gsub("'", "'\"'\"'")
|
|
|
|
local cmd = string.format(
|
|
"tmux send-keys -t %s '%s' Enter",
|
|
pane_id,
|
|
text
|
|
)
|
|
|
|
local _, err = utils.exec(cmd)
|
|
return err == nil
|
|
end
|
|
|
|
-- Send multi-line text to a pane (for batched content)
|
|
function M.send_text_to_pane(pane_id, text)
|
|
if not pane_id then return false end
|
|
|
|
-- Create a temporary file to hold the text
|
|
local tmpfile = os.tmpname()
|
|
local file = io.open(tmpfile, 'w')
|
|
if not file then
|
|
vim.notify('Failed to create temporary file for text', vim.log.levels.ERROR)
|
|
return false
|
|
end
|
|
|
|
file:write(text)
|
|
file:close()
|
|
|
|
-- Use tmux load-buffer and paste-buffer to send the content
|
|
local cmd = string.format(
|
|
"tmux load-buffer -t %s '%s' && tmux paste-buffer -t %s && rm '%s'",
|
|
pane_id, tmpfile, pane_id, tmpfile
|
|
)
|
|
|
|
local _, err = utils.exec(cmd)
|
|
if err then
|
|
-- Clean up temp file on error
|
|
os.remove(tmpfile)
|
|
vim.notify('Failed to send text to pane: ' .. err, vim.log.levels.ERROR)
|
|
return false
|
|
end
|
|
|
|
-- Don't send Enter after pasting to avoid submitting the message
|
|
-- User can manually submit when ready
|
|
|
|
return true
|
|
end
|
|
|
|
-- Create new tmux window for agent
|
|
function M.create_agent_window(name, cwd)
|
|
local base_cmd = string.format("tmux new-window -n '%s'", name)
|
|
if cwd and cwd ~= '' then
|
|
base_cmd = base_cmd .. string.format(" -c '%s'", cwd)
|
|
end
|
|
|
|
local cmd_with_fmt = base_cmd .. " -P -F '#{window_id}'"
|
|
|
|
-- Try with -F first (preferred)
|
|
local result, err = utils.exec(cmd_with_fmt)
|
|
if not err and result and result ~= '' and not result:match('error') then
|
|
return result:gsub('\n', '')
|
|
end
|
|
|
|
-- Fallback: simple -P
|
|
local cmd_simple = base_cmd .. ' -P'
|
|
result, err = utils.exec(cmd_simple)
|
|
if not err and result and result ~= '' then
|
|
return result:gsub('\n', '')
|
|
end
|
|
|
|
vim.notify('nvim-claude: tmux new-window failed: ' .. (err or result or 'unknown'), vim.log.levels.ERROR)
|
|
return nil
|
|
end
|
|
|
|
-- Send keys to an entire window (select-pane 0)
|
|
function M.send_to_window(window_id, text)
|
|
if not window_id then return false end
|
|
text = text:gsub("'", "'\"'\"'")
|
|
-- Send to the window's active pane (no .0 suffix)
|
|
local cmd = string.format("tmux send-keys -t %s '%s' Enter", window_id, text)
|
|
local _, err = utils.exec(cmd)
|
|
return err == nil
|
|
end
|
|
|
|
-- Switch to window
|
|
function M.switch_to_window(window_id)
|
|
local cmd = 'tmux select-window -t ' .. window_id
|
|
local _, err = utils.exec(cmd)
|
|
return err == nil
|
|
end
|
|
|
|
-- Kill pane or window
|
|
function M.kill_pane(pane_id)
|
|
local cmd = 'tmux kill-pane -t ' .. pane_id
|
|
local _, err = utils.exec(cmd)
|
|
return err == nil
|
|
end
|
|
|
|
-- Check if tmux is running
|
|
function M.is_inside_tmux()
|
|
-- Check environment variable first
|
|
if os.getenv('TMUX') then
|
|
return true
|
|
end
|
|
|
|
-- Fallback: try to get current session name
|
|
local result = utils.exec('tmux display-message -p "#{session_name}" 2>/dev/null')
|
|
return result and result ~= '' and not result:match('error')
|
|
end
|
|
|
|
-- Validate tmux availability
|
|
function M.validate()
|
|
if not utils.has_tmux() then
|
|
vim.notify('tmux not found. Please install tmux.', vim.log.levels.ERROR)
|
|
return false
|
|
end
|
|
|
|
if not M.is_inside_tmux() then
|
|
vim.notify('Not inside tmux session. Please run nvim inside tmux.', vim.log.levels.ERROR)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Split a given window and return the new pane id
|
|
-- direction: 'h' (horizontal/right) or 'v' (vertical/bottom)
|
|
-- size_percent: number (percentage)
|
|
function M.split_window(window_id, direction, size_percent)
|
|
direction = direction == 'v' and '-v' or '-h'
|
|
|
|
local size_opt = ''
|
|
if size_percent and tonumber(size_percent) then
|
|
local size = tonumber(size_percent)
|
|
if utils.tmux_supports_length_percent() then
|
|
size_opt = string.format('-l %s%%', size)
|
|
else
|
|
size_opt = string.format('-p %s', size)
|
|
end
|
|
end
|
|
|
|
-- Build command
|
|
local parts = {
|
|
'tmux',
|
|
'split-window',
|
|
direction,
|
|
}
|
|
if size_opt ~= '' then table.insert(parts, size_opt) end
|
|
table.insert(parts, '-P -F "#{pane_id}"')
|
|
table.insert(parts, '-t ' .. window_id)
|
|
|
|
local cmd = table.concat(parts, ' ')
|
|
local pane_id, err = utils.exec(cmd)
|
|
if err or not pane_id or pane_id == '' then
|
|
vim.notify('nvim-claude: tmux split-window failed: ' .. (err or pane_id or 'unknown'), vim.log.levels.ERROR)
|
|
return nil
|
|
end
|
|
return pane_id:gsub('\n', '')
|
|
end
|
|
|
|
return M |