connected_view.rs

  1use gpui::{
  2    actions, keymap::Keystroke, AppContext, ClipboardItem, Element, ElementBox, ModelHandle,
  3    MutableAppContext, View, ViewContext,
  4};
  5
  6use crate::{connected_el::TerminalEl, Event, Terminal};
  7
  8///Event to transmit the scroll from the element to the view
  9#[derive(Clone, Debug, PartialEq)]
 10pub struct ScrollTerminal(pub i32);
 11
 12actions!(
 13    terminal,
 14    [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
 15);
 16
 17pub fn init(cx: &mut MutableAppContext) {
 18    //Global binding overrrides
 19    cx.add_action(ConnectedView::ctrl_c);
 20    cx.add_action(ConnectedView::up);
 21    cx.add_action(ConnectedView::down);
 22    cx.add_action(ConnectedView::escape);
 23    cx.add_action(ConnectedView::enter);
 24    //Useful terminal views
 25    cx.add_action(ConnectedView::copy);
 26    cx.add_action(ConnectedView::paste);
 27    cx.add_action(ConnectedView::clear);
 28}
 29
 30///A terminal view, maintains the PTY's file handles and communicates with the terminal
 31pub struct ConnectedView {
 32    terminal: ModelHandle<Terminal>,
 33    has_new_content: bool,
 34    //Currently using iTerm bell, show bell emoji in tab until input is received
 35    has_bell: bool,
 36    // Only for styling purposes. Doesn't effect behavior
 37    modal: bool,
 38}
 39
 40impl ConnectedView {
 41    pub fn from_terminal(
 42        terminal: ModelHandle<Terminal>,
 43        modal: bool,
 44        cx: &mut ViewContext<Self>,
 45    ) -> Self {
 46        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
 47        cx.subscribe(&terminal, |this, _, event, cx| match event {
 48            Event::Wakeup => {
 49                if cx.is_self_focused() {
 50                    cx.notify()
 51                } else {
 52                    this.has_new_content = true;
 53                    cx.emit(Event::TitleChanged);
 54                }
 55            }
 56            Event::Bell => {
 57                this.has_bell = true;
 58                cx.emit(Event::TitleChanged);
 59            }
 60            _ => cx.emit(*event),
 61        })
 62        .detach();
 63
 64        Self {
 65            terminal,
 66            has_new_content: true,
 67            has_bell: false,
 68            modal,
 69        }
 70    }
 71
 72    pub fn handle(&self) -> ModelHandle<Terminal> {
 73        self.terminal.clone()
 74    }
 75
 76    pub fn has_new_content(&self) -> bool {
 77        self.has_new_content
 78    }
 79
 80    pub fn has_bell(&self) -> bool {
 81        self.has_bell
 82    }
 83
 84    pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
 85        self.has_bell = false;
 86        cx.emit(Event::TitleChanged);
 87    }
 88
 89    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
 90        self.terminal.update(cx, |term, _| term.clear());
 91    }
 92
 93    ///Attempt to paste the clipboard into the terminal
 94    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
 95        self.terminal
 96            .read(cx)
 97            .copy()
 98            .map(|text| cx.write_to_clipboard(ClipboardItem::new(text)));
 99    }
100
101    ///Attempt to paste the clipboard into the terminal
102    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
103        cx.read_from_clipboard().map(|item| {
104            self.terminal.update(cx, |term, _| term.paste(item.text()));
105        });
106    }
107
108    ///Synthesize the keyboard event corresponding to 'up'
109    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
110        self.terminal.update(cx, |term, _| {
111            term.try_keystroke(&Keystroke::parse("up").unwrap());
112        });
113    }
114
115    ///Synthesize the keyboard event corresponding to 'down'
116    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
117        self.terminal.update(cx, |term, _| {
118            term.try_keystroke(&Keystroke::parse("down").unwrap());
119        });
120    }
121
122    ///Synthesize the keyboard event corresponding to 'ctrl-c'
123    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
124        self.terminal.update(cx, |term, _| {
125            term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
126        });
127    }
128
129    ///Synthesize the keyboard event corresponding to 'escape'
130    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
131        self.terminal.update(cx, |term, _| {
132            term.try_keystroke(&Keystroke::parse("escape").unwrap());
133        });
134    }
135
136    ///Synthesize the keyboard event corresponding to 'enter'
137    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
138        self.terminal.update(cx, |term, _| {
139            term.try_keystroke(&Keystroke::parse("enter").unwrap());
140        });
141    }
142}
143
144impl View for ConnectedView {
145    fn ui_name() -> &'static str {
146        "Terminal"
147    }
148
149    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
150        let terminal_handle = self.terminal.clone().downgrade();
151        TerminalEl::new(cx.handle(), terminal_handle, self.modal)
152            .contained()
153            .boxed()
154    }
155
156    fn on_focus(&mut self, _cx: &mut ViewContext<Self>) {
157        self.has_new_content = false;
158    }
159
160    fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
161        Some(0..0)
162    }
163
164    fn replace_text_in_range(
165        &mut self,
166        _: Option<std::ops::Range<usize>>,
167        text: &str,
168        cx: &mut ViewContext<Self>,
169    ) {
170        self.terminal
171            .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
172    }
173}