Skip to content

Add {input} placeholder for interactive user input#14

Open
jxyyz wants to merge 4 commits intoMSmaili:mainfrom
jxyyz:interactive-input
Open

Add {input} placeholder for interactive user input#14
jxyyz wants to merge 4 commits intoMSmaili:mainfrom
jxyyz:interactive-input

Conversation

@jxyyz
Copy link
Copy Markdown
Contributor

@jxyyz jxyyz commented Feb 24, 2026

Hello, I like this project. Good job. Because of that, I've thought about a little contribution. I've implemented a feature that will improve my own workflow as a user.

It's a complete implementation with tests and docs, but please consider this as a proposition or proof of concept for a change. I'm willing to change or adjust something in this approach.

What does this PR change?

This contribution introduces the {input} placeholder, which interactively asks the user for a value via vim.ui.input().

For example:

require("wiremux").send("question:\n{input:Question}\n\ncontext:\n{this}")

It will display an input widget for the user with "Question" as a prompt. Text provided by the user will be used in place of the placeholder.

There is also the possibility to provide a default value:

require("wiremux").send("question:\n{input:Question:Explain this code.}\n\ncontext:\n{this}")

Why?

Some placeholders (for example {selection}) won't work well when wiremux.send() is wrapped in vim.ui.input().

This snippet will not work well:

vim.ui.input({ prompt = "Question" }, function(value)
    require("wiremux").send("question: " .. value .. "\ncontext:\n{selection}")
end)

vim.ui.input() is asynchronous. I'm using Snack's input. When the prompt window is created, Neovim's state is changed and the text selected in the buffer is lost. So wiremux can't expand the {selection} placeholder.

Solution

A new {input} placeholder that prompts the user via vim.ui.input() at send time.

The placeholder expansion/resolving is done in this order:

  1. Sync phase -- context.expand() resolves all standard placeholders ({file}, {selection}, etc.) immediately, capturing the current editor state before any UI interaction occurs.
  2. Async phase -- input.resolve() then prompts the user for each {input} placeholder sequentially.

This guarantees that editor-state-dependent placeholders are captured while the state is still valid, regardless of what happens during user input.

Syntax

The placeholder supports optional prompt labels and default values:

