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
157pub 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 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 ("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) -> RefMut<'_, Option<XtermImpl>> {
278 self.xterm.borrow_mut()
281 }
282
283 pub fn update_theme(&self) -> Result<()> {
284 let el = self
285 .xterm
286 .borrow_mut()
287 .as_ref()
289 .expect("xterm is missing")
290 .get_element();
291 let css = window().get_computed_style(&el)?;
292 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 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 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 = 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 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 return Ok(());
607 }
608 "Enter" => Key::Enter,
609 _ => {
610 let printable = !e.meta_key; if !printable {
612 return Ok(());
613 }
614 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 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 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
746static 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}