Skip to content

A Neovim plugin for seamless terminal workflow integration. Smart picker-based terminal selection, flexible text sending from any buffer, and persistent configuration with comprehensive lifecycle control.

License

Notifications You must be signed in to change notification settings

waiting-for-dev/ergoterm.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

46 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ErgoTerm

A Neovim plugin for seamless terminal workflow integration. Smart picker-based terminal selection, flexible text sending from any buffer, and persistent configuration with comprehensive lifecycle control.

Note: ErgoTerm started as a fork of toggleterm.nvim but has grown into something quite different. Big thanks to @akinsho for the solid foundation!

Features

  • πŸš€ Flexible terminal creation - Spawn terminals with your preferred layout (split, float, tab, etc.)
  • 🎯 Smart terminal selection - Pick from active terminals using your favorite picker (Telescope, fzf-lua, or built-in)
  • πŸ“€ Seamless text sending - Send code, commands, or selections directly to any terminal
  • πŸ’Ύ Saved terminals - Reuse terminal configurations across Neovim sessions
  • ⚑ Powerful API - Extensive Lua API for custom workflows and integrations

πŸ“¦ Installation

Using lazy.nvim:

{
  "waiting-for-dev/ergoterm.nvim",
  config = function()
    require("ergoterm").setup()
  end
}

Using packer.nvim:

use {
  "waiting-for-dev/ergoterm.nvim",
  config = function()
    require("ergoterm").setup()
  end
}

Using vim-plug:

Plug 'waiting-for-dev/ergoterm.nvim'

Then add this to your init.lua or in a lua block:

require("ergoterm").setup()

After installation, you can verify everything is working correctly by running:

:checkhealth ergoterm

πŸš€ Basic Usage

πŸ”§ Creating Terminals

Create new terminals with :TermNew and customize them with options:

:TermNew
:TermNew layout=float name=server dir=~/my-project cmd=iex
:TermNew layout=right auto_scroll=false persist_mode=true

Available options:

  • layout - Window layout (default: below)
    • above, below, left, right, tab, float, window
  • name - Terminal name for identification (defaults to the terminal command)
  • dir - Working directory (default: current directory)
    • Accepts absolute paths (/home/user/project), relative paths (~/my-project, ./subdir), "git_dir" for auto-detected git repository root, or nil for current directory
  • cmd - Shell command to run (default: system shell)
  • auto_scroll - Automatically scroll terminal output to bottom (default: true)
  • persist_mode - Remember terminal mode between visits (default: false)
  • selectable - Show in selection picker and allow as last focused (default: true)
  • start_in_insert - Start terminal in insert mode (default: true)
  • sticky - Keep terminals visible in picker even when stopped (requires selectable to also be true) (default: false)
  • close_on_job_exit - Close terminal window when process exits (default: true)

🎯 Selecting Terminals

Choose from active terminals:

:TermSelect          " Open picker to select terminal
:TermSelect!         " Focus last focused terminal directly

Uses your configured picker (Telescope, fzf-lua, or built-in) to display all available terminals.

Advanced Picker Options

When using fzf-lua or Telescope, additional keybindings are available in the picker:

  • <Enter> - Open terminal in previous layout
  • <Ctrl-s> - Open in horizontal split
  • <Ctrl-v> - Open in vertical split
  • <Ctrl-t> - Open in new tab
  • <Ctrl-f> - Open in floating window

These keybindings can be customized through the picker.select_actions and picker.extra_select_actions configuration options (see Configuration section).

πŸ“€ Sending Text to Terminals

Send text from your buffer to any terminal:

:TermSend          " Send current line (opens picker)
:TermSend!         " Send to last focused terminal
:'<,'>TermSend     " Send visual selection

Available options:

  • text - Custom text to send (default: current line or selection)
  • action - Terminal behavior (default: interactive)
    • interactive - Focus terminal after sending
    • visible - Show terminal but keep current focus
    • silent - Send without opening terminal
  • decorator - Text transformation (default: identity)
    • identity - Send text as-is
    • markdown_code - Wrap in markdown code block
  • trim - Remove whitespace (default: true)
  • new_line - Add newline for execution (default: true)

βš™οΈ Updating Terminal Settings

Modify existing terminal configuration:

:TermUpdate layout=float     " Update via picker
:TermUpdate! name=server     " Update last focused terminal

Available options:

  • layout - Change window layout
  • name - Rename terminal
  • auto_scroll - Auto-scroll behavior
  • persist_mode - Remember terminal mode when revisiting
  • selectable - Show in selection picker and allow as last focused (can be overridden by universal selection mode)
  • start_in_insert - Start in insert mode

