|
| 1 | +--- External terminal provider for Claude Code. |
| 2 | +---Launches Claude Code in an external terminal application using a user-specified command. |
| 3 | +---@module 'claudecode.terminal.external' |
| 4 | + |
| 5 | +---@type ClaudeCodeTerminalProvider |
| 6 | +local M = {} |
| 7 | + |
| 8 | +local logger = require("claudecode.logger") |
| 9 | + |
| 10 | +local jobid = nil |
| 11 | +---@type ClaudeCodeTerminalConfig |
| 12 | +local config |
| 13 | + |
| 14 | +local function cleanup_state() |
| 15 | + jobid = nil |
| 16 | +end |
| 17 | + |
| 18 | +local function is_valid() |
| 19 | + -- For external terminals, we only track if we have a running job |
| 20 | + -- We don't manage terminal windows since they're external |
| 21 | + return jobid and jobid > 0 |
| 22 | +end |
| 23 | + |
| 24 | +---@param term_config ClaudeCodeTerminalConfig |
| 25 | +function M.setup(term_config) |
| 26 | + config = term_config or {} |
| 27 | +end |
| 28 | + |
| 29 | +---@param cmd_string string |
| 30 | +---@param env_table table |
| 31 | +function M.open(cmd_string, env_table) |
| 32 | + if is_valid() then |
| 33 | + -- External terminal is already running, we can't focus it programmatically |
| 34 | + -- Just log that it's already running |
| 35 | + logger.debug("terminal", "External Claude terminal is already running") |
| 36 | + return |
| 37 | + end |
| 38 | + |
| 39 | + -- Get external terminal command from provider_opts |
| 40 | + local external_cmd = config.provider_opts and config.provider_opts.external_terminal_cmd |
| 41 | + |
| 42 | + if not external_cmd or external_cmd == "" then |
| 43 | + vim.notify( |
| 44 | + "external_terminal_cmd not configured. Please set terminal.provider_opts.external_terminal_cmd in your config.", |
| 45 | + vim.log.levels.ERROR |
| 46 | + ) |
| 47 | + return |
| 48 | + end |
| 49 | + |
| 50 | + -- Replace %s in the template with the Claude command |
| 51 | + if not external_cmd:find("%%s") then |
| 52 | + vim.notify("external_terminal_cmd must contain '%s' placeholder for the Claude command.", vim.log.levels.ERROR) |
| 53 | + return |
| 54 | + end |
| 55 | + |
| 56 | + -- Build command by replacing %s with Claude command and splitting |
| 57 | + local full_command = string.format(external_cmd, cmd_string) |
| 58 | + local cmd_parts = vim.split(full_command, " ") |
| 59 | + |
| 60 | + -- Start the external terminal as a detached process |
| 61 | + jobid = vim.fn.jobstart(cmd_parts, { |
| 62 | + detach = true, |
| 63 | + env = env_table, |
| 64 | + on_exit = function(job_id, exit_code, _) |
| 65 | + vim.schedule(function() |
| 66 | + if job_id == jobid then |
| 67 | + cleanup_state() |
| 68 | + end |
| 69 | + end) |
| 70 | + end, |
| 71 | + }) |
| 72 | + |
| 73 | + if not jobid or jobid <= 0 then |
| 74 | + vim.notify("Failed to start external terminal with command: " .. full_command, vim.log.levels.ERROR) |
| 75 | + cleanup_state() |
| 76 | + return |
| 77 | + end |
| 78 | +end |
| 79 | + |
| 80 | +function M.close() |
| 81 | + if is_valid() then |
| 82 | + -- Try to stop the job gracefully |
| 83 | + vim.fn.jobstop(jobid) |
| 84 | + cleanup_state() |
| 85 | + end |
| 86 | +end |
| 87 | + |
| 88 | +--- Simple toggle: always start external terminal (can't hide external terminals) |
| 89 | +---@param cmd_string string |
| 90 | +---@param env_table table |
| 91 | +---@param effective_config table |
| 92 | +function M.simple_toggle(cmd_string, env_table, effective_config) |
| 93 | + if is_valid() then |
| 94 | + -- External terminal is running, stop it |
| 95 | + M.close() |
| 96 | + else |
| 97 | + -- Start external terminal |
| 98 | + M.open(cmd_string, env_table, effective_config, true) |
| 99 | + end |
| 100 | +end |
| 101 | + |
| 102 | +--- Smart focus toggle: same as simple toggle for external terminals |
| 103 | +---@param cmd_string string |
| 104 | +---@param env_table table |
| 105 | +---@param effective_config table |
| 106 | +function M.focus_toggle(cmd_string, env_table, effective_config) |
| 107 | + -- For external terminals, focus toggle behaves the same as simple toggle |
| 108 | + -- since we can't detect or control focus of external windows |
| 109 | + M.simple_toggle(cmd_string, env_table, effective_config) |
| 110 | +end |
| 111 | + |
| 112 | +--- Legacy toggle function for backward compatibility |
| 113 | +---@param cmd_string string |
| 114 | +---@param env_table table |
| 115 | +---@param effective_config table |
| 116 | +function M.toggle(cmd_string, env_table, effective_config) |
| 117 | + M.simple_toggle(cmd_string, env_table, effective_config) |
| 118 | +end |
| 119 | + |
| 120 | +---@return number? |
| 121 | +function M.get_active_bufnr() |
| 122 | + -- External terminals don't have associated Neovim buffers |
| 123 | + return nil |
| 124 | +end |
| 125 | + |
| 126 | +--- No-op function for external terminals since we can't ensure visibility of external windows |
| 127 | +function M.ensure_visible() end |
| 128 | + |
| 129 | +---@return boolean |
| 130 | +function M.is_available() |
| 131 | + -- Availability is checked by terminal.lua before this provider is selected |
| 132 | + return true |
| 133 | +end |
| 134 | + |
| 135 | +---@return table? |
| 136 | +function M._get_terminal_for_test() |
| 137 | + -- For testing purposes, return job info if available |
| 138 | + if is_valid() then |
| 139 | + return { jobid = jobid } |
| 140 | + end |
| 141 | + return nil |
| 142 | +end |
| 143 | + |
| 144 | +return M |
0 commit comments