Skip to content

Commit e64f19b

Browse files
committed
feat: add "none" terminal provider option for external terminal management
Change-Id: I92e2074200ee47e6753cf929d428a29d2564eb31 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 3e2601f commit e64f19b

File tree

5 files changed

+179
-4
lines changed

5 files changed

+179
-4
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
261261
terminal = {
262262
split_side = "right", -- "left" or "right"
263263
split_width_percentage = 0.30,
264-
provider = "auto", -- "auto", "snacks", "native", "external", or custom provider table
264+
provider = "auto", -- "auto", "snacks", "native", "external", "none", or custom provider table
265265
auto_close = true,
266266
snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()` - see Floating Window section below
267267

@@ -488,6 +488,25 @@ For complete configuration options, see:
488488

489489
## Terminal Providers
490490

491+
### None (No-Op) Provider
492+
493+
Run Claude Code without any terminal management inside Neovim. This is useful for advanced setups where you manage the CLI externally (tmux, kitty, separate terminal windows) while still using the WebSocket server and tools.
494+
495+
```lua
496+
{
497+
"coder/claudecode.nvim",
498+
opts = {
499+
terminal = {
500+
provider = "none", -- no UI actions; server + tools remain available
501+
},
502+
},
503+
}
504+
```
505+
506+
Notes:
507+
- No windows/buffers are created. `:ClaudeCode` and related commands will not open anything.
508+
- The WebSocket server still starts and broadcasts work as usual. Launch the Claude CLI externally when desired.
509+
491510
### External Terminal Provider
492511

493512
Run Claude Code in a separate terminal application outside of Neovim:

lua/claudecode/terminal.lua

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ local function get_provider()
166166
elseif defaults.provider == "native" then
167167
-- noop, will use native provider as default below
168168
logger.debug("terminal", "Using native terminal provider")
169+
elseif defaults.provider == "none" then
170+
local none_provider = load_provider("none")
171+
if none_provider then
172+
logger.debug("terminal", "Using no-op terminal provider ('none')")
173+
return none_provider
174+
else
175+
logger.warn("terminal", "'none' provider configured but failed to load. Falling back to 'native'.")
176+
end
169177
elseif type(defaults.provider) == "string" then
170178
logger.warn(
171179
"terminal",
@@ -394,7 +402,7 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
394402
)
395403
end
396404
elseif k == "provider" then
397-
if type(v) == "table" or v == "snacks" or v == "native" or v == "external" or v == "auto" then
405+
if type(v) == "table" or v == "snacks" or v == "native" or v == "external" or v == "auto" or v == "none" then
398406
defaults.provider = v
399407
else
400408
vim.notify(

lua/claudecode/terminal/none.lua

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
--- No-op terminal provider for Claude Code.
2+
--- Performs zero UI actions and never manages terminals inside Neovim.
3+
---@module 'claudecode.terminal.none'
4+
5+
---@type ClaudeCodeTerminalProvider
6+
local M = {}
7+
8+
---Stored config (not used, but kept for parity with other providers)
9+
---@type ClaudeCodeTerminalConfig|nil
10+
local config
11+
12+
---Setup the no-op provider
13+
---@param term_config ClaudeCodeTerminalConfig
14+
function M.setup(term_config)
15+
config = term_config
16+
end
17+
18+
---Open terminal (no-op)
19+
---@param cmd_string string
20+
---@param env_table table
21+
---@param effective_config ClaudeCodeTerminalConfig
22+
---@param focus boolean|nil
23+
function M.open(cmd_string, env_table, effective_config, focus)
24+
-- intentionally no-op
25+
end
26+
27+
---Close terminal (no-op)
28+
function M.close()
29+
-- intentionally no-op
30+
end
31+
32+
---Simple toggle (no-op)
33+
---@param cmd_string string
34+
---@param env_table table
35+
---@param effective_config ClaudeCodeTerminalConfig
36+
function M.simple_toggle(cmd_string, env_table, effective_config)
37+
-- intentionally no-op
38+
end
39+
40+
---Focus toggle (no-op)
41+
---@param cmd_string string
42+
---@param env_table table
43+
---@param effective_config ClaudeCodeTerminalConfig
44+
function M.focus_toggle(cmd_string, env_table, effective_config)
45+
-- intentionally no-op
46+
end
47+
48+
---Legacy toggle (no-op)
49+
---@param cmd_string string
50+
---@param env_table table
51+
---@param effective_config ClaudeCodeTerminalConfig
52+
function M.toggle(cmd_string, env_table, effective_config)
53+
-- intentionally no-op
54+
end
55+
56+
---Ensure visible (no-op)
57+
function M.ensure_visible() end
58+
59+
---Return active buffer number (always nil)
60+
---@return number|nil
61+
function M.get_active_bufnr()
62+
return nil
63+
end
64+
65+
---Provider availability (always true; explicit opt-in required)
66+
---@return boolean
67+
function M.is_available()
68+
return true
69+
end
70+
71+
---Testing hook (no state to return)
72+
---@return table|nil
73+
function M._get_terminal_for_test()
74+
return nil
75+
end
76+
77+
return M

lua/claudecode/types.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
---@alias ClaudeCodeSplitSide "left"|"right"
4040

4141
-- In-tree terminal provider names
42-
---@alias ClaudeCodeTerminalProviderName "auto"|"snacks"|"native"|"external"
42+
---@alias ClaudeCodeTerminalProviderName "auto"|"snacks"|"native"|"external"|"none"
4343

4444
-- Terminal provider-specific options
4545
---@class ClaudeCodeTerminalProviderOptions
46-
---@field external_terminal_cmd string|fun(cmd: string, env: table): string|table|nil Command for external terminal (string template with %s or function)
46+
---@field external_terminal_cmd string|(fun(cmd: string, env: table): string)|table|nil Command for external terminal (string template with %s or function)
4747

4848
-- Working directory resolution context and provider
4949
---@class ClaudeCodeCwdContext
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
require("tests.busted_setup")
2+
require("tests.mocks.vim")
3+
4+
describe("none terminal provider", function()
5+
local terminal
6+
local saved_packages
7+
8+
local termopen_calls
9+
local jobstart_calls
10+
11+
before_each(function()
12+
-- Prepare vim.fn helpers used by terminal module
13+
vim.fn = vim.fn or {}
14+
vim.fn.getcwd = function()
15+
return "/mock/cwd"
16+
end
17+
vim.fn.expand = function(val)
18+
return val
19+
end
20+
21+
-- Spy-able termopen/jobstart that count invocations
22+
termopen_calls = 0
23+
jobstart_calls = 0
24+
vim.fn.termopen = function(...)
25+
termopen_calls = termopen_calls + 1
26+
return 1
27+
end
28+
vim.fn.jobstart = function(...)
29+
jobstart_calls = jobstart_calls + 1
30+
return 1
31+
end
32+
33+
-- Minimal logger + server mocks
34+
package.loaded["claudecode.logger"] = {
35+
debug = function() end,
36+
warn = function() end,
37+
error = function() end,
38+
info = function() end,
39+
setup = function() end,
40+
}
41+
package.loaded["claudecode.server.init"] = { state = { port = 12345 } }
42+
43+
-- Ensure fresh terminal module load
44+
package.loaded["claudecode.terminal"] = nil
45+
package.loaded["claudecode.terminal.none"] = nil
46+
package.loaded["claudecode.terminal.native"] = nil
47+
package.loaded["claudecode.terminal.snacks"] = nil
48+
49+
terminal = require("claudecode.terminal")
50+
terminal.setup({ provider = "none" }, nil, {})
51+
end)
52+
53+
it("does not invoke any terminal APIs", function()
54+
-- Exercise all public actions
55+
terminal.open({}, "--help")
56+
terminal.simple_toggle({}, "--resume")
57+
terminal.focus_toggle({}, "--continue")
58+
terminal.ensure_visible({}, nil)
59+
terminal.toggle_open_no_focus({}, nil)
60+
terminal.close()
61+
62+
-- Assert no terminal processes/windows were spawned
63+
assert.are.equal(0, termopen_calls)
64+
assert.are.equal(0, jobstart_calls)
65+
end)
66+
67+
it("returns nil for active buffer", function()
68+
local bufnr = terminal.get_active_terminal_bufnr()
69+
assert.is_nil(bufnr)
70+
end)
71+
end)

0 commit comments

Comments
 (0)