🌐 Universal Selection Mode

Toggle universal selection mode to temporarily override the selectable setting:

:TermToggleUniversalSelection

When enabled, all terminals become selectable and can be set as last focused, regardless of their individual selectable setting. This provides a way to access non-selectable terminals through pickers and bang commands when needed.

⌨️ Example Keymaps

Here are some useful keymaps to get you started:

local map = vim.keymap.set
local opts = { noremap = true, silent = true }

-- Terminal creation with different layouts
map("n", "<leader>cs", ":TermNew layout=below<CR>", opts)   -- Split below
map("n", "<leader>cv", ":TermNew layout=right<CR>", opts)   -- Vertical split
map("n", "<leader>cf", ":TermNew layout=float<CR>", opts)   -- Floating window
map("n", "<leader>ct", ":TermNew layout=tab<CR>", opts)     -- New tab

-- Open terminal picker
map("n", "<leader>cl", ":TermSelect<CR>", opts)  -- List and select terminals

-- Send text to last focused terminal
map("n", "<leader>cs", ":TermSend! new_line=false<CR>", opts)  -- Send line without newline
map("x", "<leader>cs", ":TermSend! new_line=false<CR>", opts)  -- Send selection without newline

-- Send and show output without focusing terminal
map("n", "<leader>cx", ":TermSend! action=visible<CR>", opts)  -- Execute in terminal, keep focus
map("x", "<leader>cx", ":TermSend! action=visible<CR>", opts)  -- Execute selection in terminal, keep focus

-- Send as markdown code block
map("n", "<leader>cS", ":TermSend! action=visible trim=false decorator=markdown_code<CR>", opts)
map("x", "<leader>cS", ":TermSend! action=visible trim=false decorator=markdown_code<CR>", opts)

πŸ’Ύ Standalone Terminals

Create persistent terminal configurations that survive across Neovim sessions. These terminals are defined once and can be quickly accessed with a single command.

πŸ—οΈ Creating Standalone Terminals

Define terminals in your configuration:

local terms = require("ergoterm.terminal")

-- Create standalone terminals
local lazygit = terms.Terminal:new({
  name = "lazygit",
  cmd = "lazygit",
  layout = "float",
  dir = "git_dir",
  selectable = false
})

local aider = terms.Terminal:new({
  name = "aider",
  cmd = "aider",
  layout = "right",
  dir = "git_dir",
  selectable = false
})

-- Map to keybindings for quick access
vim.keymap.set("n", "<leader>gg", function() lazygit:toggle() end, { desc = "Open lazygit" })
vim.keymap.set("n", "<leader>ai", function() aider:toggle() end, { desc = "Open aider" })

πŸ“ Project-Specific Terminals with .nvim.lua

For project-specific terminal configurations, you can leverage a .nvim.lua file in your project root. This is especially useful with the sticky option to keep project terminals available even when stopped:

local term = require("ergoterm.terminal").Terminal

term:new({
  name = "Phoenix Server",
  cmd = "iex -S mix phx.server",
  layout = "right",
  sticky = true,
  close_on_job_exit = false
})

term:new({
  name = "DB Console",
  cmd = "psql -U postgres my_database",
  layout = "below",
  sticky = true
})

With sticky = true, these terminals remain visible in the picker (:TermSelect) even when stopped, making it easy to restart your development environment. The terminals are automatically loaded when you open the project in Neovim.

πŸ”§ Available Options

