1use std::{io::Write, marker::PhantomData, sync::LazyLock};
2
3use lender::Lender;
4
5use super::{Cursors, EditHelper, IncSearcher, KeyCode, KeyEvent, Mode, key};
6use crate::{
7 cmd, context, form,
8 hooks::{self, SearchPerformed, SearchUpdated},
9 text::{Key, Point, Searcher, Tag, Text, text},
10 ui::{Area, Ui},
11 widgets::{PromptLine, Widget},
12};
13
14static PROMPT_KEY: LazyLock<Key> = LazyLock::new(Key::new);
15static KEY: LazyLock<Key> = LazyLock::new(Key::new);
16
17#[derive(Clone)]
18pub struct Prompt<M: PromptMode<U>, U: Ui>(M, PhantomData<U>);
19
20impl<M: PromptMode<U>, U: Ui> Prompt<M, U> {
21 fn new(mode: M) -> Self {
22 Self(mode, PhantomData)
23 }
24}
25
26impl<M: PromptMode<U>, U: Ui> Mode<U> for Prompt<M, U> {
27 type Widget = PromptLine<U>;
28
29 fn send_key(&mut self, key: KeyEvent, widget: &mut Self::Widget, area: &U::Area) {
30 let mut helper = EditHelper::new(widget, area);
31
32 match key {
33 key!(KeyCode::Backspace) => {
34 if helper.text().is_empty() {
35 helper.cursors_mut().clear();
36 self.0.update(helper.text_mut(), area);
37 self.0.before_exit(helper.text_mut(), area);
38 super::reset();
39 } else {
40 let mut e = helper.edit_main();
41 e.move_hor(-1);
42 e.replace("");
43 self.0.update(helper.text_mut(), area);
44 }
45 }
46 key!(KeyCode::Delete) => {
47 helper.edit_main().replace("");
48 self.0.update(helper.text_mut(), area);
49 }
50
51 key!(KeyCode::Char(char)) => {
52 let mut e = helper.edit_main();
53 e.insert(char);
54 e.move_hor(1);
55 self.0.update(helper.text_mut(), area);
56 }
57 key!(KeyCode::Left) => {
58 helper.edit_main().move_hor(-1);
59 self.0.update(helper.text_mut(), area);
60 }
61 key!(KeyCode::Right) => {
62 helper.edit_main().move_hor(1);
63 self.0.update(helper.text_mut(), area);
64 }
65
66 key!(KeyCode::Esc) => {
67 let p = helper.text().len();
68 let mut e = helper.edit_main();
69 e.move_to(Point::default());
70 e.set_anchor();
71 e.move_to(p);
72 e.replace("");
73 helper.cursors_mut().clear();
74 self.0.update(helper.text_mut(), area);
75 self.0.before_exit(helper.text_mut(), area);
76 super::reset();
77 }
78 key!(KeyCode::Enter) => {
79 helper.cursors_mut().clear();
80 self.0.update(helper.text_mut(), area);
81 self.0.before_exit(helper.text_mut(), area);
82 super::reset();
83 }
84 _ => {}
85 }
86 }
87
88 fn on_switch(&mut self, widget: &mut Self::Widget, area: &<U as Ui>::Area) {
89 *widget.text_mut() = Text::new_with_cursors();
90 run_once::<M, U>();
91
92 let tag = Tag::Ghost(0, match widget.prompt_of::<M>() {
93 Some(text) => text,
94 None => self.0.prompt(),
95 });
96 widget.text_mut().insert_tag(*PROMPT_KEY, tag);
97
98 self.0.on_switch(widget.text_mut(), area);
99 }
100}
101
102#[allow(unused_variables)]
103pub trait PromptMode<U: Ui>: Clone + Send + 'static {
104 fn update(&mut self, text: &mut Text, area: &U::Area) {}
105
106 fn on_switch(&mut self, text: &mut Text, area: &U::Area) {}
107
108 fn before_exit(&mut self, text: &mut Text, area: &U::Area) {}
109
110 fn once() {}
111
112 fn prompt(&self) -> Text;
113}
114
115#[derive(Default, Clone)]
116pub struct RunCommands;
117
118impl RunCommands {
119 pub fn new<U: Ui>() -> Prompt<Self, U> {
120 Prompt::new(Self)
121 }
122}
123
124impl<U: Ui> PromptMode<U> for RunCommands {
125 fn update(&mut self, text: &mut Text, _area: &U::Area) {
126 text.remove_tags(.., *KEY);
127
128 let command = text.to_string();
129 let caller = command.split_whitespace().next();
130 if let Some(caller) = caller {
131 if let Some((ok_ranges, err_range)) = cmd::check_args(&command) {
132 let id = form::id_of!("CallerExists");
133 text.insert_tag(*KEY, Tag::Form(0..caller.len(), id, 0));
134
135 let id = form::id_of!("ParameterOk");
136 for range in ok_ranges {
137 text.insert_tag(*KEY, Tag::Form(range, id, 0));
138 }
139 if let Some((range, _)) = err_range {
140 let id = form::id_of!("ParameterErr");
141 text.insert_tag(*KEY, Tag::Form(range, id, 0));
142 }
143 } else {
144 let id = form::id_of!("CallerNotFound");
145 text.insert_tag(*KEY, Tag::Form(0..caller.len(), id, 0));
146 }
147 }
148 }
149
150 fn before_exit(&mut self, text: &mut Text, _area: &U::Area) {
151 let text = std::mem::take(text);
152
153 let command = text.to_string();
154 if !command.is_empty() {
155 crate::thread::spawn(move || cmd::run_notify(command));
156 }
157 }
158
159 fn once() {
160 form::set_weak("CallerExists", "AccentOk");
161 form::set_weak("CallerNotFound", "AccentErr");
162 form::set_weak("ParameterOk", "DefaultOk");
163 form::set_weak("ParameterErr", "DefaultErr");
164 }
165
166 fn prompt(&self) -> Text {
167 text!([Prompt.colon] ":")
168 }
169}
170
171#[derive(Clone)]
172pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
173 inc: I,
174 orig: Option<(Cursors, <U::Area as Area>::PrintInfo)>,
175 ghost: PhantomData<U>,
176 prev: String,
177}
178
179impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
180 pub fn new(inc: I) -> Prompt<Self, U> {
181 Prompt::new(Self {
182 inc,
183 orig: None,
184 ghost: PhantomData,
185 prev: String::new(),
186 })
187 }
188}
189
190impl<I: IncSearcher<U>, U: Ui> PromptMode<U> for IncSearch<I, U> {
191 fn update(&mut self, text: &mut Text, _area: &U::Area) {
192 let orig = self.orig.as_ref().unwrap();
193 text.remove_tags(.., *KEY);
194
195 let mut ff = context::fixed_file::<U>().unwrap();
196
197 match Searcher::new(text.to_string()) {
198 Ok(searcher) => {
199 let (mut file, area) = ff.write();
200 self.inc.search(orig, &mut file, area, searcher);
201 }
202 Err(err) => {
203 let regex_syntax::Error::Parse(err) = *err else {
204 unreachable!("As far as I can tell, regex_syntax has goofed up");
205 };
206
207 let span = err.span();
208 let id = form::id_of!("ParseCommandErr");
209
210 text.insert_tag(*KEY, Tag::Form(span.start.offset..span.end.offset, id, 0));
211 }
212 }
213
214 if *text != self.prev {
215 let prev = std::mem::replace(&mut self.prev, text.to_string());
216 hooks::trigger::<SearchUpdated>((prev, self.prev.clone()));
217 }
218 }
219
220 fn before_exit(&mut self, text: &mut Text, _area: &<U as Ui>::Area) {
221 if !text.is_empty() {
222 hooks::trigger::<SearchPerformed>(text.to_string());
223 }
224 }
225
226 fn on_switch(&mut self, _text: &mut Text, _area: &U::Area) {
227 let mut ff = context::fixed_file::<U>().unwrap();
228 let (file, area) = ff.read();
229 self.orig = Some((file.cursors().clone(), area.print_info()));
230 }
231
232 fn once() {
233 form::set("Regex.err", "DefaultErr");
234 }
235
236 fn prompt(&self) -> Text {
237 self.inc.prompt()
238 }
239}
240
241#[derive(Clone, Copy)]
242pub struct PipeSelections<U>(PhantomData<U>);
243
244impl<U: Ui> PipeSelections<U> {
245 pub fn new() -> Prompt<Self, U> {
246 Prompt::new(Self(PhantomData))
247 }
248}
249
250impl<U: Ui> PromptMode<U> for PipeSelections<U> {
251 fn update(&mut self, text: &mut Text, _area: &U::Area) {
252 fn is_in_path(program: &str) -> bool {
253 if let Ok(path) = std::env::var("PATH") {
254 for p in path.split(":") {
255 let p_str = format!("{}/{}", p, program);
256 if let Ok(true) = std::fs::exists(p_str) {
257 return true;
258 }
259 }
260 }
261 false
262 }
263
264 text.remove_tags(.., *KEY);
265
266 let command = text.to_string();
267 let Some(caller) = command.split_whitespace().next() else {
268 return;
269 };
270
271 let args = cmd::args_iter(&command);
272
273 let (caller_id, args_id) = if is_in_path(caller) {
274 (form::id_of!("CallerExists"), form::id_of!("ParameterOk"))
275 } else {
276 (form::id_of!("CallerNotFound"), form::id_of!("ParameterErr"))
277 };
278
279 let c_s = command.len() - command.trim_start().len();
280 text.insert_tag(*KEY, Tag::Form(c_s..c_s + caller.len(), caller_id, 0));
281
282 for (_, range) in args {
283 text.insert_tag(*KEY, Tag::Form(range, args_id, 0));
284 }
285 }
286
287 fn before_exit(&mut self, text: &mut Text, _area: &U::Area) {
288 use std::process::{Command, Stdio};
289 let text = std::mem::take(text);
290
291 let command = text.to_string();
292 let Some(caller) = command.split_whitespace().next() else {
293 return;
294 };
295
296 let mut ff = context::fixed_file::<U>().unwrap();
297 let (mut file, area) = ff.write();
298 let mut helper = EditHelper::new(&mut *file, area);
299 helper.edit_iter().for_each(|mut e| {
300 let Ok(mut child) = Command::new(caller)
301 .args(cmd::args_iter(&command).map(|(a, _)| a))
302 .stdin(Stdio::piped())
303 .stdout(Stdio::piped())
304 .spawn()
305 else {
306 return;
307 };
308
309 let input: String = e.selection().collect();
310 if let Some(mut stdin) = child.stdin.take() {
311 crate::thread::spawn(move || {
312 stdin.write_all(input.as_bytes()).unwrap();
313 });
314 }
315 if let Ok(out) = child.wait_with_output() {
316 let out = String::from_utf8_lossy(&out.stdout);
317 e.replace(out);
318 }
319 });
320 }
321
322 fn prompt(&self) -> Text {
323 text!([Prompt] "pipe" [Prompt.colon] ":")
324 }
325}
326
327fn run_once<M: PromptMode<U>, U: Ui>() {
331 use std::{any::TypeId, sync::LazyLock};
332
333 use crate::data::RwData;
334 static LIST: LazyLock<RwData<Vec<TypeId>>> = LazyLock::new(|| RwData::new(Vec::new()));
335
336 let mut list = LIST.write();
337 if !list.contains(&TypeId::of::<M>()) {
338 M::once();
339 list.push(TypeId::of::<M>());
340 }
341}