duat_core/cmd/
mod.rs

1//! Creation and execution of commands.
2//!
3//! Commands on Duat are bits of code that can be executed on the
4//! [`PromptLine`] widget. They can also be invoked from other parts
5//! of the code, but their use is mostly intended for user calls.
6//!
7//! # Running commands
8//!
9//! There are two environments where you'd run commands. When you have
10//! a [`Pass`] available, and when you don't.
11//!
12//! ## When you have a [`Pass`]
13//!
14//! If you have a `Pass` available, it means you are in the main
15//! thread of execution, and can safely execute commands. The
16//! advantage of using a `Pass` is that you can retrieve the value
17//! returned by the command:
18//!
19//! ```rust
20//! # duat_core::doc_duat!(duat);
21//! setup_duat!(setup);
22//! use duat::prelude::*;
23//! fn main_thread_function(pa: &mut Pass) {
24//!     let result = cmd::call(pa, "colorscheme solarized");
25//!     if result.is_ok() {
26//!         context::info!("[a]Awesome!");
27//!     }
28//! }
29//! ```
30//!
31//! The code above runs the `colorscheme` command. In this case, if
32//! the command succeds or fails, no notification will be shown, if
33//! you want notifications, you should use [`cmd::call_notify`], and
34//! the return value would still be acquired.
35//!
36//! ## When you don't have a [`Pass`]
37//!
38//! you may not have a `Pass` if, for example, you are not on the
39//! main thread of execution. In this case, there is [`cmd::queue`],
40//! which queues up a call to be executed later.
41//! This means that you can't retrieve the return value, since it will
42//! be executed asynchronously:
43//!
44//! ```rust
45//! # duat_core::doc_duat!(duat);
46//! setup_duat!(setup);
47//! use duat::prelude::*;
48//! fn on_a_thread_far_far_away() {
49//!     cmd::queue("set-form --flag -abc punctuation.delimiter rgb 255 0 0 hsl 1");
50//! }
51//! ```
52//!
53//! The `set-form` command above will fail, since the hsl [`Color`]
54//! [`Parameter`] was not completely matched, missing the saturation
55//! and lightness arguments. It also shows two flag arguments, word
56//! flags (`"--flag"`) and blob flags (`"-abc"`). Even failing, no
57//! notification will be sent, because I called [`queue`], if you want
58//! notifications, call [`queue_notify`].
59//!
60//! If you want to "react" to the result of a queued call, you can use
61//! the [`cmd::queue_and`] function, which lets you also send a
62//! function that takes [`CmdResult`] as parameter:
63//!
64//! ```rust
65//! # mod duat { pub mod prelude { pub use duat_core::cmd; } }
66//! # use std::sync::atomic::{AtomicUsize, Ordering};
67//! use duat::prelude::*;
68//!
69//! static FAILURE_COUNT: AtomicUsize = AtomicUsize::new(0);
70//!
71//! fn on_a_thread_far_far_away() {
72//!     cmd::queue_and(
73//!         "set-form --flag -abc punctuation.delimiter rgb 255 0 0 hsl 1",
74//!         |res| {
75//!             if res.is_err() {
76//!                 FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
77//!             }
78//!         },
79//!     );
80//! }
81//! ```
82//!
83//! Note that, because this function might be sent to another thread,
84//! it must be [`Send + 'static`].
85//!
86//! # Adding commands
87//!
88//! Commands are added through the [`add!`] macro. This macro takes in
89//! two arguments. The first argument is a list of callers, for
90//! example `["quit", "q"]`, or just a caller, like `"reload"`.
91//!
92//! The second argument is a _rust-like_ "closure" that receives a
93//! variable number of [`Parameter`]s as arguments, alongside a
94//! [`Pass`]. Inside of this "closure", you will have access to the
95//! ful breatdh of duat's shareable state (just like any other time
96//! you have a `Pass`).
97//!
98//! Most Rust [`std`] types (that would make sense) are implemented as
99//! [`Parameter`]s, so you can place [`String`]s, [`f32`]s, [`bool`]s,
100//! and all sorts of other types as `Parameter`s for your command.
101//!
102//! Types like [`Vec`] and [`Option`] are also implemented as
103//! [`Parameter`]s, so you can have a list of `Parameter`s or an
104//! optional `Parameter`.
105//!
106//! ```rust
107//! # mod duat { pub mod prelude { pub use duat_core::{
108//! #     cmd, data::Pass, form::{self, Form}, text::txt
109//! # };}}
110//! use duat::prelude::*;
111//!
112//! let callers = ["unset-form", "uf"];
113//! // A `Vec<T>` parameter will try to collect all
114//! // remaining arguments as `T` in a list.
115//! let result = cmd::add!(callers, |pa: &mut Pass, forms: Vec<cmd::FormName>| {
116//!     for form in forms.iter() {
117//!         form::set("form", Form::new());
118//!     }
119//!     // You can return a success message, but must
120//!     // return an error message if there is a problem.
121//!     // For those, you should use the `txt!` macro.
122//!     Ok(Some(txt!("Unset [a]{}[] forms", forms.len())))
123//! });
124//! ```
125//!
126//! In the command above, you'll notice that [`Ok`] values return
127//! [`Option<Text>`]. This is because you may not care about
128//! announcing that the command succedeed. For the [`Err`] variant,
129//! however, the return value is just [`Text`], because you should say
130//! what went wrong in the command. Most of the time, this happens
131//! because of something out of your control, like a buffer not
132//! existing. In these cases, the `?` is enough to return an
133//! appropriate `Text`.
134//!
135//! There are other builtin types of [`Parameter`]s in `cmd` that
136//! can be used on a variety of things. For example, the [`Remainder`]
137//! `Parameter` is one that just takes the remaining arguments and
138//! collects them into a single [`String`].
139//!
140//! ```rust
141//! # duat_core::doc_duat!(duat);
142//! setup_duat!(setup);
143//! use duat::prelude::*;
144//!
145//! cmd::add!("pip", |_pa, args: cmd::Remainder| {
146//!     let child = std::process::Command::new("pip").spawn()?;
147//!     let res = child.wait_with_output()?;
148//!
149//!     Ok(Some(txt!("{res:?}")))
150//! });
151//! ```
152//!
153//! [`PromptLine`]: https://docs.rs/duat/latest/duat/widgets/struct.PromptLine.html
154//! [`cmd::call_notify`]: call_notify
155//! [`cmd::queue`]: queue
156//! [`cmd::queue_and`]: queue_and
157//! [`Send + 'static`]: Send
158//! [`Color`]: crate::form::Color
159//! [`txt!`]: crate::text::txt
160//! [`Ok(Some({Text}))`]: Ok
161//! [`Form`]: crate::form::Form
162//! [`Handle`]: crate::context::Handle
163use std::{
164    collections::HashMap,
165    fmt::Display,
166    ops::Range,
167    sync::{Arc, LazyLock},
168};
169
170use crossterm::style::Color;
171
172pub use self::{global::*, parameters::*};
173use crate::{
174    buffer::{Buffer, PathKind},
175    context::{self, Handle, sender},
176    data::{Pass, RwData},
177    form::FormId,
178    mode,
179    session::DuatEvent,
180    text::{Text, txt},
181    ui::{Node, Widget},
182};
183
184mod parameters;
185
186/// Adds all the usual session commands
187pub(crate) fn add_session_commands() {
188    add!("alias", |pa, alias: &str, command: Remainder| {
189        crate::cmd::alias(pa, alias, command)
190    });
191
192    add!(["write", "w"], |pa, path: Option<ValidBuffer>| {
193        let handle = context::current_buffer(pa).clone();
194        let buffer = handle.write(pa);
195
196        let (bytes, name) = if let Some(path) = path {
197            (buffer.save_to(&path)?, path)
198        } else if let Some(name) = buffer.name_set() {
199            (buffer.save()?, std::path::PathBuf::from(name))
200        } else {
201            return Err(txt!("Buffer has no name path to write to"));
202        };
203
204        match bytes {
205            Some(bytes) => Ok(Some(txt!("Wrote [a]{bytes}[] bytes to [buffer]{name}"))),
206            None => Ok(Some(txt!("Nothing to be written"))),
207        }
208    });
209
210    add!(["write-quit", "wq"], |pa, path: Option<ValidBuffer>| {
211        let handle = context::current_buffer(pa).clone();
212
213        let (bytes, name) = {
214            let buffer = handle.write(pa);
215            let bytes = if let Some(path) = path {
216                buffer.save_quit_to(path, true)?
217            } else {
218                buffer.save_quit(true)?
219            };
220            (bytes, buffer.name())
221        };
222
223        context::windows().close(pa, &handle)?;
224
225        match bytes {
226            Some(bytes) => Ok(Some(txt!(
227                "Closed [buffer]{name}[], writing [a]{bytes}[] bytes"
228            ))),
229            None => Ok(Some(txt!("Closed [buffer]{name}[]"))),
230        }
231    });
232
233    add!(["write-all", "wa"], |pa| {
234        let windows = context::windows();
235
236        let mut written = 0;
237        let handles: Vec<_> = windows
238            .buffers(pa)
239            .filter(|handle| handle.read(pa).path_set().is_some())
240            .collect();
241
242        for handle in &handles {
243            written += handle.write(pa).save().is_ok() as usize;
244        }
245
246        if written == handles.len() {
247            Ok(Some(txt!("Wrote to [a]{written}[] buffers")))
248        } else {
249            let unwritten = handles.len() - written;
250            let plural = if unwritten == 1 { "" } else { "s" };
251            Err(txt!("Failed to write to [a]{unwritten}[] buffer{plural}"))
252        }
253    });
254
255    add!(["write-all-quit", "waq"], |pa| {
256        let windows = context::windows();
257
258        let mut written = 0;
259        let handles: Vec<_> = windows
260            .buffers(pa)
261            .filter(|handle| handle.read(pa).path_set().is_some())
262            .collect();
263        for handle in &handles {
264            written += handle.write(pa).save_quit(true).is_ok() as usize;
265        }
266
267        if written == handles.len() {
268            sender().send(DuatEvent::Quit).unwrap();
269            Ok(None)
270        } else {
271            let unwritten = handles.len() - written;
272            let plural = if unwritten == 1 { "" } else { "s" };
273            Err(txt!("Failed to write to [a]{unwritten}[] buffer{plural}"))
274        }
275    });
276
277    add!(["write-all-quit!", "waq!"], |pa| {
278        let handles: Vec<_> = context::windows().buffers(pa).collect();
279
280        for handle in handles {
281            let _ = handle.write(pa).save_quit(true);
282        }
283
284        sender().send(DuatEvent::Quit).unwrap();
285        Ok(None)
286    });
287
288    add!(["quit", "q"], |pa, handle: Option<Buffer>| {
289        let handle = match handle {
290            Some(handle) => handle,
291            None => context::current_buffer(pa).clone(),
292        };
293
294        let buffer = handle.read(pa);
295        if buffer.text().has_unsaved_changes() && buffer.exists() {
296            return Err(txt!("{} has unsaved changes", buffer.name()));
297        }
298
299        context::windows().close(pa, &handle)?;
300
301        Ok(Some(txt!("Closed [buffer]{}", handle.read(pa).name())))
302    });
303
304    add!(["quit!", "q!"], |pa, handle: Option<Buffer>| {
305        let handle = match handle {
306            Some(handle) => handle,
307            None => context::current_buffer(pa).clone(),
308        };
309
310        context::windows().close(pa, &handle)?;
311
312        Ok(Some(txt!("Forcefully closed {}", handle.read(pa).name())))
313    });
314
315    add!(["quit-all", "qa"], |pa| {
316        let windows = context::windows();
317        let unwritten = windows
318            .buffers(pa)
319            .filter(|handle| {
320                let buffer = handle.read(pa);
321                buffer.text().has_unsaved_changes() && buffer.exists()
322            })
323            .count();
324
325        if unwritten == 0 {
326            sender().send(DuatEvent::Quit).unwrap();
327            Ok(None)
328        } else if unwritten == 1 {
329            Err(txt!("There is [a]1[] unsaved buffer"))
330        } else {
331            Err(txt!("There are [a]{unwritten}[] unsaved buffers"))
332        }
333    });
334
335    add!(["quit-all!", "qa!"], |_pa| {
336        sender().send(DuatEvent::Quit).unwrap();
337        Ok(None)
338    });
339
340    add!(["reload"], |_pa, flags: Flags, profile: Option<String>| {
341        sender()
342            .send(DuatEvent::RequestReload(crate::session::ReloadEvent {
343                clean: flags.word("clean"),
344                update: flags.word("update"),
345                profile: profile.unwrap_or(crate::utils::profile().to_string()),
346            }))
347            .unwrap();
348
349        // This has to be done on Windows, since you can't remove
350        // loaded dlls. Thus, we need to quit the curent
351        // configuration first, and then we can start compiling the
352        // new version of the config crate.
353        #[cfg(target_os = "windows")]
354        sender().send(DuatEvent::ReloadSucceeded).unwrap();
355
356        Ok(None)
357    });
358
359    add!(["edit", "e"], |pa, arg: PathOrBufferOrCfg| {
360        let windows = context::windows();
361
362        let pk = match arg {
363            PathOrBufferOrCfg::Cfg => {
364                PathKind::from(crate::utils::crate_dir()?.join("src").join("lib.rs"))
365            }
366            PathOrBufferOrCfg::CfgManifest => {
367                PathKind::from(crate::utils::crate_dir()?.join("Cargo.toml"))
368            }
369            PathOrBufferOrCfg::Path(path) => PathKind::from(path),
370            PathOrBufferOrCfg::Buffer(handle) => {
371                mode::reset_to(handle.to_dyn());
372                return Ok(Some(txt!("Switched to {}", handle.read(pa).name())));
373            }
374        };
375
376        let buffer = Buffer::new(pk.as_path(), *crate::session::FILE_CFG.get().unwrap());
377        let handle = windows.new_buffer(pa, buffer);
378        context::set_current_node(pa, handle);
379
380        return Ok(Some(txt!("Opened {pk}")));
381    });
382
383    add!(["open", "o"], |pa, arg: PathOrBufferOrCfg| {
384        let windows = context::windows();
385
386        let (pk, msg) = match arg {
387            PathOrBufferOrCfg::Cfg => (
388                PathKind::from(crate::utils::crate_dir()?.join("src").join("lib.rs")),
389                None,
390            ),
391            PathOrBufferOrCfg::CfgManifest => (
392                PathKind::from(crate::utils::crate_dir()?.join("Cargo.toml")),
393                None,
394            ),
395            PathOrBufferOrCfg::Path(path) => (PathKind::from(path), None),
396            PathOrBufferOrCfg::Buffer(handle) => {
397                let pk = handle.read(pa).path_kind();
398                let (win, ..) = windows.buffer_entry(pa, pk.clone()).unwrap();
399                if windows.get(pa, win).unwrap().buffers(pa).len() == 1 {
400                    (pk.clone(), Some(txt!("Switched to {pk}")))
401                } else {
402                    (pk.clone(), Some(txt!("Moved {pk} to a new window")))
403                }
404            }
405        };
406
407        let file_cfg = *crate::session::FILE_CFG.get().unwrap();
408        let node = windows.open_or_move_to_new_window(pa, pk.clone(), file_cfg);
409
410        return Ok(msg.or_else(|| Some(txt!("Opened {pk} on new window"))));
411    });
412
413    add!(["buffer", "b"], |pa, handle: OtherBuffer| {
414        mode::reset_to(handle.to_dyn());
415        Ok(Some(txt!("Switched to [buffer]{}", handle.read(pa).name())))
416    });
417
418    add!("next-buffer", |pa, flags: Flags| {
419        let windows = context::windows();
420        let handle = context::current_buffer(pa);
421        let win = context::current_win_index(pa);
422
423        let wid = windows
424            .get(pa, win)
425            .unwrap()
426            .nodes(pa)
427            .position(|node| handle.ptr_eq(node.widget()))
428            .unwrap_or_else(|| panic!("{}, {win}", handle.read(pa).name()));
429
430        let handle = if flags.word("global") {
431            windows
432                .iter_around(pa, win, wid)
433                .find_map(as_buffer_handle)
434                .ok_or_else(|| txt!("There are no other open buffers"))?
435        } else {
436            windows
437                .iter_around(pa, win, wid)
438                .filter(|(lhs, ..)| *lhs == win)
439                .find_map(as_buffer_handle)
440                .ok_or_else(|| txt!("There are no other buffers open in this window"))?
441        };
442
443        mode::reset_to(handle.to_dyn());
444        Ok(Some(txt!("Switched to [buffer]{}", handle.read(pa).name())))
445    });
446
447    add!("prev-buffer", |pa, flags: Flags| {
448        let windows = context::windows();
449        let handle = context::current_buffer(pa);
450        let win = context::current_win_index(pa);
451
452        let wid = windows
453            .get(pa, win)
454            .unwrap()
455            .nodes(pa)
456            .position(|node| handle.ptr_eq(node.widget()))
457            .unwrap();
458
459        let handle = if flags.word("global") {
460            windows
461                .iter_around_rev(pa, win, wid)
462                .find_map(as_buffer_handle)
463                .ok_or_else(|| txt!("There are no other open buffers"))?
464        } else {
465            windows
466                .iter_around(pa, win, wid)
467                .filter(|(lhs, ..)| *lhs == win)
468                .find_map(as_buffer_handle)
469                .ok_or_else(|| txt!("There are no other buffers open in this window"))?
470        };
471
472        mode::reset_to(handle.to_dyn());
473        Ok(Some(txt!("Switched to [buffer]{}", handle.read(pa).name())))
474    });
475
476    add!("last-buffer", |pa| {
477        let handle = context::windows().last_buffer(pa)?;
478        Ok(Some(txt!("Switched to [buffer]{}", handle.read(pa).name())))
479    });
480
481    add!("swap", |pa, lhs: Buffer, rhs: Option<Buffer>| {
482        let rhs = rhs.unwrap_or_else(|| context::current_buffer(pa).clone());
483
484        context::windows().swap(pa, &lhs.to_dyn(), &rhs.to_dyn())?;
485
486        Ok(Some(txt!(
487            "Swapped {} and {}",
488            lhs.read(pa).name(),
489            rhs.read(pa).name()
490        )))
491    });
492
493    add!("colorscheme", |_pa, scheme: ColorSchemeArg| {
494        crate::form::set_colorscheme(scheme);
495        Ok(Some(txt!("Set colorscheme to [a]{scheme}[]")))
496    });
497
498    add!(
499        "set-form",
500        |_pa, name: FormName, colors: Between<0, 3, Color>| {
501            let mut form = crate::form::Form::new();
502            form.style.foreground_color = colors.first().cloned();
503            form.style.background_color = colors.get(1).cloned();
504            form.style.underline_color = colors.get(2).cloned();
505            crate::form::set(name, form);
506
507            Ok(Some(txt!("Set [a]{name}[] to a new Form")))
508        }
509    );
510}
511
512mod global {
513    use std::ops::Range;
514
515    use super::{CheckerFn, CmdFn, CmdResult, Commands};
516    #[doc(inline)]
517    pub use crate::__add__ as add;
518    use crate::{
519        context, data::Pass, form::FormId, main_thread_only::MainThreadOnly, session::DuatEvent,
520        text::Text,
521    };
522
523    static COMMANDS: MainThreadOnly<Commands> = MainThreadOnly::new(Commands::new());
524
525    /// Adds a command to Duat
526    ///
527    /// Anyone will be able to execute this command, and it can take
528    /// any number of [`Parameter`] arguments. These arguments will be
529    /// automatically matched and checked before the command starts
530    /// execution, providing feedback to the user as he is typing the
531    /// commands.
532    ///
533    /// # Examples
534    ///
535    /// In the config crate:
536    ///
537    /// ```rust
538    /// # duat_core::doc_duat!(duat);
539    /// setup_duat!(setup);
540    /// use duat::prelude::*;
541    ///
542    /// fn setup() {
543    ///     let var = data::RwData::new(35);
544    ///
545    ///     let var_clone = var.clone();
546    ///     cmd::add!("set-var", |pa: &mut Pass, value: usize| {
547    ///         *var_clone.write(pa) = value;
548    ///         Ok(None)
549    ///     });
550    ///
551    ///     hook::add::<WindowCreated>(move |mut pa, window| {
552    ///         // status! macro is from duat.
553    ///         status!("The value is currently {var}")
554    ///             .above()
555    ///             .push_on(pa, window);
556    ///         Ok(())
557    ///     });
558    /// }
559    /// ```
560    ///
561    /// Since `var` is an [`RwData`], it will be updated
562    /// automatically in the [`StatusLine`]
563    ///
564    /// [`StatusLine`]: https://docs.rs/duat/latest/duat/widgets/struct.StatusLine.html
565    /// [`RwData`]: crate::data::RwData
566    /// [`Parameter`]: super::Parameter
567    #[macro_export]
568    #[doc(hidden)]
569    macro_rules! __add__ {
570        ($callers:expr, |$pa:ident $(: &mut Pass)? $(, $arg:tt: $t:ty)* $(,)?| $f:block) => {{
571            use std::{sync::Arc, cell::UnsafeCell};
572            #[allow(unused_imports)]
573            use $crate::{
574                data::{Pass, RwData},
575                cmd::{Args, Caller, CmdFn, CmdResult, Parameter, Remainder, add_inner}
576            };
577
578            #[allow(unused_variables, unused_mut)]
579            let cmd = move |pa: &mut Pass, mut args: Args| -> CmdResult {
580                $(
581                    let ($arg, form): (<$t as Parameter>::Returns, _) =
582                        <$t as Parameter>::new(pa, &mut args)?;
583                )*
584
585                if let Ok(arg) = args.next() {
586                    return Err($crate::text::txt!("Too many arguments"));
587                }
588
589                let mut $pa = pa;
590
591                $f
592            };
593
594            #[allow(unused_variables, unused_mut)]
595            let check_args = |pa: &Pass, mut args: Args| {
596                let mut ok_ranges = Vec::new();
597
598                $(
599                    let start = args.next_start();
600                    let result = <$t as Parameter>::new(pa, &mut args);
601                    match result {
602                        Ok((_, form)) => if let Some(start) = start
603                            .filter(|s| args.param_range().end > *s)
604                        {
605                            ok_ranges.push((start..args.param_range().end, form));
606                        }
607                        Err(err) => return (ok_ranges, Some((args.param_range(), err)))
608                    }
609                )*
610
611                let start = args.next_start();
612                if let (Ok(_), Some(start)) = (args.next_as::<Remainder>(pa), start) {
613                    let err = $crate::text::txt!("Too many arguments");
614                    return (ok_ranges, Some((start..args.param_range().end, err)))
615                }
616
617                (ok_ranges, None)
618            };
619
620            let callers: Vec<String> = $callers.into_callers().map(str::to_string).collect();
621            // SAFETY: This type will never actually be queried
622            let cmd: CmdFn = unsafe { RwData::new_unsized::<()>(Arc::new(UnsafeCell::new(cmd))) };
623
624            add_inner(callers, cmd, check_args)
625        }}
626    }
627
628    /// Canonical way to quit Duat.
629    ///
630    /// By calling the quit command, all threads will finish their
631    /// tasks, and then Duat will execute a program closing
632    /// function, as defined by the [Ui].
633    ///
634    /// [Ui]: crate::ui::traits::RawUi
635    pub fn quit() {
636        queue("quit");
637    }
638
639    /// Switches to/opens a [`Buffer`] with the given name.
640    ///
641    /// If you wish to specifically switch to buffers that are already
642    /// open, use [`buffer`].
643    ///
644    /// If there are more arguments, they will be ignored.
645    ///
646    /// [`Buffer`]: crate::buffer::Buffer
647    pub fn edit(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
648        call(pa, format!("edit {buffer}"))
649    }
650
651    /// Switches to a [`Buffer`] with the given name.
652    ///
653    /// If there is no buffer open with that name, does nothing. Use
654    /// [`edit`] if you wish to open buffers.
655    ///
656    /// If there are more arguments, they will be ignored.
657    ///
658    /// [`Buffer`]: crate::buffer::Buffer
659    pub fn buffer(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
660        call(pa, format!("buffer {buffer}"))
661    }
662
663    /// Switches to the next [`Buffer`].
664    ///
665    /// This function will only look at buffers that are opened in the
666    /// current window. If you want to include other windows in the
667    /// search, use [`next_global_buffer`].
668    ///
669    /// [`Buffer`]: crate::buffer::Buffer
670    pub fn next_buffer(pa: &mut Pass) -> CmdResult {
671        call(pa, "next-buffer")
672    }
673
674    /// Switches to the previous [`Buffer`].
675    ///
676    /// This function will only look at buffers that are opened in the
677    /// current window. If you want to include other windows in the
678    /// search, use [`prev_global_buffer`].
679    ///
680    /// [`Buffer`]: crate::buffer::Buffer
681    pub fn prev_buffer(pa: &mut Pass) -> CmdResult {
682        call(pa, "prev-buffer")
683    }
684
685    /// Switches to the next [`Buffer`].
686    ///
687    /// This function will look for buffers in all windows. If you
688    /// want to limit the search to just the current window, use
689    /// [`next_buffer`].
690    ///
691    /// [`Buffer`]: crate::buffer::Buffer
692    pub fn next_global_buffer(pa: &mut Pass) -> CmdResult {
693        call(pa, "next-buffer --global")
694    }
695
696    /// Switches to the previous [`Buffer`].
697    ///
698    /// This function will look for buffers in all windows. If you
699    /// want to limit the search to just the current window, use
700    /// [`prev_buffer`].
701    ///
702    /// [`Buffer`]: crate::buffer::Buffer
703    pub fn prev_global_buffer(pa: &mut Pass) -> CmdResult {
704        call(pa, "prev-buffer --global")
705    }
706
707    /// Tries to alias a `caller` to an existing `command`.
708    ///
709    /// Returns an [`Err`] if the `caller` is already a caller for
710    /// another command, or if `command` is not a real caller to an
711    /// existing command.
712    pub fn alias(pa: &mut Pass, alias: impl ToString, command: impl ToString) -> CmdResult {
713        // SAFETY: Function has a Pass argument.
714        unsafe { COMMANDS.get() }.alias(pa, alias, command)
715    }
716
717    /// Runs a full command synchronously, with a [`Pass`].
718    ///
719    /// If you call commands through this function, you will be able
720    /// to retrieve their return values,  but because of the [`Pass`],
721    /// this function can only be used on the main thread of
722    /// execution. If you want to call commands from other threads,
723    /// see [`cmd::queue`].
724    ///
725    /// When running the command, the ordering of [`Flags`] does not
726    /// matter, as long as they are placed before the arguments to the
727    /// command.
728    ///
729    /// # Examples
730    ///
731    /// ```rust
732    /// # duat_core::doc_duat!(duat);
733    /// setup_duat!(setup);
734    /// use duat::prelude::*;
735    ///
736    /// fn main_thread_function(pa: &mut Pass) {
737    ///     cmd::call(pa, "set-prompt new-prompt");
738    /// }
739    /// ```
740    ///
741    /// In this case we're running a command that will affect the most
742    /// relevant [`PromptLine`], and no notifications are sent. That's
743    /// because I used [`call`]. If you want notifications, see
744    /// [`cmd::call_notify`].
745    ///
746    /// [`cmd::queue`]: queue
747    /// [`cmd::call_notify`]: call_notify
748    /// [`PromptLine`]: https://docs.rs/duat/latest/duat/widgets/struct.PromptLine.html
749    /// [`Flags`]: super::Flags
750    pub fn call(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
751        // SAFETY: Function has a Pass argument.
752        unsafe { COMMANDS.get() }.run(pa, call)
753    }
754
755    /// Like [`call`], but notifies the result
756    #[allow(unused_must_use)]
757    pub fn call_notify(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
758        // SAFETY: Function has a Pass argument.
759        let result = unsafe { COMMANDS.get() }.run(pa, call.to_string());
760        context::logs().push_cmd_result(result.clone());
761
762        result
763    }
764
765    /// Queues a command call
766    ///
767    /// You should only use this if you're not in the main thread of
768    /// execution, or if you don't have a [`Pass`] for some other
769    /// reason, like you are in the middle of accessing an [`RwData`]
770    /// or something like that.
771    ///
772    /// Since this function will run outside of the current scope, its
773    /// [`Result`] will not be returned.
774    ///
775    /// [`RwData`]: crate::data::RwData
776    pub fn queue(call: impl std::fmt::Display) {
777        let call = call.to_string();
778        crate::context::sender()
779            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
780                // SAFETY: Closure has Pass argument.
781                let _ = unsafe { COMMANDS.get() }.run(pa, call);
782            })))
783            .unwrap();
784    }
785
786    /// Like [`queue`], but notifies the result
787    pub fn queue_notify(call: impl std::fmt::Display) {
788        let call = call.to_string();
789        crate::context::sender()
790            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
791                context::logs()
792                    .push_cmd_result(unsafe { COMMANDS.get() }.run(pa, call.clone()).clone());
793            })))
794            .unwrap()
795    }
796
797    /// Like [`queue`], but acts on the [`Result`]
798    pub fn queue_and(call: impl std::fmt::Display, map: impl FnOnce(CmdResult) + Send + 'static) {
799        let call = call.to_string();
800        crate::context::sender()
801            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
802                // SAFETY: Function has a Pass argument.
803                map(unsafe { COMMANDS.get() }.run(pa, call));
804            })))
805            .unwrap()
806    }
807
808    /// Like [`queue_and`], but also notifies the [`Result`]
809    pub fn queue_notify_and(
810        call: impl std::fmt::Display,
811        map: impl FnOnce(CmdResult) + Send + 'static,
812    ) {
813        let call = call.to_string();
814        crate::context::sender()
815            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
816                // SAFETY: Function has a Pass argument.
817                let result = unsafe { COMMANDS.get() }.run(pa, call.clone());
818                context::logs().push_cmd_result(result.clone());
819
820                map(result)
821            })))
822            .unwrap()
823    }
824
825    /// Don't call this function, use [`cmd::add`] instead
826    ///
827    /// [`cmd::add`]: add
828    #[doc(hidden)]
829    pub fn add_inner(callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) {
830        // SAFETY: There is no way to obtain an external RwData of Commands,
831        // so you can modify it from anywhere in the main thread.
832        let mut pa = unsafe { Pass::new() };
833        unsafe { COMMANDS.get() }.add(&mut pa, callers, cmd, check_args)
834    }
835
836    /// Check if the arguments for a given `caller` are correct
837    pub fn check_args(
838        pa: &Pass,
839        caller: &str,
840    ) -> Option<(
841        Vec<(Range<usize>, Option<FormId>)>,
842        Option<(Range<usize>, Text)>,
843    )> {
844        // SAFETY: There is a Pass argument
845        unsafe { COMMANDS.get() }.check_args(pa, caller)
846    }
847}
848
849/// A list of commands.
850///
851/// This list contains all of the commands that have been
852/// added to Duat, as well as info on the current [`Buffer`],
853/// [widget] and all of the [windows].
854///
855/// [`Buffer`]: crate::buffer::Buffer
856/// [widget]: crate::ui::Widget
857/// [windows]: crate::ui::Window
858struct Commands(LazyLock<RwData<InnerCommands>>);
859
860impl Commands {
861    /// Returns a new instance of [`Commands`].
862    const fn new() -> Self {
863        Self(LazyLock::new(|| {
864            RwData::new(InnerCommands {
865                list: Vec::new(),
866                aliases: HashMap::new(),
867            })
868        }))
869    }
870
871    /// Aliases a command to a specific word
872    fn alias(&self, pa: &mut Pass, alias: impl ToString, command: impl ToString) -> CmdResult {
873        self.0
874            .write(pa)
875            .try_alias(alias.to_string(), command.to_string())
876    }
877
878    /// Runs a command from a call
879    fn run(&self, pa: &mut Pass, call: impl Display) -> CmdResult {
880        let call = call.to_string();
881        let mut args = call.split_whitespace();
882        let caller = args.next().ok_or(txt!("The command is empty"))?.to_string();
883
884        let inner = self.0.read(pa);
885
886        let (command, call) = {
887            if let Some(command) = inner.aliases.get(&caller) {
888                let (command, call) = command;
889                let mut call = call.clone() + " ";
890                call.extend(args);
891
892                (command.clone(), call)
893            } else {
894                let command = inner
895                    .list
896                    .iter()
897                    .find(|cmd| cmd.callers().contains(&caller))
898                    .ok_or(txt!("[a]{caller}[]: No such command"))?;
899
900                (command.clone(), call.clone())
901            }
902        };
903
904        let args = get_args(&call);
905
906        if let (_, Some((_, err))) = (command.check_args)(pa, args.clone()) {
907            return Err(err);
908        }
909
910        let silent = call.len() > call.trim_start().len();
911        command.cmd.write(&mut unsafe { Pass::new() })(pa, args).map(|ok| ok.filter(|_| !silent))
912    }
913
914    /// Adds a command to the list of commands
915    fn add(&self, pa: &mut Pass, callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) {
916        let cmd = Command::new(callers, cmd, check_args);
917        self.0.write(pa).add(cmd)
918    }
919
920    /// Gets the parameter checker for a command, if it exists
921    fn check_args(
922        &self,
923        pa: &Pass,
924        call: &str,
925    ) -> Option<(
926        Vec<(Range<usize>, Option<FormId>)>,
927        Option<(Range<usize>, Text)>,
928    )> {
929        let mut args = call.split_whitespace();
930        let caller = args.next()?.to_string();
931
932        let inner = self.0.read(pa);
933        if let Some((command, _)) = inner.aliases.get(&caller) {
934            Some((command.check_args)(pa, get_args(call)))
935        } else {
936            let command = inner
937                .list
938                .iter()
939                .find(|cmd| cmd.callers().contains(&caller))?;
940
941            Some((command.check_args)(pa, get_args(call)))
942        }
943    }
944}
945
946/// The standard error that should be returned when [`call`]ing
947/// commands.
948///
949/// This error _must_ include an error message in case of failure. It
950/// may also include a success message, but that is not required.
951///
952/// [`call`]: global::call
953pub type CmdResult = Result<Option<Text>, Text>;
954
955/// A function that can be called by name.
956#[derive(Clone)]
957struct Command {
958    callers: Arc<[String]>,
959    cmd: CmdFn,
960    check_args: CheckerFn,
961}
962
963impl Command {
964    /// Returns a new instance of command.
965    fn new(callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) -> Self {
966        if let Some(caller) = callers
967            .iter()
968            .find(|caller| caller.split_whitespace().count() != 1)
969        {
970            panic!("Command caller \"{caller}\" contains more than one word");
971        }
972        Self { cmd, check_args, callers: callers.into() }
973    }
974
975    /// The list of callers that will trigger this command.
976    fn callers(&self) -> &[String] {
977        &self.callers
978    }
979}
980
981struct InnerCommands {
982    list: Vec<Command>,
983    aliases: HashMap<String, (Command, String)>,
984}
985
986impl InnerCommands {
987    /// Adds a command to the list
988    ///
989    /// Overrides previous commands with the same name.
990    fn add(&mut self, command: Command) {
991        let mut new_callers = command.callers().iter();
992
993        self.list
994            .retain(|cmd| new_callers.all(|caller| !cmd.callers.contains(caller)));
995
996        self.list.push(command);
997    }
998
999    /// Tries to alias a full command (caller, flags, and
1000    /// arguments) to an alias.
1001    fn try_alias(&mut self, alias: String, call: String) -> Result<Option<Text>, Text> {
1002        if alias.split_whitespace().count() != 1 {
1003            return Err(txt!("Alias [a]{alias}[] is not a single word"));
1004        }
1005
1006        let caller = call
1007            .split_whitespace()
1008            .next()
1009            .ok_or(txt!("The command is empty"))?
1010            .to_string();
1011
1012        let mut cmds = self.list.iter();
1013
1014        if let Some(command) = cmds.find(|cmd| cmd.callers().contains(&caller)) {
1015            let entry = (command.clone(), call.clone());
1016            Ok(Some(match self.aliases.insert(alias.clone(), entry) {
1017                Some((_, prev_call)) => {
1018                    txt!("Aliased [a]{alias}[] from [a]{prev_call}[] to [a]{call}")
1019                }
1020                None => txt!("Aliased [a]{alias}[] to [a]{call}"),
1021            }))
1022        } else {
1023            Err(txt!("The caller [a]{caller}[] was not found"))
1024        }
1025    }
1026}
1027
1028/// A list of names to call a command with
1029pub trait Caller<'a>: Sized {
1030    /// An [`Iterator`] over said callers
1031    fn into_callers(self) -> impl Iterator<Item = &'a str>;
1032}
1033
1034impl<'a> Caller<'a> for &'a str {
1035    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1036        [self].into_iter()
1037    }
1038}
1039
1040impl<'a> Caller<'a> for &'a [&'a str] {
1041    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1042        self.iter().cloned()
1043    }
1044}
1045
1046impl<'a, const N: usize> Caller<'a> for [&'a str; N] {
1047    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1048        self.into_iter()
1049    }
1050}
1051
1052/// Inner function for Commands
1053#[doc(hidden)]
1054pub type CmdFn = RwData<dyn FnMut(&mut Pass, Args) -> CmdResult + Send + 'static>;
1055
1056/// Inner checking function
1057#[doc(hidden)]
1058pub type CheckerFn = fn(
1059    &Pass,
1060    Args,
1061) -> (
1062    Vec<(Range<usize>, Option<FormId>)>,
1063    Option<(Range<usize>, Text)>,
1064);
1065
1066pub(crate) fn as_buffer_handle((.., node): (usize, &Node)) -> Option<Handle> {
1067    node.try_downcast()
1068}