1616from prompt_toolkit .formatted_text import FormattedText
1717from prompt_toolkit .history import FileHistory
1818from prompt_toolkit .key_binding import KeyBindings
19+ from prompt_toolkit .keys import Keys
1920from prompt_toolkit .patch_stdout import patch_stdout
2021from rich .console import Console
2122from rich .text import Text
@@ -78,13 +79,20 @@ def _history_path() -> str:
7879 return str (d / "input_history" )
7980
8081
81- def _make_key_bindings () -> KeyBindings :
82+ def _make_key_bindings (on_image_paste : Callable [[ str ], None ] | None = None ) -> KeyBindings :
8283 """Esc+Enter (or Alt+Enter) submits; plain Enter inserts a newline.
8384
8485 Tab triggers completion explicitly (first Tab inserts the common
8586 part / opens the menu, further Tabs cycle), Shift+Tab cycles
8687 backwards — prompt_toolkit's defaults don't reliably bind Tab in
8788 every mode/version.
89+
90+ ``on_image_paste`` (when given) is called with the temp-file path of
91+ a clipboard image captured on paste; the TUI records it as a pending
92+ attachment and a human-readable marker is inserted into the buffer.
93+ Passing the path via the callback (not an ``@path`` text token)
94+ avoids the whitespace-delimited ``@file`` parser truncating a temp
95+ path that contains spaces (e.g. macOS ``/var/folders/.../T/``).
8896 """
8997 kb = KeyBindings ()
9098
@@ -108,11 +116,57 @@ def _complete_backward(event: Any) -> None:
108116 else :
109117 b .start_completion (select_first = True )
110118
119+ @kb .add (Keys .BracketedPaste )
120+ def _paste (event : Any ) -> None :
121+ """Handle a terminal paste.
122+
123+ A terminal paste delivers text, never image bytes. So on paste
124+ we check the OS clipboard out-of-band: if it holds an image, it
125+ is written to a temp file and handed to ``on_image_paste`` (the
126+ TUI records it as a pending attachment); a short marker is
127+ inserted so the user sees the image was captured. Otherwise the
128+ pasted text is inserted as usual, preserving default paste
129+ behavior.
130+
131+ Image capture is best-effort and must never break paste: any
132+ failure falls back to inserting the pasted text.
133+ """
134+ buffer = event .current_buffer
135+ data = event .data or ""
136+ # Only inspect the clipboard for an image when the paste carried
137+ # no text: an image paste delivers empty/whitespace bracketed
138+ # data, while a text paste delivers the text. This keeps text
139+ # pastes (the common case) from spawning a clipboard subprocess.
140+ if on_image_paste is not None and not data .strip ():
141+ try :
142+ from ..clipboard import grab_clipboard_image
143+
144+ path = grab_clipboard_image ()
145+ except Exception : # noqa: BLE001 - capture must never break paste
146+ path = None
147+ if path :
148+ on_image_paste (path )
149+ # A non-@ marker: the path is tracked out-of-band, so it
150+ # need not (and must not) survive the @file parser. The
151+ # trailing space lets the user keep typing a prompt.
152+ from ..attachments import clipboard_image_marker
153+
154+ buffer .insert_text (clipboard_image_marker (path ))
155+ return
156+ # Normal text paste: normalise line endings exactly like
157+ # prompt_toolkit's default BracketedPaste handler (some terminals
158+ # paste \r\n / \r), which this binding overrides.
159+ data = data .replace ("\r \n " , "\n " ).replace ("\r " , "\n " )
160+ buffer .insert_text (data )
161+
111162 return kb
112163
113164
114165def _make_prompt_session (
115- history : FileHistory , completer : Completer , ** kwargs : Any
166+ history : FileHistory ,
167+ completer : Completer ,
168+ on_image_paste : Callable [[str ], None ] | None = None ,
169+ ** kwargs : Any ,
116170) -> PromptSession :
117171 """Create the TUI's input session.
118172
@@ -122,6 +176,9 @@ def _make_prompt_session(
122176 then bails out without inserting the common part). Tab must be the
123177 single, deterministic trigger.
124178
179+ ``on_image_paste`` is forwarded to the key bindings so a clipboard
180+ image captured on paste is recorded as a pending attachment.
181+
125182 ``enable_suspend`` is off on Windows: Ctrl-Z (suspend) is a Unix
126183 terminal feature with no Windows equivalent.
127184
@@ -135,7 +192,7 @@ def _make_prompt_session(
135192 try :
136193 return PromptSession (
137194 history = history ,
138- key_bindings = _make_key_bindings (),
195+ key_bindings = _make_key_bindings (on_image_paste ),
139196 completer = completer ,
140197 complete_while_typing = False ,
141198 multiline = True ,
@@ -148,7 +205,7 @@ def _make_prompt_session(
148205
149206 return PromptSession (
150207 history = history ,
151- key_bindings = _make_key_bindings (),
208+ key_bindings = _make_key_bindings (on_image_paste ),
152209 completer = completer ,
153210 complete_while_typing = False ,
154211 multiline = True ,
@@ -236,7 +293,10 @@ def get_completions(self, document: Any, complete_event: Any):
236293 yield from self ._complete_paths (arg )
237294 return
238295 token = text .rsplit (" " , 1 )[- 1 ] if " " in text else text
239- if token .startswith ("~" ) or "/" in token :
296+ if token .startswith ("@" ):
297+ # @file reference: complete the path after @
298+ yield from self ._complete_paths (token [1 :])
299+ elif token .startswith ("~" ) or "/" in token :
240300 yield from self ._complete_paths (token )
241301
242302
0 commit comments