Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 985b4b1

Browse files
marcinjahnThomasK33
andauthored
feat: add external provider to run Claude in separate terminal (#102)
* feat: add provider: external to run Claude in separate terminal I think it's pretty convenient to have Claude running in a separate window, separate from Neovim window. I think this is particularly useful on tiling window managers. * Update lua/claudecode/terminal/external.lua Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com> * Update lua/claudecode/terminal/external.lua Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com> * Update lua/claudecode/terminal/external.lua Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com> * Fix annotations * fix formatting * Update README.md * Update lua/claudecode/config.lua Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com> --------- Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com>
1 parent 1489c70 commit 985b4b1

File tree

6 files changed

+271
-7
lines changed

6 files changed

+271
-7
lines changed

‎CLAUDE.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The `fixtures/` directory contains test Neovim configurations for verifying plug
6464
3. **Lock File System** (`lua/claudecode/lockfile.lua`) - Creates discovery files for Claude CLI at `~/.claude/ide/`
6565
4. **Selection Tracking** (`lua/claudecode/selection.lua`) - Monitors text selections and sends updates to Claude
6666
5. **Diff Integration** (`lua/claudecode/diff.lua`) - Native Neovim diff support for Claude's file comparisons
67-
6. **Terminal Integration** (`lua/claudecode/terminal.lua`) - Manages Claude CLI terminal sessions
67+
6. **Terminal Integration** (`lua/claudecode/terminal.lua`) - Manages Claude CLI terminal sessions with support for internal Neovim terminals and external terminal applications
6868

6969
### WebSocket Server Implementation
7070

@@ -106,6 +106,28 @@ The WebSocket server implements secure authentication using:
106106

107107
**Format Compliance**: All tools return MCP-compliant format: `{content: [{type: "text", text: "JSON-stringified-data"}]}`
108108

109+
### Terminal Integration Options
110+
111+
**Internal Terminals** (within Neovim):
112+
113+
- **Snacks.nvim**: `terminal/snacks.lua` - Advanced terminal with floating windows
114+
- **Native**: `terminal/native.lua` - Built-in Neovim terminal as fallback
115+
116+
**External Terminals** (separate applications):
117+
118+
- **External Provider**: `terminal/external.lua` - Launches Claude in external terminal apps
119+
120+
**Configuration Example**:
121+
122+
```lua
123+
opts = {
124+
terminal = {
125+
provider = "external", -- "auto", "snacks", "native", or "external"
126+
external_terminal_cmd = "alacritty -e %s" -- Required for external provider
127+
}
128+
}
129+
```
130+
109131
### Key File Locations
110132

111133
- `lua/claudecode/init.lua` - Main entry point and setup

‎README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,14 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
257257
terminal = {
258258
split_side = "right", -- "left" or "right"
259259
split_width_percentage = 0.30,
260-
provider = "auto", -- "auto", "snacks", "native", or custom provider table
260+
provider = "auto", -- "auto", "snacks", "native", "external", or custom provider table
261261
auto_close = true,
262262
snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()` - see Floating Window section below
263+
264+
-- Provider-specific options
265+
provider_opts = {
266+
external_terminal_cmd = nil, -- Command template for external terminal provider (e.g., "alacritty -e %s")
267+
},
263268
},
264269

265270
-- Diff Integration
@@ -440,7 +445,27 @@ For complete configuration options, see:
440445
- [Snacks.nvim Terminal Documentation](https://github.com/folke/snacks.nvim/blob/main/docs/terminal.md)
441446
- [Snacks.nvim Window Documentation](https://github.com/folke/snacks.nvim/blob/main/docs/win.md)
442447

443-
## Custom Terminal Providers
448+
## Terminal Providers
449+
450+
### External Terminal Provider
451+
452+
Run Claude Code in a separate terminal application outside of Neovim:
453+
454+
```lua
455+
{
456+
"coder/claudecode.nvim",
457+
opts = {
458+
terminal = {
459+
provider = "external",
460+
provider_opts = {
461+
external_terminal_cmd = "alacritty -e %s", -- Replace with your preferred terminal program. %s is replaced with claude command
462+
},
463+
},
464+
},
465+
}
466+
```
467+
468+
### Custom Terminal Providers
444469

445470
You can create custom terminal providers by passing a table with the required functions instead of a string provider name:
446471

‎lua/claudecode/config.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ function M.validate(config)
5252

5353
assert(config.terminal_cmd == nil or type(config.terminal_cmd) == "string", "terminal_cmd must be nil or a string")
5454

55+
-- Validate terminal config
56+
assert(type(config.terminal) == "table", "terminal must be a table")
57+
58+
-- Validate provider_opts if present
59+
if config.terminal.provider_opts then
60+
assert(type(config.terminal.provider_opts) == "table", "terminal.provider_opts must be a table")
61+
62+
-- Validate external_terminal_cmd in provider_opts
63+
if config.terminal.provider_opts.external_terminal_cmd then
64+
assert(
65+
type(config.terminal.provider_opts.external_terminal_cmd) == "string",
66+
"terminal.provider_opts.external_terminal_cmd must be a string"
67+
)
68+
if config.terminal.provider_opts.external_terminal_cmd ~= "" then
69+
assert(
70+
config.terminal.provider_opts.external_terminal_cmd:find("%%s"),
71+
"terminal.provider_opts.external_terminal_cmd must contain '%s' placeholder for the Claude command"
72+
)
73+
end
74+
end
75+
end
76+
5577
local valid_log_levels = { "trace", "debug", "info", "warn", "error" }
5678
local is_valid_log_level = false
5779
for _, level in ipairs(valid_log_levels) do

‎lua/claudecode/terminal.lua

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ local defaults = {
1313
provider = "auto",
1414
show_native_term_exit_tip = true,
1515
terminal_cmd = nil,
16+
provider_opts = {
17+
external_terminal_cmd = nil,
18+
},
1619
auto_close = true,
1720
env = {},
1821
snacks_win_opts = {},
@@ -134,6 +137,22 @@ local function get_provider()
134137
else
135138
logger.warn("terminal", "'snacks' provider configured, but Snacks.nvim not available. Falling back to 'native'.")
136139
end
140+
elseif defaults.provider == "external" then
141+
local external_provider = load_provider("external")
142+
if external_provider then
143+
-- Check availability based on our config instead of provider's internal state
144+
local external_cmd = defaults.provider_opts and defaults.provider_opts.external_terminal_cmd
145+
146+
local has_external_cmd = external_cmd and external_cmd ~= "" and external_cmd:find("%%s")
147+
if has_external_cmd then
148+
return external_provider
149+
else
150+
logger.warn(
151+
"terminal",
152+
"'external' provider configured, but provider_opts.external_terminal_cmd not properly set. Falling back to 'native'."
153+
)
154+
end
155+
end
137156
elseif defaults.provider == "native" then
138157
-- noop, will use native provider as default below
139158
logger.debug("terminal", "Using native terminal provider")
@@ -300,12 +319,39 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
300319
end
301320

302321
for k, v in pairs(user_term_config) do
303-
if defaults[k] ~= nil and k ~= "terminal_cmd" then -- terminal_cmd is handled above
322+
if k == "terminal_cmd" then
323+
-- terminal_cmd is handled above, skip
324+
break
325+
elseif k == "provider_opts" then
326+
-- Handle nested provider options
327+
if type(v) == "table" then
328+
defaults[k] = defaults[k] or {}
329+
for opt_k, opt_v in pairs(v) do
330+
if opt_k == "external_terminal_cmd" then
331+
if opt_v == nil or type(opt_v) == "string" then
332+
defaults[k][opt_k] = opt_v
333+
else
334+
vim.notify(
335+
"claudecode.terminal.setup: Invalid value for provider_opts.external_terminal_cmd: " .. tostring(opt_v),
336+
vim.log.levels.WARN
337+
)
338+
end
339+
else
340+
-- For other provider options, just copy them
341+
defaults[k][opt_k] = opt_v
342+
end
343+
end
344+
else
345+
vim.notify("claudecode.terminal.setup: Invalid value for provider_opts: " .. tostring(v), vim.log.levels.WARN)
346+
end
347+
elseif defaults[k] ~= nil then -- Other known config keys
304348
if k == "split_side" and (v == "left" or v == "right") then
305349
defaults[k] = v
306350
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
307351
defaults[k] = v
308-
elseif k == "provider" and (v == "snacks" or v == "native" or v == "auto" or type(v) == "table") then
352+
elseif
353+
k == "provider" and (v == "snacks" or v == "native" or v == "external" or v == "auto" or type(v) == "table")
354+
then
309355
defaults[k] = v
310356
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
311357
defaults[k] = v
@@ -316,7 +362,7 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
316362
else
317363
vim.notify("claudecode.terminal.setup: Invalid value for " .. k .. ": " .. tostring(v), vim.log.levels.WARN)
318364
end
319-
elseifk~="terminal_cmd" then-- Avoid warning for terminal_cmd if passed in user_term_config
365+
else
320366
vim.notify("claudecode.terminal.setup: Unknown configuration key: " .. k, vim.log.levels.WARN)
321367
end
322368
end

‎lua/claudecode/terminal/external.lua

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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

‎lua/claudecode/types.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
---@alias ClaudeCodeSplitSide "left"|"right"
3333

3434
-- In-tree terminal provider names
35-
---@alias ClaudeCodeTerminalProviderName "auto"|"snacks"|"native"
35+
---@alias ClaudeCodeTerminalProviderName "auto"|"snacks"|"native"|"external"
36+
37+
-- Terminal provider-specific options
38+
---@class ClaudeCodeTerminalProviderOptions
39+
---@field external_terminal_cmd string? Command template for external terminal (e.g., "alacritty -e %s")
3640

3741
-- @ mention queued for Claude Code
3842
---@class ClaudeCodeMention
@@ -61,6 +65,7 @@
6165
---@field provider ClaudeCodeTerminalProviderName|ClaudeCodeTerminalProvider
6266
---@field show_native_term_exit_tip boolean
6367
---@field terminal_cmd string?
68+
---@field provider_opts ClaudeCodeTerminalProviderOptions?
6469
---@field auto_close boolean
6570
---@field env table<string, string>
6671
---@field snacks_win_opts table

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /