workflow_terminal/terminal/
mod.rs

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