Skip to main content

workflow_terminal/terminal/
mod.rs

1//!
2//! Module implementing the terminal interface abstraction
3//!
4
5use crate::CrLf;
6use crate::UnicodeString;
7use crate::clear::*;
8use crate::cli::Cli;
9use crate::cursor::*;
10use crate::error::Error;
11use crate::keys::Key;
12use crate::result::Result;
13use cfg_if::cfg_if;
14use futures::*;
15pub use pad::PadStr;
16use regex::Regex;
17use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
18use std::sync::{Arc, LockResult, Mutex, MutexGuard};
19use workflow_core::channel::{Channel, DuplexChannel, Receiver, Sender, unbounded};
20use workflow_core::task::spawn;
21use workflow_log::log_error;
22
23const DEFAULT_PARA_WIDTH: usize = 80;
24
25/// State of keyboard modifier keys that were held during an event
26/// (such as a link click).
27pub struct Modifiers {
28    /// Whether the Alt key was held.
29    pub alt: bool,
30    /// Whether the Shift key was held.
31    pub shift: bool,
32    /// Whether the Ctrl key was held.
33    pub ctrl: bool,
34    /// Whether the Meta (Command/Windows) key was held.
35    pub meta: bool,
36}
37/// Callback invoked when a registered link matcher matches, receiving the
38/// active keyboard [`Modifiers`] and the matched text.
39pub type LinkMatcherHandlerFn = Arc<Box<dyn Fn(Modifiers, &str)>>;
40
41/// Terminal events emitted to registered event handlers.
42#[derive(Debug, Clone)]
43pub enum Event {
44    /// The user requested a copy (e.g. via keyboard shortcut).
45    Copy,
46    /// The user requested a paste (e.g. via keyboard shortcut).
47    Paste,
48}
49/// Callback invoked when a terminal [`Event`] occurs.
50pub type EventHandlerFn = Arc<Box<dyn Fn(Event)>>;
51
52mod options;
53pub use options::Options;
54pub use options::TargetElement;
55
56pub mod bindings;
57/// xterm.js-based terminal interface bindings for WASM/browser targets.
58pub mod xterm;
59pub use xterm::{Theme, ThemeOption};
60
61cfg_if! {
62    if #[cfg(target_arch = "wasm32")] {
63        // pub mod xterm;
64        // pub mod bindings;
65        use crate::terminal::xterm::Xterm as Interface;
66
67    } else if #[cfg(feature = "termion")] {
68        pub mod termion;
69        use crate::terminal::termion::Termion as Interface;
70    } else {
71        /// Crossterm-based terminal interface for native targets.
72        pub mod crossterm;
73        use crate::terminal::crossterm::Crossterm as Interface;
74        pub use crate::terminal::crossterm::{disable_raw_mode,init_panic_hook};
75    }
76}
77
78/// Mutable interior state of a [`Terminal`]: the current line buffer,
79/// command history, and cursor position.
80#[derive(Debug)]
81pub struct Inner {
82    /// The text currently entered on the active input line.
83    pub buffer: UnicodeString,
84    history: Vec<UnicodeString>,
85    /// Cursor position as a character offset within `buffer`.
86    pub cursor: usize,
87    history_index: usize,
88}
89
90impl Default for Inner {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl Inner {
97    /// Create empty interior state with an empty buffer and history.
98    pub fn new() -> Self {
99        Inner {
100            buffer: UnicodeString::default(),
101            history: vec![],
102            cursor: 0,
103            history_index: 0,
104        }
105    }
106
107    /// Clear the current input line buffer and reset the cursor to the start.
108    pub fn reset_line_buffer(&mut self) {
109        self.buffer.clear();
110        self.cursor = 0;
111    }
112}
113
114#[derive(Clone)]
115struct UserInput {
116    prompt: Arc<Mutex<Option<String>>>,
117    buffer: Arc<Mutex<UnicodeString>>,
118    enabled: Arc<AtomicBool>,
119    secret: Arc<AtomicBool>,
120    kbhit: Arc<AtomicBool>,
121    terminate: Arc<AtomicBool>,
122    sender: Sender<String>,
123    receiver: Receiver<String>,
124}
125
126impl UserInput {
127    pub fn new() -> Self {
128        let (sender, receiver) = unbounded();
129        UserInput {
130            prompt: Arc::new(Mutex::new(None)),
131            buffer: Arc::new(Mutex::new(UnicodeString::default())),
132            enabled: Arc::new(AtomicBool::new(false)),
133            secret: Arc::new(AtomicBool::new(false)),
134            kbhit: Arc::new(AtomicBool::new(false)),
135            terminate: Arc::new(AtomicBool::new(false)),
136            sender,
137            receiver,
138        }
139    }
140
141    pub fn get_prompt(&self) -> Option<String> {
142        self.prompt.lock().unwrap().clone()
143    }
144
145    pub fn get_buffer(&self) -> String {
146        self.buffer.lock().unwrap().clone().to_string()
147    }
148
149    pub fn open(&self, secret: bool, kbhit: bool, prompt: Option<String>) -> Result<()> {
150        *self.prompt.lock().unwrap() = prompt;
151        self.enabled.store(true, Ordering::SeqCst);
152        self.secret.store(secret, Ordering::SeqCst);
153        self.kbhit.store(kbhit, Ordering::SeqCst);
154        self.terminate.store(false, Ordering::SeqCst);
155        Ok(())
156    }
157
158    pub fn close(&self) -> Result<()> {
159        let s = {
160            self.prompt.lock().unwrap().take();
161            let mut buffer = self.buffer.lock().unwrap();
162            let s = buffer.clone();
163            buffer.clear();
164            s
165        };
166
167        self.enabled.store(false, Ordering::SeqCst);
168        self.terminate.store(true, Ordering::SeqCst);
169        self.sender.try_send(s.to_string()).unwrap();
170        Ok(())
171    }
172
173    pub async fn capture(
174        &self,
175        secret: bool,
176        kbhit: bool,
177        prompt: Option<String>,
178        term: &Arc<Terminal>,
179    ) -> Result<String> {
180        self.open(secret, kbhit, prompt)?;
181
182        let term = term.clone();
183        let terminate = self.terminate.clone();
184
185        cfg_if! {
186
187            // TODO - refactor
188            // this is currently a workaround due to DOM
189            // clipboard API using JsPromise.
190            if #[cfg(target_arch = "wasm32")] {
191                workflow_core::task::dispatch(async move {
192                    let _result = term.term().intake(&terminate).await;
193                });
194            } else {
195                workflow_core::task::spawn(async move {
196                    let _result = term.term().intake(&terminate).await;
197                });
198            }
199        }
200
201        let string = self.receiver.recv().await?;
202        Ok(string)
203    }
204
205    fn is_enabled(&self) -> bool {
206        self.enabled.load(Ordering::SeqCst)
207    }
208
209    fn is_secret(&self) -> bool {
210        self.secret.load(Ordering::SeqCst)
211    }
212
213    fn is_kbhit(&self) -> bool {
214        self.kbhit.load(Ordering::SeqCst)
215    }
216
217    fn ingest(&self, key: Key, term: &Arc<Terminal>) -> Result<()> {
218        match key {
219            Key::Ctrl('c') => {
220                self.close()?;
221                term.abort();
222            }
223            Key::Char(ch) => {
224                self.buffer.lock().unwrap().push(ch);
225                if !self.is_secret() {
226                    term.write(ch);
227                }
228                if self.is_kbhit() {
229                    term.crlf();
230                    self.close()?;
231                }
232            }
233            Key::Backspace => {
234                self.buffer.lock().unwrap().pop();
235                if !self.is_secret() {
236                    term.write("\x08 \x08");
237                }
238            }
239            Key::Enter => {
240                // term.writeln("");
241                term.crlf();
242                self.close()?;
243            }
244            _ => {}
245        }
246        Ok(())
247    }
248
249    #[allow(dead_code)]
250    pub fn inject<S: ToString>(&self, text: S, term: &Terminal) -> Result<()> {
251        self.inject_impl(text.to_string().into(), term)?;
252        Ok(())
253    }
254
255    fn inject_impl(&self, text: UnicodeString, term: &Terminal) -> Result<()> {
256        let mut buffer = self.buffer.lock().unwrap();
257        if !self.is_secret() {
258            text.iter().for_each(|ch| {
259                term.write(ch);
260            });
261        }
262        buffer.extend(text);
263        Ok(())
264    }
265}
266
267/// Terminal interface
268#[derive(Clone)]
269pub struct Terminal {
270    inner: Arc<Mutex<Inner>>,
271    /// Set while a submitted command is being processed by the [`Cli`] handler.
272    pub running: Arc<AtomicBool>,
273    /// The default prompt string rendered before each input line.
274    pub prompt: Arc<Mutex<String>>,
275    /// The underlying platform-specific terminal interface.
276    pub term: Arc<Interface>,
277    /// The command-line processor that handles submitted commands.
278    pub handler: Arc<dyn Cli>,
279    /// When set, the terminal exits its run loop after the current command.
280    pub terminate: Arc<AtomicBool>,
281    user_input: UserInput,
282    /// Pipe for writing raw text (without newline conversion) to the terminal.
283    pub pipe_raw: Channel<String>,
284    /// Pipe for writing lines (terminated with CRLF) to the terminal.
285    pub pipe_crlf: Channel<String>,
286    /// Duplex control channel used to start and stop the pipe processing task.
287    pub pipe_ctl: DuplexChannel<()>,
288    /// Fallback paragraph wrap width (in columns) when the terminal width is unknown.
289    pub para_width: Arc<AtomicUsize>,
290}
291
292impl Terminal {
293    /// Create a new default terminal instance bound to the supplied command-line processor [`Cli`].
294    pub fn try_new(handler: Arc<dyn Cli>, prompt: &str) -> Result<Self> {
295        let term = Arc::new(Interface::try_new()?);
296
297        let terminal = Self {
298            inner: Arc::new(Mutex::new(Inner::new())),
299            running: Arc::new(AtomicBool::new(false)),
300            prompt: Arc::new(Mutex::new(prompt.to_string())),
301            term,
302            handler,
303            terminate: Arc::new(AtomicBool::new(false)),
304            user_input: UserInput::new(),
305            pipe_raw: Channel::unbounded(),
306            pipe_crlf: Channel::unbounded(),
307            pipe_ctl: DuplexChannel::oneshot(),
308            para_width: Arc::new(AtomicUsize::new(DEFAULT_PARA_WIDTH)),
309        };
310
311        Ok(terminal)
312    }
313
314    /// Create a new terminal instance bound to the supplied command-line processor [`Cli`].
315    /// Receives [`options::Options`] that allow terminal customization.
316    pub fn try_new_with_options(
317        handler: Arc<dyn Cli>,
318        // prompt : &str,
319        options: Options,
320    ) -> Result<Self> {
321        let term = Arc::new(Interface::try_new_with_options(&options)?);
322
323        let terminal = Self {
324            inner: Arc::new(Mutex::new(Inner::new())),
325            running: Arc::new(AtomicBool::new(false)),
326            prompt: Arc::new(Mutex::new(options.prompt())),
327            term,
328            handler,
329            terminate: Arc::new(AtomicBool::new(false)),
330            user_input: UserInput::new(),
331            pipe_raw: Channel::unbounded(),
332            pipe_crlf: Channel::unbounded(),
333            pipe_ctl: DuplexChannel::oneshot(),
334            para_width: Arc::new(AtomicUsize::new(DEFAULT_PARA_WIDTH)),
335        };
336
337        Ok(terminal)
338    }
339
340    /// Init the terminal instance
341    pub async fn init(self: &Arc<Self>) -> Result<()> {
342        self.term.init(self).await?;
343
344        self.handler.clone().init(self)?;
345
346        Ok(())
347    }
348
349    /// Access to the underlying terminal instance
350    pub fn inner(&self) -> LockResult<MutexGuard<'_, Inner>> {
351        self.inner.lock()
352    }
353
354    /// Get terminal command line history list as `Vec<String>`
355    pub fn history(&self) -> Vec<UnicodeString> {
356        let data = self.inner().unwrap();
357        data.history.clone()
358    }
359
360    /// Clear the current input line buffer and reset the cursor to the start.
361    pub fn reset_line_buffer(&self) {
362        self.inner().unwrap().reset_line_buffer();
363    }
364
365    /// Get the current terminal prompt string
366    pub fn get_prompt(&self) -> String {
367        if let Some(prompt) = self.handler.prompt() {
368            prompt
369        } else {
370            self.prompt.lock().unwrap().clone()
371        }
372    }
373
374    /// Render the current prompt in the terminal
375    pub fn prompt(&self) {
376        let mut data = self.inner().unwrap();
377        data.cursor = 0;
378        data.buffer.clear();
379        self.term().write(self.get_prompt());
380    }
381
382    /// Output CRLF sequence
383    pub fn crlf(&self) {
384        self.term().write("\n\r".to_string());
385    }
386
387    /// Write a string
388    pub fn write<S>(&self, s: S)
389    where
390        S: ToString,
391    {
392        self.term().write(s);
393    }
394
395    /// Write a string ending with CRLF sequence
396    pub fn writeln<S>(&self, s: S)
397    where
398        S: ToString,
399    {
400        if self.is_running() {
401            if self.user_input.is_enabled() {
402                if let Some(prompt) = self.user_input.get_prompt() {
403                    self.write(format!("{}{}\n\r", ClearLine, s.to_string()));
404                    self.write(prompt);
405                    if !self.user_input.secret.load(Ordering::SeqCst) {
406                        self.write(self.user_input.get_buffer());
407                    }
408                }
409            } else {
410                self.write(format!("{}\n\r", s.to_string()));
411            }
412        } else {
413            self.write(format!("{}{}\n\r", ClearLine, s.to_string()));
414            let data = self.inner().unwrap();
415            let p = format!("{}{}", self.get_prompt(), data.buffer);
416            self.write(p);
417            let l = data.buffer.len() - data.cursor;
418            for _ in 0..l {
419                self.write("\x08".to_string());
420            }
421        }
422    }
423
424    /// Refreshes the prompt and the user input buffer. This function
425    /// is useful when the prompt is handled externally and contains
426    /// data that should be updated.
427    pub fn refresh_prompt(&self) {
428        if !self.is_running() {
429            self.write(format!("{}", ClearLine));
430            let data = self.inner().unwrap();
431            let p = format!("{}{}", self.get_prompt(), data.buffer);
432            self.write(p);
433            let l = data.buffer.len() - data.cursor;
434            for _ in 0..l {
435                self.write("\x08".to_string());
436            }
437        }
438    }
439
440    /// Write `text` to the terminal as a paragraph, word-wrapped to the
441    /// current terminal width (falling back to [`Terminal::para_width`]).
442    pub fn para<S>(&self, text: S)
443    where
444        S: Into<String>,
445    {
446        let width = self
447            .term()
448            .cols()
449            .unwrap_or_else(|| self.para_width.load(Ordering::SeqCst));
450        let options = textwrap::Options::new(width).line_ending(textwrap::LineEnding::CRLF);
451
452        textwrap::wrap(text.into().as_str(), options)
453            .into_iter()
454            .for_each(|line| self.writeln(line));
455    }
456
457    /// Write `text` to the terminal as a paragraph, word-wrapped using the
458    /// supplied width or [`textwrap::Options`].
459    pub fn para_with_options<'a, S, Opt>(&self, width_or_options: Opt, text: S)
460    where
461        S: Into<String>,
462        Opt: Into<textwrap::Options<'a>>,
463    {
464        // use textwrap::wrap;
465
466        textwrap::wrap(text.into().crlf().as_str(), width_or_options.into())
467            .into_iter()
468            .for_each(|line| self.writeln(line));
469    }
470
471    /// Render a formatted, alphabetically-sorted help listing of
472    /// `(command, description)` pairs, wrapping descriptions to fit the
473    /// terminal width. `separator` (default a single space) is placed between
474    /// each command and its description.
475    pub fn help<S: ToString, H: ToString>(
476        &self,
477        list: &[(S, H)],
478        separator: Option<&str>,
479    ) -> Result<()> {
480        let mut list = list
481            .iter()
482            .map(|(cmd, help)| (cmd.to_string(), help.to_string()))
483            .collect::<Vec<_>>();
484        list.sort_by_key(|(cmd, _)| cmd.to_string());
485        let separator = separator.unwrap_or(" ");
486        let term_width: usize = self.cols().unwrap_or(80);
487        let cmd_width = list.iter().map(|(c, _)| c.len()).fold(0, |a, b| a.max(b)) + 2;
488        let help_width = term_width - cmd_width - 2 - 4 - separator.len();
489        let cmd_space = "".pad_to_width(cmd_width);
490        self.writeln("");
491        for (cmd, help) in list {
492            let mut first = true;
493            let options =
494                textwrap::Options::new(help_width).line_ending(textwrap::LineEnding::CRLF);
495            textwrap::wrap(help.as_str(), options)
496                .into_iter()
497                .for_each(|line| {
498                    if first {
499                        self.writeln(format!(
500                            "{:>4}{}{}{}",
501                            "",
502                            cmd.pad_to_width(cmd_width),
503                            separator,
504                            line
505                        ));
506                        first = false;
507                    } else {
508                        self.writeln(format!("{:>4}{cmd_space}{}{}", "", separator, line));
509                    }
510                });
511        }
512        self.writeln("");
513
514        Ok(())
515    }
516
517    /// Get a clone of Arc of the underlying terminal instance
518    pub fn term(&self) -> Arc<Interface> {
519        Arc::clone(&self.term)
520    }
521
522    async fn pipe_start(self: &Arc<Self>) -> Result<()> {
523        let self_ = self.clone();
524        spawn(async move {
525            loop {
526                select! {
527                    _ = self_.pipe_ctl.request.receiver.recv().fuse() => {
528                        break;
529                    },
530                    raw = self_.pipe_raw.receiver.recv().fuse() => {
531                        raw.map(|text|self_.write(text)).unwrap_or_else(|err|log_error!("Error writing from raw pipe: {err}"));
532                    },
533                    text = self_.pipe_crlf.receiver.recv().fuse() => {
534                        text.map(|text|self_.writeln(text)).unwrap_or_else(|err|log_error!("Error writing from crlf pipe: {err}"));
535                    },
536                }
537            }
538
539            self_
540                .pipe_ctl
541                .response
542                .sender
543                .send(())
544                .await
545                .unwrap_or_else(|err| log_error!("Error posting shutdown ctl: {err}"));
546        });
547        Ok(())
548    }
549
550    async fn pipe_stop(self: &Arc<Self>) -> Result<()> {
551        self.pipe_ctl.signal(()).await?;
552        Ok(())
553    }
554
555    fn pipe_abort(self: &Arc<Self>) -> Result<()> {
556        self.pipe_ctl.request.try_send(())?;
557        Ok(())
558    }
559
560    /// Execute the async terminal processing loop.
561    /// Once started, it should be stopped using
562    /// [`Terminal::exit`]
563    pub async fn run(self: &Arc<Self>) -> Result<()> {
564        // self.prompt();
565
566        self.pipe_start().await?;
567        self.term().run().await
568    }
569
570    /// Exits the async terminal processing loop (async fn)
571    pub async fn exit(self: &Arc<Self>) {
572        self.terminate.store(true, Ordering::SeqCst);
573        self.pipe_stop().await.unwrap_or_else(|err| panic!("{err}"));
574        self.term.exit();
575    }
576
577    /// Exits the async terminal processing loop (sync fn)
578    pub fn abort(self: &Arc<Self>) {
579        self.terminate.store(true, Ordering::SeqCst);
580        self.pipe_abort().unwrap_or_else(|err| panic!("{err}"));
581        self.term.exit();
582    }
583
584    /// Ask a question (input a string until CRLF).
585    /// `secret` argument suppresses echoing of the
586    /// user input (useful for password entry)
587    pub async fn ask(self: &Arc<Terminal>, secret: bool, prompt: &str) -> Result<String> {
588        self.reset_line_buffer();
589        self.term().write(prompt.to_string());
590        self.user_input
591            .capture(secret, false, Some(prompt.to_string()), self)
592            .await
593    }
594
595    /// Wait for a single keystroke, optionally displaying `prompt` first, and
596    /// return the captured key without echoing it.
597    pub async fn kbhit(self: &Arc<Terminal>, prompt: Option<&str>) -> Result<String> {
598        self.reset_line_buffer();
599        if let Some(prompt) = prompt {
600            self.term().write(prompt.to_string());
601        }
602        self.user_input
603            .capture(true, true, prompt.map(String::from), self)
604            .await
605    }
606
607    /// Inject a string into the current cursor position
608    pub fn inject_unicode_string(&self, text: UnicodeString) -> Result<()> {
609        let mut data = self.inner()?;
610        self.inject_impl(&mut data, text)?;
611        Ok(())
612    }
613
614    /// Insert `text` into the current input line at the cursor position.
615    pub fn inject<S: ToString>(&self, text: S) -> Result<()> {
616        let mut data = self.inner()?;
617        self.inject_impl(&mut data, text.to_string().into())
618    }
619
620    fn inject_impl(&self, data: &mut Inner, text: UnicodeString) -> Result<()> {
621        if self.user_input.is_enabled() {
622            self.user_input.inject_impl(text, self)
623        } else {
624            let len = text.len();
625            data.buffer.insert(data.cursor, text);
626            self.trail(data.cursor, &data.buffer, true, false, len);
627            data.cursor += len;
628            Ok(())
629        }
630    }
631
632    /// Insert a single character into the current input line at the cursor
633    /// position.
634    pub fn inject_char(&self, ch: char) -> Result<()> {
635        let mut data = self.inner()?;
636        self.inject_char_impl(&mut data, ch)?;
637        Ok(())
638    }
639
640    fn inject_char_impl(&self, data: &mut Inner, ch: char) -> Result<()> {
641        data.buffer.insert_char(data.cursor, ch);
642        self.trail(data.cursor, &data.buffer, true, false, 1);
643        data.cursor += 1;
644        Ok(())
645    }
646
647    async fn ingest(self: &Arc<Terminal>, key: Key, _term_key: String) -> Result<()> {
648        if self.user_input.is_enabled() {
649            self.user_input.ingest(key, self)?;
650            return Ok(());
651        }
652
653        match key {
654            Key::Backspace => {
655                let mut data = self.inner()?;
656                if data.cursor == 0 {
657                    return Ok(());
658                }
659                self.write("\x08".to_string());
660                data.cursor -= 1;
661                let idx = data.cursor;
662                data.buffer.remove(idx);
663                self.trail(data.cursor, &data.buffer, true, true, 0);
664            }
665            Key::ArrowUp => {
666                let mut data = self.inner()?;
667                if data.history_index == 0 {
668                    return Ok(());
669                }
670                let current_buffer = data.buffer.clone();
671                let index = data.history_index;
672                //log_trace!("ArrowUp: index {}, data.history.len(): {}", index, data.history.len());
673                if data.history.len() <= index {
674                    data.history.push(current_buffer);
675                } else {
676                    data.history[index] = current_buffer;
677                }
678                data.history_index -= 1;
679
680                data.buffer = data.history[data.history_index].clone();
681                self.write(format!("{}{}{}", ClearLine, self.get_prompt(), data.buffer));
682                data.cursor = data.buffer.len();
683            }
684            Key::ArrowDown => {
685                let mut data = self.inner()?;
686                let len = data.history.len();
687                if data.history_index >= len {
688                    return Ok(());
689                }
690                let index = data.history_index;
691                data.history[index] = data.buffer.clone();
692                data.history_index += 1;
693                if data.history_index == len {
694                    data.buffer.clear();
695                } else {
696                    data.buffer = data.history[data.history_index].clone();
697                }
698
699                self.write(format!("{}{}{}", ClearLine, self.get_prompt(), data.buffer));
700                data.cursor = data.buffer.len();
701            }
702            Key::ArrowLeft => {
703                let mut data = self.inner()?;
704                if data.cursor == 0 {
705                    return Ok(());
706                }
707                data.cursor -= 1;
708                self.write(Left(1));
709            }
710            Key::ArrowRight => {
711                let mut data = self.inner()?;
712                if data.cursor < data.buffer.len() {
713                    data.cursor += 1;
714                    self.write(Right(1));
715                }
716            }
717            Key::Enter => {
718                let cmd = {
719                    let mut data = self.inner()?;
720                    let buffer = data.buffer.clone();
721                    let length = data.history.len();
722
723                    data.buffer.clear();
724                    data.cursor = 0;
725
726                    if !buffer.is_empty() {
727                        let cmd = buffer.clone();
728
729                        if length == 0 || !data.history[length - 1].is_empty() {
730                            data.history_index = length;
731                        } else {
732                            data.history_index = length - 1;
733                        }
734                        let index = data.history_index;
735                        if length <= index {
736                            data.history.push(buffer);
737                        } else {
738                            data.history[index] = buffer;
739                        }
740                        data.history_index += 1;
741
742                        Some(cmd)
743                    } else {
744                        None
745                    }
746                };
747
748                self.crlf();
749
750                if let Some(cmd) = cmd {
751                    self.running.store(true, Ordering::SeqCst);
752                    self.exec(cmd).await.ok();
753                    self.running.store(false, Ordering::SeqCst);
754                } else {
755                    self.prompt();
756                }
757            }
758            Key::Alt(_c) => {
759                return Ok(());
760            }
761            Key::Ctrl('c') => {
762                cfg_if! {
763                    if #[cfg(not(target_arch = "wasm32"))] {
764                        self.exit().await;
765                    }
766                }
767                return Ok(());
768            }
769            Key::Ctrl(_c) => {
770                return Ok(());
771            }
772            Key::Char(ch) => {
773                self.inject_char(ch)?;
774            }
775            _ => {
776                return Ok(());
777            }
778        }
779
780        Ok(())
781    }
782
783    fn trail(
784        &self,
785        cursor: usize,
786        buffer: &UnicodeString,
787        rewind: bool,
788        erase_last: bool,
789        offset: usize,
790    ) {
791        let mut tail = UnicodeString::from(&buffer.0[cursor..]); //.to_vec();//.to_string();
792        if erase_last {
793            tail.push(' ');
794        }
795        self.write(&tail);
796        if rewind {
797            let mut l = tail.len();
798            if offset > 0 {
799                l -= offset;
800            }
801            for _ in 0..l {
802                self.write("\x08"); // backspace
803            }
804        }
805    }
806
807    /// Indicates that the terminal has received command input
808    /// and has not yet returned from the processing. This flag
809    /// is set to true when delivering the user command to the
810    /// [`Cli`] handler and is reset to false when the [`Cli`]
811    /// handler returns.
812    #[inline]
813    pub fn is_running(&self) -> bool {
814        self.running.load(Ordering::SeqCst)
815    }
816
817    /// Submit `cmd` to the [`Cli`] handler for processing, printing any error
818    /// it returns and then either exiting (if termination was requested) or
819    /// re-rendering the prompt.
820    pub async fn exec<S: ToString>(self: &Arc<Terminal>, cmd: S) -> Result<()> {
821        if let Err(err) = self
822            .handler
823            .clone()
824            .digest(self.clone(), cmd.to_string())
825            .await
826        {
827            self.writeln(err);
828        }
829        if self.terminate.load(Ordering::SeqCst) {
830            self.term().exit();
831        } else {
832            self.prompt();
833        }
834        Ok(())
835    }
836
837    /// Apply the supplied color [`Theme`] (effective on the WASM/xterm.js target).
838    pub fn set_theme(&self, _theme: Theme) -> Result<()> {
839        #[cfg(target_arch = "wasm32")]
840        self.term.set_theme(_theme)?;
841        Ok(())
842    }
843
844    /// Re-apply the current theme to the terminal (effective on the WASM/xterm.js target).
845    pub fn update_theme(&self) -> Result<()> {
846        #[cfg(target_arch = "wasm32")]
847        self.term.update_theme()?;
848        Ok(())
849    }
850
851    /// Copy the current terminal selection to the system clipboard
852    /// (effective on the WASM/xterm.js target).
853    pub fn clipboard_copy(&self) -> Result<()> {
854        #[cfg(target_arch = "wasm32")]
855        self.term.clipboard_copy()?;
856        Ok(())
857    }
858
859    /// Paste the system clipboard contents into the terminal
860    /// (effective on the WASM/xterm.js target).
861    pub fn clipboard_paste(&self) -> Result<()> {
862        #[cfg(target_arch = "wasm32")]
863        self.term.clipboard_paste()?;
864        Ok(())
865    }
866
867    /// Increase the terminal font size, returning the new size if applicable.
868    pub fn increase_font_size(&self) -> Result<Option<f64>> {
869        self.term.increase_font_size()
870    }
871
872    /// Decrease the terminal font size, returning the new size if applicable.
873    pub fn decrease_font_size(&self) -> Result<Option<f64>> {
874        self.term.decrease_font_size()
875    }
876
877    /// Set the terminal font size to `font_size`.
878    pub fn set_font_size(&self, font_size: f64) -> Result<()> {
879        self.term.set_font_size(font_size)
880    }
881
882    /// Return the current terminal font size, if known.
883    pub fn get_font_size(&self) -> Result<Option<f64>> {
884        self.term.get_font_size()
885    }
886
887    /// Return the terminal width in columns, if known.
888    pub fn cols(&self) -> Option<usize> {
889        self.term.cols()
890    }
891
892    /// Prompt the user to choose one item from `list` by its index. Returns
893    /// `None` for an empty list, the sole item for a single-element list, and
894    /// otherwise repeatedly displays the choices until a valid index is entered;
895    /// pressing enter on an empty line aborts with [`Error::UserAbort`].
896    pub async fn select<T>(self: &Arc<Terminal>, prompt: &str, list: &[T]) -> Result<Option<T>>
897    where
898        T: std::fmt::Display + Clone, // + IdT + Clone + Send + Sync + 'static,
899    {
900        if list.is_empty() {
901            Ok(None)
902        } else if list.len() == 1 {
903            Ok(list.first().cloned())
904        } else {
905            let mut selection = None;
906            while selection.is_none() {
907                list.iter().enumerate().for_each(|(seq, item)| {
908                    self.writeln(format!("{seq}: {item}"));
909                });
910
911                let text = self
912                    .ask(
913                        false,
914                        &format!("{prompt} [{}..{}] or <enter> to abort: ", 0, list.len() - 1),
915                    )
916                    .await?
917                    .trim()
918                    .to_string();
919                if text.is_empty() {
920                    self.writeln("aborting...");
921                    return Err(Error::UserAbort);
922                } else {
923                    match text.parse::<usize>() {
924                        Ok(seq) if seq < list.len() => selection = list.get(seq).cloned(),
925                        _ => {}
926                    };
927                }
928            }
929
930            Ok(selection)
931        }
932    }
933
934    /// Register a handler invoked on terminal [`Event`]s such as copy and paste
935    /// (effective on the WASM/xterm.js target).
936    pub fn register_event_handler(self: &Arc<Self>, _handler: EventHandlerFn) -> Result<()> {
937        #[cfg(target_arch = "wasm32")]
938        self.term.register_event_handler(_handler)?;
939        Ok(())
940    }
941
942    /// Register a handler invoked when terminal text matching `_regexp` is
943    /// clicked (effective on the WASM/xterm.js target).
944    pub fn register_link_matcher(
945        &self,
946        _regexp: &js_sys::RegExp,
947        _handler: LinkMatcherHandlerFn,
948    ) -> Result<()> {
949        cfg_if! {
950            if #[cfg(target_arch = "wasm32")] {
951                self.term.register_link_matcher(_regexp, _handler)?;
952            }
953        }
954        Ok(())
955    }
956}
957
958/// Utility function to strip multiple white spaces and return a `Vec<String>`
959pub fn parse(s: &str) -> Vec<String> {
960    let regex = Regex::new(r"\s+").unwrap();
961    let s = regex.replace_all(s.trim(), " ");
962    s.split(' ').map(|s| s.to_string()).collect::<Vec<String>>()
963}