Skip to content

Commit 5d7ab85

Browse files
authored
feat: add support for custom terminal providers (#91)
Change-Id: I2f559e355aa6036ca94b8aca13d53739c6b5e021 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 7573e8e commit 5d7ab85

File tree

4 files changed

+725
-15
lines changed

4 files changed

+725
-15
lines changed

README.md

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,6 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
130130

131131
## Advanced Configuration
132132

133-
<details>
134-
<summary>Complete configuration options</summary>
135-
136133
```lua
137134
{
138135
"coder/claudecode.nvim",
@@ -152,7 +149,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
152149
terminal = {
153150
split_side = "right", -- "left" or "right"
154151
split_width_percentage = 0.30,
155-
provider = "auto", -- "auto", "snacks", or "native"
152+
provider = "auto", -- "auto", "snacks", "native", or custom provider table
156153
auto_close = true,
157154
snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()`
158155
},
@@ -170,7 +167,119 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
170167
}
171168
```
172169

173-
</details>
170+
## Custom Terminal Providers
171+
172+
You can create custom terminal providers by passing a table with the required functions instead of a string provider name:
173+
174+
```lua
175+
require("claudecode").setup({
176+
terminal = {
177+
provider = {
178+
-- Required functions
179+
setup = function(config)
180+
-- Initialize your terminal provider
181+
end,
182+
183+
open = function(cmd_string, env_table, effective_config, focus)
184+
-- Open terminal with command and environment
185+
-- focus parameter controls whether to focus terminal (defaults to true)
186+
end,
187+
188+
close = function()
189+
-- Close the terminal
190+
end,
191+
192+
simple_toggle = function(cmd_string, env_table, effective_config)
193+
-- Simple show/hide toggle
194+
end,
195+
196+
focus_toggle = function(cmd_string, env_table, effective_config)
197+
-- Smart toggle: focus terminal if not focused, hide if focused
198+
end,
199+
200+
get_active_bufnr = function()
201+
-- Return terminal buffer number or nil
202+
return 123 -- example
203+
end,
204+
205+
is_available = function()
206+
-- Return true if provider can be used
207+
return true
208+
end,
209+
210+
-- Optional functions (auto-generated if not provided)
211+
toggle = function(cmd_string, env_table, effective_config)
212+
-- Defaults to calling simple_toggle for backward compatibility
213+
end,
214+
215+
_get_terminal_for_test = function()
216+
-- For testing only, defaults to return nil
217+
return nil
218+
end,
219+
},
220+
},
221+
})
222+
```
223+
224+
### Custom Provider Example
225+
226+
Here's a complete example using a hypothetical `my_terminal` plugin:
227+
228+
```lua
229+
local my_terminal_provider = {
230+
setup = function(config)
231+
-- Store config for later use
232+
self.config = config
233+
end,
234+
235+
open = function(cmd_string, env_table, effective_config, focus)
236+
if focus == nil then focus = true end
237+
238+
local my_terminal = require("my_terminal")
239+
my_terminal.open({
240+
cmd = cmd_string,
241+
env = env_table,
242+
width = effective_config.split_width_percentage,
243+
side = effective_config.split_side,
244+
focus = focus,
245+
})
246+
end,
247+
248+
close = function()
249+
require("my_terminal").close()
250+
end,
251+
252+
simple_toggle = function(cmd_string, env_table, effective_config)
253+
require("my_terminal").toggle()
254+
end,
255+
256+
focus_toggle = function(cmd_string, env_table, effective_config)
257+
local my_terminal = require("my_terminal")
258+
if my_terminal.is_focused() then
259+
my_terminal.hide()
260+
else
261+
my_terminal.focus()
262+
end
263+
end,
264+
265+
get_active_bufnr = function()
266+
return require("my_terminal").get_bufnr()
267+
end,
268+
269+
is_available = function()
270+
local ok, _ = pcall(require, "my_terminal")
271+
return ok
272+
end,
273+
}
274+
275+
require("claudecode").setup({
276+
terminal = {
277+
provider = my_terminal_provider,
278+
},
279+
})
280+
```
281+
282+
The custom provider will automatically fall back to the native provider if validation fails or `is_available()` returns false.
174283

175284
## Troubleshooting
176285

lua/claudecode/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ M.state = {
8888
---@alias ClaudeCode.TerminalOpts { \
8989
--- split_side?: "left"|"right", \
9090
--- split_width_percentage?: number, \
91-
--- provider?: "auto"|"snacks"|"native", \
91+
--- provider?: "auto"|"snacks"|"native"|table, \
9292
--- show_native_term_exit_tip?: boolean, \
9393
--- snacks_win_opts?: table }
9494
---

lua/claudecode/terminal.lua

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,88 @@ local function load_provider(provider_name)
4646
return providers[provider_name]
4747
end
4848

49+
--- Validates and enhances a custom table provider with smart defaults
50+
--- @param provider table The custom provider table to validate
51+
--- @return TerminalProvider|nil provider The enhanced provider, or nil if invalid
52+
--- @return string|nil error Error message if validation failed
53+
local function validate_and_enhance_provider(provider)
54+
if type(provider) ~= "table" then
55+
return nil, "Custom provider must be a table"
56+
end
57+
58+
-- Required functions that must be implemented
59+
local required_functions = {
60+
"setup",
61+
"open",
62+
"close",
63+
"simple_toggle",
64+
"focus_toggle",
65+
"get_active_bufnr",
66+
"is_available",
67+
}
68+
69+
-- Validate all required functions exist and are callable
70+
for _, func_name in ipairs(required_functions) do
71+
local func = provider[func_name]
72+
if not func then
73+
return nil, "Custom provider missing required function: " .. func_name
74+
end
75+
-- Check if it's callable (function or table with __call metamethod)
76+
local is_callable = type(func) == "function"
77+
or (type(func) == "table" and getmetatable(func) and getmetatable(func).__call)
78+
if not is_callable then
79+
return nil, "Custom provider field '" .. func_name .. "' must be callable, got: " .. type(func)
80+
end
81+
end
82+
83+
-- Create enhanced provider with defaults for optional functions
84+
-- Note: Don't deep copy to preserve spy functions in tests
85+
local enhanced_provider = provider
86+
87+
-- Add default toggle function if not provided (calls simple_toggle for backward compatibility)
88+
if not enhanced_provider.toggle then
89+
enhanced_provider.toggle = function(cmd_string, env_table, effective_config)
90+
return enhanced_provider.simple_toggle(cmd_string, env_table, effective_config)
91+
end
92+
end
93+
94+
-- Add default test function if not provided
95+
if not enhanced_provider._get_terminal_for_test then
96+
enhanced_provider._get_terminal_for_test = function()
97+
return nil
98+
end
99+
end
100+
101+
return enhanced_provider, nil
102+
end
103+
49104
--- Gets the effective terminal provider, guaranteed to return a valid provider
50105
--- Falls back to native provider if configured provider is unavailable
51106
--- @return TerminalProvider provider The terminal provider module (never nil)
52107
local function get_provider()
53108
local logger = require("claudecode.logger")
54109

55-
if config.provider == "auto" then
110+
-- Handle custom table provider
111+
if type(config.provider) == "table" then
112+
local enhanced_provider, error_msg = validate_and_enhance_provider(config.provider)
113+
if enhanced_provider then
114+
-- Check if custom provider is available
115+
local is_available_ok, is_available = pcall(enhanced_provider.is_available)
116+
if is_available_ok and is_available then
117+
logger.debug("terminal", "Using custom table provider")
118+
return enhanced_provider
119+
else
120+
local availability_msg = is_available_ok and "provider reports not available" or "error checking availability"
121+
logger.warn(
122+
"terminal",
123+
"Custom table provider configured but " .. availability_msg .. ". Falling back to 'native'."
124+
)
125+
end
126+
else
127+
logger.warn("terminal", "Invalid custom table provider: " .. error_msg .. ". Falling back to 'native'.")
128+
end
129+
-- Fall through to native provider
130+
elseif config.provider == "auto" then
56131
-- Try snacks first, then fallback to native silently
57132
local snacks_provider = load_provider("snacks")
58133
if snacks_provider and snacks_provider.is_available() then
@@ -69,8 +144,13 @@ local function get_provider()
69144
elseif config.provider == "native" then
70145
-- noop, will use native provider as default below
71146
logger.debug("terminal", "Using native terminal provider")
72-
else
147+
elseif type(config.provider) == "string" then
73148
logger.warn("terminal", "Invalid provider configured: " .. tostring(config.provider) .. ". Defaulting to 'native'.")
149+
else
150+
logger.warn(
151+
"terminal",
152+
"Invalid provider type: " .. type(config.provider) .. ". Must be string or table. Defaulting to 'native'."
153+
)
74154
end
75155

76156
local native_provider = load_provider("native")
@@ -188,7 +268,7 @@ end
188268
-- @param user_term_config table (optional) Configuration options for the terminal.
189269
-- @field user_term_config.split_side string 'left' or 'right' (default: 'right').
190270
-- @field user_term_config.split_width_percentage number Percentage of screen width (0.0 to 1.0, default: 0.30).
191-
-- @field user_term_config.provider string 'snacks' or 'native' (default: 'snacks').
271+
-- @field user_term_config.provider string|table 'auto', 'snacks', 'native', or custom provider table (default: 'auto').
192272
-- @field user_term_config.show_native_term_exit_tip boolean Show tip for exiting native terminal (default: true).
193273
-- @field user_term_config.snacks_win_opts table Opts to pass to `Snacks.terminal.open()` (default: {}).
194274
-- @param p_terminal_cmd string|nil The command to run in the terminal (from main config).
@@ -227,7 +307,7 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
227307
config[k] = v
228308
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
229309
config[k] = v
230-
elseif k == "provider" and (v == "snacks" or v == "native") then
310+
elseif k == "provider" and (v == "snacks" or v == "native" or v == "auto" or type(v) == "table") then
231311
config[k] = v
232312
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
233313
config[k] = v
@@ -314,11 +394,11 @@ end
314394
--- Gets the managed terminal instance for testing purposes.
315395
-- NOTE: This function is intended for use in tests to inspect internal state.
316396
-- The underscore prefix indicates it's not part of the public API for regular use.
317-
-- @return snacks.terminal|nil The managed Snacks terminal instance, or nil.
397+
-- @return table|nil The managed terminal instance, or nil.
318398
function M._get_managed_terminal_for_test()
319-
local snacks_provider = load_provider("snacks")
320-
if snacks_provider and snacks_provider._get_terminal_for_test then
321-
return snacks_provider._get_terminal_for_test()
399+
local provider = get_provider()
400+
if provider and provider._get_terminal_for_test then
401+
return provider._get_terminal_for_test()
322402
end
323403
return nil
324404
end

0 commit comments

Comments
 (0)