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| c.replace(""));
213 update(pa);
214 }
215
216 event!(Left) => {
217 handle.edit_main(pa, |mut c| c.move_hor(-1));
218 update(pa);
219 }
220 event!(Right) => {
221 handle.edit_main(pa, |mut c| c.move_hor(1));
222 update(pa);
223 }
224 event!(Up) => {
225 let history = HISTORY.lock().unwrap();
226 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
227 return;
228 };
229
230 let index = if let Some(index) = &mut self.history_index {
231 *index = index.saturating_sub(1);
232 *index
233 } else {
234 self.history_index = Some(ty_history.len() - 1);
235 ty_history.len() - 1
236 };
237
238 handle.edit_main(pa, |mut c| {
239 c.move_to(..);
240 c.replace(ty_history[index].clone());
241 c.unset_anchor();
242 });
243
244 update(pa);
245 }
246 event!(Down) => {
247 let history = HISTORY.lock().unwrap();
248 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
249 return;
250 };
251
252 if let Some(index) = &mut self.history_index {
253 if *index + 1 < ty_history.len() {
254 *index = (*index + 1).min(ty_history.len() - 1);
255
256 handle.edit_main(pa, |mut c| {
257 c.move_to(..);
258 c.replace(ty_history[*index].clone());
259 c.unset_anchor();
260 })
261 } else {
262 self.history_index = None;
263 handle.edit_main(pa, |mut c| {
264 c.move_to(..);
265 c.replace("");
266 c.unset_anchor();
267 })
268 }
269 };
270
271 update(pa);
272 }
273
274 event!(Tab) => {
275 Completions::scroll(pa, 1);
276 update(pa);
277 }
278 shift!(BackTab) => {
279 Completions::scroll(pa, -1);
280 update(pa);
281 }
282
283 event!(Esc) => {
284 handle.edit_main(pa, |mut c| {
285 c.move_to(..);
286 c.replace("");
287 });
288 handle.write(pa).text_mut().selections_mut().clear();
289
290 update(pa);
291 reset(pa, self);
292 }
293 event!(Enter) => {
294 handle.write(pa).text_mut().selections_mut().clear();
295
296 if handle.text(pa).is_empty() {
297 let history = HISTORY.lock().unwrap();
298 if let Some((_, ty_history)) = history.iter().find(ty_eq) {
299 handle.edit_main(pa, |mut c| {
300 c.move_to(..);
301 c.replace(ty_history.last().unwrap());
302 });
303 }
304 }
305
306 update(pa);
307 reset(pa, self);
308 }
309 _ => {}
310 }
311
312 self.mode.post_update(pa, &handle);
313 self.show_preview(pa, handle);
314 }
315
316 fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
317 let text = {
318 let pl = handle.write(pa);
319 pl.text = Text::with_default_main_selection();
320 pl.text_mut().replace_range(0..0, &self.starting_text);
321
322 let tag = Ghost::new(match pl.prompt_of_id(self.ty) {
323 Some(text) => txt!("{text}[prompt.colon]:"),
324 None => txt!("{}[prompt.colon]:", self.mode.prompt()),
325 });
326 pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
327
328 std::mem::take(&mut pl.text)
329 };
330
331 let text = self.mode.on_switch(pa, text, handle.area());
332
333 handle.write(pa).text = text;
334
335 self.show_preview(pa, handle);
336 }
337
338 fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
339 let text = std::mem::take(&mut handle.write(pa).text);
340 if !text.is_empty() {
341 let mut history = HISTORY.lock().unwrap();
342 if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == self.ty) {
343 if ty_history.last().is_none_or(|last| last != text.bytes()) {
344 ty_history.push(text.to_string());
345 }
346 } else {
347 history.push((self.ty, vec![text.to_string()]));
348 }
349 }
350
351 self.mode.before_exit(pa, text, handle.area());
352 }
353}
354
355impl Clone for Prompt {
356 fn clone(&self) -> Self {
357 Self {
358 mode: self.clone_fn.lock().unwrap()(),
359 starting_text: self.starting_text.clone(),
360 ty: self.ty,
361 clone_fn: self.clone_fn.clone(),
362 reset_fn: self.reset_fn,
363 history_index: None,
364 }
365 }
366}
367
368#[allow(unused_variables)]
428pub trait PromptMode: Send + 'static {
429 type ExitWidget: Widget
434 where
435 Self: Sized;
436
437 fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
442
443 fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
450 text
451 }
452
453 fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
461
462 fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {}
471
472 fn prompt(&self) -> Text;
475
476 fn return_handle(&self) -> Option<Handle<dyn Widget>> {
480 None
481 }
482}
483
484#[derive(Default, Clone)]
489pub struct RunCommands(Option<Completion>);
490
491impl RunCommands {
492 #[allow(clippy::new_ret_no_self)]
494 pub fn new() -> Prompt {
495 Self::call_once();
496 Prompt::new(Self(None))
497 }
498
499 pub fn new_with(initial: impl ToString) -> Prompt {
501 Self::call_once();
502 Prompt::new_with(Self(None), initial)
503 }
504
505 fn call_once() {
506 static ONCE: Once = Once::new();
507 ONCE.call_once(|| {
508 form::set_weak("caller.info", "accent.info");
509 form::set_weak("caller.error", "accent.error");
510 form::set_weak("param.info", "default.info");
511 form::set_weak("param.error", "default.error");
512 });
513 }
514}
515
516impl PromptMode for RunCommands {
517 type ExitWidget = Buffer;
518
519 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
520 text.remove_tags(*TAGGER, ..);
521
522 let command = text.to_string();
523 let caller = command.split_whitespace().next();
524 if let Some(caller) = caller {
525 if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
526 let id = form::id_of!("caller.info");
527 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
528
529 let default_id = form::id_of!("param.info");
530 for (range, id) in ok_ranges {
531 text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
532 }
533 if let Some((range, _)) = err_range {
534 let id = form::id_of!("param.error");
535 text.insert_tag(*TAGGER, range, id.to_tag(0));
536 }
537 } else {
538 let id = form::id_of!("caller.error");
539 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
540 }
541 }
542
543 text
544 }
545
546 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
547 let call = text.to_string();
548 if !call.is_empty() {
549 _ = cmd::call_notify(pa, call);
550 }
551 }
552
553 fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {
554 let text = handle.text(pa);
555 let Some(main) = text.get_main_sel() else {
556 Completions::close(pa);
557 return;
558 };
559
560 let is_parameter = text
561 .chars_rev(..main.caret())
562 .unwrap()
563 .any(|(_, char)| char.is_whitespace());
564
565 let new_completion = if is_parameter {
566 let call = &text.strs(..main.caret()).unwrap().to_string();
567 let Some(parameters) = cmd::last_parsed_parameters(pa, call) else {
568 self.0 = None;
569 Completions::close(pa);
570 return;
571 };
572
573 Completion::Parameters(parameters)
574 } else {
575 Completion::Caller
576 };
577
578 if self.0.as_ref() != Some(&new_completion) {
579 match &new_completion {
580 Completion::Caller => Completions::builder()
581 .with_provider(CommandsCompletions::new(pa))
582 .open(pa),
583 Completion::Parameters(params) => Completions::open_for(pa, params),
584 }
585 }
586
587 self.0 = Some(new_completion)
588 }
589
590 fn prompt(&self) -> Text {
591 Text::default()
592 }
593}
594
595#[derive(Clone, Copy)]
602pub struct PipeSelections;
603
604impl PipeSelections {
605 #[allow(clippy::new_ret_no_self)]
608 pub fn new() -> Prompt {
609 Prompt::new(Self)
610 }
611}
612
613impl PromptMode for PipeSelections {
614 type ExitWidget = Buffer;
615
616 fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
617 fn is_in_path(program: &str) -> bool {
618 if let Ok(path) = std::env::var("PATH") {
619 for p in path.split(":") {
620 let p_str = format!("{p}/{program}");
621 if let Ok(true) = std::fs::exists(p_str) {
622 return true;
623 }
624 }
625 }
626 false
627 }
628
629 text.remove_tags(*TAGGER, ..);
630
631 let command = text.to_string();
632 let Some(caller) = command.split_whitespace().next() else {
633 return text;
634 };
635
636 let args = cmd::ArgsIter::new(&command);
637
638 let (caller_id, args_id) = if is_in_path(caller) {
639 (form::id_of!("caller.info"), form::id_of!("param.info"))
640 } else {
641 (form::id_of!("caller.error"), form::id_of!("param.error"))
642 };
643
644 let c_s = command.len() - command.trim_start().len();
645 text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
646
647 for (_, range, _) in args {
648 text.insert_tag(*TAGGER, range, args_id.to_tag(0));
649 }
650
651 text
652 }
653
654 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
655 use std::process::{Command, Stdio};
656
657 let command = text.to_string();
658 let Some(caller) = command.split_whitespace().next() else {
659 return;
660 };
661
662 let handle = context::current_buffer(pa);
663 handle.edit_all(pa, |mut c| {
664 let Ok(mut child) = Command::new(caller)
665 .args(cmd::ArgsIter::new(&command).map(|(a, ..)| a))
666 .stdin(Stdio::piped())
667 .stdout(Stdio::piped())
668 .spawn()
669 else {
670 return;
671 };
672
673 let input = c.selection().to_string();
674 if let Some(mut stdin) = child.stdin.take() {
675 std::thread::spawn(move || {
676 stdin.write_all(input.as_bytes()).unwrap();
677 });
678 }
679 if let Ok(out) = child.wait_with_output() {
680 let out = String::from_utf8_lossy(&out.stdout);
681 c.set_anchor_if_needed();
682 c.replace(out);
683 }
684 });
685 }
686
687 fn prompt(&self) -> Text {
688 txt!("[prompt]pipe")
689 }
690}
691
692type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;
693
694#[derive(Clone, Eq)]
695enum Completion {
696 Caller,
697 Parameters(Vec<TypeId>),
698}
699
700impl PartialEq for Completion {
701 fn eq(&self, other: &Self) -> bool {
702 match (self, other) {
703 (Self::Parameters(l0), Self::Parameters(r0)) => {
704 l0.iter().all(|param| r0.contains(param))
705 && r0.iter().all(|param| l0.contains(param))
706 }
707 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
708 }
709 }
710}