Skip to content

Commit 4bebd0e

Browse files
authored
Merge pull request #40 from coder/feat/two-command-toggle-behavior
2 parents 21f984b + e1817e2 commit 4bebd0e

File tree

9 files changed

+314
-33
lines changed

9 files changed

+314
-33
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim):
5151
keys = {
5252
{ "<leader>a", nil, desc = "AI/Claude Code" },
5353
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
54+
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
5455
{ "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
5556
{ "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },
5657
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
@@ -80,13 +81,19 @@ That's it! For more configuration options, see [Advanced Setup](#advanced-setup)
8081

8182
## Commands
8283

83-
- `:ClaudeCode [arguments]` - Toggle the Claude Code terminal window (arguments are passed to claude command)
84+
- `:ClaudeCode [arguments]` - Toggle the Claude Code terminal window (simple show/hide behavior)
85+
- `:ClaudeCodeFocus [arguments]` - Smart focus/toggle Claude terminal (switches to terminal if not focused, hides if focused)
8486
- `:ClaudeCode --resume` - Resume a previous Claude conversation
8587
- `:ClaudeCode --continue` - Continue Claude conversation
8688
- `:ClaudeCodeSend` - Send current visual selection to Claude, or add files from tree explorer
8789
- `:ClaudeCodeTreeAdd` - Add selected file(s) from tree explorer to Claude context (also available via ClaudeCodeSend)
8890
- `:ClaudeCodeAdd <file-path> [start-line] [end-line]` - Add a specific file or directory to Claude context by path with optional line range
8991

92+
### Toggle Behavior
93+
94+
- **`:ClaudeCode`** - Simple toggle: Always show/hide terminal regardless of current focus
95+
- **`:ClaudeCodeFocus`** - Smart focus: Focus terminal if not active, hide if currently focused
96+
9097
### Tree Integration
9198

9299
The `<leader>as` keybinding has context-aware behavior:
@@ -213,6 +220,7 @@ See [DEVELOPMENT.md](./DEVELOPMENT.md) for build instructions and development gu
213220
keys = {
214221
{ "<leader>a", nil, desc = "AI/Claude Code" },
215222
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
223+
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
216224
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
217225
{
218226
"<leader>as",

dev-config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ return {
1313

1414
-- Core Claude commands
1515
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
16+
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
1617
{ "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
1718
{ "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },
1819

lua/claudecode/init.lua

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,10 +664,22 @@ function M._create_commands()
664664
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
665665
end
666666
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
667-
terminal.toggle({}, cmd_args)
667+
terminal.simple_toggle({}, cmd_args)
668668
end, {
669669
nargs = "*",
670-
desc = "Toggle the Claude Code terminal window with optional arguments",
670+
desc = "Toggle the Claude Code terminal window (simple show/hide) with optional arguments",
671+
})
672+
673+
vim.api.nvim_create_user_command("ClaudeCodeFocus", function(opts)
674+
local current_mode = vim.fn.mode()
675+
if current_mode == "v" or current_mode == "V" or current_mode == "\22" then
676+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
677+
end
678+
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
679+
terminal.focus_toggle({}, cmd_args)
680+
end, {
681+
nargs = "*",
682+
desc = "Smart focus/toggle Claude Code terminal (switches to terminal if not focused, hides if focused)",
671683
})
672684

673685
vim.api.nvim_create_user_command("ClaudeCodeOpen", function(opts)

lua/claudecode/terminal.lua

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,32 @@ function M.close()
204204
get_provider().close()
205205
end
206206

207-
--- Toggles the Claude terminal open or closed.
207+
--- Simple toggle: always show/hide the Claude terminal regardless of focus.
208208
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
209209
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
210-
function M.toggle(opts_override, cmd_args)
210+
function M.simple_toggle(opts_override, cmd_args)
211211
local effective_config = build_config(opts_override)
212212
local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)
213213

214-
get_provider().toggle(cmd_string, claude_env_table, effective_config)
214+
get_provider().simple_toggle(cmd_string, claude_env_table, effective_config)
215+
end
216+
217+
--- Smart focus toggle: switches to terminal if not focused, hides if currently focused.
218+
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
219+
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
220+
function M.focus_toggle(opts_override, cmd_args)
221+
local effective_config = build_config(opts_override)
222+
local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)
223+
224+
get_provider().focus_toggle(cmd_string, claude_env_table, effective_config)
225+
end
226+
227+
--- Toggles the Claude terminal open or closed (legacy function - use simple_toggle or focus_toggle).
228+
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
229+
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
230+
function M.toggle(opts_override, cmd_args)
231+
-- Default to simple toggle for backward compatibility
232+
M.simple_toggle(opts_override, cmd_args)
215233
end
216234

217235
--- Gets the buffer number of the currently active Claude Code terminal.

lua/claudecode/terminal/native.lua

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,51 @@ function M.close()
276276
close_terminal()
277277
end
278278

279+
--- Simple toggle: always show/hide terminal regardless of focus
279280
--- @param cmd_string string
280281
--- @param env_table table
281282
--- @param effective_config table
282-
function M.toggle(cmd_string, env_table, effective_config)
283+
function M.simple_toggle(cmd_string, env_table, effective_config)
284+
-- Check if we have a valid terminal buffer (process running)
285+
local has_buffer = bufnr and vim.api.nvim_buf_is_valid(bufnr)
286+
local is_visible = has_buffer and is_terminal_visible()
287+
288+
if is_visible then
289+
-- Terminal is visible, hide it (but keep process running)
290+
hide_terminal()
291+
else
292+
-- Terminal is not visible
293+
if has_buffer then
294+
-- Terminal process exists but is hidden, show it
295+
if show_hidden_terminal(effective_config) then
296+
logger.debug("terminal", "Showing hidden terminal")
297+
else
298+
logger.error("terminal", "Failed to show hidden terminal")
299+
end
300+
else
301+
-- No terminal process exists, check if there's an existing one we lost track of
302+
local existing_buf, existing_win = find_existing_claude_terminal()
303+
if existing_buf and existing_win then
304+
-- Recover the existing terminal
305+
bufnr = existing_buf
306+
winid = existing_win
307+
logger.debug("terminal", "Recovered existing Claude terminal")
308+
focus_terminal()
309+
else
310+
-- No existing terminal found, create a new one
311+
if not open_terminal(cmd_string, env_table, effective_config) then
312+
vim.notify("Failed to open Claude terminal using native fallback (simple_toggle).", vim.log.levels.ERROR)
313+
end
314+
end
315+
end
316+
end
317+
end
318+
319+
--- Smart focus toggle: switches to terminal if not focused, hides if currently focused
320+
--- @param cmd_string string
321+
--- @param env_table table
322+
--- @param effective_config table
323+
function M.focus_toggle(cmd_string, env_table, effective_config)
283324
-- Check if we have a valid terminal buffer (process running)
284325
local has_buffer = bufnr and vim.api.nvim_buf_is_valid(bufnr)
285326
local is_visible = has_buffer and is_terminal_visible()
@@ -325,12 +366,20 @@ function M.toggle(cmd_string, env_table, effective_config)
325366
else
326367
-- No existing terminal found, create a new one
327368
if not open_terminal(cmd_string, env_table, effective_config) then
328-
vim.notify("Failed to open Claude terminal using native fallback (toggle).", vim.log.levels.ERROR)
369+
vim.notify("Failed to open Claude terminal using native fallback (focus_toggle).", vim.log.levels.ERROR)
329370
end
330371
end
331372
end
332373
end
333374

375+
--- Legacy toggle function for backward compatibility (defaults to simple_toggle)
376+
--- @param cmd_string string
377+
--- @param env_table table
378+
--- @param effective_config table
379+
function M.toggle(cmd_string, env_table, effective_config)
380+
M.simple_toggle(cmd_string, env_table, effective_config)
381+
end
382+
334383
--- @return number|nil
335384
function M.get_active_bufnr()
336385
if is_valid() then

lua/claudecode/terminal/snacks.lua

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,39 @@ function M.close()
124124
end
125125
end
126126

127+
--- Simple toggle: always show/hide terminal regardless of focus
127128
--- @param cmd_string string
128129
--- @param env_table table
129130
--- @param config table
130-
function M.toggle(cmd_string, env_table, config)
131+
function M.simple_toggle(cmd_string, env_table, config)
132+
if not is_available() then
133+
vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
134+
return
135+
end
136+
137+
local logger = require("claudecode.logger")
138+
139+
-- Check if terminal exists and is visible
140+
if terminal and terminal:buf_valid() and terminal.win then
141+
-- Terminal is visible, hide it
142+
logger.debug("terminal", "Simple toggle: hiding visible terminal")
143+
terminal:toggle()
144+
elseif terminal and terminal:buf_valid() and not terminal.win then
145+
-- Terminal exists but not visible, show it
146+
logger.debug("terminal", "Simple toggle: showing hidden terminal")
147+
terminal:toggle()
148+
else
149+
-- No terminal exists, create new one
150+
logger.debug("terminal", "Simple toggle: creating new terminal")
151+
M.open(cmd_string, env_table, config)
152+
end
153+
end
154+
155+
--- Smart focus toggle: switches to terminal if not focused, hides if currently focused
156+
--- @param cmd_string string
157+
--- @param env_table table
158+
--- @param config table
159+
function M.focus_toggle(cmd_string, env_table, config)
131160
if not is_available() then
132161
vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
133162
return
@@ -137,7 +166,7 @@ function M.toggle(cmd_string, env_table, config)
137166

138167
-- Terminal exists, is valid, but not visible
139168
if terminal and terminal:buf_valid() and not terminal.win then
140-
logger.debug("terminal", "Toggle existing managed Snacks terminal")
169+
logger.debug("terminal", "Focus toggle: showing hidden terminal")
141170
terminal:toggle()
142171
-- Terminal exists, is valid, and is visible
143172
elseif terminal and terminal:buf_valid() and terminal.win then
@@ -146,9 +175,11 @@ function M.toggle(cmd_string, env_table, config)
146175

147176
-- you're IN it
148177
if claude_term_neovim_win_id == current_neovim_win_id then
178+
logger.debug("terminal", "Focus toggle: hiding terminal (currently focused)")
149179
terminal:toggle()
150180
-- you're NOT in it
151181
else
182+
logger.debug("terminal", "Focus toggle: focusing terminal")
152183
vim.api.nvim_set_current_win(claude_term_neovim_win_id)
153184
if terminal.buf and vim.api.nvim_buf_is_valid(terminal.buf) then
154185
if vim.api.nvim_buf_get_option(terminal.buf, "buftype") == "terminal" then
@@ -160,11 +191,19 @@ function M.toggle(cmd_string, env_table, config)
160191
end
161192
-- No terminal exists
162193
else
163-
logger.debug("terminal", "No valid terminal exists, creating new one")
194+
logger.debug("terminal", "Focus toggle: creating new terminal")
164195
M.open(cmd_string, env_table, config)
165196
end
166197
end
167198

199+
--- Legacy toggle function for backward compatibility (defaults to simple_toggle)
200+
--- @param cmd_string string
201+
--- @param env_table table
202+
--- @param config table
203+
function M.toggle(cmd_string, env_table, config)
204+
M.simple_toggle(cmd_string, env_table, config)
205+
end
206+
168207
--- @return number|nil
169208
function M.get_active_bufnr()
170209
if terminal and terminal:buf_valid() and terminal.buf then

tests/unit/init_spec.lua

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ describe("claudecode.init", function()
291291
before_each(function()
292292
mock_terminal = {
293293
toggle = spy.new(function() end),
294+
simple_toggle = spy.new(function() end),
295+
focus_toggle = spy.new(function() end),
294296
open = spy.new(function() end),
295297
close = spy.new(function() end),
296298
setup = spy.new(function() end),
@@ -369,8 +371,8 @@ describe("claudecode.init", function()
369371

370372
command_handler({ args = "--resume --verbose" })
371373

372-
assert(#mock_terminal.toggle.calls > 0, "terminal.toggle was not called")
373-
local call_args = mock_terminal.toggle.calls[1].vals
374+
assert(#mock_terminal.simple_toggle.calls > 0, "terminal.simple_toggle was not called")
375+
local call_args = mock_terminal.simple_toggle.calls[1].vals
374376
assert.is_table(call_args[1], "First argument should be a table")
375377
assert.is_equal("--resume --verbose", call_args[2], "Second argument should be the command args")
376378
end)
@@ -412,8 +414,8 @@ describe("claudecode.init", function()
412414

413415
command_handler({ args = "" })
414416

415-
assert(#mock_terminal.toggle.calls > 0, "terminal.toggle was not called")
416-
local call_args = mock_terminal.toggle.calls[1].vals
417+
assert(#mock_terminal.simple_toggle.calls > 0, "terminal.simple_toggle was not called")
418+
local call_args = mock_terminal.simple_toggle.calls[1].vals
417419
assert.is_nil(call_args[2], "Second argument should be nil for empty args")
418420
end)
419421

@@ -431,8 +433,8 @@ describe("claudecode.init", function()
431433

432434
command_handler({ args = nil })
433435

434-
assert(#mock_terminal.toggle.calls > 0, "terminal.toggle was not called")
435-
local call_args = mock_terminal.toggle.calls[1].vals
436+
assert(#mock_terminal.simple_toggle.calls > 0, "terminal.simple_toggle was not called")
437+
local call_args = mock_terminal.simple_toggle.calls[1].vals
436438
assert.is_nil(call_args[2], "Second argument should be nil when args is nil")
437439
end)
438440

@@ -450,8 +452,8 @@ describe("claudecode.init", function()
450452

451453
command_handler({})
452454

453-
assert(#mock_terminal.toggle.calls > 0, "terminal.toggle was not called")
454-
local call_args = mock_terminal.toggle.calls[1].vals
455+
assert(#mock_terminal.simple_toggle.calls > 0, "terminal.simple_toggle was not called")
456+
local call_args = mock_terminal.simple_toggle.calls[1].vals
455457
assert.is_nil(call_args[2], "Second argument should be nil when no args provided")
456458
end)
457459
end)

0 commit comments

Comments
 (0)