connected_view.rs

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