All options default to values from your configuration:

  • auto_scroll - Automatically scroll terminal output to bottom
  • cmd - Command to execute in the terminal
  • clear_env - Use clean environment for the job
  • close_on_job_exit - Close terminal window when process exits
  • dir - Working directory for the terminal
    • Accepts absolute paths, relative paths (with ~ expansion), "git_dir" for git repository root, or nil for current directory
  • env - Environment variables for the job (table of key-value pairs)
    • Example: { PATH = "/custom/path", DEBUG = "1" }
  • float_opts - Floating window configuration options
  • float_winblend - Transparency level for floating windows
  • layout - Default window layout when opening
  • name - Display name for the terminal
  • on_close - Called when the terminal window is closed. Receives the terminal instance as its only argument
  • on_create - Called when the terminal buffer is first created. Receives the terminal instance as its only argument
  • on_focus - Called when the terminal window gains focus. Receives the terminal instance as its only argument
  • on_job_exit - Called when the terminal process exits. Receives the terminal instance, job ID, exit code, and event name
  • on_job_stderr - Called when the terminal process outputs to stderr. Receives the terminal instance, channel ID, data lines, and stream name
  • on_job_stdout - Called when the terminal process outputs to stdout. Receives the terminal instance, channel ID, data lines, and stream name
  • on_open - Called when the terminal window is opened. Receives the terminal instance as its only argument
  • on_start - Called when the terminal job process starts. Receives the terminal instance as its only argument
  • on_stop - Called when the terminal job process stops. Receives the terminal instance as its only argument
  • persist_mode - Remember terminal mode between visits
  • selectable - Include terminal in selection picker and allow as last focused (can be overridden by universal selection mode)
  • sticky - Keep terminal visible in picker even when stopped (requires selectable to also be true)
  • size - Size configuration for different window layouts (table with above, below, left, right keys)
    • Each direction accepts either a string with percentage (e.g., "30%") or a number for absolute size
    • Example: { below = 20, right = "40%" } - 20 lines high for below splits, 40% width for right splits
  • start_in_insert - Start terminal in insert mode

⚑ API Overview

ErgoTerm provides a comprehensive Lua API centered around terminal lifecycle management. The design follows a hierarchical pattern where higher-level methods automatically call lower-level ones as needed.

πŸ”„ Terminal Lifecycle

Every terminal follows this lifecycle progression:

  1. Create - Terminal:new() - Creates terminal instance with configuration
  2. Start - Terminal:start() - Initializes buffer and job process
  3. Open - Terminal:open() - Creates window for the terminal
  4. Focus - Terminal:focus() - Brings terminal into active focus

Each method is idempotent and will automatically call prerequisite methods:

local terms = require("ergoterm.terminal")

-- Create a terminal instance
local term = terms.Terminal:new({ cmd = "htop", layout = "float" })

-- These methods cascade - focus() will start() and open() if needed
term:focus()  -- Automatically calls start() and open() if not already done

-- You can also call methods individually
term:start()  -- Just start the job process
term:open()   -- Just create the window (calls start() if needed)

πŸ› οΈ Core Methods

  • start() - Creates buffer and starts job process
  • open(layout?) - Creates window with optional layout override
  • focus(layout?) - Brings terminal into focus, cascades through start/open
  • close() - Closes window but keeps job running
  • stop() - Terminates job and cleans up buffer
  • cleanup() - Cleans up terminal resources
  • toggle(layout?) - Closes if open, focuses if closed
  • send(input, opts) - Sends text to terminal with various behaviors

πŸ” State Queries

  • is_started() - Has active buffer and job
  • is_open() - Has visible window
  • is_focused() - Is currently active window
  • is_stopped() - Job has been terminated

πŸ“€ Sending Text to Terminals

The Terminal:send(input, opts) method provides flexible text input to terminals with various interaction modes:

-- Send current line interactively (focuses terminal)
term:send("single_line")

-- Send custom text without focusing terminal
term:send({"echo hello", "ls -la"}, { action = "visible" })

-- Send visual selection silently (no UI changes)
term:send("visual_selection", { action = "silent" })

-- Send with custom formatting
term:send({"print('hello')"}, { trim = false, decorator = "markdown_code" })

Input types:

  • string[] - Array of text lines to send directly
  • "single_line" - Current line under cursor
  • "visual_lines" - Current visual line selection
  • "visual_selection" - Current visual character selection

Action modes:

  • "interactive" - Focus terminal after sending (default)
  • "visible" - Show terminal output without stealing focus
  • "silent" - Send text without any UI changes

For complete API documentation and advanced usage patterns, see lua/ergoterm/terminal.lua.

🎨 Custom Text Decorators

Create custom text transformations for sending code to terminals:

-- Add timestamp to each line
local function timestamp_decorator(text)
  local timestamp = os.date("%H:%M:%S")
  local result = {}
  for _, line in ipairs(text) do
    table.insert(result, string.format("[%s] %s", timestamp, line))
  end
  return result
end

-- Use with Terminal:send()
terminal:send({"echo hello"}, { decorator = timestamp_decorator })

πŸ€– Example: AI-Assisted Development with Aider

Here's an example showing how to integrate Aider for AI-assisted coding:

local terms = require("ergoterm.terminal")

-- Create persistent Aider terminal
local aider = terms.Terminal:new({
  name = "aider",
  cmd = "aider",
  layout = "right",
  dir = "git_dir",
  selectable = false
})

local map = vim.keymap.set
local opts = { noremap = true, silent = true }

