workflow_terminal/terminal/
xterm.rs

1use super::bindings::*;
2use super::{LinkMatcherHandlerFn, Modifiers};
3use crate::keys::Key;
4use crate::terminal::Event;
5use crate::terminal::EventHandlerFn;
6use crate::terminal::Options;
7use crate::terminal::TargetElement;
8use crate::terminal::Terminal;
9use crate::Result;
10use std::cell::{RefCell, RefMut};
11use std::fmt::Debug;
12use std::rc::Rc;
13use std::sync::atomic::AtomicBool;
14use std::sync::atomic::Ordering;
15use std::sync::{Arc, Mutex};
16use wasm_bindgen::JsValue;
17use web_sys::Element;
18use workflow_core::channel::{unbounded, Receiver, Sender};
19use workflow_core::runtime::{self, platform, Platform};
20use workflow_dom::clipboard;
21use workflow_dom::inject::*;
22use workflow_dom::utils::body;
23use workflow_dom::utils::*;
24use workflow_log::*;
25use workflow_wasm::jserror::*;
26use workflow_wasm::prelude::*;
27use workflow_wasm::utils::*;
28
29#[derive(Default)]
30pub struct Theme {
31    pub background: Option<String>,
32    pub foreground: Option<String>,
33    pub selection: Option<String>,
34    pub cursor: Option<String>,
35}
36
37pub enum ThemeOption {
38    Background,
39    Foreground,
40    Selection,
41    Cursor,
42}
43impl ThemeOption {
44    pub fn list() -> Vec<Self> {
45        Vec::from([
46            Self::Background,
47            Self::Foreground,
48            Self::Selection,
49            Self::Cursor,
50        ])
51    }
52}
53
54impl std::fmt::Display for ThemeOption {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::Background => write!(f, "Background"),
58            Self::Foreground => write!(f, "Foreground"),
59            Self::Selection => write!(f, "Selection"),
60            Self::Cursor => write!(f, "Cursor"),
61        }
62    }
63}
64
65impl Theme {
66    pub fn new() -> Self {
67        Self {
68            ..Default::default()
69        }
70    }
71    pub fn get(&self, key: &ThemeOption) -> Option<String> {
72        match key {
73            ThemeOption::Background => self.background.clone(),
74            ThemeOption::Foreground => self.foreground.clone(),
75            ThemeOption::Selection => self.selection.clone(),
76            ThemeOption::Cursor => self.cursor.clone(),
77        }
78    }
79    pub fn set(&mut self, key: ThemeOption, value: Option<String>) {
80        match key {
81            ThemeOption::Background => {
82                self.background = value;
83            }
84            ThemeOption::Foreground => {
85                self.foreground = value;
86            }
87            ThemeOption::Selection => {
88                self.selection = value;
89            }
90            ThemeOption::Cursor => {
91                self.cursor = value;
92            }
93        }
94    }
95}
96
97enum Ctl {
98    SinkEvent(SinkEvent),
99    Copy(Option<String>),
100    Paste(Option<String>),
101    Close,
102}
103
104#[derive(Debug)]
105pub struct SinkEvent {
106    key: String,
107    term_key: String,
108    ctrl_key: bool,
109    alt_key: bool,
110    meta_key: bool,
111}
112
113impl SinkEvent {
114    fn new(key: String, term_key: String, ctrl_key: bool, alt_key: bool, meta_key: bool) -> Self {
115        Self {
116            key,
117            term_key,
118            ctrl_key,
119            alt_key,
120            meta_key,
121        }
122    }
123}
124
125#[derive(Clone)]
126pub struct Sink {
127    receiver: Receiver<Ctl>,
128    sender: Sender<Ctl>,
129}
130
131impl Default for Sink {
132    fn default() -> Self {
133        let (sender, receiver) = unbounded();
134        Sink { receiver, sender }
135    }
136}
137
138pub struct ResizeObserverInfo {
139    #[allow(dead_code)]
140    observer: ResizeObserver,
141    #[allow(dead_code)]
142    callback: Callback<CallbackClosure<JsValue>>,
143}
144
145impl ResizeObserverInfo {
146    pub fn new(observer: ResizeObserver, callback: Callback<CallbackClosure<JsValue>>) -> Self {
147        Self { observer, callback }
148    }
149}
150
151pub struct XtermOptions {
152    pub font_family: Option<String>,
153    pub font_size: Option<f64>,
154    pub scrollback: Option<u32>,
155}
156
157///
158/// # Xterm
159///
160/// Wrapper around XtermJS - <https://github.com/xtermjs/xterm.js>
161///
162/// TODO enhance API to match <https://github.com/xtermjs/xterm.js/blob/4.14.1/typings/xterm.d.ts>
163///
164///
165pub struct Xterm {
166    pub element: Element,
167    xterm: Rc<RefCell<Option<XtermImpl>>>,
168    terminal: Arc<Mutex<Option<Arc<Terminal>>>>,
169    listener: Arc<Mutex<Option<Callback<CallbackClosure<XtermEvent>>>>>,
170    sink: Arc<Sink>,
171    resize: Rc<RefCell<Option<ResizeObserverInfo>>>,
172    fit: Rc<RefCell<Option<FitAddon>>>,
173    _web_links: Rc<RefCell<Option<WebLinksAddon>>>,
174    terminate: Arc<AtomicBool>,
175    disable_clipboard_handling: bool,
176    callbacks: CallbackMap,
177    defaults: XtermOptions,
178    event_handler: Rc<RefCell<Option<EventHandlerFn>>>,
179}
180
181unsafe impl Send for Xterm {}
182unsafe impl Sync for Xterm {}
183
184impl Xterm {
185    pub fn try_new() -> Result<Self> {
186        Self::try_new_with_options(&Options::default())
187    }
188
189    pub fn try_new_with_options(options: &Options) -> Result<Self> {
190        let el = match &options.element {
191            TargetElement::Body => body().expect("Unable to get 'body' element"),
192            TargetElement::Element(el) => el.clone(),
193            TargetElement::TagName(tag) => document()
194                .get_elements_by_tag_name(tag)
195                .item(0)
196                .ok_or("Unable to locate parent element for terminal")?,
197            TargetElement::Id(id) => document()
198                .get_element_by_id(id)
199                .ok_or("Unable to locate parent element for terminal")?,
200        };
201        Self::try_new_with_element(&el, options)
202    }
203
204    pub fn try_new_with_element(parent: &Element, options: &Options) -> Result<Self> {
205        let element = document().create_element("div")?;
206        element.set_attribute("class", "terminal")?;
207        parent.append_child(&element)?;
208        let defaults = XtermOptions {
209            font_size: options.font_size,
210            font_family: options.font_family.clone(),
211            scrollback: options.scrollback,
212        };
213        let terminal = Xterm {
214            element,
215            listener: Arc::new(Mutex::new(None)),
216            xterm: Rc::new(RefCell::new(None)),
217            terminal: Arc::new(Mutex::new(None)),
218            sink: Arc::new(Sink::default()),
219            resize: Rc::new(RefCell::new(None)),
220            // addons: Arc::new(Mutex::new(Vec::new())),
221            fit: Rc::new(RefCell::new(None)),
222            _web_links: Rc::new(RefCell::new(None)),
223            terminate: Arc::new(AtomicBool::new(false)),
224            disable_clipboard_handling: options.disable_clipboard_handling,
225            callbacks: CallbackMap::default(),
226            event_handler: Rc::new(RefCell::new(None)),
227            defaults,
228        };
229        Ok(terminal)
230    }
231
232    fn init_xterm(defaults: &XtermOptions) -> Result<XtermImpl> {
233        let theme = js_sys::Object::new();
234        let theme_opts = Vec::from([
235            ("background", JsValue::from("rgba(255,255,255,1)")),
236            ("foreground", JsValue::from("#000")),
237            ("selection", JsValue::from("rgba(0,0,0,0.25)")),
238            // ("background", JsValue::from("rgba(0,0,0,1)")),
239            // ("foreground", JsValue::from("#FFF")),
240            ("cursor", JsValue::from("#000")),
241        ]);
242        for (k, v) in theme_opts {
243            js_sys::Reflect::set(&theme, &k.into(), &v)?;
244        }
245
246        let font = match platform() {
247            Platform::MacOS | Platform::IOS => "Menlo",
248            Platform::Windows => "Consolas",
249            Platform::Linux => "Ubuntu Mono",
250            _ => "monospace",
251        };
252
253        let options = js_sys::Object::new();
254        let opts = Vec::from([
255            ("allowTransparency", JsValue::from(true)),
256            ("fontFamily", JsValue::from(font)),
257            (
258                "fontSize",
259                JsValue::from(defaults.font_size.unwrap_or(20.0)),
260            ),
261            ("cursorBlink", JsValue::from(true)),
262            ("theme", JsValue::from(theme)),
263        ]);
264        for (k, v) in opts {
265            js_sys::Reflect::set(&options, &k.into(), &v)?;
266        }
267        if let Some(scrollback) = defaults.scrollback {
268            js_sys::Reflect::set(&options, &"scrollback".into(), &JsValue::from(scrollback))?;
269        }
270
271        let term = XtermImpl::new(options);
272
273        Ok(term)
274    }
275
276    // pub fn xterm(&self) -> MutexGuard<Option<XtermImpl>> {
277    pub fn xterm(&self) -> RefMut<'_, Option<XtermImpl>> {
278        //MutexGuard<Option<XtermImpl>> {
279        // self.xterm.lock().unwrap()
280        self.xterm.borrow_mut()
281    }
282
283    pub fn update_theme(&self) -> Result<()> {
284        let el = self
285            .xterm
286            .borrow_mut()
287            // .unwrap()
288            .as_ref()
289            .expect("xterm is missing")
290            .get_element();
291        let css = window().get_computed_style(&el)?;
292        //log_trace!("css: {:?}", css);
293        if let Some(css) = css {
294            let keys = Vec::from([
295                ("background", "--workflow-terminal-background"),
296                ("foreground", "--workflow-terminal-foreground"),
297                ("cursor", "--workflow-terminal-cursor"),
298                ("selection", "--workflow-terminal-selection"),
299            ]);
300            let theme_obj = js_sys::Object::new();
301            for (key, css_var) in keys {
302                if let Ok(value) = css.get_property_value(css_var) {
303                    // log_trace!("workflow-terminal: `{}`: {:?}", key, value);
304                    js_sys::Reflect::set(
305                        &theme_obj,
306                        &JsValue::from(key),
307                        &JsValue::from(value.trim()),
308                    )?;
309                }
310            }
311
312            let term = self.xterm.borrow_mut();
313            let term = term.as_ref().expect("xterm is missing");
314            term.set_theme(theme_obj);
315        }
316
317        Ok(())
318    }
319    pub fn set_theme(&self, theme: Theme) -> Result<()> {
320        let theme_obj = js_sys::Object::new();
321        let properties = ThemeOption::list();
322
323        for key in properties {
324            if let Some(v) = theme.get(&key) {
325                js_sys::Reflect::set(
326                    &theme_obj,
327                    &JsValue::from(key.to_string().to_lowercase()),
328                    &JsValue::from(v),
329                )?;
330            }
331        }
332
333        let term = self.xterm.borrow_mut();
334        let term = term.as_ref().expect("xterm is missing");
335        term.set_theme(theme_obj);
336        Ok(())
337    }
338
339    fn init_addons(&self, xterm: &XtermImpl) -> Result<()> {
340        let fit = FitAddon::new();
341        xterm.load_addon(fit.clone());
342        *self.fit.borrow_mut() = Some(fit);
343        Ok(())
344    }
345
346    pub async fn init(self: &Arc<Self>, terminal: &Arc<Terminal>) -> Result<()> {
347        load_scripts().await?;
348
349        let xterm = Self::init_xterm(&self.defaults)?;
350
351        self.init_addons(&xterm)?;
352
353        xterm.open(&self.element);
354        xterm.focus();
355
356        self.init_kbd_listener(&xterm)?;
357        self.init_resize_observer()?;
358        if runtime::is_macos() && !self.disable_clipboard_handling {
359            self.init_clipboard_listener_for_macos(&xterm)?;
360        }
361
362        *self.xterm.borrow_mut() = Some(xterm);
363        *self.terminal.lock().unwrap() = Some(terminal.clone());
364
365        Ok(())
366    }
367
368    pub fn set_option(&self, name: &str, option: JsValue) -> Result<()> {
369        let xterm = self.xterm();
370        let xterm = xterm.as_ref().expect("unable to get xterm");
371        xterm.set_option(name, option);
372        Ok(())
373    }
374
375    pub fn get_option(&self, name: &str) -> Result<JsValue> {
376        let xterm = self.xterm();
377        let xterm = xterm.as_ref().expect("unable to get xterm");
378        Ok(xterm.get_option(name))
379    }
380
381    pub fn refresh(&self, start: u32, stop: u32) {
382        let xterm = self.xterm();
383        let xterm = xterm.as_ref().expect("unable to get xterm");
384        xterm.refresh(start, stop);
385    }
386
387    fn event_handler(&self) -> Option<EventHandlerFn> {
388        self.event_handler.borrow().clone()
389    }
390
391    #[allow(dead_code)]
392    pub(super) fn register_event_handler(self: &Arc<Self>, handler: EventHandlerFn) -> Result<()> {
393        self.event_handler.borrow_mut().replace(handler);
394        Ok(())
395    }
396
397    #[allow(dead_code)]
398    pub(super) fn register_link_matcher(
399        self: &Arc<Self>,
400        regexp: &js_sys::RegExp,
401        handler: LinkMatcherHandlerFn,
402    ) -> Result<()> {
403        let xterm = self.xterm();
404        let xterm = xterm.as_ref().expect("unable to get xterm");
405
406        #[rustfmt::skip]
407        let callback = callback!(
408            move |e: web_sys::MouseEvent, link: String| -> std::result::Result<(), JsValue> {
409                let modifiers = Modifiers {
410                    shift: e.shift_key(),
411                    ctrl: e.ctrl_key(),
412                    alt: e.alt_key(),
413                    meta: e.meta_key(),
414                };
415                handler(modifiers, link.as_str());
416                Ok(())
417            }
418        );
419        xterm.register_link_matcher(regexp, callback.as_ref());
420        self.callbacks.retain(callback)?;
421
422        Ok(())
423    }
424
425    pub fn paste(&self, text: Option<String>) -> Result<()> {
426        self.sink
427            .sender
428            .try_send(Ctl::Paste(text))
429            .map_err(|_| "Unable to send paste Ctl")?;
430        Ok(())
431    }
432
433    fn init_resize_observer(self: &Arc<Self>) -> Result<()> {
434        let this = self.clone();
435        let resize_callback = callback!(move |_| -> std::result::Result<(), JsValue> {
436            if let Err(err) = this.resize() {
437                log_error!("terminal resize error: {:?}", err);
438            }
439            Ok(())
440        });
441        let resize_observer = ResizeObserver::new(resize_callback.as_ref())?;
442        resize_observer.observe(&self.element);
443        *self.resize.borrow_mut() = Some(ResizeObserverInfo::new(resize_observer, resize_callback));
444
445        Ok(())
446    }
447
448    fn init_clipboard_listener_for_macos(self: &Arc<Self>, xterm: &XtermImpl) -> Result<()> {
449        let this = self.clone();
450        let clipboard_callback = callback!(
451            move |e: web_sys::KeyboardEvent| -> std::result::Result<(), JsValue> {
452                // log_trace!("xterm: key:{}, ctrl_key:{}, meta_key:{},  {:?}", e.key(), e.ctrl_key(), e.meta_key(), e);
453                if e.key() == "v" && (e.ctrl_key() || e.meta_key()) {
454                    this.sink
455                        .sender
456                        .try_send(Ctl::Paste(None))
457                        .expect("Unable to send paste Ctl");
458                }
459                if e.key() == "c" && (e.ctrl_key() || e.meta_key()) {
460                    let text = this.xterm().as_ref().unwrap().get_selection();
461                    this.sink
462                        .sender
463                        .try_send(Ctl::Copy(Some(text)))
464                        .expect("Unable to send copy Ctl");
465                }
466                Ok(())
467            }
468        );
469
470        xterm
471            .get_element()
472            .add_event_listener_with_callback("keydown", clipboard_callback.as_ref())?;
473        self.callbacks.retain(clipboard_callback)?;
474
475        Ok(())
476    }
477
478    fn init_kbd_listener(self: &Arc<Self>, xterm: &XtermImpl) -> Result<()> {
479        let this = self.clone();
480        let callback = callback!(move |e: XtermEvent| -> std::result::Result<(), JsValue> {
481            //let term_key = try_get_string(&e, "key")?;
482            let term_key = e.get_key();
483            let dom_event = e.get_dom_event();
484            let key = dom_event.key();
485            let ctrl_key = dom_event.ctrl_key();
486            let alt_key = dom_event.alt_key();
487            let meta_key = dom_event.meta_key();
488
489            // log_info!("key: {key}, ctrl: {ctrl_key}, alt: {alt_key}, meta: {meta_key}");
490
491            if !runtime::is_macos() && !this.disable_clipboard_handling {
492                if (key == "v" || key == "V") && (ctrl_key || meta_key) {
493                    this.sink
494                        .sender
495                        .try_send(Ctl::Paste(None))
496                        .expect("Unable to send paste Ctl");
497                    return Ok(());
498                } else if (key == "c" || key == "C") && (ctrl_key || meta_key) {
499                    let text = this.xterm().as_ref().unwrap().get_selection();
500                    this.sink
501                        .sender
502                        .try_send(Ctl::Copy(Some(text)))
503                        .expect("Unable to send copy Ctl");
504                    return Ok(());
505                }
506            }
507
508            this.sink
509                .sender
510                .try_send(Ctl::SinkEvent(SinkEvent::new(
511                    key, term_key, ctrl_key, alt_key, meta_key,
512                )))
513                .unwrap();
514
515            Ok(())
516        });
517
518        xterm.on_key(callback.as_ref());
519        *self.listener.lock().unwrap() = Some(callback);
520
521        Ok(())
522    }
523
524    pub fn terminal(&self) -> Arc<Terminal> {
525        self.terminal.lock().unwrap().as_ref().unwrap().clone()
526    }
527
528    pub async fn run(self: &Arc<Self>) -> Result<()> {
529        self.intake(&self.terminate).await?;
530        Ok(())
531    }
532
533    pub async fn intake(self: &Arc<Self>, terminate: &Arc<AtomicBool>) -> Result<()> {
534        loop {
535            if terminate.load(Ordering::SeqCst) {
536                break;
537            }
538
539            let event = self.sink.receiver.recv().await?;
540            match event {
541                Ctl::SinkEvent(event) => {
542                    self.sink(event).await?;
543                }
544                Ctl::Copy(text) => {
545                    let text =
546                        text.unwrap_or_else(|| self.xterm().as_ref().unwrap().get_selection());
547                    if runtime::is_nw() {
548                        let clipboard = nw_sys::clipboard::get();
549                        clipboard.set(&text);
550                    } else if let Err(err) = clipboard::write_text(&text).await {
551                        log_error!("{}", JsErrorData::from(err));
552                    }
553
554                    if let Some(handler) = self.event_handler() {
555                        handler(Event::Copy);
556                    }
557                }
558                Ctl::Paste(text) => {
559                    if let Some(text) = text {
560                        self.terminal().inject(text)?;
561                    } else if runtime::is_nw() {
562                        let clipboard = nw_sys::clipboard::get();
563                        let text = clipboard.get();
564                        if !text.is_empty() {
565                            self.terminal().inject(text)?;
566                        }
567                    } else {
568                        let data_js_value = clipboard::read_text().await;
569                        if let Some(text) = data_js_value.as_string() {
570                            self.terminal().inject(text)?;
571                        }
572                    }
573
574                    if let Some(handler) = self.event_handler() {
575                        handler(Event::Copy);
576                    }
577                }
578                Ctl::Close => {
579                    break;
580                }
581            }
582        }
583
584        Ok(())
585    }
586
587    pub fn exit(&self) {
588        self.terminate.store(true, Ordering::SeqCst);
589        self.sink
590            .sender
591            .try_send(Ctl::Close)
592            .expect("Unable to send exit Ctl");
593    }
594
595    async fn sink(&self, e: SinkEvent) -> Result<()> {
596        let key = match e.key.as_str() {
597            "Backspace" => Key::Backspace,
598            "ArrowUp" => Key::ArrowUp,
599            "ArrowDown" => Key::ArrowDown,
600            "ArrowLeft" => Key::ArrowLeft,
601            "ArrowRight" => Key::ArrowRight,
602            "Escape" => Key::Esc,
603            "Delete" => Key::Delete,
604            "Tab" => {
605                // TODO implement completion handler
606                return Ok(());
607            }
608            "Enter" => Key::Enter,
609            _ => {
610                let printable = !e.meta_key; // ! (e.ctrl_key || e.alt_key || e.meta_key);
611                if !printable {
612                    return Ok(());
613                }
614                //log_trace!("e:{:?}", e);
615                if let Some(c) = e.key.chars().next() {
616                    if e.ctrl_key {
617                        Key::Ctrl(c)
618                    } else if e.alt_key {
619                        Key::Alt(c)
620                    } else {
621                        Key::Char(c)
622                    }
623                } else {
624                    return Ok(());
625                }
626            }
627        };
628
629        self.terminal().ingest(key, e.term_key).await?;
630
631        Ok(())
632    }
633
634    pub fn write<S>(&self, s: S)
635    where
636        S: ToString,
637    {
638        self.xterm
639            .borrow_mut()
640            .as_ref()
641            .expect("Xterm is not initialized")
642            .write(s);
643    }
644
645    pub fn measure(&self) -> Result<()> {
646        let xterm = self.xterm.borrow_mut();
647        let xterm = xterm.as_ref().unwrap();
648        let core = try_get_js_value_prop(xterm, "_core").expect("Unable to get xterm core");
649        let char_size_service = try_get_js_value_prop(&core, "_charSizeService")
650            .expect("Unable to get xterm charSizeService");
651        let has_valid_size = try_get_js_value_prop(&char_size_service, "hasValidSize")
652            .expect("Unable to get xterm charSizeService::hasValidSize");
653        if has_valid_size.is_falsy() {
654            apply_with_args0(&char_size_service, "measure")?;
655        }
656
657        Ok(())
658    }
659
660    pub fn resize(&self) -> Result<()> {
661        //self.measure()?;
662        if let Some(xterm) = self.xterm.borrow_mut().as_ref() {
663            let el = xterm.get_element();
664            let height = el.client_height();
665            if height < 1 {
666                return Ok(());
667            }
668        } else {
669            return Ok(());
670        }
671
672        let fit = self.fit.borrow();
673        let fit = fit.as_ref().unwrap();
674        // TODO review if this is correct
675        //fit.propose_dimensions();
676        // TODO review if this is correct
677        fit.fit();
678
679        Ok(())
680    }
681
682    pub fn get_font_size(&self) -> Result<Option<f64>> {
683        let font_size = self.get_option("fontSize")?;
684        Ok(font_size.as_f64())
685    }
686
687    pub fn set_font_size(&self, font_size: f64) -> Result<()> {
688        self.set_option("fontSize", JsValue::from_f64(font_size))
689    }
690
691    pub fn cols(&self) -> Option<usize> {
692        self.xterm().as_ref().map(|xterm| xterm.cols() as usize)
693    }
694
695    pub fn rows(&self) -> Option<usize> {
696        self.xterm().as_ref().map(|xterm| xterm.rows() as usize)
697    }
698
699    fn adjust_font_size(&self, delta: f64) -> Result<Option<f64>> {
700        let font_size = self.get_option("fontSize")?;
701        let mut font_size = font_size.as_f64().ok_or("Unable to get font size")?;
702        font_size += delta;
703        if font_size < 4.0 {
704            font_size = 4.0;
705        }
706
707        self.set_option("fontSize", JsValue::from_f64(font_size))?;
708        self.resize()?;
709        Ok(Some(font_size))
710    }
711
712    pub fn increase_font_size(&self) -> Result<Option<f64>> {
713        self.adjust_font_size(1.0)
714    }
715
716    pub fn decrease_font_size(&self) -> Result<Option<f64>> {
717        self.adjust_font_size(-1.0)
718    }
719
720    pub fn clipboard_copy(&self) -> Result<()> {
721        let text = self.xterm().as_ref().unwrap().get_selection();
722        self.sink
723            .sender
724            .try_send(Ctl::Copy(Some(text)))
725            .expect("Unable to send copy Ctl");
726        if let Some(handler) = self.event_handler() {
727            handler(Event::Copy);
728        }
729
730        Ok(())
731    }
732
733    pub fn clipboard_paste(&self) -> Result<()> {
734        self.sink
735            .sender
736            .try_send(Ctl::Paste(None))
737            .expect("Unable to send paste Ctl");
738        if let Some(handler) = self.event_handler() {
739            handler(Event::Paste);
740        }
741
742        Ok(())
743    }
744}
745
746// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
747
748static mut XTERM_LOADED: bool = false;
749
750pub async fn load_scripts() -> Result<()> {
751    if unsafe { XTERM_LOADED } {
752        return Ok(());
753    }
754
755    let xterm_js = include_bytes!("../../extern/resources/xterm.js");
756    inject_blob(Content::Script(None, xterm_js)).await?;
757    let xterm_addon_fit_js = include_bytes!("../../extern/resources/xterm-addon-fit.js");
758    inject_blob(Content::Script(None, xterm_addon_fit_js)).await?;
759    let xterm_addon_web_links_js =
760        include_bytes!("../../extern/resources/xterm-addon-web-links.js");
761    inject_blob(Content::Script(None, xterm_addon_web_links_js)).await?;
762    let xterm_css = include_bytes!("../../extern/resources/xterm.css");
763    inject_blob(Content::Style(None, xterm_css)).await?;
764
765    unsafe { XTERM_LOADED = true };
766
767    Ok(())
768}