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