1use std::{io::Write, marker::PhantomData, sync::LazyLock};
2
3use duat_core::{prelude::*, text::Searcher};
4
5use super::IncSearcher;
6use crate::{
7 hooks::{SearchPerformed, SearchUpdated},
8 widgets::PromptLine,
9};
10
11static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
12static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
13
14#[derive(Clone)]
37pub struct Prompt<M: PromptMode<U>, U: Ui>(M, PhantomData<U>);
38
39impl<M: PromptMode<U>, U: Ui> Prompt<M, U> {
40 pub fn new(mode: M) -> Self {
46 Self(mode, PhantomData)
47 }
48}
49
50impl<M: PromptMode<U>, U: Ui> mode::Mode<U> for Prompt<M, U> {
51 type Widget = PromptLine<U>;
52
53 fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget, U>) {
54 match key {
55 key!(KeyCode::Backspace) => {
56 if handle.read(pa, |pl, _| pl.text().is_empty()) {
57 handle.write_selections(pa, |c| c.clear());
58
59 let text = handle.take_text(pa);
60 let text = self.0.update(pa, text, handle.area());
61 handle.replace_text(pa, text);
62
63 if let Some(ret_handle) = self.0.return_handle() {
64 mode::reset_to(ret_handle);
65 } else {
66 mode::reset::<M::ExitWidget, U>();
67 }
68 } else {
69 handle.edit_main(pa, |mut e| {
70 e.move_hor(-1);
71 e.replace("");
72 });
73 let text = handle.take_text(pa);
74 let text = self.0.update(pa, text, handle.area());
75 handle.replace_text(pa, text);
76 }
77 }
78 key!(KeyCode::Delete) => {
79 handle.edit_main(pa, |mut e| e.replace(""));
80 let text = handle.take_text(pa);
81 let text = self.0.update(pa, text, handle.area());
82 handle.replace_text(pa, text);
83 }
84
85 key!(KeyCode::Char(char)) => {
86 handle.edit_main(pa, |mut e| {
87 e.insert(char);
88 e.move_hor(1);
89 });
90 let text = handle.take_text(pa);
91 let text = self.0.update(pa, text, handle.area());
92 handle.replace_text(pa, text);
93 }
94 key!(KeyCode::Left) => {
95 handle.edit_main(pa, |mut e| e.move_hor(-1));
96 let text = handle.take_text(pa);
97 let text = self.0.update(pa, text, handle.area());
98 handle.replace_text(pa, text);
99 }
100 key!(KeyCode::Right) => {
101 handle.edit_main(pa, |mut e| e.move_hor(1));
102 let text = handle.take_text(pa);
103 let text = self.0.update(pa, text, handle.area());
104 handle.replace_text(pa, text);
105 }
106
107 key!(KeyCode::Esc) => {
108 let p = handle.read(pa, |wid, _| wid.text().len());
109 handle.edit_main(pa, |mut e| {
110 e.move_to_start();
111 e.set_anchor();
112 e.move_to(p);
113 e.replace("");
114 });
115 handle.write_selections(pa, |c| c.clear());
116 let text = handle.take_text(pa);
117 let text = self.0.update(pa, text, handle.area());
118 handle.replace_text(pa, text);
119
120 if let Some(ret_handle) = self.0.return_handle() {
121 mode::reset_to(ret_handle);
122 } else {
123 mode::reset::<M::ExitWidget, U>();
124 }
125 }
126 key!(KeyCode::Enter) => {
127 handle.write_selections(pa, |c| c.clear());
128 let text = handle.take_text(pa);
129 let text = self.0.update(pa, text, handle.area());
130 handle.replace_text(pa, text);
131
132 if let Some(ret_handle) = self.0.return_handle() {
133 mode::reset_to(ret_handle);
134 } else {
135 mode::reset::<M::ExitWidget, U>();
136 }
137 }
138 _ => {}
139 }
140 }
141
142 fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
143 let text = handle.write(pa, |wid, _| {
144 *wid.text_mut() = Text::new_with_selections();
145 run_once::<M, U>();
146
147 let tag = Ghost(match wid.prompt_of::<M>() {
148 Some(text) => text,
149 None => self.0.prompt(),
150 });
151 wid.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
152
153 std::mem::take(wid.text_mut())
154 });
155
156 let text = self.0.on_switch(pa, text, handle.area());
157
158 handle.widget().replace_text(pa, text);
159 }
160
161 fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
162 let text = handle.take_text(pa);
163 self.0.before_exit(pa, text, handle.area());
164 }
165}
166
167#[allow(unused_variables)]
227pub trait PromptMode<U: Ui>: Clone + 'static {
228 type ExitWidget: Widget<U> = File<U>;
231
232 fn update(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text;
237
238 fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text {
245 text
246 }
247
248 fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &U::Area) {}
254
255 fn once() {}
257
258 fn prompt(&self) -> Text;
261
262 fn return_handle(&self) -> Option<Handle<Self::ExitWidget, U>> {
266 None
267 }
268}
269
270#[derive(Default, Clone)]
275pub struct RunCommands;
276
277impl RunCommands {
278 pub fn new<U: Ui>() -> Prompt<Self, U> {
280 Prompt::new(Self)
281 }
282}
283
284impl<U: Ui> PromptMode<U> for RunCommands {
285 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
286 text.remove_tags(*TAGGER, ..);
287
288 let command = text.to_string();
289 let caller = command.split_whitespace().next();
290 if let Some(caller) = caller {
291 if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
292 let id = form::id_of!("caller.info");
293 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
294
295 let id = form::id_of!("parameter.info");
296 for range in ok_ranges {
297 text.insert_tag(*TAGGER, range, id.to_tag(0));
298 }
299 if let Some((range, _)) = err_range {
300 let id = form::id_of!("parameter.error");
301 text.insert_tag(*TAGGER, range, id.to_tag(0));
302 }
303 } else {
304 let id = form::id_of!("caller.error");
305 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
306 }
307 }
308
309 text
310 }
311
312 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
313 let call = text.to_string();
314 if !call.is_empty() {
315 cmd::queue_notify(call);
316 }
317 }
318
319 fn once() {
320 form::set_weak("caller.info", "accent.info");
321 form::set_weak("caller.error", "accent.error");
322 form::set_weak("parameter.info", "default.info");
323 form::set_weak("parameter.error", "default.error");
324 }
325
326 fn prompt(&self) -> Text {
327 txt!("[prompt.colon]:").build()
328 }
329}
330
331#[derive(Clone)]
347pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
348 inc: I,
349 orig: Option<(mode::Selections, <U::Area as RawArea>::PrintInfo)>,
350 ghost: PhantomData<U>,
351 prev: String,
352}
353
354impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
355 pub fn new(inc: I) -> Prompt<Self, U> {
358 Prompt::new(Self {
359 inc,
360 orig: None,
361 ghost: PhantomData,
362 prev: String::new(),
363 })
364 }
365}
366
367impl<I: IncSearcher<U>, U: Ui> PromptMode<U> for IncSearch<I, U> {
368 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
369 let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
370 text.remove_tags(*TAGGER, ..);
371
372 let handle = context::fixed_file::<U>(pa).unwrap().handle(pa);
373
374 if text == self.prev {
375 return text;
376 } else {
377 let prev = std::mem::replace(&mut self.prev, text.to_string());
378 hook::queue(SearchUpdated((prev, self.prev.clone())));
379 }
380
381 match Searcher::new(text.to_string()) {
382 Ok(searcher) => {
383 handle.write(pa, |file, area| {
384 area.set_print_info(orig_print_info.clone());
385 *file.selections_mut().unwrap() = orig_selections.clone();
386 });
387
388 let ast = regex_syntax::ast::parse::Parser::new()
389 .parse(&text.to_string())
390 .unwrap();
391
392 crate::tag_from_ast(*TAGGER, &mut text, &ast);
393
394 self.inc.search(pa, handle.attach_searcher(searcher));
395 }
396 Err(err) => {
397 let regex_syntax::Error::Parse(err) = *err else {
398 unreachable!("As far as I can tell, regex_syntax has goofed up");
399 };
400
401 let span = err.span();
402 let id = form::id_of!("regex.error");
403
404 text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
405 }
406 }
407
408 text
409 }
410
411 fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) -> Text {
412 let handle = context::fixed_file::<U>(pa).unwrap();
413 handle.read(pa, |file, area| {
414 self.orig = Some((file.selections().clone(), area.print_info()));
415 });
416
417 text
418 }
419
420 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
421 if !text.is_empty() {
422 if let Err(err) = Searcher::new(text.to_string()) {
423 let regex_syntax::Error::Parse(err) = *err else {
424 unreachable!("As far as I can tell, regex_syntax has goofed up");
425 };
426
427 let range = err.span().start.offset..err.span().end.offset;
428 let err = txt!(
429 "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
430 range,
431 text.strs(range),
432 err.kind()
433 );
434
435 context::error!(target: self.inc.prompt().to_string(), "{err}")
436 } else {
437 hook::queue(SearchPerformed(text.to_string()));
438 }
439 }
440 }
441
442 fn once() {
443 form::set_weak("regex.error", "accent.error");
444 form::set_weak("regex.operator", "operator");
445 form::set_weak("regex.class", "constant");
446 form::set_weak("regex.bracket", "punctuation.bracket");
447 }
448
449 fn prompt(&self) -> Text {
450 txt!("{}[prompt.colon]:", self.inc.prompt()).build()
451 }
452}
453
454#[derive(Clone, Copy)]
461pub struct PipeSelections<U>(PhantomData<U>);
462
463impl<U: Ui> PipeSelections<U> {
464 pub fn new() -> Prompt<Self, U> {
467 Prompt::new(Self(PhantomData))
468 }
469}
470
471impl<U: Ui> PromptMode<U> for PipeSelections<U> {
472 fn update(&mut self, _: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
473 fn is_in_path(program: &str) -> bool {
474 if let Ok(path) = std::env::var("PATH") {
475 for p in path.split(":") {
476 let p_str = format!("{p}/{program}");
477 if let Ok(true) = std::fs::exists(p_str) {
478 return true;
479 }
480 }
481 }
482 false
483 }
484
485 text.remove_tags(*TAGGER, ..);
486
487 let command = text.to_string();
488 let Some(caller) = command.split_whitespace().next() else {
489 return text;
490 };
491
492 let args = cmd::args_iter(&command);
493
494 let (caller_id, args_id) = if is_in_path(caller) {
495 (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
496 } else {
497 (
498 form::id_of!("caller.error"),
499 form::id_of!("parameter.error"),
500 )
501 };
502
503 let c_s = command.len() - command.trim_start().len();
504 text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
505
506 for (_, range) in args {
507 text.insert_tag(*TAGGER, range, args_id.to_tag(0));
508 }
509
510 text
511 }
512
513 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) {
514 use std::process::{Command, Stdio};
515
516 let command = text.to_string();
517 let Some(caller) = command.split_whitespace().next() else {
518 return;
519 };
520
521 let handle = context::fixed_file::<U>(pa).unwrap().handle(pa);
522 handle.edit_all(pa, |mut e| {
523 let Ok(mut child) = Command::new(caller)
524 .args(cmd::args_iter(&command).map(|(a, _)| a))
525 .stdin(Stdio::piped())
526 .stdout(Stdio::piped())
527 .spawn()
528 else {
529 return;
530 };
531
532 let input: String = e.selection().collect();
533 if let Some(mut stdin) = child.stdin.take() {
534 std::thread::spawn(move || {
535 stdin.write_all(input.as_bytes()).unwrap();
536 });
537 }
538 if let Ok(out) = child.wait_with_output() {
539 let out = String::from_utf8_lossy(&out.stdout);
540 e.replace(out);
541 }
542 });
543 }
544
545 fn prompt(&self) -> Text {
546 txt!("[prompt]pipe").build()
547 }
548}
549
550fn run_once<M: PromptMode<U>, U: Ui>() {
554 use std::{any::TypeId, sync::Mutex};
555
556 static LIST: LazyLock<Mutex<Vec<TypeId>>> = LazyLock::new(|| Mutex::new(Vec::new()));
557
558 let mut list = LIST.lock().unwrap();
559 if !list.contains(&TypeId::of::<M>()) {
560 M::once();
561 list.push(TypeId::of::<M>());
562 }
563}