Send text from Neovim to tmux panes and windows - perfect for AI assistants, terminals, and dev tools.
demo.mp4
Wiremux connects your editor to anything running in tmux. Common uses:
- Chat with AI assistants (Claude, OpenCode, etc.) about your code
- Run tests or build commands without leaving your editor
- Quick terminal access for any shell commands
It works by creating "targets" (tmux panes/windows) and sending them text with smart placeholders like {file}, {selection}, or {this}.
- Persistent - Your targets survive Neovim restarts (stored in tmux)
- Smart text - Send context-aware snippets with placeholders
- Zero startup cost - Lazy-loaded, nothing runs until you use it
- Neovim 0.10+
- tmux 3.0+ recommended
- Neovim must run inside tmux
Add wiremux to your plugin manager. The optional fzf-lua dependency gives you a nicer picker interface.
{
"MSmaili/wiremux.nvim",
dependencies = {
"ibhagwan/fzf-lua", -- optional, for better picker UI
},
opts = {},
}-- packer.nvim
use {
"MSmaili/wiremux.nvim",
requires = { "ibhagwan/fzf-lua" }, -- optional
config = function()
require("wiremux").setup()
end,
}
-- vim-plug
Plug 'MSmaili/wiremux.nvim'Default Configuration
These are the full defaults from config.lua. You only need to override what you want to change.
{
log_level = "warn",
targets = {
definitions = {}, -- your target definitions go here
},
actions = {
close = { behavior = "pick" },
create = { behavior = "pick", focus = true },
send = { behavior = "pick", focus = true },
focus = { behavior = "last", focus = true },
toggle = { behavior = "last", focus = false },
},
context = {
resolvers = {}, -- custom placeholder resolvers
},
picker = {
adapter = nil, -- "fzf-lua" | "vim.ui.select" | custom function
instances = {
filter = function(inst, state) -- default: filter by origin pane
return inst.origin == state.origin_pane_id
end,
sort = function(a, b) -- default: most recently used first
return (a.last_used_at or 0) > (b.last_used_at or 0)
end,
},
targets = {
filter = nil,
sort = nil,
},
},
}A target is a tmux pane or window that wiremux manages. Add this minimal setup:
require("wiremux").setup({
targets = {
definitions = {
-- A simple terminal
terminal = { kind = "pane", split = "horizontal" },
},
},
})Target Definition Fields Reference
| Field | Type | Default | Description |
|---|---|---|---|
cmd |
string? |
- | Command to run when creating the pane/window |
kind |
"pane" | "window" | table |
"pane" |
Target type. Use table like {"pane","window"} to prompt at runtime |
split |
"horizontal" | "vertical" |
"horizontal" |
Split direction (panes only) |
split_mode |
"before" | "after" |
"after" |
Split placement relative to source pane (panes only) |
shell |
boolean |
true |
true: types cmd into a shell. false: runs cmd directly |
label |
string | function? |
target name | Display name in picker |
title |
string? |
label or name | Tmux window name (windows only) |
Run :Wiremux create — a picker appears listing your defined targets. Select "terminal" and wiremux opens a tmux pane. Or use Lua:
-- Create the target (opens a tmux pane)
require("wiremux").create()
-- Send text to it
require("wiremux").send("ls -la")-- Using lazy.nvim keys:
keys = {
-- Toggle terminal visibility
{ "<leader>tt", function() require("wiremux").toggle() end, desc = "Toggle terminal" },
-- Send current file path
{ "<leader>tf", function() require("wiremux").send("{file}") end, desc = "Send file path" },
-- Send visual selection
{ "<leader>tv", function() require("wiremux").send("{selection}") end, mode = "x", desc = "Send selection" },
}Two key concepts to remember:
| Concept | What it is | Example |
|---|---|---|
| Definition | A template describing how to create a target | { cmd = "claude", kind = "pane" } |
| Instance | A running tmux pane/window created from a definition | The actual claude pane open in tmux |
Definitions live in your config. Instances are created on-demand and persist in tmux.
The send() function is your main tool. You can send simple strings or create a picker menu.
Send text directly to your target:
-- Send the current file path
require("wiremux").send("{file}")
-- Send with focus (jumps to the target pane)
require("wiremux").send("{selection}", { focus = true })
-- Send a custom message
require("wiremux").send("Hello from Neovim!")Pass a list of items to get a menu:
require("wiremux").send({
{ label = "Explain this", value = "Explain {this}" },
{ label = "Review changes", value = "Review my changes:\n{changes}" },
{ label = "Run tests", value = "npm test", submit = true },
})Each item in the picker can have:
| Field | What it does | Example |
|---|---|---|
value |
(Required) The text to send | "Explain {file}" |
label |
Display name in the picker | "Explain file" |
submit |
Auto-press Enter after sending | true (useful for commands) |
visible |
Show/hide this item dynamically | function() return vim.bo.filetype == "lua" end |
pre_keys |
Keystrokes to send before pasting | "C-c", {"C-c", "i"} |
post_keys |
Keystrokes to send after pasting | "Escape", {"Escape", "Enter"} |
Some TUI apps need keystrokes sent before/after the pasted text — for example, C-c to cancel any in-progress input, or Escape to return to a neutral state after pasting:
-- Cancel current input before pasting, return to normal state after
require("wiremux").send({
value = "my text",
pre_keys = { "C-c" },
post_keys = { "Escape" },
})
-- Vim-mode editors: enter insert mode before pasting, Escape after
require("wiremux").send({
value = "my text",
pre_keys = { "i" },
post_keys = { "Escape" },
})
-- Per-call opts: all items in this keymap use the same keys
require("wiremux").send({
{ label = "Explain", value = "Explain {this}" },
{ label = "Review", value = "Review {changes}" },
}, { pre_keys = { "i" }, target = "claude" })Item-level pre_keys/post_keys override opts-level when both are set.
wiremux expands {placeholders} before sending.
| Placeholder | What it expands to |
|---|---|
{file} |
current buffer path |
{filename} |
basename of {file} |
{position} |
file:line:col (1-based line/col) |
{line} |
current line text |
{selection} |
visual selection (empty if not in visual mode) |
{this} |
{position} plus {selection} when available |
{diagnostics} |
diagnostics on current line |
{diagnostics_all} |
all diagnostics in current buffer |
{quickfix} |
formatted quickfix list |
{buffers} |
list of listed, loaded buffers |
{changes} |
git diff HEAD -- {file} (or "No changes") |
You can add custom placeholders:
require("wiremux").setup({
context = {
resolvers = {
git_branch = function()
local result = vim.system({ "git", "branch", "--show-current" }, { text = true }):wait()
return result.code == 0 and vim.trim(result.stdout) or nil
end,
},
},
})When you run an action, wiremux decides which targets to show. You can control this with four options:
1. Specific Target - Skip the picker and use a named target:
require("wiremux").send("{this}", { target = "claude" })
require("wiremux").focus({ target = "claude" })If matching instances exist, they're used. Otherwise wiremux falls back to creating from the definition. Filters still apply; if a filter excludes the target, it won't be found.
2. Behavior - How to handle multiple targets:
| Behavior | What happens | Use when... |
|---|---|---|
pick |
Show picker to choose | You want to select each time |
last |
Use most recent target | You want quick repeat actions |
all |
Send to every target | Broadcasting to multiple AIs |
3. Mode - Where to look for targets (only for send() and toggle()):
| Mode | What it shows | Use when... |
|---|---|---|
auto |
Instances first, then definitions | Default - smart fallback |
instances |
Only existing panes/windows | Managing current targets |
definitions |
Only templates to create new | Starting fresh sessions |
all |
Everything | Full overview |
4. Filters - Fine-grained control:
By default, only targets created from your current tmux pane are shown. You can override this:
-- Show all targets regardless of which pane created them
picker = {
instances = {
filter = nil,
},
}
-- Only show targets from current directory
picker = {
instances = {
filter = function(inst, state)
return inst.origin_cwd == vim.fn.getcwd()
end,
},
}Here's a comprehensive example with multiple AIs, project commands, and smart filtering:
{
"MSmaili/wiremux.nvim",
opts = {
picker = { adapter = "fzf-lua" },
targets = {
definitions = {
-- AI assistants
claude = { cmd = "claude", kind = { "pane", "window" }, shell = false },
opencode = { cmd = "opencode", kind = { "pane", "window" }, shell = false },
-- Interactive shell
shell = { kind = { "pane", "window" }, shell = true },
-- Quick command runner
quick = { kind = { "pane", "window" }, shell = false },
},
},
},
keys = {
{ "<leader>aa", function() require("wiremux").toggle() end, desc = "Toggle target" },
{ "<leader>ac", function() require("wiremux").create() end, desc = "Create target" },
-- Send context
{ "<leader>af", function() require("wiremux").send("{file}") end, desc = "Send file" },
{ "<leader>at", function() require("wiremux").send("{this}") end, mode = { "x", "n" }, desc = "Send this" },
{ "<leader>av", function() require("wiremux").send("{selection}") end, mode = "x", desc = "Send selection" },
{ "<leader>ad", function() require("wiremux").send("{diagnostics}") end, desc = "Send diagnostics" },
-- Send motion (works like an operator: ga + motion, e.g. gaip sends a paragraph)
{ "ga", function() require("wiremux").send_motion() end, desc = "Send motion to target" },
-- AI prompts picker
{
"<leader>ap",
function()
require("wiremux").send({
{ label = "Review changes", value = "Can you review my changes?\n{changes}" },
{ label = "Fix diagnostics", value = "Can you help me fix this?\n{diagnostics}", visible = function() return require("wiremux.context").is_available("diagnostics") end },
{ label = "Explain", value = "Explain {this}" },
{ label = "Write tests", value = "Can you write tests for {this}?" },
})
end,
mode = { "n", "x" },
desc = "AI prompts",
},
-- Project commands (only show "quick" target)
{
"<leader>ar",
function()
require("wiremux").send({
{ label = "npm test", value = "npm test; exec $SHELL", submit = true, visible = function() return vim.fn.filereadable("package.json") == 1 end },
{ label = "go test", value = "go test ./...", submit = true, visible = function() return vim.bo.filetype == "go" end },
}, { mode = "definitions", filter = { definitions = function(name) return name == "quick" end } })
end,
desc = "Run command",
},
},
}These are the main ways to interact with wiremux targets. You can use them as Lua functions (for keybindings) or Vim commands (for command line):
| Lua Function | Vim Command | What it does | Common use case |
|---|---|---|---|
send() |
:Wiremux send <text> |
Sends text to a target | Send code, prompts, or commands to an AI or terminal |
send_motion() |
:Wiremux send-motion |
Sends text covered by a motion (operator) | Works like y: map to ga, then gaip sends a paragraph |
create() |
:Wiremux create |
Creates a new target from a definition | Start a new AI assistant or terminal pane |
toggle() |
:Wiremux toggle |
Shows/hides the last used target | Quick hide/show your AI or terminal |
focus() |
:Wiremux focus |
Switches focus to a target | Jump to your terminal or AI pane |
close() |
:Wiremux close |
Closes a target | Shut down an AI or terminal you're done with |
Tip: Lua functions give you more power (placeholders, options, dynamic content), while commands are great for quick command-line use or when mapping from Vimscript.
Display the number of active wiremux targets in your statusline.
-- lualine
{
require("wiremux").statusline.component(),
padding = { left = 1, right = 1 },
}
-- heirline / feline
{ provider = require("wiremux").statusline.component() }
For full control, use get_info():
function()
local info = require("wiremux").statusline.get_info()
if info.count == 0 then return "" end
local icon = info.last_used.kind == "window" and "" or ""
return string.format("%s %d", icon, info.count)
endAPI: statusline.get_info() returns { loading, count, last_used } - statusline.component() returns a lualine-compatible function - statusline.refresh() forces an immediate refresh.
wiremux stores state in tmux pane variables, not in Neovim. Your targets survive editor restarts, and multiple Neovim instances can share them.
- Run
:checkhealth wiremux - Make sure Neovim is running inside tmux (
$TMUXis set)
:h wiremux
- folke/sidekick.nvim - inspiration for the idea and reference for a few implementation patterns
AI-assisted tools were used during development. All generated code was reviewed and adjusted manually.