-- Toggle Aider terminal
map("n", "<leader>ai", function() aider:toggle() end, { desc = "Toggle Aider" })

-- Add current file to Aider session
map("n", "<leader>aa", function()
  local file = vim.fn.expand("%:p")
  aider:send({ "/add " .. file })
end, opts)

-- Sends current line to Aider session
map("n", "<leader>as", function()
  aider:send("single_line")
end, opts)

-- Sends current visual selection to Aider session
map("v", "<leader>as", function()
  aider:send("visual_selection", { trim = false })
end, opts)

-- Send code to Aider as markdown (preserves formatting)
map("n", "<leader>aS", function()
  aider:send("single_line", { trim = false, decorator = "markdown_code" })
end, opts)
map("v", "<leader>aS", function()
  aider:send("visual_selection", { trim = false, decorator = "markdown_code" })
end, opts)

βš™οΈ Configuration

ErgoTerm can be customized through the setup() function. Here are the defaults:

require("ergoterm").setup({
  -- Terminal defaults - applied to all new terminals but overridable per instance
  terminal_defaults = {
    -- Default shell command
    shell = vim.o.shell,
    
    -- Default window layout
    layout = "below",
    
    -- Auto-scroll terminal output
    auto_scroll = true,
    
    -- Close terminal window when job exits
    close_on_job_exit = true,
    
    -- Remember terminal mode between visits
    persist_mode = false,
    
    -- Start terminals in insert mode
    start_in_insert = true,
    
    -- Show terminals in picker by default
    selectable = true,
    
    -- Keep terminals visible in picker even when stopped, provided `selectable` is also true
    sticky = false,

    -- Floating window options
    float_opts = {
      title_pos = "left",
      relative = "editor",
      border = "single",
      zindex = 50
    },
    
    -- Floating window transparency
    float_winblend = 10,
    
    -- Size configuration for different layouts
    size = {
      below = "50%",   -- 50% of screen height
      above = "50%",   -- 50% of screen height
      left = "50%",    -- 50% of screen width
      right = "50%"    -- 50% of screen width
    },

    -- Clean job environment
    clear_env = false,

    -- Environment variables for terminal jobs
    env = nil,  -- Example: { PATH = "/custom/path", DEBUG = "1" }
    
    -- Default callbacks (all no-ops by default)
    on_close = function(term) end,
    on_create = function(term) end,
    on_focus = function(term) end,
    on_job_exit = function(term, job_id, exit_code, event_name) end,
    on_job_stderr = function(term, channel_id, data_lines, stream_name) end,
    on_job_stdout = function(term, channel_id, data_lines, stream_name) end,
    on_open = function(term) end,
    on_start = function(term) end,
    on_stop = function(term) end,
  },
  
  -- Picker configuration
  picker = {
    -- Picker to use for terminal selection
    -- Can be "telescope", "fzf-lua", "vim-ui-select", or a custom picker object
    -- nil = auto-detect (telescope > fzf-lua > vim.ui.select)
    picker = nil,

    -- Default actions available in terminal picker
    -- These replace the built-in actions entirely
    select_actions = {
      default = { fn = function(term) term:focus() end, desc = "Open" },
      ["<C-s>"] = { fn = function(term) term:focus("below") end, desc = "Open in horizontal split" },
      ["<C-v>"] = { fn = function(term) term:focus("right") end, desc = "Open in vertical split" },
      ["<C-t>"] = { fn = function(term) term:focus("tab") end, desc = "Open in tab" },
      ["<C-f>"] = { fn = function(term) term:focus("float") end, desc = "Open in float window" }
    },

    -- Additional actions to append to select_actions
    -- These are merged with select_actions, allowing you to add custom actions
    -- without replacing the defaults
    extra_select_actions = {}
  }
})

🀝 Contributing

Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.

πŸ› οΈ Development Setup

  1. Clone the repository
  2. Install dependencies for testing:
    # Install busted for Lua testing
    luarocks install busted
  3. Run tests:
    busted

πŸ“‹ Guidelines

  • Follow existing code style and conventions
  • Add tests for new features
  • Update documentation for user-facing changes
  • Keep commits focused and write clear commit messages

πŸ“„ License

This project is licensed under the GPL-3.0 License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Thanks to @akinsho for toggleterm.nvim, which provided the foundation for this project
  • The Neovim community for their excellent plugin ecosystem and documentation

About

A Neovim plugin for seamless terminal workflow integration. Smart picker-based terminal selection, flexible text sending from any buffer, and persistent configuration with comprehensive lifecycle control.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages