Skip to content

Commit 52fff7b

Browse files
committed
feat: add "none" terminal provider option for external terminal management
Change-Id: I92e2074200ee47e6753cf929d428a29d2564eb31 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 3e2601f commit 52fff7b

File tree

5 files changed

+176
-4
lines changed

5 files changed

+176
-4
lines changed

README.md

Lines changed: 21 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,26 @@ 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+
508+
- No windows/buffers are created. `:ClaudeCode` and related commands will not open anything.
509+
- The WebSocket server still starts and broadcasts work as usual. Launch the Claude CLI externally when desired.
510+
491511
### External Terminal Provider
492512

493513
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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
---Setup the no-op provider
10+
---@param term_config ClaudeCodeTerminalConfig
11+
function M.setup(term_config)
12+
-- intentionally no-op
13+
end
14+
15+
---Open terminal (no-op)
16+
---@param cmd_string string
17+
---@param env_table table
18+
---@param effective_config ClaudeCodeTerminalConfig
19+
---@param focus boolean|nil
20+
function M.open(cmd_string, env_table, effective_config, focus)
21+
-- intentionally no-op
22+
end
23+
24+
---Close terminal (no-op)
25+
function M.close()
26+
-- intentionally no-op
27+
end
28+
29+
---Simple toggle (no-op)
30+
---@param cmd_string string
31+
---@param env_table table
32+
---@param effective_config ClaudeCodeTerminalConfig
33+
function M.simple_toggle(cmd_string, env_table, effective_config)
34+
-- intentionally no-op
35+
end
36+
37+
---Focus toggle (no-op)
38+
---@param cmd_string string
39+
---@param env_table table
40+
---@param effective_config ClaudeCodeTerminalConfig
41+
function M.focus_toggle(cmd_string, env_table, effective_config)
42+
-- intentionally no-op
43+
end
44+
45+
---Legacy toggle (no-op)
46+
---@param cmd_string string
47+
---@param env_table table
48+
---@param effective_config ClaudeCodeTerminalConfig
49+
function M.toggle(cmd_string, env_table, effective_config)
50+
-- intentionally no-op
51+
end
52+
53+
---Ensure visible (no-op)
54+
function M.ensure_visible() end
55+
56+
---Return active buffer number (always nil)
57+
---@return number|nil
58+
function M.get_active_bufnr()
59+
return nil
60+
end
61+
62+
---Provider availability (always true; explicit opt-in required)
63+
---@return boolean
64+
function M.is_available()
65+
return true
66+
end
67+
68+
---Testing hook (no state to return)
69+
---@return table|nil
70+
function M._get_terminal_for_test()
71+
return nil
72+
end
73+
74+
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
require("tests.busted_setup")
2+
require("tests.mocks.vim")
3+
4+
describe("none terminal provider", function()
5+
local terminal
6+
7+
local termopen_calls
8+
local jobstart_calls
9+
10+
before_each(function()
11+
-- Prepare vim.fn helpers used by terminal module
12+
vim.fn = vim.fn or {}
13+
vim.fn.getcwd = function()
14+
return "/mock/cwd"
15+
end
16+
vim.fn.expand = function(val)
17+
return val
18+
end
19+
20+
-- Spy-able termopen/jobstart that count invocations
21+
termopen_calls = 0
22+
jobstart_calls = 0
23+
vim.fn.termopen = function(...)
24+
termopen_calls = termopen_calls + 1
25+
return 1
26+
end
27+
vim.fn.jobstart = function(...)
28+
jobstart_calls = jobstart_calls + 1
29+
return 1
30+
end
31+
32+
-- Minimal logger + server mocks
33+
package.loaded["claudecode.logger"] = {
34+
debug = function() end,
35+
warn = function() end,
36+
error = function() end,
37+
info = function() end,
38+
setup = function() end,
39+
}
40+
package.loaded["claudecode.server.init"] = { state = { port = 12345 } }
41+
42+
-- Ensure fresh terminal module load
43+
package.loaded["claudecode.terminal"] = nil
44+
package.loaded["claudecode.terminal.none"] = nil
45+
package.loaded["claudecode.terminal.native"] = nil
46+
package.loaded["claudecode.terminal.snacks"] = nil
47+
48+
terminal = require("claudecode.terminal")
49+
terminal.setup({ provider = "none" }, nil, {})
50+
end)
51+
52+
it("does not invoke any terminal APIs", function()
53+
-- Exercise all public actions
54+
terminal.open({}, "--help")
55+
terminal.simple_toggle({}, "--resume")
56+
terminal.focus_toggle({}, "--continue")
57+
terminal.ensure_visible({}, nil)
58+
terminal.toggle_open_no_focus({}, nil)
59+
terminal.close()
60+
61+
-- Assert no terminal processes/windows were spawned
62+
assert.are.equal(0, termopen_calls)
63+
assert.are.equal(0, jobstart_calls)
64+
end)
65+
66+
it("returns nil for active buffer", function()
67+
local bufnr = terminal.get_active_terminal_bufnr()
68+
assert.is_nil(bufnr)
69+
end)
70+
end)

0 commit comments

Comments
 (0)