1use std::{
13 any::TypeId,
14 io::Write,
15 marker::PhantomData,
16 sync::{
17 Arc, LazyLock,
18 atomic::{AtomicBool, Ordering},
19 },
20};
21
22use parking_lot::RwLock;
23
24use super::File;
25use crate::{
26 cfg::PrintCfg,
27 cmd::{self, args_iter},
28 data::{RoData, RwData, context},
29 form::{self, Form},
30 hooks::{self, KeySent},
31 mode::{self, Command, EditHelper, IncSearcher},
32 text::{Ghost, Key, Searcher, Tag, Text, text},
33 ui::{PushSpecs, Ui},
34 widgets::{Widget, WidgetCfg},
35};
36
37pub struct CmdLineCfg<U> {
38 prompt: String,
39 specs: PushSpecs,
40 ghost: PhantomData<U>,
41}
42
43impl<U> CmdLineCfg<U> {
44 pub fn new() -> Self {
45 CmdLineCfg {
46 prompt: String::from(":"),
47 specs: PushSpecs::below().with_ver_len(1.0),
48 ghost: PhantomData,
49 }
50 }
51}
52
53impl<U: Ui> Default for CmdLineCfg<U> {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl<U: Ui> CmdLineCfg<U> {
60 pub fn with_prompt(self, prompt: impl ToString) -> Self {
61 Self { prompt: prompt.to_string(), ..self }
62 }
63
64 pub fn above(self) -> Self {
65 Self {
66 specs: PushSpecs::above().with_ver_len(1.0),
67 ..self
68 }
69 }
70
71 pub fn left_ratioed(self, den: u16, div: u16) -> Self {
72 Self {
73 specs: PushSpecs::left().with_hor_ratio(den, div),
74 ..self
75 }
76 }
77}
78
79impl<U: Ui> WidgetCfg<U> for CmdLineCfg<U> {
80 type Widget = CmdLine<U>;
81
82 fn build(self, _: bool) -> (Self::Widget, impl Fn() -> bool, PushSpecs) {
83 let mode: RwData<dyn CmdLineMode<U>> = if hooks::group_exists("CmdLineNotifications") {
84 run_once::<ShowNotifications<U>, U>();
85 RwData::new_unsized::<ShowNotifications<U>>(Arc::new(RwLock::new(
86 ShowNotifications::new(),
87 )))
88 } else {
89 run_once::<RunCommands<U>, U>();
90 RwData::new_unsized::<RunCommands<U>>(Arc::new(RwLock::new(RunCommands::new())))
91 };
92
93 let widget = CmdLine {
94 text: Text::default(),
95 prompt: RwData::new(self.prompt.clone()),
96 mode: RwData::new(mode),
97 };
98
99 let checker = {
100 let mode = RoData::from(&widget.mode);
101 move || mode.read().write().has_changed()
102 };
103
104 (widget, checker, self.specs)
105 }
106}
107
108pub struct CmdLine<U: Ui> {
130 text: Text,
131 prompt: RwData<String>,
132 mode: RwData<RwData<dyn CmdLineMode<U>>>,
133}
134
135impl<U: Ui> CmdLine<U> {
136 pub(crate) fn set_mode<M: CmdLineMode<U>>(&mut self, mode: M) {
137 run_once::<M, U>();
138 if mode.do_focus() {
139 mode::set::<U>(Command);
140 }
141 *self.mode.write() = RwData::new_unsized::<M>(Arc::new(RwLock::new(mode)));
142 }
143}
144
145impl<U: Ui> Widget<U> for CmdLine<U> {
146 type Cfg = CmdLineCfg<U>;
147
148 fn cfg() -> Self::Cfg {
149 CmdLineCfg::new()
150 }
151
152 fn update(&mut self, _area: &<U as Ui>::Area) {
153 self.mode.read().write().update(&mut self.text);
154 }
155
156 fn text(&self) -> &Text {
157 &self.text
158 }
159
160 fn text_mut(&mut self) -> &mut Text {
161 &mut self.text
162 }
163
164 fn print_cfg(&self) -> PrintCfg {
165 PrintCfg::default_for_input().with_forced_scrolloff()
166 }
167
168 fn once() -> Result<(), crate::Error<()>> {
169 form::set_weak("Prompt", Form::cyan());
170 form::set_weak("ParseCommandErr", "DefaultErr");
171
172 cmd::add_for!(
173 "set-prompt",
174 |cmd_line: CmdLine<U>, _: U::Area, new: String| {
175 *cmd_line.prompt.write() = new;
176 Ok(None)
177 }
178 )
179 }
180
181 fn on_focus(&mut self, _area: &U::Area) {
182 self.text = text!({ Ghost(text!({ &self.prompt })) });
183 self.mode.read().write().on_focus(&mut self.text);
184 }
185
186 fn on_unfocus(&mut self, _area: &<U as Ui>::Area) {
187 self.mode.read().write().on_unfocus(&mut self.text);
188 }
189}
190
191#[allow(unused_variables)]
192pub trait CmdLineMode<U: Ui>: Send + Sync + 'static {
193 fn on_focus(&mut self, text: &mut Text) {}
194
195 fn on_unfocus(&mut self, text: &mut Text) {}
196
197 fn update(&mut self, text: &mut Text) {}
198
199 fn has_changed(&mut self) -> bool {
200 false
201 }
202
203 fn do_focus(&self) -> bool {
204 false
205 }
206
207 fn once()
208 where
209 Self: Sized,
210 {
211 }
212}
213
214pub struct RunCommands<U> {
215 key: Key,
216 ghost: PhantomData<U>,
217}
218
219impl<U: Ui> RunCommands<U> {
220 pub fn new() -> Self {
221 Self { key: Key::new(), ghost: PhantomData }
222 }
223}
224
225impl<U: Ui> CmdLineMode<U> for RunCommands<U> {
226 fn update(&mut self, text: &mut Text) {
227 text.remove_tags(.., self.key);
228
229 let command = text.to_string();
230 let caller = command.split_whitespace().next();
231 if let Some(caller) = caller {
232 if let Some((ok_ranges, err_range)) = cmd::check_params(&command) {
233 let id = form::id_of!("CallerExists");
234 text.insert_tag(0, Tag::PushForm(id), self.key);
235 text.insert_tag(caller.len(), Tag::PopForm(id), self.key);
236
237 let id = form::id_of!("ParameterOk");
238 for range in ok_ranges {
239 text.insert_tag(range.start, Tag::PushForm(id), self.key);
240 text.insert_tag(range.end, Tag::PopForm(id), self.key);
241 }
242 if let Some((range, _)) = err_range {
243 let id = form::id_of!("ParameterErr");
244 text.insert_tag(range.start, Tag::PushForm(id), self.key);
245 text.insert_tag(range.end, Tag::PopForm(id), self.key);
246 }
247 } else {
248 let id = form::id_of!("CallerNotFound");
249 text.insert_tag(0, Tag::PushForm(id), self.key);
250 text.insert_tag(caller.len(), Tag::PopForm(id), self.key);
251 }
252 }
253 }
254
255 fn on_unfocus(&mut self, text: &mut Text) {
256 let text = std::mem::take(text);
257
258 let command = text.to_string();
259 if !command.is_empty() {
260 crate::thread::queue(move || cmd::run_notify(command));
261 }
262 }
263
264 fn do_focus(&self) -> bool {
265 true
266 }
267
268 fn once() {
269 form::set_weak("CallerExists", "AccentOk");
270 form::set_weak("CallerNotFound", "AccentErr");
271 form::set_weak("ParameterOk", "DefaultOk");
272 form::set_weak("ParameterErr", "DefaultErr");
273 }
274}
275
276impl<U: Ui> Default for RunCommands<U> {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282impl<U: Ui> Clone for RunCommands<U> {
283 fn clone(&self) -> Self {
284 Self::new()
285 }
286}
287
288pub struct ShowNotifications<U> {
289 notifications: RwData<Text>,
290 text: Text,
291 ghost: PhantomData<U>,
292}
293
294impl<U: Ui> ShowNotifications<U> {
295 pub fn new() -> Self {
296 Self {
297 notifications: context::notifications().clone(),
298 text: Text::default(),
299 ghost: PhantomData,
300 }
301 }
302}
303
304static REMOVE_NOTIFS: AtomicBool = AtomicBool::new(false);
305
306impl<U: Ui> CmdLineMode<U> for ShowNotifications<U> {
307 fn has_changed(&mut self) -> bool {
308 if self.notifications.has_changed() {
309 REMOVE_NOTIFS.store(false, Ordering::Release);
310 self.text = self.notifications.read().clone();
311 true
312 } else {
313 !self.text.is_empty() && REMOVE_NOTIFS.load(Ordering::Acquire)
314 }
315 }
316
317 fn update(&mut self, text: &mut Text) {
318 if REMOVE_NOTIFS.load(Ordering::Acquire) {
319 self.text = Text::default();
320 REMOVE_NOTIFS.store(false, Ordering::Release);
321 }
322 if self.notifications.has_changed() {
323 self.text = self.notifications.read().clone();
324 }
325 *text = self.text.clone();
326 }
327
328 fn once()
329 where
330 Self: Sized,
331 {
332 hooks::add::<KeySent<U>>(|_| {
333 REMOVE_NOTIFS.store(true, Ordering::Release);
334 });
335 }
336}
337
338impl<U: Ui> Default for ShowNotifications<U> {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344impl<U: Ui> Clone for ShowNotifications<U> {
345 fn clone(&self) -> Self {
346 Self::new()
347 }
348}
349
350pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
351 fn_or_inc: FnOrInc<I, U>,
352 key: Key,
353 ghost: PhantomData<U>,
354}
355
356impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
357 pub fn new(f: impl IncFn<I, U> + Send + Sync + 'static) -> Self {
358 Self {
359 fn_or_inc: FnOrInc::Fn(Some(Box::new(f))),
360 key: Key::new(),
361 ghost: PhantomData,
362 }
363 }
364}
365
366impl<I: IncSearcher<U>, U: Ui> CmdLineMode<U> for IncSearch<I, U> {
367 fn update(&mut self, text: &mut Text) {
368 let FnOrInc::Inc(inc, _) = &mut self.fn_or_inc else {
369 unreachable!();
370 };
371
372 text.remove_tags(.., self.key);
373
374 let cur_file = context::cur_file::<U>().unwrap();
375
376 match Searcher::new(text.to_string()) {
377 Ok(searcher) => {
378 cur_file.mutate_data(|file, area| {
379 inc.search(&mut file.raw_write(), area, searcher);
380 });
381 }
382 Err(err) => {
383 let regex_syntax::Error::Parse(err) = *err else {
384 unreachable!("As far as I can tell, regex_syntax has goofed up");
385 };
386
387 let span = err.span();
388 let id = crate::form::id_of!("ParseCommandErr");
389
390 text.insert_tag(span.start.offset, Tag::PushForm(id), self.key);
391 text.insert_tag(span.end.offset, Tag::PopForm(id), self.key);
392 }
393 }
394 }
395
396 fn on_focus(&mut self, _text: &mut Text) {
397 context::cur_file::<U>().unwrap().mutate_data(|file, area| {
398 self.fn_or_inc.as_inc(&mut file.raw_write(), area);
399 })
400 }
401
402 fn on_unfocus(&mut self, _text: &mut Text) {
403 let FnOrInc::Inc(inc, _) = &mut self.fn_or_inc else {
404 unreachable!();
405 };
406
407 context::cur_file::<U>()
408 .unwrap()
409 .mutate_data(|file, area| inc.finish(&mut file.raw_write(), area));
410 }
411
412 fn do_focus(&self) -> bool {
413 true
414 }
415}
416
417impl<I: IncSearcher<U>, U: Ui> Clone for IncSearch<I, U> {
418 fn clone(&self) -> Self {
419 Self::new(I::new)
420 }
421}
422
423pub struct PipeSelections<U> {
424 key: Key,
425 _ghost: PhantomData<U>,
426}
427
428impl<U: Ui> PipeSelections<U> {
429 pub fn new() -> Self {
430 Self { key: Key::new(), _ghost: PhantomData }
431 }
432}
433
434impl<U: Ui> CmdLineMode<U> for PipeSelections<U> {
435 fn update(&mut self, text: &mut Text) {
436 fn is_in_path(program: &str) -> bool {
437 if let Ok(path) = std::env::var("PATH") {
438 for p in path.split(":") {
439 let p_str = format!("{}/{}", p, program);
440 if let Ok(true) = std::fs::exists(p_str) {
441 return true;
442 }
443 }
444 }
445 false
446 }
447
448 text.remove_tags(.., self.key);
449
450 let command = text.to_string();
451 let Some(caller) = command.split_whitespace().next() else {
452 return;
453 };
454
455 let args = args_iter(&command);
456
457 let (caller_id, args_id) = if is_in_path(caller) {
458 (form::id_of!("CallerExists"), form::id_of!("ParameterOk"))
459 } else {
460 (form::id_of!("CallerNotFound"), form::id_of!("ParameterErr"))
461 };
462
463 text.insert_tag(0, Tag::PushForm(caller_id), self.key);
464 text.insert_tag(caller.len(), Tag::PopForm(caller_id), self.key);
465
466 for (_, range) in args {
467 text.insert_tag(range.start, Tag::PushForm(args_id), self.key);
468 text.insert_tag(range.end, Tag::PopForm(args_id), self.key);
469 }
470 }
471
472 fn on_unfocus(&mut self, text: &mut Text) {
473 use std::process::{Command, Stdio};
474 let text = std::mem::take(text);
475
476 let command = text.to_string();
477 let Some(caller) = command.split_whitespace().next() else {
478 return;
479 };
480
481 context::cur_file::<U>().unwrap().mutate_data(|file, area| {
482 let mut file = file.write();
483 let mut helper = EditHelper::new(&mut *file, area);
484
485 helper.edit_many(.., |e| {
486 let Ok(mut child) = Command::new(caller)
487 .args(args_iter(&command).map(|(a, _)| a))
488 .stdin(Stdio::piped())
489 .stdout(Stdio::piped())
490 .spawn()
491 else {
492 return;
493 };
494
495 let input: String = e.selection().collect();
496 if let Some(mut stdin) = child.stdin.take() {
497 crate::thread::spawn(move || {
498 stdin.write_all(input.as_bytes()).unwrap();
499 });
500 }
501 if let Ok(out) = child.wait_with_output() {
502 let out = String::from_utf8_lossy(&out.stdout);
503 e.replace(out);
504 }
505 });
506 });
507 }
508
509 fn do_focus(&self) -> bool {
510 true
511 }
512}
513
514impl<U: Ui> Default for PipeSelections<U> {
515 fn default() -> Self {
516 Self::new()
517 }
518}
519
520impl<U: Ui> Clone for PipeSelections<U> {
521 fn clone(&self) -> Self {
522 Self::new()
523 }
524}
525
526fn run_once<M: CmdLineMode<U>, U: Ui>() {
530 static LIST: LazyLock<RwData<Vec<TypeId>>> = LazyLock::new(|| RwData::new(Vec::new()));
531
532 let mut list = LIST.write();
533 if !list.contains(&TypeId::of::<M>()) {
534 M::once();
535 list.push(TypeId::of::<M>());
536 }
537}
538
539enum FnOrInc<I, U: Ui> {
540 Fn(Option<Box<dyn IncFn<I, U> + Send + Sync>>),
541 Inc(I, PhantomData<U>),
542}
543
544impl<I, U: Ui> FnOrInc<I, U> {
545 fn as_inc(&mut self, file: &mut File, area: &U::Area) {
546 let FnOrInc::Fn(f) = self else {
547 unreachable!();
548 };
549
550 let inc = f.take().unwrap()(file, area);
551 *self = FnOrInc::Inc(inc, PhantomData);
552 }
553}
554
555trait IncFn<I, U: Ui> = FnOnce(&mut File, &U::Area) -> I;