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 e737c52

Browse files
authored
feat: support function for external_terminal_cmd configuration (#119)
* feat: support function for external_terminal_cmd configuration Allow external_terminal_cmd to be either a string template with %s placeholder or a function that receives (cmd, env) and returns the command to execute. This enables more dynamic terminal command generation based on environment or runtime conditions. Examples: - String: "alacritty -e %s" - Function: function(cmd, env) return { "osascript", "-e", ... } end * fix: fix unit tests
1 parent 985b4b1 commit e737c52

File tree

8 files changed

+494
-17
lines changed

8 files changed

+494
-17
lines changed

‎README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
263263

264264
-- Provider-specific options
265265
provider_opts = {
266-
external_terminal_cmd = nil, -- Command template for external terminal provider (e.g., "alacritty -e %s")
266+
-- Command for external terminal provider. Can be:
267+
-- 1. String with %s placeholder: "alacritty -e %s"
268+
-- 2. Function returning command: function(cmd, env) return "alacritty -e " .. cmd end
269+
external_terminal_cmd = nil,
267270
},
268271
},
269272

@@ -452,13 +455,34 @@ For complete configuration options, see:
452455
Run Claude Code in a separate terminal application outside of Neovim:
453456

454457
```lua
458+
-- Using a string template (simple)
455459
{
456460
"coder/claudecode.nvim",
457461
opts = {
458462
terminal = {
459463
provider = "external",
460464
provider_opts = {
461-
external_terminal_cmd = "alacritty -e %s", -- Replace with your preferred terminal program. %s is replaced with claude command
465+
external_terminal_cmd = "alacritty -e %s", -- %s is replaced with claude command
466+
},
467+
},
468+
},
469+
}
470+
471+
-- Using a function for dynamic command generation (advanced)
472+
{
473+
"coder/claudecode.nvim",
474+
opts = {
475+
terminal = {
476+
provider = "external",
477+
provider_opts = {
478+
external_terminal_cmd = function(cmd, env)
479+
-- You can build complex commands based on environment or conditions
480+
if vim.fn.has("mac") == 1 then
481+
return { "osascript", "-e", string.format('tell app "Terminal" to do script "%s"', cmd) }
482+
else
483+
return "alacritty -e " .. cmd
484+
end
485+
end,
462486
},
463487
},
464488
},

‎lua/claudecode/config.lua

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ function M.validate(config)
6161

6262
-- Validate external_terminal_cmd in provider_opts
6363
if config.terminal.provider_opts.external_terminal_cmd then
64+
local cmd_type = type(config.terminal.provider_opts.external_terminal_cmd)
6465
assert(
65-
type(config.terminal.provider_opts.external_terminal_cmd) == "string",
66-
"terminal.provider_opts.external_terminal_cmd must be a string"
66+
cmd_type == "string" orcmd_type=="function",
67+
"terminal.provider_opts.external_terminal_cmd must be a string or function"
6768
)
68-
if config.terminal.provider_opts.external_terminal_cmd ~= "" then
69+
-- Only validate %s placeholder for strings
70+
if cmd_type == "string" and config.terminal.provider_opts.external_terminal_cmd ~= "" then
6971
assert(
7072
config.terminal.provider_opts.external_terminal_cmd:find("%%s"),
7173
"terminal.provider_opts.external_terminal_cmd must contain '%s' placeholder for the Claude command"
@@ -108,7 +110,9 @@ function M.validate(config)
108110
assert(type(config.diff_opts.show_diff_stats) == "boolean", "diff_opts.show_diff_stats must be a boolean")
109111
assert(type(config.diff_opts.vertical_split) == "boolean", "diff_opts.vertical_split must be a boolean")
110112
assert(type(config.diff_opts.open_in_current_tab) == "boolean", "diff_opts.open_in_current_tab must be a boolean")
111-
assert(type(config.diff_opts.keep_terminal_focus) == "boolean", "diff_opts.keep_terminal_focus must be a boolean")
113+
if config.diff_opts.keep_terminal_focus ~= nil then
114+
assert(type(config.diff_opts.keep_terminal_focus) == "boolean", "diff_opts.keep_terminal_focus must be a boolean")
115+
end
112116

113117
-- Validate env
114118
assert(type(config.env) == "table", "env must be a table")

‎lua/claudecode/terminal.lua

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,13 @@ local function get_provider()
143143
-- Check availability based on our config instead of provider's internal state
144144
local external_cmd = defaults.provider_opts and defaults.provider_opts.external_terminal_cmd
145145

146-
local has_external_cmd = external_cmd and external_cmd ~= "" and external_cmd:find("%%s")
146+
local has_external_cmd = false
147+
if type(external_cmd) == "function" then
148+
has_external_cmd = true
149+
elseif type(external_cmd) == "string" and external_cmd ~= "" and external_cmd:find("%%s") then
150+
has_external_cmd = true
151+
end
152+
147153
if has_external_cmd then
148154
return external_provider
149155
else
@@ -328,7 +334,7 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
328334
defaults[k] = defaults[k] or {}
329335
for opt_k, opt_v in pairs(v) do
330336
if opt_k == "external_terminal_cmd" then
331-
if opt_v == nil or type(opt_v) == "string" then
337+
if opt_v == nil or type(opt_v) == "string" ortype(opt_v) =="function" then
332338
defaults[k][opt_k] = opt_v
333339
else
334340
vim.notify(

‎lua/claudecode/terminal/external.lua

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,62 @@ function M.open(cmd_string, env_table)
3939
-- Get external terminal command from provider_opts
4040
local external_cmd = config.provider_opts and config.provider_opts.external_terminal_cmd
4141

42-
if not external_cmd orexternal_cmd=="" then
42+
if not external_cmd then
4343
vim.notify(
4444
"external_terminal_cmd not configured. Please set terminal.provider_opts.external_terminal_cmd in your config.",
4545
vim.log.levels.ERROR
4646
)
4747
return
4848
end
4949

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)
50+
local cmd_parts
51+
local full_command
52+
53+
-- Handle both string and function types
54+
if type(external_cmd) == "function" then
55+
-- Call the function with the Claude command and env table
56+
local result = external_cmd(cmd_string, env_table)
57+
if not result then
58+
vim.notify("external_terminal_cmd function returned nil or false", vim.log.levels.ERROR)
59+
return
60+
end
61+
62+
-- Result can be either a string or a table
63+
if type(result) == "string" then
64+
-- Parse the string into command parts
65+
cmd_parts = vim.split(result, " ")
66+
full_command = result
67+
elseif type(result) == "table" then
68+
-- Use the table directly as command parts
69+
cmd_parts = result
70+
full_command = table.concat(result, " ")
71+
else
72+
vim.notify(
73+
"external_terminal_cmd function must return a string or table, got: " .. type(result),
74+
vim.log.levels.ERROR
75+
)
76+
return
77+
end
78+
elseif type(external_cmd) == "string" then
79+
if external_cmd == "" then
80+
vim.notify("external_terminal_cmd string cannot be empty", vim.log.levels.ERROR)
81+
return
82+
end
83+
84+
-- Replace %s in the template with the Claude command
85+
if not external_cmd:find("%%s") then
86+
vim.notify("external_terminal_cmd must contain '%s' placeholder for the Claude command.", vim.log.levels.ERROR)
87+
return
88+
end
89+
90+
-- Build command by replacing %s with Claude command and splitting
91+
full_command = string.format(external_cmd, cmd_string)
92+
cmd_parts = vim.split(full_command, " ")
93+
else
94+
vim.notify("external_terminal_cmd must be a string or function, got: " .. type(external_cmd), vim.log.levels.ERROR)
5395
return
5496
end
5597

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-
6098
-- Start the external terminal as a detached process
6199
jobid = vim.fn.jobstart(cmd_parts, {
62100
detach = true,

‎lua/claudecode/types.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
-- Terminal provider-specific options
3838
---@class ClaudeCodeTerminalProviderOptions
39-
---@field external_terminal_cmd string? Command template for external terminal (e.g., "alacritty -e %s")
39+
---@field external_terminal_cmd string|fun(cmd: string, env: table): string|table|nil Command for external terminal (string template with %s or function)
4040

4141
-- @ mention queued for Claude Code
4242
---@class ClaudeCodeMention

‎tests/busted_setup.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ _G.expect = function(value)
5353
to_be_truthy = function()
5454
assert.is_truthy(value)
5555
end,
56+
to_match = function(pattern)
57+
assert.is_string(value)
58+
assert.is_true(
59+
string.find(value, pattern, 1, true) ~= nil,
60+
"Expected string '" .. value .. "' to match pattern '" .. pattern .. "'"
61+
)
62+
end,
5663
}
5764
end
5865

‎tests/unit/config_spec.lua

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe("Configuration", function()
66

77
local function setup()
88
package.loaded["claudecode.config"] = nil
9+
package.loaded["claudecode.terminal"] = nil
910

1011
config = require("claudecode.config")
1112
end
@@ -196,5 +197,113 @@ describe("Configuration", function()
196197
expect(success).to_be_false()
197198
end)
198199

200+
it("should accept function for external_terminal_cmd", function()
201+
local valid_config = {
202+
port_range = { min = 10000, max = 65535 },
203+
auto_start = true,
204+
log_level = "info",
205+
track_selection = true,
206+
visual_demotion_delay_ms = 50,
207+
connection_wait_delay = 200,
208+
connection_timeout = 10000,
209+
queue_timeout = 5000,
210+
diff_opts = {
211+
auto_close_on_accept = true,
212+
show_diff_stats = true,
213+
vertical_split = true,
214+
open_in_current_tab = true,
215+
},
216+
env = {},
217+
models = {
218+
{ name = "Test Model", value = "test" },
219+
},
220+
terminal = {
221+
provider = "external",
222+
provider_opts = {
223+
external_terminal_cmd = function(cmd, env)
224+
return "terminal " .. cmd
225+
end,
226+
},
227+
},
228+
}
229+
230+
local success, _ = pcall(function()
231+
config.validate(valid_config)
232+
end)
233+
234+
expect(success).to_be_true()
235+
end)
236+
237+
it("should accept string for external_terminal_cmd", function()
238+
local valid_config = {
239+
port_range = { min = 10000, max = 65535 },
240+
auto_start = true,
241+
log_level = "info",
242+
track_selection = true,
243+
visual_demotion_delay_ms = 50,
244+
connection_wait_delay = 200,
245+
connection_timeout = 10000,
246+
queue_timeout = 5000,
247+
diff_opts = {
248+
auto_close_on_accept = true,
249+
show_diff_stats = true,
250+
vertical_split = true,
251+
open_in_current_tab = true,
252+
},
253+
env = {},
254+
models = {
255+
{ name = "Test Model", value = "test" },
256+
},
257+
terminal = {
258+
provider = "external",
259+
provider_opts = {
260+
external_terminal_cmd = "alacritty -e %s",
261+
},
262+
},
263+
}
264+
265+
local success, _ = pcall(function()
266+
config.validate(valid_config)
267+
end)
268+
269+
expect(success).to_be_true()
270+
end)
271+
272+
it("should reject invalid type for external_terminal_cmd", function()
273+
local invalid_config = {
274+
port_range = { min = 10000, max = 65535 },
275+
auto_start = true,
276+
log_level = "info",
277+
track_selection = true,
278+
visual_demotion_delay_ms = 50,
279+
connection_wait_delay = 200,
280+
connection_timeout = 10000,
281+
queue_timeout = 5000,
282+
diff_opts = {
283+
auto_close_on_accept = true,
284+
show_diff_stats = true,
285+
vertical_split = true,
286+
open_in_current_tab = true,
287+
},
288+
env = {},
289+
models = {
290+
{ name = "Test Model", value = "test" },
291+
},
292+
terminal = {
293+
provider = "external",
294+
provider_opts = {
295+
external_terminal_cmd = 123, -- Invalid: number
296+
},
297+
},
298+
}
299+
300+
local success, err = pcall(function()
301+
config.validate(invalid_config)
302+
end)
303+
304+
expect(success).to_be_false()
305+
expect(tostring(err)).to_match("must be a string or function")
306+
end)
307+
199308
teardown()
200309
end)

0 commit comments

Comments
(0)

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