1use std::cell::RefCell;
9use std::rc::Rc;
10
11use rich_interact::{Key, KeyCode};
12
13use crate::app::Ctx;
14use crate::complete::{CompleteOptions, Completer, Completion, InputHistory};
15use crate::node::{Axis, Node};
16use crate::reactive::{NodeId, Runtime, Signal, signal};
17use crate::screen::Rect;
18use crate::text_area::{Options, TextArea};
19use crate::widget::{Canvas, DrawCx, EventCx, MeasureCx, Used, Widget, WidgetEvent, widget};
20
21type OnSubmit = Box<dyn FnMut(&str, &mut Ctx) -> Result<(), String>>;
24type OnChange = Box<dyn FnMut(&str, &mut Ctx)>;
25type OnCancel = Box<dyn FnMut(&mut Ctx)>;
26
27#[derive(Default)]
43#[must_use = "builder methods return a new value; it does nothing until it is used"]
44pub struct Mode {
45 prefix: String,
46 title: Option<String>,
47 initial: String,
48 complete: Option<(Completer, CompleteOptions)>,
49 history: Option<InputHistory>,
50 submit: Option<OnSubmit>,
51 change: Option<OnChange>,
52 cancel: Option<OnCancel>,
53}
54
55impl Mode {
56 pub fn new(prefix: impl Into<String>) -> Mode {
58 Mode {
59 prefix: prefix.into(),
60 ..Mode::default()
61 }
62 }
63
64 pub fn title(mut self, title: impl Into<String>) -> Mode {
67 self.title = Some(title.into());
68 self
69 }
70
71 pub fn initial(mut self, text: impl Into<String>) -> Mode {
73 self.initial = text.into();
74 self
75 }
76
77 pub fn complete(self, complete: impl Fn(&str, usize) -> Vec<Completion> + 'static) -> Mode {
79 self.complete_with(complete, CompleteOptions::default())
80 }
81
82 pub fn complete_with(
84 mut self,
85 complete: impl Fn(&str, usize) -> Vec<Completion> + 'static,
86 options: CompleteOptions,
87 ) -> Mode {
88 self.complete = Some((Rc::new(complete), options));
89 self
90 }
91
92 pub fn history(mut self, history: impl Into<InputHistory>) -> Mode {
95 self.history = Some(history.into());
96 self
97 }
98
99 pub fn on_submit(
102 mut self,
103 submit: impl FnMut(&str, &mut Ctx) -> Result<(), String> + 'static,
104 ) -> Mode {
105 self.submit = Some(Box::new(submit));
106 self
107 }
108
109 pub fn on_change(mut self, change: impl FnMut(&str, &mut Ctx) + 'static) -> Mode {
111 self.change = Some(Box::new(change));
112 self
113 }
114
115 pub fn on_cancel(mut self, cancel: impl FnMut(&mut Ctx) + 'static) -> Mode {
117 self.cancel = Some(Box::new(cancel));
118 self
119 }
120}
121
122type OnYes = Box<dyn FnOnce(&mut Ctx)>;
124
125#[derive(Clone, Default)]
127enum Bar {
128 #[default]
130 Idle,
131 Message(String),
132 Error(String),
133 Typing(Rc<RefCell<Mode>>, Option<String>),
135 Confirm(String, Vec<Key>, Rc<RefCell<Option<OnYes>>>),
137}
138
139impl PartialEq for Bar {
140 fn eq(&self, other: &Bar) -> bool {
142 match (self, other) {
143 (Bar::Idle, Bar::Idle) => true,
144 (Bar::Message(a), Bar::Message(b)) | (Bar::Error(a), Bar::Error(b)) => a == b,
145 (Bar::Typing(a, x), Bar::Typing(b, y)) => Rc::ptr_eq(a, b) && x == y,
146 (Bar::Confirm(_, _, a), Bar::Confirm(_, _, b)) => Rc::ptr_eq(a, b),
147 _ => false,
148 }
149 }
150}
151
152#[derive(Clone, Copy)]
156pub struct CommandLine {
157 bar: Signal<Bar>,
158 opened: Signal<u64>,
160 value: Signal<String>,
161 id: Signal<Option<NodeId>>,
162 before: Signal<Option<NodeId>>,
164}
165
166impl Default for CommandLine {
167 fn default() -> CommandLine {
168 CommandLine::new()
169 }
170}
171
172impl CommandLine {
173 pub fn new() -> CommandLine {
175 CommandLine {
176 bar: signal(Bar::Idle),
177 opened: signal(0),
178 value: signal(String::new()),
179 id: signal(None),
180 before: signal(None),
181 }
182 }
183
184 pub fn open(&self, cx: &mut Ctx, mode: Mode) {
187 self.value.set(mode.initial.clone());
188 self.bar.set(Bar::Typing(Rc::new(RefCell::new(mode)), None));
189 self.opened.update(|n| *n += 1);
190 self.take_focus(cx);
191 }
192
193 pub fn confirm(
196 &self,
197 cx: &mut Ctx,
198 question: impl Into<String>,
199 on_yes: impl FnOnce(&mut Ctx) + 'static,
200 ) {
201 self.confirm_keys(cx, question, "y", on_yes);
202 }
203
204 pub fn confirm_keys(
208 &self,
209 cx: &mut Ctx,
210 question: impl Into<String>,
211 keys: &str,
212 on_yes: impl FnOnce(&mut Ctx) + 'static,
213 ) {
214 let keys = keys.split_whitespace().filter_map(Key::parse).collect();
215 let on_yes: OnYes = Box::new(on_yes);
216 self.bar.set(Bar::Confirm(
217 question.into(),
218 keys,
219 Rc::new(RefCell::new(Some(on_yes))),
220 ));
221 self.take_focus(cx);
222 }
223
224 pub fn error(&self, message: impl Into<String>) {
227 let message = message.into();
228 let typing = self.bar.with_untracked(|bar| match bar {
229 Bar::Typing(mode, _) => Some(mode.clone()),
230 _ => None,
231 });
232 match typing {
233 Some(mode) => self.bar.set(Bar::Typing(mode, Some(message))),
234 None => self.bar.set(Bar::Error(message)),
235 }
236 }
237
238 pub fn message(&self, markup: impl Into<String>) {
240 self.bar.set(Bar::Message(markup.into()));
241 }
242
243 pub fn close(&self, cx: &mut Ctx) {
245 let open = self.is_open_untracked();
246 self.bar.set(Bar::Idle);
247 if open && let Some(before) = self.before.get_untracked() {
248 cx.focus(before);
249 }
250 }
251
252 pub fn is_open(&self) -> bool {
254 self.bar
255 .with(|bar| matches!(bar, Bar::Typing(..) | Bar::Confirm(..)))
256 }
257
258 fn is_open_untracked(&self) -> bool {
259 self.bar
260 .with_untracked(|bar| matches!(bar, Bar::Typing(..) | Bar::Confirm(..)))
261 }
262
263 pub fn text(&self) -> String {
265 self.value.get()
266 }
267
268 fn take_focus(&self, cx: &mut Ctx) {
269 let Some(id) = self.id.get_untracked() else {
270 return;
271 };
272 let focused = Runtime::current()
273 .focus_path()
274 .and_then(|path| path.last().copied());
275 if focused != Some(id) {
276 self.before.set(focused);
277 }
278 cx.focus(id);
279 }
280}
281
282#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
285#[must_use = "options do nothing until they are passed to what takes them"]
286pub struct CommandLineOptions {
287 pub(crate) two_rows: bool,
288}
289
290impl CommandLineOptions {
291 pub fn new() -> CommandLineOptions {
292 CommandLineOptions::default()
293 }
294
295 pub fn two_rows(mut self) -> CommandLineOptions {
297 self.two_rows = true;
298 self
299 }
300}
301
302pub fn command_line(state: CommandLine) -> Node {
344 command_line_with(state, CommandLineOptions::default())
345}
346
347pub fn command_line_with(state: CommandLine, options: CommandLineOptions) -> Node {
349 let area = TextArea::new(
350 state.value,
351 Options {
352 single_line: true,
353 ..Options::default()
354 },
355 );
356 let node = widget(CommandBar {
357 state,
358 area,
359 options,
360 opened: u64::MAX,
361 input: Rect::default(),
362 })
363 .what("command_line");
364 state.id.set(Some(node.id()));
365 node
366}
367
368struct CommandBar {
369 state: CommandLine,
370 area: TextArea,
371 options: CommandLineOptions,
372 opened: u64,
374 input: Rect,
376}
377
378impl CommandBar {
379 fn set_up(&mut self) {
381 let opened = self.state.opened.get_untracked();
382 if opened == self.opened {
383 return;
384 }
385 self.opened = opened;
386 let (complete, history) = self.state.bar.with_untracked(|bar| match bar {
387 Bar::Typing(mode, _) => {
388 let mode = mode.borrow();
389 (mode.complete.clone(), mode.history)
390 }
391 _ => (None, None),
392 });
393 let (completer, options) = match complete {
394 Some((completer, options)) => (Some(completer), options),
395 None => (None, CompleteOptions::default()),
396 };
397 self.area.assist.completer = completer;
398 self.area.assist.options = options;
399 self.area.assist.history = history.map(|h| Rc::new(move || Some(h)) as _);
400 self.area.assist.popup = None;
401 self.area.assist.browse = None;
402 }
403
404 fn mode(&self) -> Option<Rc<RefCell<Mode>>> {
405 self.state.bar.with_untracked(|bar| match bar {
406 Bar::Typing(mode, _) => Some(mode.clone()),
407 _ => None,
408 })
409 }
410
411 fn typing(&mut self, cx: &mut EventCx, key: Key, mode: Rc<RefCell<Mode>>) -> Used {
413 let plain = !key.modifiers.ctrl && !key.modifiers.alt;
414 let popup = self.area.assist.popup.is_some();
415 match key.code {
416 KeyCode::Enter if plain && !popup => {
417 let text = self.state.value.get_untracked();
418 if let Some(history) = self.area.assist.history() {
419 history.push(&text);
420 }
421 let result = match mode.borrow_mut().submit.as_mut() {
422 Some(submit) => submit(&text, cx.app()),
423 None => Ok(()),
424 };
425 let still = self.mode().is_some_and(|m| Rc::ptr_eq(&m, &mode));
427 match result {
428 Ok(()) if still => self.state.close(cx.app()),
429 Ok(()) => {}
430 Err(message) => self.state.error(message),
431 }
432 cx.redraw();
433 Used::Yes
434 }
435 KeyCode::Escape if !popup && self.area.buffer_selection().is_none() => {
436 self.state.close(cx.app());
437 if let Some(cancel) = mode.borrow_mut().cancel.as_mut() {
438 cancel(cx.app());
439 }
440 cx.redraw();
441 Used::Yes
442 }
443 KeyCode::Tab | KeyCode::BackTab => {
445 self.area.event(cx, &WidgetEvent::Key(key));
446 Used::Yes
447 }
448 _ => {
449 let before = self.state.value.get_untracked();
450 let used = self.area.event(cx, &WidgetEvent::Key(key));
451 let after = self.state.value.get_untracked();
452 if after != before
453 && let Some(change) = mode.borrow_mut().change.as_mut()
454 {
455 change(&after, cx.app());
456 }
457 used
458 }
459 }
460 }
461}
462
463impl Widget for CommandBar {
464 fn name(&self) -> &'static str {
465 "command_line"
466 }
467
468 fn role(&self) -> crate::a11y::Role {
469 crate::a11y::Role::TextBox
470 }
471
472 fn focusable(&self) -> bool {
473 self.state.is_open_untracked()
474 }
475
476 fn measure(&mut self, _cx: &MeasureCx, axis: Axis, width: u16, _height: u16) -> u16 {
477 match axis {
478 Axis::Vertical => 1 + self.options.two_rows as u16,
479 Axis::Horizontal => width.max(20),
480 }
481 }
482
483 fn caret(&self) -> Option<(u16, u16)> {
484 self.mode()?;
485 let (x, y) = self.area.caret()?;
486 Some((self.input.x + x, self.input.y + y))
487 }
488
489 fn draw(&mut self, cx: &mut DrawCx, canvas: &mut Canvas) {
490 let bar = self.state.bar.get();
491 self.state.opened.get();
492 let (width, height) = (canvas.width(), canvas.height());
493 let error = cx.theme().error_style("error");
494 let title_style = cx.style("command.title", "reverse");
495 let question = cx.style("command.question", "bold");
496 let line = height.saturating_sub(1);
498 let mut input = None;
499 match &bar {
500 Bar::Idle => {}
501 Bar::Message(markup) => {
502 canvas.markup(cx.console(), 0, line, width, markup, None);
503 }
504 Bar::Error(message) => {
505 canvas.print(0, line, message, Some(&error));
506 }
507 Bar::Confirm(text, _, _) => {
508 canvas.markup(cx.console(), 0, line, width, text, Some(&question));
509 }
510 Bar::Typing(mode, failed) => {
511 let mode = mode.borrow();
512 let mut x = 0;
513 match (&mode.title, failed, self.options.two_rows) {
514 (_, Some(message), true) => {
515 canvas.fill(0, 0, width, 1, Some(&error));
516 canvas.print(0, 0, message, Some(&error));
517 }
518 (Some(title), None, true) => {
519 canvas.fill(0, 0, width, 1, Some(&title_style));
520 canvas.markup(cx.console(), 0, 0, width, title, Some(&title_style));
521 }
522 (_, Some(message), false) => {
523 x = canvas.markup(
524 cx.console(),
525 0,
526 line,
527 width,
528 &crate::rich::markup::escape(message),
529 Some(&error),
530 );
531 x = (x + 1).min(width);
532 }
533 (Some(title), None, false) => {
534 x = canvas.markup(cx.console(), 0, line, width, title, Some(&title_style));
535 x = (x + 1).min(width);
536 }
537 (None, None, _) => {}
538 }
539 if !mode.prefix.is_empty() {
540 canvas.print(x, line, &mode.prefix, None);
541 x = (x + crate::rich::cells::cell_len(&mode.prefix) as u16).min(width);
542 }
543 input = Some(Rect::new(x, line, width - x, 1));
544 }
545 }
546 let Some(at) = input else {
547 self.input = Rect::default();
548 if std::mem::take(&mut self.area.assist.floated) {
550 cx.float(None);
551 }
552 return;
553 };
554 self.set_up();
555 self.input = at;
556 let rect = Rect::new(
557 canvas.rect.x + at.x,
558 canvas.rect.y + at.y,
559 at.width,
560 at.height,
561 );
562 let mut part = Canvas {
563 screen: &mut *canvas.screen,
564 rect,
565 written: &mut *canvas.written,
566 };
567 let mut inner = DrawCx {
568 console: cx.console,
569 theme: cx.theme,
570 id: cx.id,
571 rect: Rect::new(cx.rect.x + at.x, cx.rect.y + at.y, at.width, at.height),
572 repaint: true,
573 focus_path: cx.focus_path,
574 hover_path: cx.hover_path,
575 pointer: cx.pointer,
576 shift: cx.shift,
577 wants_hover: cx.wants_hover,
578 watchers: cx.watchers,
579 disabled: cx.disabled,
580 };
581 self.area.draw(&mut inner, &mut part);
582 }
583
584 fn event(&mut self, cx: &mut EventCx, event: &WidgetEvent) -> Used {
585 let bar = self.state.bar.get_untracked();
586 match (event, bar) {
587 (WidgetEvent::Key(key), Bar::Confirm(_, yes, on_yes)) => {
588 let key = *key;
589 let no = ["n", "q", "esc"]
590 .iter()
591 .any(|k| Key::parse(k).is_some_and(|k| k == key));
592 if yes.contains(&key) {
593 self.state.close(cx.app());
594 if let Some(on_yes) = on_yes.borrow_mut().take() {
595 on_yes(cx.app());
596 }
597 } else if no {
598 self.state.close(cx.app());
599 } else if key.code == KeyCode::Char('c') && key.modifiers.ctrl {
600 return Used::No;
601 }
602 cx.redraw();
603 Used::Yes
604 }
605 (WidgetEvent::Key(key), Bar::Typing(mode, failed)) => {
606 self.set_up();
607 if failed.is_some() {
609 self.state.bar.set(Bar::Typing(mode.clone(), None));
610 }
611 self.typing(cx, *key, mode)
612 }
613 (WidgetEvent::Paste(_), Bar::Typing(mode, _)) => {
614 self.set_up();
615 let before = self.state.value.get_untracked();
616 let used = self.area.event(cx, event);
617 let after = self.state.value.get_untracked();
618 if after != before
619 && let Some(change) = mode.borrow_mut().change.as_mut()
620 {
621 change(&after, cx.app());
622 }
623 used
624 }
625 (WidgetEvent::Focus(_), Bar::Typing(..)) => self.area.event(cx, event),
626 _ => Used::No,
627 }
628 }
629
630 fn claims_key(&self, key: Key) -> bool {
631 self.state.is_open_untracked() && self.area.claims_key(key)
632 }
633}