duat_base/modes/
prompt.rs

1//! Multi modal for controlling the [`Prompt`] widget
2//!
3//! This mode's purpose is to do actions based on a [`PromptMode`]
4//! implementor. `PromptMode` implementors take in the [`Text`] of the
5//! [`Prompt`], and output some transformation to said `Text` (e.g.
6//! formatting), while also doing actions given the global access
7//! through the [`Pass`].
8//!
9//! Examples of [`PromptMode`]s are [`RunCommands`] and [`IncSearch`].
10//! The former is used to run Duat's commands, while the latter
11//! searches based on an input regex.
12//!
13//! `IncSearch` itself is _also_ multimodal, in an even more niche
14//! sense. It takes in an [`IncSearcher`] implementor, and searches
15//! through the [`Buffer`] according to its rules. Examples of this
16//! are [`SearchFwd`] and [`SearchRev`], which take in the regex and
17//! search in their respective directions. There are also more
18//! "advanced" `IncSearcher`s, like the ones in the `duatmode` crate,
19//! which can split a [`Selection`] by a regex, or keeps `Selections`s
20//! that match, that sort of thing.
21//!
22//! [`SearchFwd`]: super::SearchFwd
23//! [`SearchRev`]: super::SearchRev
24//! [`Selection`]: duat_core::mode::Selection
25use std::{
26    any::TypeId,
27    io::Write,
28    sync::{Arc, LazyLock, Mutex, Once},
29};
30
31use duat_core::{
32    buffer::Buffer,
33    cmd,
34    context::{self, Handle},
35    data::Pass,
36    form, hook,
37    mode::{self, KeyEvent, event},
38    text::{Ghost, Searcher, Tagger, Text, txt},
39    ui::{PrintInfo, RwArea, Widget},
40};
41
42use super::IncSearcher;
43use crate::{
44    hooks::{SearchPerformed, SearchUpdated},
45    widgets::PromptLine,
46};
47
48static HISTORY: Mutex<Vec<(TypeId, Vec<String>)>> = Mutex::new(Vec::new());
49static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
50static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
51static PREVIEW_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
52
53/// A [`Mode`] for the [`PromptLine`]
54///
55/// This mode abstracts over what the inner [`PromptMode`] actually
56/// does, by letting them focus on just updating the [`Text`] and
57/// acting on user input, instead of having to worry about which keys
58/// do what, and when to update.
59///
60/// There are currently three [`PromptMode`]s:
61///
62/// - [`RunCommands`] is just your regular command runner, it can also
63///   detect if your [`Parameter`]s are correct and show that in real
64///   time.
65/// - [`PipeSelections`] pipes each [`Selection`]'s selection in the
66///   current [`Buffer`] to an external application, replacing each
67///   selection with the returned value.
68/// - [`IncSearch`] has a further inner abstraction, [`IncSearcher`],
69///   which lets you abstract over what the incremental search will
70///   actually do. I.c. will it search for the next ocurrence, split
71///   selections by matches, things of the sort.
72///
73/// [`Parameter`]: cmd::Parameter
74/// [`Selection`]: mode::Selection
75/// [`Mode`]: duat_core::mode::Mode
76pub struct Prompt {
77    mode: Box<dyn PromptMode>,
78    starting_text: String,
79    ty: TypeId,
80    clone_fn: Arc<Mutex<ModeCloneFn>>,
81    reset_fn: fn(),
82    history_index: Option<usize>,
83}
84
85impl Prompt {
86    /// Returns a new [`Prompt`] from this [`PromptMode`]
87    ///
88    /// For convenience, you should make it so `new` methods in
89    /// [`PromptMode`] implementors return a [`Prompt<Self>`],
90    /// rather than the [`PromptMode`] itself.
91    pub fn new<M: PromptMode + Clone>(mode: M) -> Self {
92        let clone_fn = Arc::new(Mutex::new({
93            let mode = mode.clone();
94            move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
95        }));
96
97        Self {
98            mode: Box::new(mode),
99            starting_text: String::new(),
100            ty: TypeId::of::<M>(),
101            clone_fn,
102            reset_fn: mode::reset::<M::ExitWidget>,
103            history_index: None,
104        }
105    }
106
107    /// Returns a new [`Prompt`] with some initial text
108    ///
109    /// This is useful if you wish to open this [`Mode`] with some
110    /// text already in it.
111    ///
112    /// [`Mode`]: mode::Mode
113    pub fn new_with<M: PromptMode + Clone>(mode: M, initial: impl ToString) -> Self {
114        let clone_fn = Arc::new(Mutex::new({
115            let mode = mode.clone();
116            move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
117        }));
118        Self {
119            mode: Box::new(mode),
120            starting_text: initial.to_string(),
121            ty: TypeId::of::<M>(),
122            clone_fn,
123            reset_fn: mode::reset::<M::ExitWidget>,
124            history_index: None,
125        }
126    }
127
128    /// Shows the preview [`Ghost`]
129    fn show_preview(&mut self, pa: &mut Pass, handle: Handle<PromptLine>) {
130        let history = HISTORY.lock().unwrap();
131        if handle.text(pa).is_empty()
132            && let Some((_, ty_history)) = history.iter().find(|(ty, _)| *ty == self.ty)
133        {
134            handle.text_mut(pa).insert_tag_after(
135                *PREVIEW_TAGGER,
136                0,
137                Ghost::new(txt!("[prompt.preview]{}", ty_history.last().unwrap())),
138            );
139        }
140    }
141}
142
143impl mode::Mode for Prompt {
144    type Widget = PromptLine;
145
146    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
147        use duat_core::mode::KeyCode::*;
148
149        let ty_eq = |&&(ty, _): &&(TypeId, _)| ty == self.ty;
150
151        let mut update = |pa: &mut Pass| {
152            let text = std::mem::take(handle.write(pa).text_mut());
153            let text = self.mode.update(pa, text, handle.area());
154            *handle.write(pa).text_mut() = text;
155        };
156
157        let reset = |prompt: &mut Self| {
158            if let Some(ret_handle) = prompt.mode.return_handle() {
159                mode::reset_to(ret_handle);
160            } else {
161                (prompt.reset_fn)();
162            }
163        };
164
165        handle.text_mut(pa).remove_tags(*PREVIEW_TAGGER, ..);
166
167        match key {
168            event!(Backspace) => {
169                if handle.read(pa).text().is_empty() {
170                    handle.write(pa).text_mut().selections_mut().clear();
171
172                    update(pa);
173
174                    if let Some(ret_handle) = self.mode.return_handle() {
175                        mode::reset_to(ret_handle);
176                    } else {
177                        (self.reset_fn)();
178                    }
179                } else {
180                    handle.edit_main(pa, |mut c| {
181                        c.move_hor(-1);
182                        c.set_anchor_if_needed();
183                        c.replace("");
184                        c.unset_anchor();
185                    });
186                    update(pa);
187                }
188            }
189            event!(Delete) => {
190                handle.edit_main(pa, |mut c| c.replace(""));
191                update(pa);
192            }
193
194            event!(Char(char)) => {
195                handle.edit_main(pa, |mut c| {
196                    c.insert(char);
197                    c.move_hor(1);
198                });
199                update(pa);
200            }
201            event!(Left) => {
202                handle.edit_main(pa, |mut c| c.move_hor(-1));
203                update(pa);
204            }
205            event!(Right) => {
206                handle.edit_main(pa, |mut c| c.move_hor(1));
207                update(pa);
208            }
209            event!(Up) => {
210                let history = HISTORY.lock().unwrap();
211                let Some((_, ty_history)) = history.iter().find(ty_eq) else {
212                    return;
213                };
214
215                let index = if let Some(index) = &mut self.history_index {
216                    *index = index.saturating_sub(1);
217                    *index
218                } else {
219                    self.history_index = Some(ty_history.len() - 1);
220                    ty_history.len() - 1
221                };
222
223                handle.edit_main(pa, |mut c| {
224                    c.move_to(..);
225                    c.replace(ty_history[index].clone());
226                    c.unset_anchor();
227                })
228            }
229            event!(Down) => {
230                let history = HISTORY.lock().unwrap();
231                let Some((_, ty_history)) = history.iter().find(ty_eq) else {
232                    return;
233                };
234
235                if let Some(index) = &mut self.history_index {
236                    if *index + 1 < ty_history.len() {
237                        *index = (*index + 1).min(ty_history.len() - 1);
238
239                        handle.edit_main(pa, |mut c| {
240                            c.move_to(..);
241                            c.replace(ty_history[*index].clone());
242                            c.unset_anchor();
243                        })
244                    } else {
245                        self.history_index = None;
246                        handle.edit_main(pa, |mut c| {
247                            c.move_to(..);
248                            c.replace("");
249                            c.unset_anchor();
250                        })
251                    }
252                };
253            }
254
255            event!(Esc) => {
256                handle.edit_main(pa, |mut c| {
257                    c.move_to(..);
258                    c.replace("");
259                });
260                handle.write(pa).text_mut().selections_mut().clear();
261                update(pa);
262                reset(self);
263            }
264            event!(Enter) => {
265                handle.write(pa).text_mut().selections_mut().clear();
266
267                if handle.text(pa).is_empty() {
268                    let history = HISTORY.lock().unwrap();
269                    if let Some((_, ty_history)) = history.iter().find(ty_eq) {
270                        handle.edit_main(pa, |mut c| {
271                            c.move_to(..);
272                            c.replace(ty_history.last().unwrap());
273                        });
274                    }
275                }
276
277                update(pa);
278                reset(self);
279            }
280            _ => {}
281        }
282
283        self.show_preview(pa, handle);
284    }
285
286    fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
287        let text = {
288            let pl = handle.write(pa);
289            *pl.text_mut() = Text::with_default_main_selection();
290            pl.text_mut().replace_range(0..0, &self.starting_text);
291
292            let tag = Ghost::new(match pl.prompt_of_id(self.ty) {
293                Some(text) => txt!("{text}[prompt.colon]:"),
294                None => txt!("{}[prompt.colon]:", self.mode.prompt()),
295            });
296            pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
297
298            std::mem::take(pl.text_mut())
299        };
300
301        let text = self.mode.on_switch(pa, text, handle.area());
302
303        *handle.write(pa).text_mut() = text;
304
305        self.show_preview(pa, handle);
306    }
307
308    fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
309        let text = std::mem::take(handle.write(pa).text_mut());
310        if !text.is_empty() {
311            let mut history = HISTORY.lock().unwrap();
312            if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == self.ty) {
313                if ty_history.last().is_none_or(|last| last != text.bytes()) {
314                    ty_history.push(text.to_string());
315                }
316            } else {
317                history.push((self.ty, vec![text.to_string()]));
318            }
319        }
320
321        self.mode.before_exit(pa, text, handle.area());
322    }
323}
324
325impl Clone for Prompt {
326    fn clone(&self) -> Self {
327        Self {
328            mode: self.clone_fn.lock().unwrap()(),
329            starting_text: self.starting_text.clone(),
330            ty: self.ty,
331            clone_fn: self.clone_fn.clone(),
332            reset_fn: self.reset_fn,
333            history_index: None,
334        }
335    }
336}
337
338/// A mode to control the [`Prompt`]
339///
340/// Through the [`Pass`], one can act on the entirety of Duat's shared
341/// state:
342///
343/// ```rust
344/// # duat_core::doc_duat!(duat);
345/// # use duat_base::modes::PromptMode;
346/// use duat::prelude::*;
347///
348/// #[derive(Default, Clone)]
349/// struct RealTimeSwitch {
350///     initial: Option<String>,
351///     current: Option<String>,
352///     name_was_correct: bool,
353/// };
354///
355/// impl PromptMode for RealTimeSwitch {
356///     type ExitWidget = Buffer;
357///
358///     fn update(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
359///         let name = text.to_string();
360///
361///         self.name_was_correct = if name != *self.current.as_ref().unwrap() {
362///             if cmd::buffer(pa, &name).is_ok() {
363///                 self.current = Some(name);
364///                 true
365///             } else {
366///                 false
367///             }
368///         } else {
369///             true
370///         };
371///
372///         text
373///     }
374///
375///     fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
376///         self.initial = Some(context::current_buffer(pa).read(pa).name());
377///         self.current = self.initial.clone();
378///
379///         text
380///     }
381///
382///     fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) {
383///         if !self.name_was_correct {
384///             cmd::buffer(pa, self.initial.take().unwrap());
385///         }
386///     }
387///
388///     fn prompt(&self) -> Text {
389///         txt!("[prompt]switch to")
390///     }
391/// }
392/// ```
393///
394/// The [`PromptMode`] above will switch to the buffer with the same
395/// name as the one in the [`PromptLine`], returning to the initial
396/// buffer if the match failed.
397#[allow(unused_variables)]
398pub trait PromptMode: Send + 'static {
399    /// What [`Widget`] to exit to, upon pressing enter, esc, or
400    /// backspace in an empty [`PromptLine`]
401    ///
402    /// Usually, this would be [`Buffer`]
403    type ExitWidget: Widget
404    where
405        Self: Sized;
406
407    /// Updates the [`PromptLine`] and [`Text`] of the [`Prompt`]
408    ///
409    /// This function is triggered every time the user presses a key
410    /// in the [`Prompt`] mode.
411    fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
412
413    /// What to do when switchin onto this [`PromptMode`]
414    ///
415    /// The initial [`Text`] is always empty, except for the [prompt]
416    /// [`Ghost`] at the beginning of the line.
417    ///
418    /// [prompt]: PromptMode::prompt
419    fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
420        text
421    }
422
423    /// What to do before exiting the [`PromptMode`]
424    ///
425    /// This usually involves some sor of "commitment" to the result,
426    /// e.g., [`RunCommands`] executes the call, [`IncSearch`]
427    /// finishes the search, etc.
428    fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
429
430    /// What text should be at the beginning of the [`PromptLine`], as
431    /// a [`Ghost`]
432    fn prompt(&self) -> Text;
433
434    /// An optional returning [`Handle`] for the [`ExitWidget`]
435    ///
436    /// [`ExitWidget`]: PromptMode::ExitWidget
437    fn return_handle(&self) -> Option<Handle<dyn Widget>> {
438        None
439    }
440}
441
442/// Runs Duat commands, with syntax highlighting for correct
443/// [`Parameter`]s
444///
445/// [`Parameter`]: duat_core::cmd::Parameter
446#[derive(Default, Clone)]
447pub struct RunCommands;
448
449impl RunCommands {
450    /// Crates a new [`RunCommands`]
451    #[allow(clippy::new_ret_no_self)]
452    pub fn new() -> Prompt {
453        Self::call_once();
454        Prompt::new(Self)
455    }
456
457    /// Opens a [`RunCommands`] with some initial text
458    pub fn new_with(initial: impl ToString) -> Prompt {
459        Self::call_once();
460        Prompt::new_with(Self, initial)
461    }
462
463    fn call_once() {
464        static ONCE: Once = Once::new();
465        ONCE.call_once(|| {
466            form::set_weak("caller.info", "accent.info");
467            form::set_weak("caller.error", "accent.error");
468            form::set_weak("parameter.info", "default.info");
469            form::set_weak("parameter.error", "default.error");
470        });
471    }
472}
473
474impl PromptMode for RunCommands {
475    type ExitWidget = Buffer;
476
477    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
478        text.remove_tags(*TAGGER, ..);
479
480        let command = text.to_string();
481        let caller = command.split_whitespace().next();
482        if let Some(caller) = caller {
483            if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
484                let id = form::id_of!("caller.info");
485                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
486
487                let default_id = form::id_of!("parameter.info");
488                for (range, id) in ok_ranges {
489                    text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
490                }
491                if let Some((range, _)) = err_range {
492                    let id = form::id_of!("parameter.error");
493                    text.insert_tag(*TAGGER, range, id.to_tag(0));
494                }
495            } else {
496                let id = form::id_of!("caller.error");
497                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
498            }
499        }
500
501        text
502    }
503
504    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
505        let call = text.to_string();
506        if !call.is_empty() {
507            cmd::queue_notify(call);
508        }
509    }
510
511    fn prompt(&self) -> Text {
512        Text::default()
513    }
514}
515
516/// The [`PromptMode`] that makes use of [`IncSearcher`]s
517///
518/// In order to make use of incremental search, you'd do something
519/// like this:
520///
521/// ```rust
522/// # duat_core::doc_duat!(duat);
523/// # use duat_base::modes::{IncSearch, SearchFwd};
524/// use duat::prelude::*;
525///
526/// #[derive(Clone)]
527/// struct Emacs;
528///
529/// impl Mode for Emacs {
530///     type Widget = Buffer;
531///
532///     fn send_key(&mut self, pa: &mut Pass, event: KeyEvent, handle: Handle) {
533///         match event {
534///             ctrl!('s') => mode::set(IncSearch::new(SearchFwd)),
535///             other_keys_oh_god => todo!(),
536///         }
537///     }
538/// }
539/// ```
540///
541/// This function returns a [`Prompt<IncSearch<SearchFwd>>`],
542pub struct IncSearch<I: IncSearcher> {
543    inc: I,
544    orig: Option<(mode::Selections, PrintInfo)>,
545    prev: String,
546}
547
548impl<I: IncSearcher> Clone for IncSearch<I> {
549    fn clone(&self) -> Self {
550        Self {
551            inc: self.inc.clone(),
552            orig: self.orig.clone(),
553            prev: self.prev.clone(),
554        }
555    }
556}
557
558impl<I: IncSearcher> IncSearch<I> {
559    /// Returns a [`Prompt`] with [`IncSearch<I>`] as its
560    /// [`PromptMode`]
561    #[allow(clippy::new_ret_no_self)]
562    pub fn new(inc: I) -> Prompt {
563        static ONCE: Once = Once::new();
564        ONCE.call_once(|| {
565            form::set_weak("regex.error", "accent.error");
566            form::set_weak("regex.operator", "operator");
567            form::set_weak("regex.class", "constant");
568            form::set_weak("regex.bracket", "punctuation.bracket");
569        });
570        Prompt::new(Self { inc, orig: None, prev: String::new() })
571    }
572}
573
574impl<I: IncSearcher> PromptMode for IncSearch<I> {
575    type ExitWidget = Buffer;
576
577    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
578        let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
579        text.remove_tags(*TAGGER, ..);
580
581        let handle = context::current_buffer(pa).clone();
582
583        if text == self.prev {
584            return text;
585        } else {
586            let prev = std::mem::replace(&mut self.prev, text.to_string());
587            hook::queue(SearchUpdated((prev, self.prev.clone())));
588        }
589
590        match Searcher::new(text.to_string()) {
591            Ok(searcher) => {
592                handle.area().set_print_info(pa, orig_print_info.clone());
593                let buffer = handle.write(pa);
594                *buffer.selections_mut() = orig_selections.clone();
595
596                let ast = regex_syntax::ast::parse::Parser::new()
597                    .parse(&text.to_string())
598                    .unwrap();
599
600                crate::tag_from_ast(*TAGGER, &mut text, &ast);
601
602                if !text.is_empty() {
603                    self.inc.search(pa, handle.attach_searcher(searcher));
604                }
605            }
606            Err(err) => {
607                let regex_syntax::Error::Parse(err) = *err else {
608                    unreachable!("As far as I can tell, regex_syntax has goofed up");
609                };
610
611                let span = err.span();
612                let id = form::id_of!("regex.error");
613
614                text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
615            }
616        }
617
618        text
619    }
620
621    fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
622        let handle = context::current_buffer(pa);
623
624        self.orig = Some((
625            handle.read(pa).selections().clone(),
626            handle.area().get_print_info(pa),
627        ));
628
629        text
630    }
631
632    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
633        if !text.is_empty() {
634            if let Err(err) = Searcher::new(text.to_string()) {
635                let regex_syntax::Error::Parse(err) = *err else {
636                    unreachable!("As far as I can tell, regex_syntax has goofed up");
637                };
638
639                let range = err.span().start.offset..err.span().end.offset;
640                let err = txt!(
641                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
642                    range,
643                    text.strs(range).unwrap(),
644                    err.kind()
645                );
646
647                context::error!("{err}")
648            } else {
649                hook::queue(SearchPerformed(text.to_string()));
650            }
651        }
652    }
653
654    fn prompt(&self) -> Text {
655        txt!("{}", self.inc.prompt())
656    }
657}
658
659/// Pipes the selections of a [`Buffer`] through an external command
660///
661/// This can be useful if you, for example, don't have access to a
662/// formatter, but want to format text, so you pass it to
663/// [`PipeSelections`] with `fold` as the command, or things of the
664/// sort.
665#[derive(Clone, Copy)]
666pub struct PipeSelections;
667
668impl PipeSelections {
669    /// Returns a [`Prompt`] with [`PipeSelections`] as its
670    /// [`PromptMode`]
671    #[allow(clippy::new_ret_no_self)]
672    pub fn new() -> Prompt {
673        Prompt::new(Self)
674    }
675}
676
677impl PromptMode for PipeSelections {
678    type ExitWidget = Buffer;
679
680    fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
681        fn is_in_path(program: &str) -> bool {
682            if let Ok(path) = std::env::var("PATH") {
683                for p in path.split(":") {
684                    let p_str = format!("{p}/{program}");
685                    if let Ok(true) = std::fs::exists(p_str) {
686                        return true;
687                    }
688                }
689            }
690            false
691        }
692
693        text.remove_tags(*TAGGER, ..);
694
695        let command = text.to_string();
696        let Some(caller) = command.split_whitespace().next() else {
697            return text;
698        };
699
700        let args = cmd::ArgsIter::new(&command);
701
702        let (caller_id, args_id) = if is_in_path(caller) {
703            (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
704        } else {
705            (
706                form::id_of!("caller.error"),
707                form::id_of!("parameter.error"),
708            )
709        };
710
711        let c_s = command.len() - command.trim_start().len();
712        text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
713
714        for (_, range) in args {
715            text.insert_tag(*TAGGER, range, args_id.to_tag(0));
716        }
717
718        text
719    }
720
721    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
722        use std::process::{Command, Stdio};
723
724        let command = text.to_string();
725        let Some(caller) = command.split_whitespace().next() else {
726            return;
727        };
728
729        let handle = context::current_buffer(pa).clone();
730        handle.edit_all(pa, |mut c| {
731            let Ok(mut child) = Command::new(caller)
732                .args(cmd::ArgsIter::new(&command).map(|(a, _)| a))
733                .stdin(Stdio::piped())
734                .stdout(Stdio::piped())
735                .spawn()
736            else {
737                return;
738            };
739
740            let input: String = c.selection().collect();
741            if let Some(mut stdin) = child.stdin.take() {
742                std::thread::spawn(move || {
743                    stdin.write_all(input.as_bytes()).unwrap();
744                });
745            }
746            if let Ok(out) = child.wait_with_output() {
747                let out = String::from_utf8_lossy(&out.stdout);
748                c.set_anchor_if_needed();
749                c.replace(out);
750            }
751        });
752    }
753
754    fn prompt(&self) -> Text {
755        txt!("[prompt]pipe")
756    }
757}
758
759type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;