duat_utils/modes/
prompt.rs

1use std::{io::Write, marker::PhantomData, sync::LazyLock};
2
3use duat_core::{prelude::*, text::Searcher};
4
5use super::IncSearcher;
6use crate::{
7    hooks::{SearchPerformed, SearchUpdated},
8    widgets::PromptLine,
9};
10
11static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
12static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
13
14/// A [`Mode`] for the [`PromptLine`]
15///
16/// This mode abstracts over what the inner [`PromptMode`] actually
17/// does, by letting them focus on just updating the [`Text`] and
18/// acting on user input, instead of having to worry about which keys
19/// do what, and when to update.
20///
21/// There are currently three [`PromptMode`]s:
22///
23/// - [`RunCommands`] is just your regular command runner, it can also
24///   detect if your [`Parameter`]s are correct and show that in real
25///   time.
26/// - [`PipeSelections`] pipes each [`Selection`]'s selection in the
27///   current [`File`] to an external application, replacing each
28///   selection with the returned value.
29/// - [`IncSearch`] has a further inner abstraction, [`IncSearcher`],
30///   which lets you abstract over what the incremental search will
31///   actually do. I.e. will it search for the next ocurrence, split
32///   selections by matches, things of the sort.
33///
34/// [`Parameter`]: cmd::Parameter
35/// [`Selection`]: mode::Selection
36#[derive(Clone)]
37pub struct Prompt<M: PromptMode<U>, U: Ui>(M, PhantomData<U>);
38
39impl<M: PromptMode<U>, U: Ui> Prompt<M, U> {
40    /// Returns a new [`Prompt`] from this [`PromptMode`]
41    ///
42    /// For convenience, you should make it so `new` methods in
43    /// [`PromptMode`] implementors return a [`Prompt<Self, U>`],
44    /// rather than the [`PromptMode`] itself.
45    pub fn new(mode: M) -> Self {
46        Self(mode, PhantomData)
47    }
48}
49
50impl<M: PromptMode<U>, U: Ui> mode::Mode<U> for Prompt<M, U> {
51    type Widget = PromptLine<U>;
52
53    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget, U>) {
54        match key {
55            key!(KeyCode::Backspace) => {
56                if handle.read(pa, |pl, _| pl.text().is_empty()) {
57                    handle.write_selections(pa, |c| c.clear());
58
59                    let text = handle.take_text(pa);
60                    let text = self.0.update(pa, text, handle.area());
61                    handle.replace_text(pa, text);
62
63                    if let Some(ret_handle) = self.0.return_handle() {
64                        mode::reset_to(ret_handle);
65                    } else {
66                        mode::reset::<M::ExitWidget, U>();
67                    }
68                } else {
69                    handle.edit_main(pa, |mut e| {
70                        e.move_hor(-1);
71                        e.replace("");
72                    });
73                    let text = handle.take_text(pa);
74                    let text = self.0.update(pa, text, handle.area());
75                    handle.replace_text(pa, text);
76                }
77            }
78            key!(KeyCode::Delete) => {
79                handle.edit_main(pa, |mut e| e.replace(""));
80                let text = handle.take_text(pa);
81                let text = self.0.update(pa, text, handle.area());
82                handle.replace_text(pa, text);
83            }
84
85            key!(KeyCode::Char(char)) => {
86                handle.edit_main(pa, |mut e| {
87                    e.insert(char);
88                    e.move_hor(1);
89                });
90                let text = handle.take_text(pa);
91                let text = self.0.update(pa, text, handle.area());
92                handle.replace_text(pa, text);
93            }
94            key!(KeyCode::Left) => {
95                handle.edit_main(pa, |mut e| e.move_hor(-1));
96                let text = handle.take_text(pa);
97                let text = self.0.update(pa, text, handle.area());
98                handle.replace_text(pa, text);
99            }
100            key!(KeyCode::Right) => {
101                handle.edit_main(pa, |mut e| e.move_hor(1));
102                let text = handle.take_text(pa);
103                let text = self.0.update(pa, text, handle.area());
104                handle.replace_text(pa, text);
105            }
106
107            key!(KeyCode::Esc) => {
108                let p = handle.read(pa, |wid, _| wid.text().len());
109                handle.edit_main(pa, |mut e| {
110                    e.move_to_start();
111                    e.set_anchor();
112                    e.move_to(p);
113                    e.replace("");
114                });
115                handle.write_selections(pa, |c| c.clear());
116                let text = handle.take_text(pa);
117                let text = self.0.update(pa, text, handle.area());
118                handle.replace_text(pa, text);
119
120                if let Some(ret_handle) = self.0.return_handle() {
121                    mode::reset_to(ret_handle);
122                } else {
123                    mode::reset::<M::ExitWidget, U>();
124                }
125            }
126            key!(KeyCode::Enter) => {
127                handle.write_selections(pa, |c| c.clear());
128                let text = handle.take_text(pa);
129                let text = self.0.update(pa, text, handle.area());
130                handle.replace_text(pa, text);
131
132                if let Some(ret_handle) = self.0.return_handle() {
133                    mode::reset_to(ret_handle);
134                } else {
135                    mode::reset::<M::ExitWidget, U>();
136                }
137            }
138            _ => {}
139        }
140    }
141
142    fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
143        let text = handle.write(pa, |wid, _| {
144            *wid.text_mut() = Text::new_with_selections();
145            run_once::<M, U>();
146
147            let tag = Ghost(match wid.prompt_of::<M>() {
148                Some(text) => text,
149                None => self.0.prompt(),
150            });
151            wid.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
152
153            std::mem::take(wid.text_mut())
154        });
155
156        let text = self.0.on_switch(pa, text, handle.area());
157
158        handle.widget().replace_text(pa, text);
159    }
160
161    fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
162        let text = handle.take_text(pa);
163        self.0.before_exit(pa, text, handle.area());
164    }
165}
166
167/// A mode to control the [`Prompt`], by acting on its [`Text`] and
168/// [`U::Area`]
169///
170/// Through the [`Pass`], one can act on the entirety of Duat's shared
171/// state:
172///
173/// ```rust
174/// use duat_core::prelude::*;
175/// use duat_utils::modes::PromptMode;
176///
177/// #[derive(Default, Clone)]
178/// struct RealTimeSwitch {
179///     initial: Option<String>,
180///     current: Option<String>,
181///     name_was_correct: bool,
182/// };
183///
184/// impl<U: Ui> PromptMode<U> for RealTimeSwitch {
185///     fn update(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text {
186///         let name = text.to_string();
187///
188///         self.name_was_correct = if name != *self.current.as_ref().unwrap() {
189///             if cmd::buffer(pa, &name).is_ok() {
190///                 self.current = Some(name);
191///                 true
192///             } else {
193///                 false
194///             }
195///         } else {
196///             true
197///         };
198///
199///         text
200///     }
201///
202///     fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text {
203///         self.initial = Some(context::fixed_file::<U>(pa).unwrap().name(pa));
204///         self.current = self.initial.clone();
205///
206///         text
207///     }
208///
209///     fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &U::Area) {
210///         if !self.name_was_correct {
211///             cmd::buffer(pa, self.initial.take().unwrap());
212///         }
213///     }
214///
215///     fn prompt(&self) -> Text {
216///         txt!("[prompt]switch to").build()
217///     }
218/// }
219/// ```
220///
221/// The [`PromptMode`] above will switch to the file with the same
222/// name as the one in the [`PromptLine`], returning to the initial
223/// file if the match failed.
224///
225/// [`U::Area`]: Ui::Area
226#[allow(unused_variables)]
227pub trait PromptMode<U: Ui>: Clone + 'static {
228    /// What [`Widget`] to exit to, upon pressing enter, esc, or
229    /// backspace in an empty [`PromptLine`]
230    type ExitWidget: Widget<U> = File<U>;
231
232    /// Updates the [`PromptLine`] and [`Text`] of the [`Prompt`]
233    ///
234    /// This function is triggered every time the user presses a key
235    /// in the [`Prompt`] mode.
236    fn update(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text;
237
238    /// What to do when switchin onto this [`PromptMode`]
239    ///
240    /// The initial [`Text`] is always empty, except for the [prompt]
241    /// [`Ghost`] at the beginning of the line.
242    ///
243    /// [prompt]: PromptMode::prompt
244    fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text {
245        text
246    }
247
248    /// What to do before exiting the [`PromptMode`]
249    ///
250    /// This usually involves some sor of "commitment" to the result,
251    /// e.g., [`RunCommands`] executes the call, [`IncSearch`]
252    /// finishes the search, etc.
253    fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &U::Area) {}
254
255    /// Things to do when this [`PromptMode`] is first instantiated
256    fn once() {}
257
258    /// What text should be at the beginning of the [`PromptLine`], as
259    /// a [`Ghost`]
260    fn prompt(&self) -> Text;
261
262    /// An optional returning [`Handle`] for the [`ExitWidget`]
263    ///
264    /// [`ExitWidget`]: PromptMode::ExitWidget
265    fn return_handle(&self) -> Option<Handle<Self::ExitWidget, U>> {
266        None
267    }
268}
269
270/// Runs Duat commands, with syntax highlighting for correct
271/// [`Parameter`]s
272///
273/// [`Parameter`]: duat_core::cmd::Parameter
274#[derive(Default, Clone)]
275pub struct RunCommands;
276
277impl RunCommands {
278    /// Crates a new [`RunCommands`]
279    pub fn new<U: Ui>() -> Prompt<Self, U> {
280        Prompt::new(Self)
281    }
282}
283
284impl<U: Ui> PromptMode<U> for RunCommands {
285    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
286        text.remove_tags(*TAGGER, ..);
287
288        let command = text.to_string();
289        let caller = command.split_whitespace().next();
290        if let Some(caller) = caller {
291            if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
292                let id = form::id_of!("caller.info");
293                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
294
295                let id = form::id_of!("parameter.info");
296                for range in ok_ranges {
297                    text.insert_tag(*TAGGER, range, id.to_tag(0));
298                }
299                if let Some((range, _)) = err_range {
300                    let id = form::id_of!("parameter.error");
301                    text.insert_tag(*TAGGER, range, id.to_tag(0));
302                }
303            } else {
304                let id = form::id_of!("caller.error");
305                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
306            }
307        }
308
309        text
310    }
311
312    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
313        let call = text.to_string();
314        if !call.is_empty() {
315            cmd::queue_notify(call);
316        }
317    }
318
319    fn once() {
320        form::set_weak("caller.info", "accent.info");
321        form::set_weak("caller.error", "accent.error");
322        form::set_weak("parameter.info", "default.info");
323        form::set_weak("parameter.error", "default.error");
324    }
325
326    fn prompt(&self) -> Text {
327        txt!("[prompt.colon]:").build()
328    }
329}
330
331/// The [`PromptMode`] that makes use of [`IncSearcher`]s
332///
333/// In order to make use of incremental search, you'd do something
334/// like this:
335///
336/// ```rust
337/// use duat_core::prelude::*;
338/// use duat_utils::modes::{IncSearch, Regular, SearchFwd};
339///
340/// fn setup_generic_over_ui<U: Ui>() {
341///     mode::map::<Regular, U>("<C-s>", IncSearch::new(SearchFwd));
342/// }
343/// ```
344///
345/// This function returns a [`Prompt<IncSearch<SearchFwd, U>, U>`],
346#[derive(Clone)]
347pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
348    inc: I,
349    orig: Option<(mode::Selections, <U::Area as RawArea>::PrintInfo)>,
350    ghost: PhantomData<U>,
351    prev: String,
352}
353
354impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
355    /// Returns a [`Prompt`] with [`IncSearch<I, U>`] as its
356    /// [`PromptMode`]
357    pub fn new(inc: I) -> Prompt<Self, U> {
358        Prompt::new(Self {
359            inc,
360            orig: None,
361            ghost: PhantomData,
362            prev: String::new(),
363        })
364    }
365}
366
367impl<I: IncSearcher<U>, U: Ui> PromptMode<U> for IncSearch<I, U> {
368    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
369        let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
370        text.remove_tags(*TAGGER, ..);
371
372        let handle = context::fixed_file::<U>(pa).unwrap().handle(pa);
373
374        if text == self.prev {
375            return text;
376        } else {
377            let prev = std::mem::replace(&mut self.prev, text.to_string());
378            hook::queue(SearchUpdated((prev, self.prev.clone())));
379        }
380
381        match Searcher::new(text.to_string()) {
382            Ok(searcher) => {
383                handle.write(pa, |file, area| {
384                    area.set_print_info(orig_print_info.clone());
385                    *file.selections_mut().unwrap() = orig_selections.clone();
386                });
387
388                let ast = regex_syntax::ast::parse::Parser::new()
389                    .parse(&text.to_string())
390                    .unwrap();
391
392                crate::tag_from_ast(*TAGGER, &mut text, &ast);
393
394                self.inc.search(pa, handle.attach_searcher(searcher));
395            }
396            Err(err) => {
397                let regex_syntax::Error::Parse(err) = *err else {
398                    unreachable!("As far as I can tell, regex_syntax has goofed up");
399                };
400
401                let span = err.span();
402                let id = form::id_of!("regex.error");
403
404                text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
405            }
406        }
407
408        text
409    }
410
411    fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) -> Text {
412        let handle = context::fixed_file::<U>(pa).unwrap();
413        handle.read(pa, |file, area| {
414            self.orig = Some((file.selections().clone(), area.print_info()));
415        });
416
417        text
418    }
419
420    fn before_exit(&mut self, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
421        if !text.is_empty() {
422            if let Err(err) = Searcher::new(text.to_string()) {
423                let regex_syntax::Error::Parse(err) = *err else {
424                    unreachable!("As far as I can tell, regex_syntax has goofed up");
425                };
426
427                let range = err.span().start.offset..err.span().end.offset;
428                let err = txt!(
429                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
430                    range,
431                    text.strs(range),
432                    err.kind()
433                );
434
435                context::error!(target: self.inc.prompt().to_string(), "{err}")
436            } else {
437                hook::queue(SearchPerformed(text.to_string()));
438            }
439        }
440    }
441
442    fn once() {
443        form::set_weak("regex.error", "accent.error");
444        form::set_weak("regex.operator", "operator");
445        form::set_weak("regex.class", "constant");
446        form::set_weak("regex.bracket", "punctuation.bracket");
447    }
448
449    fn prompt(&self) -> Text {
450        txt!("{}[prompt.colon]:", self.inc.prompt()).build()
451    }
452}
453
454/// Pipes the selections of a [`File`] through an external command
455///
456/// This can be useful if you, for example, don't have access to a
457/// formatter, but want to format text, so you pass it to
458/// [`PipeSelections`] with `fold` as the command, or things of the
459/// sort.
460#[derive(Clone, Copy)]
461pub struct PipeSelections<U>(PhantomData<U>);
462
463impl<U: Ui> PipeSelections<U> {
464    /// Returns a [`Prompt`] with [`PipeSelections`] as its
465    /// [`PromptMode`]
466    pub fn new() -> Prompt<Self, U> {
467        Prompt::new(Self(PhantomData))
468    }
469}
470
471impl<U: Ui> PromptMode<U> for PipeSelections<U> {
472    fn update(&mut self, _: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
473        fn is_in_path(program: &str) -> bool {
474            if let Ok(path) = std::env::var("PATH") {
475                for p in path.split(":") {
476                    let p_str = format!("{p}/{program}");
477                    if let Ok(true) = std::fs::exists(p_str) {
478                        return true;
479                    }
480                }
481            }
482            false
483        }
484
485        text.remove_tags(*TAGGER, ..);
486
487        let command = text.to_string();
488        let Some(caller) = command.split_whitespace().next() else {
489            return text;
490        };
491
492        let args = cmd::args_iter(&command);
493
494        let (caller_id, args_id) = if is_in_path(caller) {
495            (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
496        } else {
497            (
498                form::id_of!("caller.error"),
499                form::id_of!("parameter.error"),
500            )
501        };
502
503        let c_s = command.len() - command.trim_start().len();
504        text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
505
506        for (_, range) in args {
507            text.insert_tag(*TAGGER, range, args_id.to_tag(0));
508        }
509
510        text
511    }
512
513    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) {
514        use std::process::{Command, Stdio};
515
516        let command = text.to_string();
517        let Some(caller) = command.split_whitespace().next() else {
518            return;
519        };
520
521        let handle = context::fixed_file::<U>(pa).unwrap().handle(pa);
522        handle.edit_all(pa, |mut e| {
523            let Ok(mut child) = Command::new(caller)
524                .args(cmd::args_iter(&command).map(|(a, _)| a))
525                .stdin(Stdio::piped())
526                .stdout(Stdio::piped())
527                .spawn()
528            else {
529                return;
530            };
531
532            let input: String = e.selection().collect();
533            if let Some(mut stdin) = child.stdin.take() {
534                std::thread::spawn(move || {
535                    stdin.write_all(input.as_bytes()).unwrap();
536                });
537            }
538            if let Ok(out) = child.wait_with_output() {
539                let out = String::from_utf8_lossy(&out.stdout);
540                e.replace(out);
541            }
542        });
543    }
544
545    fn prompt(&self) -> Text {
546        txt!("[prompt]pipe").build()
547    }
548}
549
550/// Runs the [`once`] function of widgets.
551///
552/// [`once`]: Widget::once
553fn run_once<M: PromptMode<U>, U: Ui>() {
554    use std::{any::TypeId, sync::Mutex};
555
556    static LIST: LazyLock<Mutex<Vec<TypeId>>> = LazyLock::new(|| Mutex::new(Vec::new()));
557
558    let mut list = LIST.lock().unwrap();
559    if !list.contains(&TypeId::of::<M>()) {
560        M::once();
561        list.push(TypeId::of::<M>());
562    }
563}