1use std::{
28 any::TypeId,
29 io::Write,
30 sync::{Arc, LazyLock, Mutex, Once},
31};
32
33use duat_core::{
34 buffer::Buffer,
35 cmd,
36 context::{self, Handle},
37 data::Pass,
38 form,
39 mode::{self, KeyEvent, event, shift},
40 text::{Ghost, Tagger, Text, txt},
41 ui::{RwArea, Widget},
42};
43
44use crate::widgets::{CommandsCompletions, Completions, PromptLine};
45
46static HISTORY: Mutex<Vec<(TypeId, Vec<String>)>> = Mutex::new(Vec::new());
47static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
48static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
49static PREVIEW_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
50
51pub struct Prompt {
77 mode: Box<dyn PromptMode>,
78 starting_text: String,
79 ty: TypeId,
80 clone_fn: Arc<Mutex<ModeCloneFn>>,
81 reset_fn: fn(pa: &mut Pass),
82 history_index: Option<usize>,
83}
84
85impl Prompt {
86 pub fn new<M: PromptMode + Clone>(mode: M) -> Self {
92 let clone_fn = Arc::new(Mutex::new({
93 let mode = mode.clone();
94 move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
95 }));
96
97 Self {
98 mode: Box::new(mode),
99 starting_text: String::new(),
100 ty: TypeId::of::<M>(),
101 clone_fn,
102 reset_fn: |pa| _ = mode::reset::<M::ExitWidget>(pa),
103 history_index: None,
104 }
105 }
106
107 pub fn new_with<M: PromptMode + Clone>(mode: M, initial: impl ToString) -> Self {
114 let clone_fn = Arc::new(Mutex::new({
115 let mode = mode.clone();
116 move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
117 }));
118
119 Self {
120 mode: Box::new(mode),
121 starting_text: initial.to_string(),
122 ty: TypeId::of::<M>(),
123 clone_fn,
124 reset_fn: |pa| _ = mode::reset::<M::ExitWidget>(pa),
125 history_index: None,
126 }
127 }
128
129 fn show_preview(&mut self, pa: &mut Pass, handle: Handle<PromptLine>) {
131 let history = HISTORY.lock().unwrap();
132 if handle.text(pa).is_empty()
133 && let Some((_, ty_history)) = history.iter().find(|(ty, _)| *ty == self.ty)
134 {
135 handle.text_mut(pa).insert_tag_after(
136 *PREVIEW_TAGGER,
137 0,
138 Ghost::new(txt!("[prompt.preview]{}", ty_history.last().unwrap())),
139 );
140 }
141 }
142}
143
144impl mode::Mode for Prompt {
145 type Widget = PromptLine;
146
147 fn bindings() -> mode::Bindings {
148 use mode::KeyCode::*;
149
150 mode::bindings!(match _ {
151 event!(Char(..)) => txt!("Insert the character"),
152 event!(Left | Right) => txt!("Move cursor"),
153 event!(Down | Up) => txt!("Move through command history"),
154 event!(Backspace | Delete) => txt!("Remove character or selection"),
155 event!(Enter) => txt!("Run command and [mode]leave"),
156 event!(Esc) => txt!("[mode]Leave[] without running command"),
157 })
158 }
159
160 fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
161 use duat_core::mode::KeyCode::*;
162
163 let ty_eq = |&&(ty, _): &&(TypeId, _)| ty == self.ty;
164
165 let mut update = |pa: &mut Pass| {
166 let text = std::mem::take(&mut handle.write(pa).text);
167 let text = self.mode.update(pa, text, handle.area());
168 handle.write(pa).text = text;
169 };
170
171 let reset = |pa: &mut Pass, prompt: &mut Self| {
172 if let Some(ret_handle) = prompt.mode.return_handle() {
173 mode::reset_to(pa, &ret_handle);
174 } else {
175 (prompt.reset_fn)(pa);
176 }
177 };
178
179 handle.text_mut(pa).remove_tags(*PREVIEW_TAGGER, ..);
180
181 match key {
182 event!(Char(char)) => {
183 handle.edit_main(pa, |mut c| {
184 c.insert(char);
185 c.move_hor(1);
186 });
187 update(pa);
188 }
189
190 event!(Backspace) => {
191 if handle.read(pa).text().is_empty() {
192 handle.write(pa).text_mut().selections_mut().clear();
193
194 update(pa);
195
196 if let Some(ret_handle) = self.mode.return_handle() {
197 mode::reset_to(pa, &ret_handle);
198 } else {
199 (self.reset_fn)(pa);
200 }
201 } else {
202 handle.edit_main(pa, |mut c| {
203 c.move_hor(-1);
204 c.set_anchor_if_needed();
205 c.replace("");
206 c.unset_anchor();
207 });
208 update(pa);
209 }
210 }
211 event!(Delete) => {
212 handle.edit_main(pa, |mut c| {
213 c.set_anchor_if_needed();
214 c.replace("");
215 });
216 update(pa);
217 }
218
219 event!(Left) => {
220 handle.edit_main(pa, |mut c| c.move_hor(-1));
221 update(pa);
222 }
223 event!(Right) => {
224 handle.edit_main(pa, |mut c| c.move_hor(1));
225 update(pa);
226 }
227 event!(Up) => {
228 let history = HISTORY.lock().unwrap();
229 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
230 return;
231 };
232
233 let index = if let Some(index) = &mut self.history_index {
234 *index = index.saturating_sub(1);
235 *index
236 } else {
237 self.history_index = Some(ty_history.len() - 1);
238 ty_history.len() - 1
239 };
240
241 handle.edit_main(pa, |mut c| {
242 c.move_to(..);
243 c.replace(ty_history[index].clone());
244 c.unset_anchor();
245 });
246
247 update(pa);
248 }
249 event!(Down) => {
250 let history = HISTORY.lock().unwrap();
251 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
252 return;
253 };
254
255 if let Some(index) = &mut self.history_index {
256 if *index + 1 < ty_history.len() {
257 *index = (*index + 1).min(ty_history.len() - 1);
258
259 handle.edit_main(pa, |mut c| {
260 c.move_to(..);
261 c.replace(ty_history[*index].clone());
262 c.unset_anchor();
263 })
264 } else {
265 self.history_index = None;
266 handle.edit_main(pa, |mut c| {
267 c.move_to(..);
268 c.replace("");
269 c.unset_anchor();
270 })
271 }
272 };
273
274 update(pa);
275 }
276
277 event!(Tab) => {
278 Completions::scroll(pa, 1);
279 update(pa);
280 }
281 shift!(BackTab) => {
282 Completions::scroll(pa, -1);
283 update(pa);
284 }
285
286 event!(Esc) => {
287 handle.edit_main(pa, |mut c| {
288 c.move_to(..);
289 c.replace("");
290 });
291 handle.write(pa).text_mut().selections_mut().clear();
292
293 update(pa);
294 reset(pa, self);
295 }
296 event!(Enter) => {
297 handle.write(pa).text_mut().selections_mut().clear();
298
299 if handle.text(pa).is_empty() {
300 let history = HISTORY.lock().unwrap();
301 if let Some((_, ty_history)) = history.iter().find(ty_eq) {
302 handle.edit_main(pa, |mut c| {
303 c.move_to(..);
304 c.replace(ty_history.last().unwrap());
305 });
306 }
307 }
308
309 update(pa);
310 reset(pa, self);
311 }
312 _ => {}
313 }
314
315 self.mode.post_update(pa, &handle);
316 self.show_preview(pa, handle);
317 }
318
319 fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
320 let text = {
321 let pl = handle.write(pa);
322 pl.text = Text::with_default_main_selection();
323 pl.text_mut().replace_range(0..0, &self.starting_text);
324
325 let tag = Ghost::new(match pl.prompt_of_id(self.ty) {
326 Some(text) => txt!("{text}[prompt.colon]:"),
327 None => txt!("{}[prompt.colon]:", self.mode.prompt()),
328 });
329 pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
330
331 std::mem::take(&mut pl.text)
332 };
333
334 let text = self.mode.on_switch(pa, text, handle.area());
335
336 handle.write(pa).text = text;
337
338 self.show_preview(pa, handle);
339 }
340
341 fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
342 let text = std::mem::take(&mut handle.write(pa).text);
343 if !text.is_empty() {
344 let mut history = HISTORY.lock().unwrap();
345 if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == self.ty) {
346 if ty_history.last().is_none_or(|last| last != text.bytes()) {
347 ty_history.push(text.to_string());
348 }
349 } else {
350 history.push((self.ty, vec![text.to_string()]));
351 }
352 }
353
354 self.mode.before_exit(pa, text, handle.area());
355 }
356}
357
358impl Clone for Prompt {
359 fn clone(&self) -> Self {
360 Self {
361 mode: self.clone_fn.lock().unwrap()(),
362 starting_text: self.starting_text.clone(),
363 ty: self.ty,
364 clone_fn: self.clone_fn.clone(),
365 reset_fn: self.reset_fn,
366 history_index: None,
367 }
368 }
369}
370
371#[allow(unused_variables)]
431pub trait PromptMode: Send + 'static {
432 type ExitWidget: Widget
437 where
438 Self: Sized;
439
440 fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
445
446 fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
453 text
454 }
455
456 fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
464
465 fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {}
474
475 fn prompt(&self) -> Text;
478
479 fn return_handle(&self) -> Option<Handle<dyn Widget>> {
483 None
484 }
485}
486
487#[derive(Default, Clone)]
492pub struct RunCommands(Option<Completion>);
493
494impl RunCommands {
495 #[allow(clippy::new_ret_no_self)]
497 pub fn new() -> Prompt {
498 Self::call_once();
499 Prompt::new(Self(None))
500 }
501
502 pub fn new_with(initial: impl ToString) -> Prompt {
504 Self::call_once();
505 Prompt::new_with(Self(None), initial)
506 }
507
508 fn call_once() {
509 static ONCE: Once = Once::new();
510 ONCE.call_once(|| {
511 form::set_weak("caller.info", "accent.info");
512 form::set_weak("caller.error", "accent.error");
513 form::set_weak("param.info", "default.info");
514 form::set_weak("param.error", "default.error");
515 });
516 }
517}
518
519impl PromptMode for RunCommands {
520 type ExitWidget = Buffer;
521
522 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
523 text.remove_tags(*TAGGER, ..);
524
525 let command = text.to_string();
526 let caller = command.split_whitespace().next();
527 if let Some(caller) = caller {
528 if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
529 let id = form::id_of!("caller.info");
530 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
531
532 let default_id = form::id_of!("param.info");
533 for (range, id) in ok_ranges {
534 text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
535 }
536 if let Some((range, _)) = err_range {
537 let id = form::id_of!("param.error");
538 text.insert_tag(*TAGGER, range, id.to_tag(0));
539 }
540 } else {
541 let id = form::id_of!("caller.error");
542 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
543 }
544 }
545
546 text
547 }
548
549 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
550 let call = text.to_string();
551 if !call.is_empty() {
552 _ = cmd::call_notify(pa, call);
553 }
554 }
555
556 fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {
557 let text = handle.text(pa);
558 let Some(main) = text.get_main_sel() else {
559 Completions::close(pa);
560 return;
561 };
562
563 let is_parameter = text
564 .chars_rev(..main.caret())
565 .unwrap()
566 .any(|(_, char)| char.is_whitespace());
567
568 let new_completion = if is_parameter {
569 let call = &text.strs(..main.caret()).unwrap().to_string();
570 let Some(parameters) = cmd::last_parsed_parameters(pa, call) else {
571 self.0 = None;
572 Completions::close(pa);
573 return;
574 };
575
576 Completion::Parameters(parameters)
577 } else {
578 Completion::Caller
579 };
580
581 if self.0.as_ref() != Some(&new_completion) {
582 match &new_completion {
583 Completion::Caller => Completions::builder()
584 .with_provider(CommandsCompletions::new(pa))
585 .open(pa),
586 Completion::Parameters(params) => Completions::open_for(pa, params),
587 }
588 }
589
590 self.0 = Some(new_completion)
591 }
592
593 fn prompt(&self) -> Text {
594 Text::default()
595 }
596}
597
598#[derive(Clone, Copy)]
605pub struct PipeSelections;
606
607impl PipeSelections {
608 #[allow(clippy::new_ret_no_self)]
611 pub fn new() -> Prompt {
612 Prompt::new(Self)
613 }
614}
615
616impl PromptMode for PipeSelections {
617 type ExitWidget = Buffer;
618
619 fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
620 fn is_in_path(program: &str) -> bool {
621 if let Ok(path) = std::env::var("PATH") {
622 for p in path.split(":") {
623 let p_str = format!("{p}/{program}");
624 if let Ok(true) = std::fs::exists(p_str) {
625 return true;
626 }
627 }
628 }
629 false
630 }
631
632 text.remove_tags(*TAGGER, ..);
633
634 let command = text.to_string();
635 let Some(caller) = command.split_whitespace().next() else {
636 return text;
637 };
638
639 let args = cmd::ArgsIter::new(&command);
640
641 let (caller_id, args_id) = if is_in_path(caller) {
642 (form::id_of!("caller.info"), form::id_of!("param.info"))
643 } else {
644 (form::id_of!("caller.error"), form::id_of!("param.error"))
645 };
646
647 let c_s = command.len() - command.trim_start().len();
648 text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
649
650 for (_, range, _) in args {
651 text.insert_tag(*TAGGER, range, args_id.to_tag(0));
652 }
653
654 text
655 }
656
657 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
658 use std::process::{Command, Stdio};
659
660 let command = text.to_string();
661 let Some(caller) = command.split_whitespace().next() else {
662 return;
663 };
664
665 let handle = context::current_buffer(pa);
666 handle.edit_all(pa, |mut c| {
667 let Ok(mut child) = Command::new(caller)
668 .args(cmd::ArgsIter::new(&command).map(|(a, ..)| a))
669 .stdin(Stdio::piped())
670 .stdout(Stdio::piped())
671 .spawn()
672 else {
673 return;
674 };
675
676 let input = c.selection().to_string();
677 if let Some(mut stdin) = child.stdin.take() {
678 std::thread::spawn(move || {
679 stdin.write_all(input.as_bytes()).unwrap();
680 });
681 }
682 if let Ok(out) = child.wait_with_output() {
683 let out = String::from_utf8_lossy(&out.stdout);
684 c.set_anchor_if_needed();
685 c.replace(out);
686 }
687 });
688 }
689
690 fn prompt(&self) -> Text {
691 txt!("[prompt]pipe")
692 }
693}
694
695type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;
696
697#[derive(Clone, Eq)]
698enum Completion {
699 Caller,
700 Parameters(Vec<TypeId>),
701}
702
703impl PartialEq for Completion {
704 fn eq(&self, other: &Self) -> bool {
705 match (self, other) {
706 (Self::Parameters(l0), Self::Parameters(r0)) => {
707 l0.iter().all(|param| r0.contains(param))
708 && r0.iter().all(|param| l0.contains(param))
709 }
710 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
711 }
712 }
713}