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!(["quit", "q"], |pa, handle: Option<Buffer>| {
193        let handle = match handle {
194            Some(handle) => handle,
195            None => context::current_buffer(pa).clone(),
196        };
197
198        let buffer = handle.read(pa);
199        if buffer.text().has_unsaved_changes() && buffer.exists() {
200            return Err(txt!("{} has unsaved changes", buffer.name()));
201        }
202
203        context::windows().close(pa, &handle)?;
204
205        Ok(Some(txt!("Closed [buffer]{}", handle.read(pa).name())))
206    });
207
208    add!(["quit!", "q!"], |pa, handle: Option<Buffer>| {
209        let handle = match handle {
210            Some(handle) => handle,
211            None => context::current_buffer(pa).clone(),
212        };
213
214        context::windows().close(pa, &handle)?;
215
216        Ok(Some(txt!("Forcefully closed {}", handle.read(pa).name())))
217    });
218
219    add!(["quit-all", "qa"], |pa| {
220        let windows = context::windows();
221        let unwritten = windows
222            .buffers(pa)
223            .filter(|handle| {
224                let buffer = handle.read(pa);
225                buffer.text().has_unsaved_changes() && buffer.exists()
226            })
227            .count();
228
229        if unwritten == 0 {
230            sender().send(DuatEvent::Quit).unwrap();
231            Ok(None)
232        } else if unwritten == 1 {
233            Err(txt!("There is [a]1[] unsaved buffer"))
234        } else {
235            Err(txt!("There are [a]{unwritten}[] unsaved buffers"))
236        }
237    });
238
239    add!(["quit-all!", "qa!"], |_pa| {
240        sender().send(DuatEvent::Quit).unwrap();
241        Ok(None)
242    });
243
244    add!(["write", "w"], |pa, path: Option<ValidBuffer>| {
245        let handle = context::current_buffer(pa).clone();
246        let buffer = handle.write(pa);
247
248        let (bytes, name) = if let Some(path) = path {
249            (buffer.save_to(&path)?, path)
250        } else if let Some(name) = buffer.name_set() {
251            (buffer.save()?, std::path::PathBuf::from(name))
252        } else {
253            return Err(txt!("Buffer has no name path to write to"));
254        };
255
256        match bytes {
257            Some(bytes) => Ok(Some(txt!("Wrote [a]{bytes}[] bytes to [buffer]{name}"))),
258            None => Ok(Some(txt!("Nothing to be written"))),
259        }
260    });
261
262    add!(["write-quit", "wq"], |pa, path: Option<ValidBuffer>| {
263        let handle = context::current_buffer(pa).clone();
264
265        let (bytes, name) = {
266            let buffer = handle.write(pa);
267            let bytes = if let Some(path) = path {
268                buffer.save_quit_to(path, true)?
269            } else {
270                buffer.save_quit(true)?
271            };
272            (bytes, buffer.name())
273        };
274
275        context::windows().close(pa, &handle)?;
276
277        match bytes {
278            Some(bytes) => Ok(Some(txt!(
279                "Closed [buffer]{name}[], writing [a]{bytes}[] bytes"
280            ))),
281            None => Ok(Some(txt!("Closed [buffer]{name}[]"))),
282        }
283    });
284
285    add!(["write-all", "wa"], |pa| {
286        let windows = context::windows();
287
288        let mut written = 0;
289        let handles: Vec<_> = windows
290            .buffers(pa)
291            .filter(|handle| handle.read(pa).path_set().is_some())
292            .collect();
293
294        for handle in &handles {
295            written += handle.write(pa).save().is_ok() as usize;
296        }
297
298        if written == handles.len() {
299            Ok(Some(txt!("Wrote to [a]{written}[] buffers")))
300        } else {
301            let unwritten = handles.len() - written;
302            let plural = if unwritten == 1 { "" } else { "s" };
303            Err(txt!("Failed to write to [a]{unwritten}[] buffer{plural}"))
304        }
305    });
306
307    add!(["write-all-quit", "waq"], |pa| {
308        let windows = context::windows();
309
310        let mut written = 0;
311        let handles: Vec<_> = windows
312            .buffers(pa)
313            .filter(|handle| handle.read(pa).path_set().is_some())
314            .collect();
315        for handle in &handles {
316            written += handle.write(pa).save_quit(true).is_ok() as usize;
317        }
318
319        if written == handles.len() {
320            sender().send(DuatEvent::Quit).unwrap();
321            Ok(None)
322        } else {
323            let unwritten = handles.len() - written;
324            let plural = if unwritten == 1 { "" } else { "s" };
325            Err(txt!("Failed to write to [a]{unwritten}[] buffer{plural}"))
326        }
327    });
328
329    add!(["write-all-quit!", "waq!"], |pa| {
330        let handles: Vec<_> = context::windows().buffers(pa).collect();
331
332        for handle in handles {
333            let _ = handle.write(pa).save_quit(true);
334        }
335
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    use crate::{
517        context, data::Pass, form::FormId, main_thread_only::MainThreadOnly, session::DuatEvent,
518        text::Text,
519    };
520
521    static COMMANDS: MainThreadOnly<Commands> = MainThreadOnly::new(Commands::new());
522
523    /// Adds a command to Duat
524    ///
525    /// Anyone will be able to execute this command, and it can take
526    /// any number of [`Parameter`] arguments. These arguments will be
527    /// automatically matched and checked before the command starts
528    /// execution, providing feedback to the user as he is typing the
529    /// commands.
530    ///
531    /// # Examples
532    ///
533    /// In the config crate:
534    ///
535    /// ```rust
536    /// # duat_core::doc_duat!(duat);
537    /// setup_duat!(setup);
538    /// use duat::prelude::*;
539    ///
540    /// fn setup() {
541    ///     let var = data::RwData::new(35);
542    ///
543    ///     let var_clone = var.clone();
544    ///     cmd::add!("set-var", |pa: &mut Pass, value: usize| {
545    ///         *var_clone.write(pa) = value;
546    ///         Ok(None)
547    ///     });
548    ///
549    ///     hook::add::<WindowCreated>(move |mut pa, window| {
550    ///         // status! macro is from duat.
551    ///         status!("The value is currently {var}")
552    ///             .above()
553    ///             .push_on(pa, window);
554    ///         Ok(())
555    ///     });
556    /// }
557    /// ```
558    ///
559    /// Since `var` is an [`RwData`], it will be updated
560    /// automatically in the [`StatusLine`]
561    ///
562    /// [`StatusLine`]: https://docs.rs/duat/latest/duat/widgets/struct.StatusLine.html
563    /// [`RwData`]: crate::data::RwData
564    /// [`Parameter`]: super::Parameter
565    pub macro add($callers:expr, |$pa:ident $(: &mut Pass)? $(, $arg:tt: $t:ty)* $(,)?| $f:block) {{
566        use std::{sync::Arc, cell::UnsafeCell};
567        #[allow(unused_imports)]
568        use $crate::{
569            data::{Pass, RwData},
570            cmd::{Args, Caller, CmdFn, CmdResult, Parameter, Remainder, add_inner}
571        };
572
573        #[allow(unused_variables, unused_mut)]
574        let cmd = move |pa: &mut Pass, mut args: Args| -> CmdResult {
575            $(
576                let ($arg, form): (<$t as Parameter>::Returns, _) =
577                    <$t as Parameter>::new(pa, &mut args)?;
578            )*
579
580            if let Ok(arg) = args.next() {
581                return Err($crate::text::txt!("Too many arguments"));
582            }
583
584            let mut $pa = pa;
585
586            $f
587        };
588
589        #[allow(unused_variables, unused_mut)]
590        let check_args = |pa: &Pass, mut args: Args| {
591            let mut ok_ranges = Vec::new();
592
593            $(
594                let start = args.next_start();
595                let result = <$t as Parameter>::new(pa, &mut args);
596                match result {
597                    Ok((_, form)) => if let Some(start) = start
598                        .filter(|s| args.param_range().end > *s)
599                    {
600                        ok_ranges.push((start..args.param_range().end, form));
601                    }
602                    Err(err) => return (ok_ranges, Some((args.param_range(), err)))
603                }
604            )*
605
606            let start = args.next_start();
607            if let (Ok(_), Some(start)) = (args.next_as::<Remainder>(pa), start) {
608                let err = $crate::text::txt!("Too many arguments");
609                return (ok_ranges, Some((start..args.param_range().end, err)))
610            }
611
612            (ok_ranges, None)
613        };
614
615        let callers: Vec<String> = $callers.into_callers().map(str::to_string).collect();
616        // SAFETY: This type will never actually be queried
617        let cmd: CmdFn = unsafe { RwData::new_unsized::<()>(Arc::new(UnsafeCell::new(cmd))) };
618
619        add_inner(callers, cmd, check_args)
620    }}
621
622    /// Canonical way to quit Duat.
623    ///
624    /// By calling the quit command, all threads will finish their
625    /// tasks, and then Duat will execute a program closing
626    /// function, as defined by the [Ui].
627    ///
628    /// [Ui]: crate::ui::traits::RawUi
629    pub fn quit() {
630        queue("quit");
631    }
632
633    /// Switches to/opens a [`Buffer`] with the given name.
634    ///
635    /// If you wish to specifically switch to buffers that are already
636    /// open, use [`buffer`].
637    ///
638    /// If there are more arguments, they will be ignored.
639    ///
640    /// [`Buffer`]: crate::buffer::Buffer
641    pub fn edit(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
642        call(pa, format!("edit {buffer}"))
643    }
644
645    /// Switches to a [`Buffer`] with the given name.
646    ///
647    /// If there is no buffer open with that name, does nothing. Use
648    /// [`edit`] if you wish to open buffers.
649    ///
650    /// If there are more arguments, they will be ignored.
651    ///
652    /// [`Buffer`]: crate::buffer::Buffer
653    pub fn buffer(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
654        call(pa, format!("buffer {buffer}"))
655    }
656
657    /// Switches to the next [`Buffer`].
658    ///
659    /// This function will only look at buffers that are opened in the
660    /// current window. If you want to include other windows in the
661    /// search, use [`next_global_buffer`].
662    ///
663    /// [`Buffer`]: crate::buffer::Buffer
664    pub fn next_buffer(pa: &mut Pass) -> CmdResult {
665        call(pa, "next-buffer")
666    }
667
668    /// Switches to the previous [`Buffer`].
669    ///
670    /// This function will only look at buffers that are opened in the
671    /// current window. If you want to include other windows in the
672    /// search, use [`prev_global_buffer`].
673    ///
674    /// [`Buffer`]: crate::buffer::Buffer
675    pub fn prev_buffer(pa: &mut Pass) -> CmdResult {
676        call(pa, "prev-buffer")
677    }
678
679    /// Switches to the next [`Buffer`].
680    ///
681    /// This function will look for buffers in all windows. If you
682    /// want to limit the search to just the current window, use
683    /// [`next_buffer`].
684    ///
685    /// [`Buffer`]: crate::buffer::Buffer
686    pub fn next_global_buffer(pa: &mut Pass) -> CmdResult {
687        call(pa, "next-buffer --global")
688    }
689
690    /// Switches to the previous [`Buffer`].
691    ///
692    /// This function will look for buffers in all windows. If you
693    /// want to limit the search to just the current window, use
694    /// [`prev_buffer`].
695    ///
696    /// [`Buffer`]: crate::buffer::Buffer
697    pub fn prev_global_buffer(pa: &mut Pass) -> CmdResult {
698        call(pa, "prev-buffer --global")
699    }
700
701    /// Tries to alias a `caller` to an existing `command`.
702    ///
703    /// Returns an [`Err`] if the `caller` is already a caller for
704    /// another command, or if `command` is not a real caller to an
705    /// existing command.
706    pub fn alias(pa: &mut Pass, alias: impl ToString, command: impl ToString) -> CmdResult {
707        // SAFETY: Function has a Pass argument.
708        unsafe { COMMANDS.get() }.alias(pa, alias, command)
709    }
710
711    /// Runs a full command synchronously, with a [`Pass`].
712    ///
713    /// If you call commands through this function, you will be able
714    /// to retrieve their return values,  but because of the [`Pass`],
715    /// this function can only be used on the main thread of
716    /// execution. If you want to call commands from other threads,
717    /// see [`cmd::queue`].
718    ///
719    /// When running the command, the ordering of [`Flags`] does not
720    /// matter, as long as they are placed before the arguments to the
721    /// command.
722    ///
723    /// # Examples
724    ///
725    /// ```rust
726    /// # duat_core::doc_duat!(duat);
727    /// setup_duat!(setup);
728    /// use duat::prelude::*;
729    ///
730    /// fn main_thread_function(pa: &mut Pass) {
731    ///     cmd::call(pa, "set-prompt new-prompt");
732    /// }
733    /// ```
734    ///
735    /// In this case we're running a command that will affect the most
736    /// relevant [`PromptLine`], and no notifications are sent. That's
737    /// because I used [`call`]. If you want notifications, see
738    /// [`cmd::call_notify`].
739    ///
740    /// [`cmd::queue`]: queue
741    /// [`cmd::call_notify`]: call_notify
742    /// [`PromptLine`]: https://docs.rs/duat/latest/duat/widgets/struct.PromptLine.html
743    /// [`Flags`]: super::Flags
744    pub fn call(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
745        // SAFETY: Function has a Pass argument.
746        unsafe { COMMANDS.get() }.run(pa, call)
747    }
748
749    /// Like [`call`], but notifies the result
750    #[allow(unused_must_use)]
751    pub fn call_notify(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
752        // SAFETY: Function has a Pass argument.
753        let result = unsafe { COMMANDS.get() }.run(pa, call.to_string());
754        context::logs().push_cmd_result(result.clone());
755
756        result
757    }
758
759    /// Queues a command call
760    ///
761    /// You should only use this if you're not in the main thread of
762    /// execution, or if you don't have a [`Pass`] for some other
763    /// reason, like you are in the middle of accessing an [`RwData`]
764    /// or something like that.
765    ///
766    /// Since this function will run outside of the current scope, its
767    /// [`Result`] will not be returned.
768    ///
769    /// [`RwData`]: crate::data::RwData
770    pub fn queue(call: impl std::fmt::Display) {
771        let call = call.to_string();
772        crate::context::sender()
773            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
774                // SAFETY: Closure has Pass argument.
775                let _ = unsafe { COMMANDS.get() }.run(pa, call);
776            })))
777            .unwrap();
778    }
779
780    /// Like [`queue`], but notifies the result
781    pub fn queue_notify(call: impl std::fmt::Display) {
782        let call = call.to_string();
783        crate::context::sender()
784            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
785                context::logs()
786                    .push_cmd_result(unsafe { COMMANDS.get() }.run(pa, call.clone()).clone());
787            })))
788            .unwrap()
789    }
790
791    /// Like [`queue`], but acts on the [`Result`]
792    pub fn queue_and(call: impl std::fmt::Display, map: impl FnOnce(CmdResult) + Send + 'static) {
793        let call = call.to_string();
794        crate::context::sender()
795            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
796                // SAFETY: Function has a Pass argument.
797                map(unsafe { COMMANDS.get() }.run(pa, call));
798            })))
799            .unwrap()
800    }
801
802    /// Like [`queue_and`], but also notifies the [`Result`]
803    pub fn queue_notify_and(
804        call: impl std::fmt::Display,
805        map: impl FnOnce(CmdResult) + Send + 'static,
806    ) {
807        let call = call.to_string();
808        crate::context::sender()
809            .send(DuatEvent::QueuedFunction(Box::new(move |pa| {
810                // SAFETY: Function has a Pass argument.
811                let result = unsafe { COMMANDS.get() }.run(pa, call.clone());
812                context::logs().push_cmd_result(result.clone());
813
814                map(result)
815            })))
816            .unwrap()
817    }
818
819    /// Don't call this function, use [`cmd::add`] instead
820    ///
821    /// [`cmd::add`]: add
822    #[doc(hidden)]
823    pub fn add_inner(callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) {
824        // SAFETY: There is no way to obtain an external RwData of Commands,
825        // so you can modify it from anywhere in the main thread.
826        let mut pa = unsafe { Pass::new() };
827        unsafe { COMMANDS.get() }.add(&mut pa, callers, cmd, check_args)
828    }
829
830    /// Check if the arguments for a given `caller` are correct
831    pub fn check_args(
832        pa: &Pass,
833        caller: &str,
834    ) -> Option<(
835        Vec<(Range<usize>, Option<FormId>)>,
836        Option<(Range<usize>, Text)>,
837    )> {
838        // SAFETY: There is a Pass argument
839        unsafe { COMMANDS.get() }.check_args(pa, caller)
840    }
841}
842
843/// A list of commands.
844///
845/// This list contains all of the commands that have been
846/// added to Duat, as well as info on the current [`Buffer`],
847/// [widget] and all of the [windows].
848///
849/// [`Buffer`]: crate::buffer::Buffer
850/// [widget]: crate::ui::Widget
851/// [windows]: crate::ui::Window
852struct Commands(LazyLock<RwData<InnerCommands>>);
853
854impl Commands {
855    /// Returns a new instance of [`Commands`].
856    const fn new() -> Self {
857        Self(LazyLock::new(|| {
858            RwData::new(InnerCommands {
859                list: Vec::new(),
860                aliases: HashMap::new(),
861            })
862        }))
863    }
864
865    /// Aliases a command to a specific word
866    fn alias(&self, pa: &mut Pass, alias: impl ToString, command: impl ToString) -> CmdResult {
867        self.0
868            .write(pa)
869            .try_alias(alias.to_string(), command.to_string())
870    }
871
872    /// Runs a command from a call
873    fn run(&self, pa: &mut Pass, call: impl Display) -> CmdResult {
874        let call = call.to_string();
875        let mut args = call.split_whitespace();
876        let caller = args.next().ok_or(txt!("The command is empty"))?.to_string();
877
878        let inner = self.0.read(pa);
879
880        let (command, call) = {
881            if let Some(command) = inner.aliases.get(&caller) {
882                let (command, call) = command;
883                let mut call = call.clone() + " ";
884                call.extend(args);
885
886                (command.clone(), call)
887            } else {
888                let command = inner
889                    .list
890                    .iter()
891                    .find(|cmd| cmd.callers().contains(&caller))
892                    .ok_or(txt!("[a]{caller}[]: No such command"))?;
893
894                (command.clone(), call.clone())
895            }
896        };
897
898        let args = get_args(&call);
899
900        if let (_, Some((_, err))) = (command.check_args)(pa, args.clone()) {
901            return Err(err);
902        }
903
904        let silent = call.len() > call.trim_start().len();
905        command.cmd.write(&mut unsafe { Pass::new() })(pa, args).map(|ok| ok.filter(|_| !silent))
906    }
907
908    /// Adds a command to the list of commands
909    fn add(&self, pa: &mut Pass, callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) {
910        let cmd = Command::new(callers, cmd, check_args);
911        self.0.write(pa).add(cmd)
912    }
913
914    /// Gets the parameter checker for a command, if it exists
915    fn check_args(
916        &self,
917        pa: &Pass,
918        call: &str,
919    ) -> Option<(
920        Vec<(Range<usize>, Option<FormId>)>,
921        Option<(Range<usize>, Text)>,
922    )> {
923        let mut args = call.split_whitespace();
924        let caller = args.next()?.to_string();
925
926        let inner = self.0.read(pa);
927        if let Some((command, _)) = inner.aliases.get(&caller) {
928            Some((command.check_args)(pa, get_args(call)))
929        } else {
930            let command = inner
931                .list
932                .iter()
933                .find(|cmd| cmd.callers().contains(&caller))?;
934
935            Some((command.check_args)(pa, get_args(call)))
936        }
937    }
938}
939
940/// The standard error that should be returned when [`call`]ing
941/// commands.
942///
943/// This error _must_ include an error message in case of failure. It
944/// may also include a success message, but that is not required.
945///
946/// [`call`]: global::call
947pub type CmdResult = Result<Option<Text>, Text>;
948
949/// A function that can be called by name.
950#[derive(Clone)]
951struct Command {
952    callers: Arc<[String]>,
953    cmd: CmdFn,
954    check_args: CheckerFn,
955}
956
957impl Command {
958    /// Returns a new instance of command.
959    fn new(callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) -> Self {
960        if let Some(caller) = callers
961            .iter()
962            .find(|caller| caller.split_whitespace().count() != 1)
963        {
964            panic!("Command caller \"{caller}\" contains more than one word");
965        }
966        Self { cmd, check_args, callers: callers.into() }
967    }
968
969    /// The list of callers that will trigger this command.
970    fn callers(&self) -> &[String] {
971        &self.callers
972    }
973}
974
975struct InnerCommands {
976    list: Vec<Command>,
977    aliases: HashMap<String, (Command, String)>,
978}
979
980impl InnerCommands {
981    /// Adds a command to the list
982    ///
983    /// Overrides previous commands with the same name.
984    fn add(&mut self, command: Command) {
985        let mut new_callers = command.callers().iter();
986
987        self.list
988            .retain(|cmd| new_callers.all(|caller| !cmd.callers.contains(caller)));
989
990        self.list.push(command);
991    }
992
993    /// Tries to alias a full command (caller, flags, and
994    /// arguments) to an alias.
995    fn try_alias(&mut self, alias: String, call: String) -> Result<Option<Text>, Text> {
996        if alias.split_whitespace().count() != 1 {
997            return Err(txt!("Alias [a]{alias}[] is not a single word"));
998        }
999
1000        let caller = call
1001            .split_whitespace()
1002            .next()
1003            .ok_or(txt!("The command is empty"))?
1004            .to_string();
1005
1006        let mut cmds = self.list.iter();
1007
1008        if let Some(command) = cmds.find(|cmd| cmd.callers().contains(&caller)) {
1009            let entry = (command.clone(), call.clone());
1010            Ok(Some(match self.aliases.insert(alias.clone(), entry) {
1011                Some((_, prev_call)) => {
1012                    txt!("Aliased [a]{alias}[] from [a]{prev_call}[] to [a]{call}")
1013                }
1014                None => txt!("Aliased [a]{alias}[] to [a]{call}"),
1015            }))
1016        } else {
1017            Err(txt!("The caller [a]{caller}[] was not found"))
1018        }
1019    }
1020}
1021
1022/// A list of names to call a command with
1023pub trait Caller<'a>: Sized {
1024    /// An [`Iterator`] over said callers
1025    fn into_callers(self) -> impl Iterator<Item = &'a str>;
1026}
1027
1028impl<'a> Caller<'a> for &'a str {
1029    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1030        [self].into_iter()
1031    }
1032}
1033
1034impl<'a> Caller<'a> for &'a [&'a str] {
1035    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1036        self.iter().cloned()
1037    }
1038}
1039
1040impl<'a, const N: usize> Caller<'a> for [&'a str; N] {
1041    fn into_callers(self) -> impl Iterator<Item = &'a str> {
1042        self.into_iter()
1043    }
1044}
1045
1046/// Inner function for Commands
1047#[doc(hidden)]
1048pub type CmdFn = RwData<dyn FnMut(&mut Pass, Args) -> CmdResult + Send + 'static>;
1049
1050/// Inner checking function
1051#[doc(hidden)]
1052pub type CheckerFn = fn(
1053    &Pass,
1054    Args,
1055) -> (
1056    Vec<(Range<usize>, Option<FormId>)>,
1057    Option<(Range<usize>, Text)>,
1058);
1059
1060pub(crate) fn as_buffer_handle((.., node): (usize, &Node)) -> Option<Handle> {
1061    node.try_downcast()
1062}