{input}                         -- prompt "Input", no default
{input:Search query}            -- prompt "Search query"
{input:Branch:main}             -- prompt "Branch", default "main"
{input:URL:http://example.com}  -- prompt "URL", default "http://example.com"

Cancellation and deduplication

  • Cancelling any prompt (returning nil from vim.ui.input()) aborts the entire send. No partial text is dispatched.
  • Identical {input} expressions are deduplicated -- prompted once, substituted everywhere they appear.
  • User-entered text is treated as literal. Placeholders typed into the prompt are not recursively expanded.

Implementation details

New module: lua/wiremux/context/input.lua

Self-contained module with four functions:

  • find(text) -- extracts unique {input...} keys from text, preserving order of first appearance. Rejects false positives like {input_var} or {input2} by requiring either bare "input" or the "input:" prefix.
  • parse(key) -- splits a key into prompt label and optional default value. The first colon after input: separates the prompt from the default, so defaults can contain colons (e.g. URLs).
  • replace(text, values) -- substitutes resolved values back into text. Unresolved keys are left intact.
  • resolve(keys, on_done) -- chains vim.ui.input() calls sequentially. Calls on_done(values) on completion or on_done(nil) on cancellation.

Test coverage

  • tests/context_input_spec.lua -- unit tests for parse, find, has_inputs, replace, and resolve, including some edge cases
  • tests/send_input_spec.lua -- integration tests for the send action, verifying resolution order, cancellation aborting send, etc.

Usage examples

-- Prompt before sending a grep command
require("wiremux").send("grep -r '{input:Search query:TODO}' {file}")

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

-- Mix with other context placeholders -- {selection} resolves first, then {input} prompts
require("wiremux").send("question:\n{input:Question}\n\ncontext:\n{this}")

Add a new context/input module that handles {input}, {input:Prompt},
and {input:Prompt:default} placeholders in send templates. These
placeholders prompt the user via vim.ui.input() at send time, enabling
dynamic values like branch names or arguments.

- Add context/input.lua with parse, find, replace, and resolve functions
- Integrate input resolution into action/send's send_single_item flow
- Skip {input} keys during sync context.expand() to avoid errors
Add unit tests for context.input module (parse, find, has_inputs,
replace, resolve) and integration tests for send action with input
placeholders. Update helpers_send with context.input mock.
Document the {input} placeholder syntax in both README.md and
doc/wiremux.txt with usage examples.

- Add interactive input section to README with syntax table, code
  examples, and behavioral notes
- Add matching section to vimdoc with {input} syntax list and examples
@MSmaili
Copy link
Copy Markdown
Owner

MSmaili commented Feb 25, 2026

Hello @jxyyz, thanks for the contribution, really appreciate it.

It seems like an interesting and nice idea.

This is not a small change/addition. I will take a look, play around with it, and analyze. Most probably by the end of this week, will contact back. Sorry for taking so long, I am a bit busier this week.

Once again, thanks.

@jxyyz
Copy link
Copy Markdown
Contributor Author

jxyyz commented Feb 25, 2026

Cool, that's the appropriate path.

If you were not happy with some design decisions, I'm open to discussing them and adjusting my code.

In the meantime, I'll open some issues with bugs/possible enhancements I encountered while using this tool.

@jxyyz
Copy link
Copy Markdown
Contributor Author

jxyyz commented Feb 26, 2026

Back ref:
Found issue (#19) related to this PR

I think it would be beneficial to resolve it before merging this PR (or via this PR).

@MSmaili
Copy link
Copy Markdown
Owner

MSmaili commented Feb 26, 2026

I've been thinking about this quite a bit. My concern with {input} is that it mixes two different concepts: data placeholders (like {file}, {selection}) and user interaction (prompting for input). When you have multiple {input} placeholders scattered in your text, it's not immediately clear when or in what order you'll be prompted, especially if you're reusing the same send command later. It feels that input does not belong to a placeholder, more like an anti-pattern, but this gave me an idea.

I have an alternative proposal that might address the same use case. Would love to hear if this would work for you.

What is my proposal:

I'm thinking about an option we can call edit for now, to add to send() that opens a floating buffer where you can modify the text before sending. This solves the same problem but in a more explicit, visual way:

-- Instead of {input} placeholders:
require("wiremux").send("question:\n{input:Question}\n\ncontext:\n{this}")

// Use edit mode:
require("wiremux").send("question:\n\n\ncontext:\n{this}", { edit = true })

When edit = true, a floating buffer opens with the text (placeholders still unresolved). You can:

  • See all the context ({this}, {selection}, etc.) before it's expanded
  • Add your question directly in the buffer
  • Add more placeholders like {file} or {changes} as needed
  • Modify or remove any placeholders
  • Press to send, or q to cancel

Why I think this has some pros over {input} placeholder:

  1. Visual feedback - You see exactly what you're sending before it goes
  2. No hidden side effects - The edit buffer is explicit, not buried in placeholder syntax
  3. More flexible - You can add multiple questions, restructure the prompt, add/remove placeholders on the fly
  4. Captures state correctly - Placeholders are resolved after you close the edit buffer, so {selection} works perfectly
  5. Per-item control - You can enable editing for specific items:
   require("wiremux").send({
     { label = "Quick send", value = "echo {file}" },
     { label = "Ask AI", value = "question:\n\ncontext:\n{this}", edit = true },
   })

Also, in the future, we could just make: Wirmemux send if it is empty to open the buffer and send something ?

What do you think? Does this approach work for your use case? The main difference is that instead of inline {input} prompts, you get a full editor buffer where you can compose your message.

If you still prefer the {input} approach, I'm open to discussing it further. I tried a quick POC of the edit approach to see how it would feel in practice, and I'll add a small recording underneath to show what it looks like:

Cap.2026-02-26.at.19.20.48.mp4

I prototyped this to see how it feels. Implementation-wise, it's quite simple: all the edit UI logic lives in one module, which is only called when needed. The existing placeholder system and send flow remain untouched.

This can start simple and evolve incrementally. Potential extensions:

  • Custom keymaps and window styling (width/height/position)
  • Completion integration (nvim-cmp/blink.cmp) to suggest placeholders when typing {
  • Option to toggle between plane placeholder and resolved ones ?(maybe useful)
  • Other improvements as use cases emerge

Not all of these are trivial, but we can design the architecture to support gradual enhancement.

What do you think @jxyyz? Do you like this idea?

@MSmaili
Copy link
Copy Markdown
Owner

MSmaili commented Feb 27, 2026

If what I proposed solves your issue, I can make a commit for it, since I have been trying it and I like it; it is useful. Then we can think about whether we need something more.

That would mean that my proposal replaces this. @jxyyz please let me know

@jxyyz
Copy link
Copy Markdown
Contributor Author

jxyyz commented Mar 1, 2026

Hello, I'll reach out to you on this topic later today, since I haven't been active lately.

@jxyyz
Copy link
Copy Markdown
Contributor Author

jxyyz commented Mar 1, 2026

Hey @MSmaili, thanks for the detailed response and the prototype — the edit buffer looks really nice in practice. I see myself using it in certain situations.

I agree with your main concern: {input} mixes data placeholders with user interaction, and the {input:Prompt:Default} syntax adds complexity that doesn't feel consistent with the rest of the placeholder system. Fair points.

I thought about this and have one suggestion:

Ship the edit buffer exactly as you proposed (edit = true), but also expose a hook (e.g. on_resolved) that gives users access to the fully resolved text before dispatch. That keeps the core clean while supporting custom interaction patterns:

-- Built-in edit buffer, exactly as you proposed:
require("wiremux").send("context:\n{this}", { edit = true })

-- User-defined hook — receives resolved text, controls dispatch:
require("wiremux").send("context:\n{this}", {
  on_resolved = function(text, send)
    vim.ui.input({ prompt = "Question: " }, function(q)
      if q then send("question: " .. q .. "\n" .. text) end
    end)
  end
})

Of course the above is only an example / proof of concept. Final implementation might be different.

Why I care about this:

My use case is treating wiremux.send() as a library of ready-made prompt templates. I don't need to see the whole prompt — just quickly fill in a missing parameter and send. Think form field vs text editor. For that workflow, the edit buffer adds steps that break the flow.

Also, I can already edit the prompt directly in the target tmux pane, so the full-buffer editing is somewhat redundant for me. Especially when target instance supports vim-mode and/or ability to edit in external editor

I'm not married to the naming or exact signature — just the idea that there's a stage after placeholder resolution where the user can step in.

What do you think?

btw. little context why I created this PR:

My first instinct was to write a custom resolver, since wiremux already supports them. But async user prompts interfered with state-dependent placeholders like {selection} — there was no guaranteed resolution order (sync first, then async). That's what led me to the two-phase approach in this PR.

@MSmaili
Copy link
Copy Markdown
Owner

MSmaili commented Mar 2, 2026

Thanks for the comment.

I agree that at first, the edit buffer can seem redundant and not very useful. However, once we start adding more features, for example, an extra file picker that allows selecting files and pasting them directly into the buffer (i tried it and it is really simple to do), using placeholders directly becomes more compelling. I have been experimenting with this approach, and so far it feels clean and practical. That said, we definitely need to evaluate it more thoroughly.

I also understand your point, and it makes a lot of sense. We need to think carefully about how to implement this feasibly. The hook-based approach still seems promising. Perhaps we can expose a single public API that allows consumers to access either the full edited buffer or just the specific part they need. I am just thinking out loud here. I do not have a concrete solution yet.

Give me a bit of time, and I will come back with a proposal that balances both perspectives.

@jxyyz
Copy link
Copy Markdown
Contributor Author

jxyyz commented Mar 2, 2026

I understand. The final decision is yours ☺️

On the other hand, if there were an editable buffer for the prompt with a unique buftype or filetype (when edit=true), then I'd be able to set autocmds or other things that still allow creating custom interactive workflows.

I still prefer a hook or one of the solutions mentioned earlier. But definitely there are different pathways to achieve the same goals.

@MSmaili
Copy link
Copy Markdown
Owner

MSmaili commented Mar 6, 2026

@jxyyz could you try this branch with the edit buffer, and let me know what you think?

I also handled the {selection} part, so your {input} request should work after this branch merge. I hope this also solves your need
Give it a try and tell me what you think; is it useless, or do you see ways it could be useful or improved?

I’m also planning to add adapters for other pickers, so selecting filename insertion works better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants