1use 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
186pub(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 #[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 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 let cmd: CmdFn = unsafe { RwData::new_unsized::<()>(Arc::new(UnsafeCell::new(cmd))) };
618
619 add_inner(callers, cmd, check_args)
620 }}
621
622 pub fn quit() {
630 queue("quit");
631 }
632
633 pub fn edit(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
642 call(pa, format!("edit {buffer}"))
643 }
644
645 pub fn buffer(pa: &mut Pass, buffer: impl std::fmt::Display) -> CmdResult {
654 call(pa, format!("buffer {buffer}"))
655 }
656
657 pub fn next_buffer(pa: &mut Pass) -> CmdResult {
665 call(pa, "next-buffer")
666 }
667
668 pub fn prev_buffer(pa: &mut Pass) -> CmdResult {
676 call(pa, "prev-buffer")
677 }
678
679 pub fn next_global_buffer(pa: &mut Pass) -> CmdResult {
687 call(pa, "next-buffer --global")
688 }
689
690 pub fn prev_global_buffer(pa: &mut Pass) -> CmdResult {
698 call(pa, "prev-buffer --global")
699 }
700
701 pub fn alias(pa: &mut Pass, alias: impl ToString, command: impl ToString) -> CmdResult {
707 unsafe { COMMANDS.get() }.alias(pa, alias, command)
709 }
710
711 pub fn call(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
745 unsafe { COMMANDS.get() }.run(pa, call)
747 }
748
749 #[allow(unused_must_use)]
751 pub fn call_notify(pa: &mut Pass, call: impl std::fmt::Display) -> CmdResult {
752 let result = unsafe { COMMANDS.get() }.run(pa, call.to_string());
754 context::logs().push_cmd_result(result.clone());
755
756 result
757 }
758
759 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 let _ = unsafe { COMMANDS.get() }.run(pa, call);
776 })))
777 .unwrap();
778 }
779
780 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 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 map(unsafe { COMMANDS.get() }.run(pa, call));
798 })))
799 .unwrap()
800 }
801
802 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 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 #[doc(hidden)]
823 pub fn add_inner(callers: Vec<String>, cmd: CmdFn, check_args: CheckerFn) {
824 let mut pa = unsafe { Pass::new() };
827 unsafe { COMMANDS.get() }.add(&mut pa, callers, cmd, check_args)
828 }
829
830 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 unsafe { COMMANDS.get() }.check_args(pa, caller)
840 }
841}
842
843struct Commands(LazyLock<RwData<InnerCommands>>);
853
854impl Commands {
855 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 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 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 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 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
940pub type CmdResult = Result<Option<Text>, Text>;
948
949#[derive(Clone)]
951struct Command {
952 callers: Arc<[String]>,
953 cmd: CmdFn,
954 check_args: CheckerFn,
955}
956
957impl Command {
958 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 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 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 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
1022pub trait Caller<'a>: Sized {
1024 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#[doc(hidden)]
1048pub type CmdFn = RwData<dyn FnMut(&mut Pass, Args) -> CmdResult + Send + 'static>;
1049
1050#[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}