Route diagnostics, selections, and commands from Neovim to any tmux pane — configurable, template-driven, zero copy-paste.
- Neovim >= 0.11.0
- tmux with at least one active session
lazy.nvim
{
"AccursedGalaxy/cursed.nvim",
config = function()
require("cursed").setup({
tmux = { pane_target = "{left}", auto_submit = true },
})
end,
}All options are optional. Defaults are shown below.
require("cursed").setup({
-- Transport
backend = "tmux", -- only "tmux" is currently supported
tmux = {
pane_target = "{left}", -- tmux target-pane: "{left}", "{right}", "%3", "session:1.0", …
auto_submit = true, -- true → paste + Enter -> so the target process starts immediately
-- false → paste only; go to pane, edit, press Enter yourself
paste_delay_ms = 300, -- ms between paste and Enter; increase on slow machines or large diagnostics
},
-- What to collect
scope = "buffer", -- "buffer" | "all_buffers" | "workspace"
min_severity = vim.diagnostic.severity.HINT, -- HINT=4 INFO=3 WARN=2 ERROR=1
-- How to format
format = "default", -- "default" | "compact" | "with_source_lines"
-- or function(diag, bufnr) -> string|nil
-- Prompt templates
-- Use {content} as the placeholder — it is replaced by the formatted
-- diagnostics or selected code depending on how the template is invoked.
-- {diagnostics} still works as a legacy alias.
-- Pass the key as an arg to :CursedSendDiags or :CursedSendSelection,
-- e.g. :CursedSendDiags explain or :CursedSendSelection fix_selection
templates = {
-- diagnostics templates
fix = "Please fix these diagnostics:\n\n{diagnostics}",
explain = "Explain these diagnostics:\n\n{diagnostics}",
test = "Write tests that cover these diagnostics:\n\n{diagnostics}",
-- selection templates
fix_selection = "Please fix the following code:\n\n{content}",
explain_selection = "Explain the following code:\n\n{content}",
review = "Review the following code and suggest improvements:\n\n{content}",
},
default_template = nil, -- apply a template automatically on every send, or nil
-- Auto-send on DiagnosticChanged
auto_send = {
enabled = false,
event = "DiagnosticChanged",
debounce_ms = 500, -- wait this long after the last event before sending
min_severity = vim.diagnostic.severity.ERROR,
scope = "buffer",
},
-- Keymaps
-- Set any value to false to disable, or a string to remap.
keymaps = {
send = nil, -- global normal-mode: e.g. "<leader>cd" → send_diagnostics()
send_selection = nil, -- visual-mode: e.g. "<leader>cs" → send selected code
preview_send = "<CR>", -- inside :CursedPreview: confirm & send
preview_close = "q", -- inside :CursedPreview: close without sending
preview_close_esc = "<Esc>", -- inside :CursedPreview: close without sending (Esc)
},
-- Pre-send command
-- Sends a command to the pane *before* pasting diagnostics.
-- Typical use: "/clear" resets Claude's context so history doesn't accumulate.
-- command = nil disables the feature entirely.
-- Per-feature keys: nil = follow global, false = suppress for that feature only.
pre_send = {
command = nil, -- string|nil: e.g. "/clear"; nil = feature off
delay_ms = 300, -- ms to wait after the command before pasting diagnostics
send = nil, -- nil: follows global (manual :CursedSendDiags / keymap)
auto_send = nil, -- nil: follows global (consider false to reduce noise)
preview = nil, -- nil: follows global (:CursedPreview send action)
pick = nil, -- nil: follows global (:CursedPick send action)
selection = nil, -- nil: follows global (:CursedSendSelection / visual keymap)
},
})scope and min_severity have two independent resolution paths:
| Send path | scope used |
min_severity used |
|---|---|---|
Manual (:CursedSendDiags, keymap, send_command) |
top-level scope |
top-level min_severity |
Auto-send (DiagnosticChanged autocmd) |
auto_send.scope |
auto_send.min_severity |
auto_send values are passed as explicit opts and never fall back to the top-level globals. If you set min_severity = ERROR at the top level expecting auto-send to follow, it will not — set auto_send.min_severity explicitly.
pre_send resolves in three steps for each feature (send, auto_send, preview, pick, selection):
pre_send.command = nil→ entire feature off; per-feature keys are ignored.pre_send.<feature> = false→ disabled for that feature only.pre_send.<feature> = nil→ inheritspre_send.command+pre_send.delay_ms.
send_command(text, opts?) sends any raw string to the configured pane. Useful for triggering commands in whatever process is running there — a REPL, a test runner, an AI tool like Claude Code, anything.
local cursed = require("cursed")
-- Fixed command (e.g. reset context in Claude Code, clear a REPL, re-run tests)
vim.keymap.set("n", "<leader>cC", function()
cursed.send_command("/clear")
end, { desc = "Send /clear to target pane" })
-- Interactive: type anything and send it to the pane
vim.keymap.set("n", "<leader>c:", function()
vim.ui.input({ prompt = "Send to pane> " }, function(input)
if input and input ~= "" then cursed.send_command(input) end
end)
end, { desc = "Send arbitrary command to target pane" })Or from the command line: :CursedSendCommand /clear
opts accepts pane_target and auto_submit to override config defaults for that call.
pane_target accepts any tmux target-pane value:
| Value | Meaning |
|---|---|
"{left}" |
The pane to the left of the current one (default) |
"{right}" |
The pane to the right |
"%3" |
Pane by ID |
"mysession:1.0" |
Explicit session:window.pane |
| Command | Description |
|---|---|
:Cursed |
Verify the plugin is loaded |
:CursedSendDiags [arg] |
Send diagnostics. Optional arg: scope (buffer, all_buffers, workspace) or template name (fix, explain, …) |
:CursedSendSelection [template] |
Send the current visual selection. Optional arg: template name (fix_selection, explain_selection, review, …). Without a template, prompts for an instruction. |
:CursedPreview |
Preview and edit the formatted text before sending (keymaps configurable via keymaps.*) |
:CursedPick |
Telescope picker to select individual diagnostics from all loaded buffers (ignores scope; requires telescope.nvim) |
:CursedSendCommand {text} |
Send any raw string to the pane (e.g. :CursedSendCommand /clear) |
- Open a file that has LSP diagnostics.
- Run
:CursedSendDiags(or your keymap). - The process in the target pane receives the formatted diagnostics and — if
auto_submit = true— Enter is sent automatically (aftertmux.paste_delay_ms). Works with Claude Code, any other AI CLI, a REPL, or any tool that reads stdin.
Send only errors:
:CursedSendDiags " uses your configured min_severitySend workspace-wide diagnostics wrapped in the "fix" template:
:CursedSendDiags workspace
" then manually pick a template, or set default_template = "fix"Preview before sending:
:CursedPreview " floating window — edit, then send with keymaps.preview_send (default <CR>)Send a visual selection:
Select code in visual mode, then:
:'<,'>CursedSendSelection " prompts for an instruction, then sends selection
:'<,'>CursedSendSelection fix_selection " applies the fix_selection template immediately
:'<,'>CursedSendSelection review " asks Claude to review the selected codeOr bind the send_selection keymap and use it directly from visual mode:
keymaps = { send_selection = "<leader>cs" }
-- In visual mode: select code, press <leader>cs, type your instruction-- lualine
require("lualine").setup({
sections = {
lualine_x = { require("cursed.statusline").lualine },
},
})
-- or plain statusline
vim.o.statusline = "%{%v:lua.require('cursed.statusline').component()%}"vim.api.nvim_create_autocmd("User", {
pattern = "CursedPostSend",
callback = function(ev)
vim.notify(string.format("cursed: sent %d chars", #ev.data.text))
end,
})Events: CursedPreSend, CursedPostSend, CursedSendFailed. Each carries data = { text, backend }.
:checkhealth cursedVerifies Neovim version, plugin load, setup() call, tmux installation, tmux server status, and LSP client presence.
make test # run tests (requires plenary.nvim)
make lint # check formatting with StyLua
make fmt # auto-format with StyLuaMIT