Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
/doc/tags
foo.*
tt.*
tmp_*
.tmp_*
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,30 @@ require("wiremux").setup({
})
```

### Interactive input

The `{input}` placeholder prompts the user via `vim.ui.input()` before sending. Use it when part of the text needs to come from the user at send time.

| Syntax | Prompt label | Default value |
| ------------------------- | -------------- | ------------- |
| `{input}` | "Input" | — |
| `{input:Search query}` | "Search query" | — |
| `{input:Query:foo bar}` | "Query" | `foo bar` |
| `{input:URL:http://example.com}` | "URL" | `http://example.com` |

```lua
-- Basic usage
require("wiremux").send("question:\n{input:Question}\n\ncontext:\n{this}")

-- Single input with a default value
require("wiremux").send("grep -r '{input:Search query:TODO}' {file}")

-- Multiple distinct inputs in one send
require("wiremux").send("git log --author='{input:Author}' --grep='{input:Grep pattern}'")
```

All sync placeholders (`{file}`, `{selection}`, etc.) resolve first, then each `{input}` prompts in order. Cancelling any prompt aborts the entire send. Text entered by the user is sent as-is — placeholders typed into the prompt are **not** expanded.

## Advanced Configuration

### Target Resolution Options
Expand Down
25 changes: 25 additions & 0 deletions doc/wiremux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,31 @@ This is useful for conditional visibility in SendItem:
}
<

Interactive input ~
*wiremux-input*
The `{input}` placeholder prompts the user via `vim.ui.input()` at send
time. Use it when part of the text needs to come from the user.

{input} prompt with label "Input", no default
{input:Search query} prompt with label "Search query"
{input:Query:foo bar} prompt with label "Query", default "foo bar"
{input:URL:http://example.com} prompt "URL", default "http://example.com"

>lua
-- Single input with a default value
require("wiremux").send("grep -r '{input:Search query:TODO}' {file}")

-- Multiple distinct inputs in one send
require("wiremux").send(
"git log --author='{input:Author}' --grep='{input:Grep pattern}'"
)
<

All sync placeholders resolve first, then each `{input}` prompts in
order. Identical `{input}` expressions are deduplicated (prompted once,
reused everywhere). Cancelling any prompt aborts the entire send.
Placeholders typed into the prompt are sent as literal text (not expanded).

==============================================================================
6. COMMANDS *wiremux-commands*

Expand Down
21 changes: 18 additions & 3 deletions lua/wiremux/action/send.lua
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ end
---@param opts wiremux.config.ActionConfig
local function send_single_item(item, opts)
local context = require("wiremux.context")
local input = require("wiremux.context.input")
local config = require("wiremux.config")

local ok, expanded = pcall(context.expand, item.value)
-- Expand sync placeholders
local ok, text = pcall(context.expand, item.value)
if not ok then
require("wiremux.utils.notify").error(expanded)
require("wiremux.utils.notify").error(text)
return
end

Expand All @@ -110,7 +112,20 @@ local function send_single_item(item, opts)
submit = opts.submit or config.opts.actions.send.submit
end

do_send(expanded, opts, submit, item.title)
-- Handle async {input} placeholders
local keys = input.find(text)
if #keys == 0 then
do_send(text, opts, submit, item.title)
return
end

input.resolve(keys, function(values)
if not values then
return
end
local resolved = input.replace(text, values)
do_send(resolved, opts, submit, item.title)
end)
end

---Send from send library (picker)
Expand Down
1 change: 1 addition & 0 deletions lua/wiremux/context/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function M.expand(text)
local cache = {}
return (
text:gsub("{([%w_]+)}", function(var)
if var == "input" then return nil end
if cache[var] == nil then
cache[var] = M.get(var)
end
Expand Down
112 changes: 112 additions & 0 deletions lua/wiremux/context/input.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
local M = {}

---Parse an input key into prompt and default value
---Key formats: "input", "input:Prompt", "input:Prompt:default"
---@param key string The full key (e.g. "input:Branch:main")
---@return string prompt
---@return string? default
function M.parse(key)
if key == "input" then
return "Input", nil
end

-- Strip "input:" prefix
local rest = key:sub(#"input:" + 1)

-- Find first colon for prompt:default split
local colon_pos = rest:find(":", 1, true)
if not colon_pos then
return rest, nil
end

local prompt = rest:sub(1, colon_pos - 1)
local default = rest:sub(colon_pos + 1)

return prompt, default
end

---Find all unique input placeholder keys in text
---Returns keys in order of first appearance
---@param text string
---@return string[]
function M.find(text)
if not text:find("{input", 1, true) then
return {}
end

local seen = {}
local keys = {}

-- Match {input...} placeholders — capture everything between { and }
for content in text:gmatch("{(input[^}]*)}") do
-- Reject names like {input_var} or {input2} — only allow bare "input" or "input:..."
if content == "input" or content:sub(1, 6) == "input:" then
if not seen[content] then
seen[content] = true
table.insert(keys, content)
end
end
end

return keys
end

---Quick check if text contains any input placeholders
---@param text string
---@return boolean
function M.has_inputs(text)
return #M.find(text) > 0
end

---Replace input placeholders in text with resolved values
---Only replaces placeholders whose keys exist in the values table
---@param text string
---@param values table<string, string>
---@return string
function M.replace(text, values)
return text:gsub("{(input[^}]*)}", function(content)
if content == "input" or content:sub(1, 6) == "input:" then
if values[content] ~= nil then
return values[content]
end
end
-- Leave non-input or unresolved placeholders intact
return "{" .. content .. "}"
end)
end

---Resolve input placeholders by chaining vim.ui.input() calls
---Calls on_done(values) with a table of key→value, or on_done(nil) if user cancels
---@param keys string[] Unique input keys to resolve
---@param on_done fun(values: table<string, string>|nil)
function M.resolve(keys, on_done)
local values = {}
local i = 0

local function next_input()
i = i + 1
if i > #keys then
on_done(values)
return
end

local key = keys[i]
local prompt, default = M.parse(key)

vim.ui.input({
prompt = prompt .. ": ",
default = default or "",
}, function(value)
if value == nil then
on_done(nil)
return
end
values[key] = value
next_input()
end)
end

next_input()
end

return M
Loading