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(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(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///     fn update(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
357///         let name = text.to_string();
358///
359///         self.name_was_correct = if name != *self.current.as_ref().unwrap() {
360///             if cmd::buffer(pa, &name).is_ok() {
361///                 self.current = Some(name);
362///                 true
363///             } else {
364///                 false
365///             }
366///         } else {
367///             true
368///         };
369///
370///         text
371///     }
372///
373///     fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
374///         self.initial = Some(context::current_buffer(pa).read(pa).name());
375///         self.current = self.initial.clone();
376///
377///         text
378///     }
379///
380///     fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) {
381///         if !self.name_was_correct {
382///             cmd::buffer(pa, self.initial.take().unwrap());
383///         }
384///     }
385///
386///     fn prompt(&self) -> Text {
387///         txt!("[prompt]switch to")
388///     }
389/// }
390/// ```
391///
392/// The [`PromptMode`] above will switch to the buffer with the same
393/// name as the one in the [`PromptLine`], returning to the initial
394/// buffer if the match failed.
395#[allow(unused_variables)]
396pub trait PromptMode: Send + 'static {
397    /// What [`Widget`] to exit to, upon pressing enter, esc, or
398    /// backspace in an empty [`PromptLine`]
399    type ExitWidget: Widget
400        = Buffer
401    where
402        Self: Sized;
403
404    /// Updates the [`PromptLine`] and [`Text`] of the [`Prompt`]
405    ///
406    /// This function is triggered every time the user presses a key
407    /// in the [`Prompt`] mode.
408    fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
409
410    /// What to do when switchin onto this [`PromptMode`]
411    ///
412    /// The initial [`Text`] is always empty, except for the [prompt]
413    /// [`Ghost`] at the beginning of the line.
414    ///
415    /// [prompt]: PromptMode::prompt
416    fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
417        text
418    }
419
420    /// What to do before exiting the [`PromptMode`]
421    ///
422    /// This usually involves some sor of "commitment" to the result,
423    /// e.g., [`RunCommands`] executes the call, [`IncSearch`]
424    /// finishes the search, etc.
425    fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
426
427    /// What text should be at the beginning of the [`PromptLine`], as
428    /// a [`Ghost`]
429    fn prompt(&self) -> Text;
430
431    /// An optional returning [`Handle`] for the [`ExitWidget`]
432    ///
433    /// [`ExitWidget`]: PromptMode::ExitWidget
434    fn return_handle(&self) -> Option<Handle<dyn Widget>> {
435        None
436    }
437}
438
439/// Runs Duat commands, with syntax highlighting for correct
440/// [`Parameter`]s
441///
442/// [`Parameter`]: duat_core::cmd::Parameter
443#[derive(Default, Clone)]
444pub struct RunCommands;
445
446impl RunCommands {
447    /// Crates a new [`RunCommands`]
448    #[allow(clippy::new_ret_no_self)]
449    pub fn new() -> Prompt {
450        Self::call_once();
451        Prompt::new(Self)
452    }
453
454    /// Opens a [`RunCommands`] with some initial text
455    pub fn new_with(initial: impl ToString) -> Prompt {
456        Self::call_once();
457        Prompt::new_with(Self, initial)
458    }
459
460    fn call_once() {
461        static ONCE: Once = Once::new();
462        ONCE.call_once(|| {
463            form::set_weak("caller.info", "accent.info");
464            form::set_weak("caller.error", "accent.error");
465            form::set_weak("parameter.info", "default.info");
466            form::set_weak("parameter.error", "default.error");
467        });
468    }
469}
470
471impl PromptMode for RunCommands {
472    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
473        text.remove_tags(*TAGGER, ..);
474
475        let command = text.to_string();
476        let caller = command.split_whitespace().next();
477        if let Some(caller) = caller {
478            if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
479                let id = form::id_of!("caller.info");
480                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
481
482                let default_id = form::id_of!("parameter.info");
483                for (range, id) in ok_ranges {
484                    text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
485                }
486                if let Some((range, _)) = err_range {
487                    let id = form::id_of!("parameter.error");
488                    text.insert_tag(*TAGGER, range, id.to_tag(0));
489                }
490            } else {
491                let id = form::id_of!("caller.error");
492                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
493            }
494        }
495
496        text
497    }
498
499    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
500        let call = text.to_string();
501        if !call.is_empty() {
502            cmd::queue_notify(call);
503        }
504    }
505
506    fn prompt(&self) -> Text {
507        Text::default()
508    }
509}
510
511/// The [`PromptMode`] that makes use of [`IncSearcher`]s
512///
513/// In order to make use of incremental search, you'd do something
514/// like this:
515///
516/// ```rust
517/// # duat_core::doc_duat!(duat);
518/// # use duat_base::modes::{IncSearch, SearchFwd};
519/// use duat::prelude::*;
520///
521/// #[derive(Clone)]
522/// struct Emacs;
523///
524/// impl Mode for Emacs {
525///     type Widget = Buffer;
526///
527///     fn send_key(&mut self, pa: &mut Pass, event: KeyEvent, handle: Handle) {
528///         match event {
529///             ctrl!('s') => mode::set(IncSearch::new(SearchFwd)),
530///             other_keys_oh_god => todo!(),
531///         }
532///     }
533/// }
534/// ```
535///
536/// This function returns a [`Prompt<IncSearch<SearchFwd>>`],
537pub struct IncSearch<I: IncSearcher> {
538    inc: I,
539    orig: Option<(mode::Selections, PrintInfo)>,
540    prev: String,
541}
542
543impl<I: IncSearcher> Clone for IncSearch<I> {
544    fn clone(&self) -> Self {
545        Self {
546            inc: self.inc.clone(),
547            orig: self.orig.clone(),
548            prev: self.prev.clone(),
549        }
550    }
551}
552
553impl<I: IncSearcher> IncSearch<I> {
554    /// Returns a [`Prompt`] with [`IncSearch<I>`] as its
555    /// [`PromptMode`]
556    #[allow(clippy::new_ret_no_self)]
557    pub fn new(inc: I) -> Prompt {
558        static ONCE: Once = Once::new();
559        ONCE.call_once(|| {
560            form::set_weak("regex.error", "accent.error");
561            form::set_weak("regex.operator", "operator");
562            form::set_weak("regex.class", "constant");
563            form::set_weak("regex.bracket", "punctuation.bracket");
564        });
565        Prompt::new(Self { inc, orig: None, prev: String::new() })
566    }
567}
568
569impl<I: IncSearcher> PromptMode for IncSearch<I> {
570    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
571        let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
572        text.remove_tags(*TAGGER, ..);
573
574        let handle = context::current_buffer(pa).clone();
575
576        if text == self.prev {
577            return text;
578        } else {
579            let prev = std::mem::replace(&mut self.prev, text.to_string());
580            hook::queue(SearchUpdated((prev, self.prev.clone())));
581        }
582
583        match Searcher::new(text.to_string()) {
584            Ok(searcher) => {
585                handle.area().set_print_info(pa, orig_print_info.clone());
586                let buffer = handle.write(pa);
587                *buffer.selections_mut() = orig_selections.clone();
588
589                let ast = regex_syntax::ast::parse::Parser::new()
590                    .parse(&text.to_string())
591                    .unwrap();
592
593                crate::tag_from_ast(*TAGGER, &mut text, &ast);
594
595                if !text.is_empty() {
596                    self.inc.search(pa, handle.attach_searcher(searcher));
597                }
598            }
599            Err(err) => {
600                let regex_syntax::Error::Parse(err) = *err else {
601                    unreachable!("As far as I can tell, regex_syntax has goofed up");
602                };
603
604                let span = err.span();
605                let id = form::id_of!("regex.error");
606
607                text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
608            }
609        }
610
611        text
612    }
613
614    fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
615        let handle = context::current_buffer(pa);
616
617        self.orig = Some((
618            handle.read(pa).selections().clone(),
619            handle.area().get_print_info(pa),
620        ));
621
622        text
623    }
624
625    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
626        if !text.is_empty() {
627            if let Err(err) = Searcher::new(text.to_string()) {
628                let regex_syntax::Error::Parse(err) = *err else {
629                    unreachable!("As far as I can tell, regex_syntax has goofed up");
630                };
631
632                let range = err.span().start.offset..err.span().end.offset;
633                let err = txt!(
634                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
635                    range,
636                    text.strs(range).unwrap(),
637                    err.kind()
638                );
639
640                context::error!("{err}")
641            } else {
642                hook::queue(SearchPerformed(text.to_string()));
643            }
644        }
645    }
646
647    fn prompt(&self) -> Text {
648        txt!("{}", self.inc.prompt())
649    }
650}
651
652/// Pipes the selections of a [`Buffer`] through an external command
653///
654/// This can be useful if you, for example, don't have access to a
655/// formatter, but want to format text, so you pass it to
656/// [`PipeSelections`] with `fold` as the command, or things of the
657/// sort.
658#[derive(Clone, Copy)]
659pub struct PipeSelections;
660
661impl PipeSelections {
662    /// Returns a [`Prompt`] with [`PipeSelections`] as its
663    /// [`PromptMode`]
664    #[allow(clippy::new_ret_no_self)]
665    pub fn new() -> Prompt {
666        Prompt::new(Self)
667    }
668}
669
670impl PromptMode for PipeSelections {
671    fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
672        fn is_in_path(program: &str) -> bool {
673            if let Ok(path) = std::env::var("PATH") {
674                for p in path.split(":") {
675                    let p_str = format!("{p}/{program}");
676                    if let Ok(true) = std::fs::exists(p_str) {
677                        return true;
678                    }
679                }
680            }
681            false
682        }
683
684        text.remove_tags(*TAGGER, ..);
685
686        let command = text.to_string();
687        let Some(caller) = command.split_whitespace().next() else {
688            return text;
689        };
690
691        let args = cmd::args_iter(&command);
692
693        let (caller_id, args_id) = if is_in_path(caller) {
694            (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
695        } else {
696            (
697                form::id_of!("caller.error"),
698                form::id_of!("parameter.error"),
699            )
700        };
701
702        let c_s = command.len() - command.trim_start().len();
703        text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
704
705        for (_, range) in args {
706            text.insert_tag(*TAGGER, range, args_id.to_tag(0));
707        }
708
709        text
710    }
711
712    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
713        use std::process::{Command, Stdio};
714
715        let command = text.to_string();
716        let Some(caller) = command.split_whitespace().next() else {
717            return;
718        };
719
720        let handle = context::current_buffer(pa).clone();
721        handle.edit_all(pa, |mut c| {
722            let Ok(mut child) = Command::new(caller)
723                .args(cmd::args_iter(&command).map(|(a, _)| a))
724                .stdin(Stdio::piped())
725                .stdout(Stdio::piped())
726                .spawn()
727            else {
728                return;
729            };
730
731            let input: String = c.selection().collect();
732            if let Some(mut stdin) = child.stdin.take() {
733                std::thread::spawn(move || {
734                    stdin.write_all(input.as_bytes()).unwrap();
735                });
736            }
737            if let Ok(out) = child.wait_with_output() {
738                let out = String::from_utf8_lossy(&out.stdout);
739                c.set_anchor_if_needed();
740                c.replace(out);
741            }
742        });
743    }
744
745    fn prompt(&self) -> Text {
746        txt!("[prompt]pipe")
747    }
748}
749
750type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;