Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ When Anthropic released Claude Code, they only supported VS Code and JetBrains.
```lua
{
"coder/claudecode.nvim",
dependencies = { "folke/snacks.nvim" },
dependencies = {
-- For Snacks terminal
"folke/snacks.nvim",
-- For ergoterm terminal
"waiting-for-dev/ergoterm.nvim",
},
config = true,
keys = {
{ "<leader>a", nil, desc = "AI/Claude Code" },
Expand Down Expand Up @@ -155,7 +160,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
terminal = {
split_side = "right", -- "left" or "right"
split_width_percentage = 0.30,
provider = "auto", -- "auto", "snacks", or "native"
provider = "auto", -- "auto", "snacks", "ergoterm", or "native"
auto_close = true,
},

Expand Down
4 changes: 2 additions & 2 deletions lua/claudecode/selection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ function M.on_cursor_moved()
end

--- Handles mode change events.
-- Triggers an immediate update of the selection.
-- Triggers a debounced update of the selection.
function M.on_mode_changed()
M.update_selection()
M.debounce_update()
end

--- Handles text change events.
Expand Down
24 changes: 19 additions & 5 deletions lua/claudecode/terminal.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- Module to manage a dedicated vertical split terminal for Claude Code.
-- Supports Snacks.nvim or a native Neovim terminal fallback.
-- Supports Snacks.nvim, ergoterm.nvim, or a native Neovim terminal fallback.
-- @module claudecode.terminal

--- @class TerminalProvider
Expand Down Expand Up @@ -51,11 +51,15 @@ local function get_provider()
local logger = require("claudecode.logger")

if config.provider == "auto" then
-- Try snacks first, then fallback to native silently
-- Try providers in order: snacks, ergoterm, then fallback to native silently
local snacks_provider = load_provider("snacks")
if snacks_provider and snacks_provider.is_available() then
return snacks_provider
end
local ergoterm_provider = load_provider("ergoterm")
if ergoterm_provider and ergoterm_provider.is_available() then
return ergoterm_provider
end
-- Fall through to native provider
elseif config.provider == "snacks" then
local snacks_provider = load_provider("snacks")
Expand All @@ -64,6 +68,16 @@ local function get_provider()
else
logger.warn("terminal", "'snacks' provider configured, but Snacks.nvim not available. Falling back to 'native'.")
end
elseif config.provider == "ergoterm" then
local ergoterm_provider = load_provider("ergoterm")
if ergoterm_provider and ergoterm_provider.is_available() then
return ergoterm_provider
else
logger.warn(
"terminal",
"'ergoterm' provider configured, but ergoterm.nvim not available. Falling back to 'native'."
)
end
elseif config.provider == "native" then
-- noop, will use native provider as default below
logger.debug("terminal", "Using native terminal provider")
Expand Down Expand Up @@ -177,7 +191,7 @@ end
-- @param user_term_config table (optional) Configuration options for the terminal.
-- @field user_term_config.split_side string 'left' or 'right' (default: 'right').
-- @field user_term_config.split_width_percentage number Percentage of screen width (0.0 to 1.0, default: 0.30).
-- @field user_term_config.provider string 'snacks' or 'native' (default: 'snacks').
-- @field user_term_config.provider string 'snacks', 'ergoterm', 'native', or 'auto' (default: 'auto').
-- @field user_term_config.show_native_term_exit_tip boolean Show tip for exiting native terminal (default: true).
-- @param p_terminal_cmd string|nil The command to run in the terminal (from main config).
function M.setup(user_term_config, p_terminal_cmd)
Expand All @@ -204,7 +218,7 @@ function M.setup(user_term_config, p_terminal_cmd)
config[k] = v
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
config[k] = v
elseif k == "provider" and (v == "snacks" or v == "native") then
elseif k == "provider" and (v == "snacks" or v == "ergoterm" or v == "native" or v == "auto") then
config[k] = v
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
config[k] = v
Expand Down Expand Up @@ -280,7 +294,7 @@ function M.toggle(opts_override, cmd_args)
end

--- Gets the buffer number of the currently active Claude Code terminal.
-- This checks both Snacks and native fallback terminals.
-- This checks Snacks, ergoterm, and native fallback terminals.
-- @return number|nil The buffer number if an active terminal is found, otherwise nil.
function M.get_active_terminal_bufnr()
return get_provider().get_active_bufnr()
Expand Down
215 changes: 215 additions & 0 deletions lua/claudecode/terminal/ergoterm.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
--- Ergoterm.nvim terminal provider for Claude Code.
-- @module claudecode.terminal.ergoterm

--- @type TerminalProvider
local M = {}

local ergoterm_available, ergoterm = pcall(require, "ergoterm.terminal")
local utils = require("claudecode.utils")
local terminal = nil

--- @return boolean
local function is_available()
return ergoterm_available and ergoterm and ergoterm.Terminal
end

--- Setup event handlers for terminal instance
--- @param term_instance table The ergoterm Terminal instance
--- @param config table Configuration options
local function setup_terminal_events(term_instance, config)
local logger = require("claudecode.logger")

-- Handle command completion/exit - only if auto_close is enabled
if config.auto_close then
-- Note: ergoterm doesn't have direct event handlers like Snacks,
-- so we'll need to monitor the terminal state differently
-- For now, we'll rely on the terminal's built-in cleanup mechanisms
logger.debug("terminal", "Ergoterm terminal created with auto_close enabled")
end
end

--- Builds ergoterm Terminal options
--- @param config table Terminal configuration (split_side, split_width_percentage, etc.)
--- @param env_table table Environment variables to set for the terminal process
--- @param cmd_string string Command to run in terminal
--- @param focus boolean|nil Whether to focus the terminal when opened (defaults to true)
--- @return table Ergoterm Terminal configuration
local function build_terminal_opts(config, env_table, cmd_string, focus)
focus = utils.normalize_focus(focus)

-- Convert split_side to ergoterm layout string format
local layout = config.split_side == "left" and "left" or "right"

return {
name = "claude-code",
cmd = cmd_string,
layout = layout,
start_in_insert = focus,
auto_scroll = true,
selectable = true,
env = env_table,
}
end

function M.setup()
-- No specific setup needed for ergoterm provider
end

--- @param cmd_string string
--- @param env_table table
--- @param config table
--- @param focus boolean|nil
function M.open(cmd_string, env_table, config, focus)
if not is_available() then
vim.notify("Ergoterm terminal provider selected but ergoterm.nvim not available.", vim.log.levels.ERROR)
return
end

focus = utils.normalize_focus(focus)

if terminal and terminal:is_started() then
-- Terminal exists
if not terminal:is_open() then
-- Terminal is hidden, show it
terminal:open()
if focus then
terminal:focus()
end
else
-- Terminal is already visible
if focus then
terminal:focus()
end
end
return
end

-- Create new terminal
local opts = build_terminal_opts(config, env_table, cmd_string, focus)
terminal = ergoterm.Terminal:new(opts)

if terminal then
setup_terminal_events(terminal, config)
-- Start and open the terminal
terminal:start()
terminal:open()
if focus then
terminal:focus()
end
else
local logger = require("claudecode.logger")
local error_msg = string.format("Failed to create ergoterm Terminal with cmd='%s'", cmd_string)
vim.notify(error_msg, vim.log.levels.ERROR)
logger.debug("terminal", error_msg)
end
end

function M.close()
if not is_available() then
return
end
if terminal and terminal:is_started() then
terminal:close()
end
end

--- Simple toggle: always show/hide terminal regardless of focus
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.simple_toggle(cmd_string, env_table, config)
if not is_available() then
vim.notify("Ergoterm terminal provider selected but ergoterm.nvim not available.", vim.log.levels.ERROR)
return
end

local logger = require("claudecode.logger")

if terminal and terminal:is_started() then
if terminal:is_open() then
-- Terminal is visible, close it
logger.debug("terminal", "Simple toggle: closing visible terminal")
terminal:close()
else
-- Terminal exists but not visible, show it
logger.debug("terminal", "Simple toggle: showing hidden terminal")
terminal:open()
terminal:focus()
end
else
-- No terminal exists, create new one
logger.debug("terminal", "Simple toggle: creating new terminal")
M.open(cmd_string, env_table, config, true)
end
end

--- Smart focus toggle: switches to terminal if not focused, hides if currently focused
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.focus_toggle(cmd_string, env_table, config)
if not is_available() then
vim.notify("Ergoterm terminal provider selected but ergoterm.nvim not available.", vim.log.levels.ERROR)
return
end

local logger = require("claudecode.logger")

if terminal and terminal:is_started() then
if terminal:is_open() then
-- Terminal is visible - check if focused
if terminal:is_focused() then
-- Currently focused, hide it
logger.debug("terminal", "Focus toggle: hiding focused terminal")
terminal:close()
else
-- Not focused, focus it
logger.debug("terminal", "Focus toggle: focusing terminal")
terminal:focus()
end
else
-- Terminal exists but not visible, show and focus it
logger.debug("terminal", "Focus toggle: showing and focusing hidden terminal")
terminal:open()
terminal:focus()
end
else
-- No terminal exists, create new one
logger.debug("terminal", "Focus toggle: creating new terminal")
M.open(cmd_string, env_table, config, true)
end
end

--- Legacy toggle function for backward compatibility (defaults to simple_toggle)
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.toggle(cmd_string, env_table, config)
M.simple_toggle(cmd_string, env_table, config)
end

--- @return number|nil
function M.get_active_bufnr()
if terminal and terminal:is_started() then
-- ergoterm doesn't expose buffer directly, but we can try to get it
-- from the terminal's internal state if available
if terminal.buf and vim.api.nvim_buf_is_valid(terminal.buf) then
return terminal.buf
end
end
return nil
end

--- @return boolean
function M.is_available()
return is_available()
end

-- For testing purposes
--- @return table|nil
function M._get_terminal_for_test()
return terminal
end

return M

Loading
Loading