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