duat_core/widgets/
command_line.rs

1//! A [`Widget`] that can have varying functionality
2//!
3//! Its primary purpose, as the name implies, is to run [commands],
4//! but it can also [show notifications], do [incremental search], and
5//! you can even [implement your own functionality] for the
6//! [`CmdLine`].
7//!
8//! [commands]: cmd
9//! [show notifications]: ShowNotifications
10//! [incremental search]: IncSearch
11//! [implement your own functionality]: CmdLineMode
12use std::{
13    any::TypeId,
14    io::Write,
15    marker::PhantomData,
16    sync::{
17        Arc, LazyLock,
18        atomic::{AtomicBool, Ordering},
19    },
20};
21
22use parking_lot::RwLock;
23
24use super::File;
25use crate::{
26    cfg::PrintCfg,
27    cmd::{self, args_iter},
28    data::{RoData, RwData, context},
29    form::{self, Form},
30    hooks::{self, KeySent},
31    mode::{self, Command, EditHelper, IncSearcher},
32    text::{Ghost, Key, Searcher, Tag, Text, text},
33    ui::{PushSpecs, Ui},
34    widgets::{Widget, WidgetCfg},
35};
36
37pub struct CmdLineCfg<U> {
38    prompt: String,
39    specs: PushSpecs,
40    ghost: PhantomData<U>,
41}
42
43impl<U> CmdLineCfg<U> {
44    pub fn new() -> Self {
45        CmdLineCfg {
46            prompt: String::from(":"),
47            specs: PushSpecs::below().with_ver_len(1.0),
48            ghost: PhantomData,
49        }
50    }
51}
52
53impl<U: Ui> Default for CmdLineCfg<U> {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl<U: Ui> CmdLineCfg<U> {
60    pub fn with_prompt(self, prompt: impl ToString) -> Self {
61        Self { prompt: prompt.to_string(), ..self }
62    }
63
64    pub fn above(self) -> Self {
65        Self {
66            specs: PushSpecs::above().with_ver_len(1.0),
67            ..self
68        }
69    }
70
71    pub fn left_ratioed(self, den: u16, div: u16) -> Self {
72        Self {
73            specs: PushSpecs::left().with_hor_ratio(den, div),
74            ..self
75        }
76    }
77}
78
79impl<U: Ui> WidgetCfg<U> for CmdLineCfg<U> {
80    type Widget = CmdLine<U>;
81
82    fn build(self, _: bool) -> (Self::Widget, impl Fn() -> bool, PushSpecs) {
83        let mode: RwData<dyn CmdLineMode<U>> = if hooks::group_exists("CmdLineNotifications") {
84            run_once::<ShowNotifications<U>, U>();
85            RwData::new_unsized::<ShowNotifications<U>>(Arc::new(RwLock::new(
86                ShowNotifications::new(),
87            )))
88        } else {
89            run_once::<RunCommands<U>, U>();
90            RwData::new_unsized::<RunCommands<U>>(Arc::new(RwLock::new(RunCommands::new())))
91        };
92
93        let widget = CmdLine {
94            text: Text::default(),
95            prompt: RwData::new(self.prompt.clone()),
96            mode: RwData::new(mode),
97        };
98
99        let checker = {
100            let mode = RoData::from(&widget.mode);
101            move || mode.read().write().has_changed()
102        };
103
104        (widget, checker, self.specs)
105    }
106}
107
108/// A multi purpose text widget
109///
110/// This widget, as the name implies, is most associated with running
111/// commands. However, it can have a variety of [modes], granting it
112/// differing functionality. In Duat, there are 3 predefined modes:
113///
114/// * [`RunCommands`], which runs commands (duh);
115/// * [`ShowNotifications`], which shows notifications, usually about
116///   commands;
117/// * [`IncSearch<Inc>`], which will perform an incremental search,
118///   based on [`Inc`].
119///
120/// By default, Duat will have the `"CmdLineNotifications"` [hook]
121/// active. This hook changes the mode of the [`CmdLine`] to
122/// [`ShowNotifications`] whenever it is unfocused. If you don't want
123/// this functionality, or want notifications somewhere else, you can
124/// use [`hooks::remove`].
125///
126/// [modes]: CmdLineMode
127/// [`Inc`]: IncSearcher
128/// [hook]: crate::hooks
129pub struct CmdLine<U: Ui> {
130    text: Text,
131    prompt: RwData<String>,
132    mode: RwData<RwData<dyn CmdLineMode<U>>>,
133}
134
135impl<U: Ui> CmdLine<U> {
136    pub(crate) fn set_mode<M: CmdLineMode<U>>(&mut self, mode: M) {
137        run_once::<M, U>();
138        if mode.do_focus() {
139            mode::set::<U>(Command);
140        }
141        *self.mode.write() = RwData::new_unsized::<M>(Arc::new(RwLock::new(mode)));
142    }
143}
144
145impl<U: Ui> Widget<U> for CmdLine<U> {
146    type Cfg = CmdLineCfg<U>;
147
148    fn cfg() -> Self::Cfg {
149        CmdLineCfg::new()
150    }
151
152    fn update(&mut self, _area: &<U as Ui>::Area) {
153        self.mode.read().write().update(&mut self.text);
154    }
155
156    fn text(&self) -> &Text {
157        &self.text
158    }
159
160    fn text_mut(&mut self) -> &mut Text {
161        &mut self.text
162    }
163
164    fn print_cfg(&self) -> PrintCfg {
165        PrintCfg::default_for_input().with_forced_scrolloff()
166    }
167
168    fn once() -> Result<(), crate::Error<()>> {
169        form::set_weak("Prompt", Form::cyan());
170        form::set_weak("ParseCommandErr", "DefaultErr");
171
172        cmd::add_for!(
173            "set-prompt",
174            |cmd_line: CmdLine<U>, _: U::Area, new: String| {
175                *cmd_line.prompt.write() = new;
176                Ok(None)
177            }
178        )
179    }
180
181    fn on_focus(&mut self, _area: &U::Area) {
182        self.text = text!({ Ghost(text!({ &self.prompt })) });
183        self.mode.read().write().on_focus(&mut self.text);
184    }
185
186    fn on_unfocus(&mut self, _area: &<U as Ui>::Area) {
187        self.mode.read().write().on_unfocus(&mut self.text);
188    }
189}
190
191#[allow(unused_variables)]
192pub trait CmdLineMode<U: Ui>: Send + Sync + 'static {
193    fn on_focus(&mut self, text: &mut Text) {}
194
195    fn on_unfocus(&mut self, text: &mut Text) {}
196
197    fn update(&mut self, text: &mut Text) {}
198
199    fn has_changed(&mut self) -> bool {
200        false
201    }
202
203    fn do_focus(&self) -> bool {
204        false
205    }
206
207    fn once()
208    where
209        Self: Sized,
210    {
211    }
212}
213
214pub struct RunCommands<U> {
215    key: Key,
216    ghost: PhantomData<U>,
217}
218
219impl<U: Ui> RunCommands<U> {
220    pub fn new() -> Self {
221        Self { key: Key::new(), ghost: PhantomData }
222    }
223}
224
225impl<U: Ui> CmdLineMode<U> for RunCommands<U> {
226    fn update(&mut self, text: &mut Text) {
227        text.remove_tags(.., self.key);
228
229        let command = text.to_string();
230        let caller = command.split_whitespace().next();
231        if let Some(caller) = caller {
232            if let Some((ok_ranges, err_range)) = cmd::check_params(&command) {
233                let id = form::id_of!("CallerExists");
234                text.insert_tag(0, Tag::PushForm(id), self.key);
235                text.insert_tag(caller.len(), Tag::PopForm(id), self.key);
236
237                let id = form::id_of!("ParameterOk");
238                for range in ok_ranges {
239                    text.insert_tag(range.start, Tag::PushForm(id), self.key);
240                    text.insert_tag(range.end, Tag::PopForm(id), self.key);
241                }
242                if let Some((range, _)) = err_range {
243                    let id = form::id_of!("ParameterErr");
244                    text.insert_tag(range.start, Tag::PushForm(id), self.key);
245                    text.insert_tag(range.end, Tag::PopForm(id), self.key);
246                }
247            } else {
248                let id = form::id_of!("CallerNotFound");
249                text.insert_tag(0, Tag::PushForm(id), self.key);
250                text.insert_tag(caller.len(), Tag::PopForm(id), self.key);
251            }
252        }
253    }
254
255    fn on_unfocus(&mut self, text: &mut Text) {
256        let text = std::mem::take(text);
257
258        let command = text.to_string();
259        if !command.is_empty() {
260            crate::thread::queue(move || cmd::run_notify(command));
261        }
262    }
263
264    fn do_focus(&self) -> bool {
265        true
266    }
267
268    fn once() {
269        form::set_weak("CallerExists", "AccentOk");
270        form::set_weak("CallerNotFound", "AccentErr");
271        form::set_weak("ParameterOk", "DefaultOk");
272        form::set_weak("ParameterErr", "DefaultErr");
273    }
274}
275
276impl<U: Ui> Default for RunCommands<U> {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282impl<U: Ui> Clone for RunCommands<U> {
283    fn clone(&self) -> Self {
284        Self::new()
285    }
286}
287
288pub struct ShowNotifications<U> {
289    notifications: RwData<Text>,
290    text: Text,
291    ghost: PhantomData<U>,
292}
293
294impl<U: Ui> ShowNotifications<U> {
295    pub fn new() -> Self {
296        Self {
297            notifications: context::notifications().clone(),
298            text: Text::default(),
299            ghost: PhantomData,
300        }
301    }
302}
303
304static REMOVE_NOTIFS: AtomicBool = AtomicBool::new(false);
305
306impl<U: Ui> CmdLineMode<U> for ShowNotifications<U> {
307    fn has_changed(&mut self) -> bool {
308        if self.notifications.has_changed() {
309            REMOVE_NOTIFS.store(false, Ordering::Release);
310            self.text = self.notifications.read().clone();
311            true
312        } else {
313            !self.text.is_empty() && REMOVE_NOTIFS.load(Ordering::Acquire)
314        }
315    }
316
317    fn update(&mut self, text: &mut Text) {
318        if REMOVE_NOTIFS.load(Ordering::Acquire) {
319            self.text = Text::default();
320            REMOVE_NOTIFS.store(false, Ordering::Release);
321        }
322        if self.notifications.has_changed() {
323            self.text = self.notifications.read().clone();
324        }
325        *text = self.text.clone();
326    }
327
328    fn once()
329    where
330        Self: Sized,
331    {
332        hooks::add::<KeySent<U>>(|_| {
333            REMOVE_NOTIFS.store(true, Ordering::Release);
334        });
335    }
336}
337
338impl<U: Ui> Default for ShowNotifications<U> {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344impl<U: Ui> Clone for ShowNotifications<U> {
345    fn clone(&self) -> Self {
346        Self::new()
347    }
348}
349
350pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
351    fn_or_inc: FnOrInc<I, U>,
352    key: Key,
353    ghost: PhantomData<U>,
354}
355
356impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
357    pub fn new(f: impl IncFn<I, U> + Send + Sync + 'static) -> Self {
358        Self {
359            fn_or_inc: FnOrInc::Fn(Some(Box::new(f))),
360            key: Key::new(),
361            ghost: PhantomData,
362        }
363    }
364}
365
366impl<I: IncSearcher<U>, U: Ui> CmdLineMode<U> for IncSearch<I, U> {
367    fn update(&mut self, text: &mut Text) {
368        let FnOrInc::Inc(inc, _) = &mut self.fn_or_inc else {
369            unreachable!();
370        };
371
372        text.remove_tags(.., self.key);
373
374        let cur_file = context::cur_file::<U>().unwrap();
375
376        match Searcher::new(text.to_string()) {
377            Ok(searcher) => {
378                cur_file.mutate_data(|file, area| {
379                    inc.search(&mut file.raw_write(), area, searcher);
380                });
381            }
382            Err(err) => {
383                let regex_syntax::Error::Parse(err) = *err else {
384                    unreachable!("As far as I can tell, regex_syntax has goofed up");
385                };
386
387                let span = err.span();
388                let id = crate::form::id_of!("ParseCommandErr");
389
390                text.insert_tag(span.start.offset, Tag::PushForm(id), self.key);
391                text.insert_tag(span.end.offset, Tag::PopForm(id), self.key);
392            }
393        }
394    }
395
396    fn on_focus(&mut self, _text: &mut Text) {
397        context::cur_file::<U>().unwrap().mutate_data(|file, area| {
398            self.fn_or_inc.as_inc(&mut file.raw_write(), area);
399        })
400    }
401
402    fn on_unfocus(&mut self, _text: &mut Text) {
403        let FnOrInc::Inc(inc, _) = &mut self.fn_or_inc else {
404            unreachable!();
405        };
406
407        context::cur_file::<U>()
408            .unwrap()
409            .mutate_data(|file, area| inc.finish(&mut file.raw_write(), area));
410    }
411
412    fn do_focus(&self) -> bool {
413        true
414    }
415}
416
417impl<I: IncSearcher<U>, U: Ui> Clone for IncSearch<I, U> {
418    fn clone(&self) -> Self {
419        Self::new(I::new)
420    }
421}
422
423pub struct PipeSelections<U> {
424    key: Key,
425    _ghost: PhantomData<U>,
426}
427
428impl<U: Ui> PipeSelections<U> {
429    pub fn new() -> Self {
430        Self { key: Key::new(), _ghost: PhantomData }
431    }
432}
433
434impl<U: Ui> CmdLineMode<U> for PipeSelections<U> {
435    fn update(&mut self, text: &mut Text) {
436        fn is_in_path(program: &str) -> bool {
437            if let Ok(path) = std::env::var("PATH") {
438                for p in path.split(":") {
439                    let p_str = format!("{}/{}", p, program);
440                    if let Ok(true) = std::fs::exists(p_str) {
441                        return true;
442                    }
443                }
444            }
445            false
446        }
447
448        text.remove_tags(.., self.key);
449
450        let command = text.to_string();
451        let Some(caller) = command.split_whitespace().next() else {
452            return;
453        };
454
455        let args = args_iter(&command);
456
457        let (caller_id, args_id) = if is_in_path(caller) {
458            (form::id_of!("CallerExists"), form::id_of!("ParameterOk"))
459        } else {
460            (form::id_of!("CallerNotFound"), form::id_of!("ParameterErr"))
461        };
462
463        text.insert_tag(0, Tag::PushForm(caller_id), self.key);
464        text.insert_tag(caller.len(), Tag::PopForm(caller_id), self.key);
465
466        for (_, range) in args {
467            text.insert_tag(range.start, Tag::PushForm(args_id), self.key);
468            text.insert_tag(range.end, Tag::PopForm(args_id), self.key);
469        }
470    }
471
472    fn on_unfocus(&mut self, text: &mut Text) {
473        use std::process::{Command, Stdio};
474        let text = std::mem::take(text);
475
476        let command = text.to_string();
477        let Some(caller) = command.split_whitespace().next() else {
478            return;
479        };
480
481        context::cur_file::<U>().unwrap().mutate_data(|file, area| {
482            let mut file = file.write();
483            let mut helper = EditHelper::new(&mut *file, area);
484
485            helper.edit_many(.., |e| {
486                let Ok(mut child) = Command::new(caller)
487                    .args(args_iter(&command).map(|(a, _)| a))
488                    .stdin(Stdio::piped())
489                    .stdout(Stdio::piped())
490                    .spawn()
491                else {
492                    return;
493                };
494
495                let input: String = e.selection().collect();
496                if let Some(mut stdin) = child.stdin.take() {
497                    crate::thread::spawn(move || {
498                        stdin.write_all(input.as_bytes()).unwrap();
499                    });
500                }
501                if let Ok(out) = child.wait_with_output() {
502                    let out = String::from_utf8_lossy(&out.stdout);
503                    e.replace(out);
504                }
505            });
506        });
507    }
508
509    fn do_focus(&self) -> bool {
510        true
511    }
512}
513
514impl<U: Ui> Default for PipeSelections<U> {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520impl<U: Ui> Clone for PipeSelections<U> {
521    fn clone(&self) -> Self {
522        Self::new()
523    }
524}
525
526/// Runs the [`once`] function of widgets.
527///
528/// [`once`]: Widget::once
529fn run_once<M: CmdLineMode<U>, U: Ui>() {
530    static LIST: LazyLock<RwData<Vec<TypeId>>> = LazyLock::new(|| RwData::new(Vec::new()));
531
532    let mut list = LIST.write();
533    if !list.contains(&TypeId::of::<M>()) {
534        M::once();
535        list.push(TypeId::of::<M>());
536    }
537}
538
539enum FnOrInc<I, U: Ui> {
540    Fn(Option<Box<dyn IncFn<I, U> + Send + Sync>>),
541    Inc(I, PhantomData<U>),
542}
543
544impl<I, U: Ui> FnOrInc<I, U> {
545    fn as_inc(&mut self, file: &mut File, area: &U::Area) {
546        let FnOrInc::Fn(f) = self else {
547            unreachable!();
548        };
549
550        let inc = f.take().unwrap()(file, area);
551        *self = FnOrInc::Inc(inc, PhantomData);
552    }
553}
554
555trait IncFn<I, U: Ui> = FnOnce(&mut File, &U::Area) -> I;