duat_core/mode/
prompt.rs

1use std::{io::Write, marker::PhantomData, sync::LazyLock};
2
3use lender::Lender;
4
5use super::{Cursors, EditHelper, IncSearcher, KeyCode, KeyEvent, Mode, key};
6use crate::{
7    cmd, context, form,
8    hooks::{self, SearchPerformed, SearchUpdated},
9    text::{Key, Point, Searcher, Tag, Text, text},
10    ui::{Area, Ui},
11    widgets::{PromptLine, Widget},
12};
13
14static PROMPT_KEY: LazyLock<Key> = LazyLock::new(Key::new);
15static KEY: LazyLock<Key> = LazyLock::new(Key::new);
16
17#[derive(Clone)]
18pub struct Prompt<M: PromptMode<U>, U: Ui>(M, PhantomData<U>);
19
20impl<M: PromptMode<U>, U: Ui> Prompt<M, U> {
21    fn new(mode: M) -> Self {
22        Self(mode, PhantomData)
23    }
24}
25
26impl<M: PromptMode<U>, U: Ui> Mode<U> for Prompt<M, U> {
27    type Widget = PromptLine<U>;
28
29    fn send_key(&mut self, key: KeyEvent, widget: &mut Self::Widget, area: &U::Area) {
30        let mut helper = EditHelper::new(widget, area);
31
32        match key {
33            key!(KeyCode::Backspace) => {
34                if helper.text().is_empty() {
35                    helper.cursors_mut().clear();
36                    self.0.update(helper.text_mut(), area);
37                    self.0.before_exit(helper.text_mut(), area);
38                    super::reset();
39                } else {
40                    let mut e = helper.edit_main();
41                    e.move_hor(-1);
42                    e.replace("");
43                    self.0.update(helper.text_mut(), area);
44                }
45            }
46            key!(KeyCode::Delete) => {
47                helper.edit_main().replace("");
48                self.0.update(helper.text_mut(), area);
49            }
50
51            key!(KeyCode::Char(char)) => {
52                let mut e = helper.edit_main();
53                e.insert(char);
54                e.move_hor(1);
55                self.0.update(helper.text_mut(), area);
56            }
57            key!(KeyCode::Left) => {
58                helper.edit_main().move_hor(-1);
59                self.0.update(helper.text_mut(), area);
60            }
61            key!(KeyCode::Right) => {
62                helper.edit_main().move_hor(1);
63                self.0.update(helper.text_mut(), area);
64            }
65
66            key!(KeyCode::Esc) => {
67                let p = helper.text().len();
68                let mut e = helper.edit_main();
69                e.move_to(Point::default());
70                e.set_anchor();
71                e.move_to(p);
72                e.replace("");
73                helper.cursors_mut().clear();
74                self.0.update(helper.text_mut(), area);
75                self.0.before_exit(helper.text_mut(), area);
76                super::reset();
77            }
78            key!(KeyCode::Enter) => {
79                helper.cursors_mut().clear();
80                self.0.update(helper.text_mut(), area);
81                self.0.before_exit(helper.text_mut(), area);
82                super::reset();
83            }
84            _ => {}
85        }
86    }
87
88    fn on_switch(&mut self, widget: &mut Self::Widget, area: &<U as Ui>::Area) {
89        *widget.text_mut() = Text::new_with_cursors();
90        run_once::<M, U>();
91
92        let tag = Tag::Ghost(0, match widget.prompt_of::<M>() {
93            Some(text) => text,
94            None => self.0.prompt(),
95        });
96        widget.text_mut().insert_tag(*PROMPT_KEY, tag);
97
98        self.0.on_switch(widget.text_mut(), area);
99    }
100}
101
102#[allow(unused_variables)]
103pub trait PromptMode<U: Ui>: Clone + Send + 'static {
104    fn update(&mut self, text: &mut Text, area: &U::Area) {}
105
106    fn on_switch(&mut self, text: &mut Text, area: &U::Area) {}
107
108    fn before_exit(&mut self, text: &mut Text, area: &U::Area) {}
109
110    fn once() {}
111
112    fn prompt(&self) -> Text;
113}
114
115#[derive(Default, Clone)]
116pub struct RunCommands;
117
118impl RunCommands {
119    pub fn new<U: Ui>() -> Prompt<Self, U> {
120        Prompt::new(Self)
121    }
122}
123
124impl<U: Ui> PromptMode<U> for RunCommands {
125    fn update(&mut self, text: &mut Text, _area: &U::Area) {
126        text.remove_tags(.., *KEY);
127
128        let command = text.to_string();
129        let caller = command.split_whitespace().next();
130        if let Some(caller) = caller {
131            if let Some((ok_ranges, err_range)) = cmd::check_args(&command) {
132                let id = form::id_of!("CallerExists");
133                text.insert_tag(*KEY, Tag::Form(0..caller.len(), id, 0));
134
135                let id = form::id_of!("ParameterOk");
136                for range in ok_ranges {
137                    text.insert_tag(*KEY, Tag::Form(range, id, 0));
138                }
139                if let Some((range, _)) = err_range {
140                    let id = form::id_of!("ParameterErr");
141                    text.insert_tag(*KEY, Tag::Form(range, id, 0));
142                }
143            } else {
144                let id = form::id_of!("CallerNotFound");
145                text.insert_tag(*KEY, Tag::Form(0..caller.len(), id, 0));
146            }
147        }
148    }
149
150    fn before_exit(&mut self, text: &mut Text, _area: &U::Area) {
151        let text = std::mem::take(text);
152
153        let command = text.to_string();
154        if !command.is_empty() {
155            crate::thread::spawn(move || cmd::run_notify(command));
156        }
157    }
158
159    fn once() {
160        form::set_weak("CallerExists", "AccentOk");
161        form::set_weak("CallerNotFound", "AccentErr");
162        form::set_weak("ParameterOk", "DefaultOk");
163        form::set_weak("ParameterErr", "DefaultErr");
164    }
165
166    fn prompt(&self) -> Text {
167        text!([Prompt.colon] ":")
168    }
169}
170
171#[derive(Clone)]
172pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
173    inc: I,
174    orig: Option<(Cursors, <U::Area as Area>::PrintInfo)>,
175    ghost: PhantomData<U>,
176    prev: String,
177}
178
179impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
180    pub fn new(inc: I) -> Prompt<Self, U> {
181        Prompt::new(Self {
182            inc,
183            orig: None,
184            ghost: PhantomData,
185            prev: String::new(),
186        })
187    }
188}
189
190impl<I: IncSearcher<U>, U: Ui> PromptMode<U> for IncSearch<I, U> {
191    fn update(&mut self, text: &mut Text, _area: &U::Area) {
192        let orig = self.orig.as_ref().unwrap();
193        text.remove_tags(.., *KEY);
194
195        let mut ff = context::fixed_file::<U>().unwrap();
196
197        match Searcher::new(text.to_string()) {
198            Ok(searcher) => {
199                let (mut file, area) = ff.write();
200                self.inc.search(orig, &mut file, area, searcher);
201            }
202            Err(err) => {
203                let regex_syntax::Error::Parse(err) = *err else {
204                    unreachable!("As far as I can tell, regex_syntax has goofed up");
205                };
206
207                let span = err.span();
208                let id = form::id_of!("ParseCommandErr");
209
210                text.insert_tag(*KEY, Tag::Form(span.start.offset..span.end.offset, id, 0));
211            }
212        }
213
214        if *text != self.prev {
215            let prev = std::mem::replace(&mut self.prev, text.to_string());
216            hooks::trigger::<SearchUpdated>((prev, self.prev.clone()));
217        }
218    }
219
220    fn before_exit(&mut self, text: &mut Text, _area: &<U as Ui>::Area) {
221        if !text.is_empty() {
222            hooks::trigger::<SearchPerformed>(text.to_string());
223        }
224    }
225
226    fn on_switch(&mut self, _text: &mut Text, _area: &U::Area) {
227        let mut ff = context::fixed_file::<U>().unwrap();
228        let (file, area) = ff.read();
229        self.orig = Some((file.cursors().clone(), area.print_info()));
230    }
231
232    fn once() {
233        form::set("Regex.err", "DefaultErr");
234    }
235
236    fn prompt(&self) -> Text {
237        self.inc.prompt()
238    }
239}
240
241#[derive(Clone, Copy)]
242pub struct PipeSelections<U>(PhantomData<U>);
243
244impl<U: Ui> PipeSelections<U> {
245    pub fn new() -> Prompt<Self, U> {
246        Prompt::new(Self(PhantomData))
247    }
248}
249
250impl<U: Ui> PromptMode<U> for PipeSelections<U> {
251    fn update(&mut self, text: &mut Text, _area: &U::Area) {
252        fn is_in_path(program: &str) -> bool {
253            if let Ok(path) = std::env::var("PATH") {
254                for p in path.split(":") {
255                    let p_str = format!("{}/{}", p, program);
256                    if let Ok(true) = std::fs::exists(p_str) {
257                        return true;
258                    }
259                }
260            }
261            false
262        }
263
264        text.remove_tags(.., *KEY);
265
266        let command = text.to_string();
267        let Some(caller) = command.split_whitespace().next() else {
268            return;
269        };
270
271        let args = cmd::args_iter(&command);
272
273        let (caller_id, args_id) = if is_in_path(caller) {
274            (form::id_of!("CallerExists"), form::id_of!("ParameterOk"))
275        } else {
276            (form::id_of!("CallerNotFound"), form::id_of!("ParameterErr"))
277        };
278
279        let c_s = command.len() - command.trim_start().len();
280        text.insert_tag(*KEY, Tag::Form(c_s..c_s + caller.len(), caller_id, 0));
281
282        for (_, range) in args {
283            text.insert_tag(*KEY, Tag::Form(range, args_id, 0));
284        }
285    }
286
287    fn before_exit(&mut self, text: &mut Text, _area: &U::Area) {
288        use std::process::{Command, Stdio};
289        let text = std::mem::take(text);
290
291        let command = text.to_string();
292        let Some(caller) = command.split_whitespace().next() else {
293            return;
294        };
295
296        let mut ff = context::fixed_file::<U>().unwrap();
297        let (mut file, area) = ff.write();
298        let mut helper = EditHelper::new(&mut *file, area);
299        helper.edit_iter().for_each(|mut e| {
300            let Ok(mut child) = Command::new(caller)
301                .args(cmd::args_iter(&command).map(|(a, _)| a))
302                .stdin(Stdio::piped())
303                .stdout(Stdio::piped())
304                .spawn()
305            else {
306                return;
307            };
308
309            let input: String = e.selection().collect();
310            if let Some(mut stdin) = child.stdin.take() {
311                crate::thread::spawn(move || {
312                    stdin.write_all(input.as_bytes()).unwrap();
313                });
314            }
315            if let Ok(out) = child.wait_with_output() {
316                let out = String::from_utf8_lossy(&out.stdout);
317                e.replace(out);
318            }
319        });
320    }
321
322    fn prompt(&self) -> Text {
323        text!([Prompt] "pipe" [Prompt.colon] ":")
324    }
325}
326
327/// Runs the [`once`] function of widgets.
328///
329/// [`once`]: Widget::once
330fn run_once<M: PromptMode<U>, U: Ui>() {
331    use std::{any::TypeId, sync::LazyLock};
332
333    use crate::data::RwData;
334    static LIST: LazyLock<RwData<Vec<TypeId>>> = LazyLock::new(|| RwData::new(Vec::new()));
335
336    let mut list = LIST.write();
337    if !list.contains(&TypeId::of::<M>()) {
338        M::once();
339        list.push(TypeId::of::<M>());
340    }
341}