connected_view.rs

  1use std::time::Duration;
  2
  3use alacritty_terminal::term::TermMode;
  4use context_menu::{ContextMenu, ContextMenuItem};
  5use gpui::{
  6    actions,
  7    elements::{ChildView, ParentElement, Stack},
  8    geometry::vector::Vector2F,
  9    impl_internal_actions,
 10    keymap::Keystroke,
 11    AnyViewHandle, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View,
 12    ViewContext, ViewHandle,
 13};
 14use smol::Timer;
 15use workspace::pane;
 16
 17use crate::{connected_el::TerminalEl, Event, Terminal};
 18
 19const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 20
 21///Event to transmit the scroll from the element to the view
 22#[derive(Clone, Debug, PartialEq)]
 23pub struct ScrollTerminal(pub i32);
 24
 25#[derive(Clone, PartialEq)]
 26pub struct DeployContextMenu {
 27    pub position: Vector2F,
 28}
 29
 30actions!(
 31    terminal,
 32    [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
 33);
 34impl_internal_actions!(project_panel, [DeployContextMenu]);
 35
 36pub fn init(cx: &mut MutableAppContext) {
 37    //Global binding overrrides
 38    cx.add_action(ConnectedView::ctrl_c);
 39    cx.add_action(ConnectedView::up);
 40    cx.add_action(ConnectedView::down);
 41    cx.add_action(ConnectedView::escape);
 42    cx.add_action(ConnectedView::enter);
 43    //Useful terminal views
 44    cx.add_action(ConnectedView::deploy_context_menu);
 45    cx.add_action(ConnectedView::copy);
 46    cx.add_action(ConnectedView::paste);
 47    cx.add_action(ConnectedView::clear);
 48}
 49
 50///A terminal view, maintains the PTY's file handles and communicates with the terminal
 51pub struct ConnectedView {
 52    terminal: ModelHandle<Terminal>,
 53    has_new_content: bool,
 54    //Currently using iTerm bell, show bell emoji in tab until input is received
 55    has_bell: bool,
 56    // Only for styling purposes. Doesn't effect behavior
 57    modal: bool,
 58    context_menu: ViewHandle<ContextMenu>,
 59    show_cursor: bool,
 60    blinking_paused: bool,
 61    blink_epoch: usize,
 62}
 63
 64impl ConnectedView {
 65    pub fn from_terminal(
 66        terminal: ModelHandle<Terminal>,
 67        modal: bool,
 68        cx: &mut ViewContext<Self>,
 69    ) -> Self {
 70        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
 71        cx.subscribe(&terminal, |this, _, event, cx| match event {
 72            Event::Wakeup => {
 73                if !cx.is_self_focused() {
 74                    this.has_new_content = true;
 75                    cx.notify();
 76                    cx.emit(Event::Wakeup);
 77                }
 78            }
 79            Event::Bell => {
 80                this.has_bell = true;
 81                cx.emit(Event::Wakeup);
 82            }
 83
 84            _ => cx.emit(*event),
 85        })
 86        .detach();
 87
 88        Self {
 89            terminal,
 90            has_new_content: true,
 91            has_bell: false,
 92            modal,
 93            context_menu: cx.add_view(ContextMenu::new),
 94            show_cursor: true,
 95            blinking_paused: false,
 96            blink_epoch: 0,
 97        }
 98    }
 99
100    pub fn handle(&self) -> ModelHandle<Terminal> {
101        self.terminal.clone()
102    }
103
104    pub fn has_new_content(&self) -> bool {
105        self.has_new_content
106    }
107
108    pub fn has_bell(&self) -> bool {
109        self.has_bell
110    }
111
112    pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
113        self.has_bell = false;
114        cx.emit(Event::Wakeup);
115    }
116
117    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
118        let menu_entries = vec![
119            ContextMenuItem::item("Clear Buffer", Clear),
120            ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
121        ];
122
123        self.context_menu
124            .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
125
126        cx.notify();
127    }
128
129    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
130        self.terminal.update(cx, |term, _| term.clear());
131        cx.notify();
132    }
133
134    //Following code copied from editor cursor
135    pub fn blink_show(&self) -> bool {
136        self.blinking_paused || self.show_cursor
137    }
138
139    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
140        if epoch == self.blink_epoch && !self.blinking_paused {
141            self.show_cursor = !self.show_cursor;
142            cx.notify();
143
144            let epoch = self.next_blink_epoch();
145            cx.spawn(|this, mut cx| {
146                let this = this.downgrade();
147                async move {
148                    Timer::after(CURSOR_BLINK_INTERVAL).await;
149                    if let Some(this) = this.upgrade(&cx) {
150                        this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
151                    }
152                }
153            })
154            .detach();
155        }
156    }
157
158    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
159        self.show_cursor = true;
160        cx.notify();
161
162        let epoch = self.next_blink_epoch();
163        cx.spawn(|this, mut cx| {
164            let this = this.downgrade();
165            async move {
166                Timer::after(CURSOR_BLINK_INTERVAL).await;
167                if let Some(this) = this.upgrade(&cx) {
168                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
169                }
170            }
171        })
172        .detach();
173    }
174
175    fn next_blink_epoch(&mut self) -> usize {
176        self.blink_epoch += 1;
177        self.blink_epoch
178    }
179
180    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
181        if epoch == self.blink_epoch {
182            self.blinking_paused = false;
183            self.blink_cursors(epoch, cx);
184        }
185    }
186
187    ///Attempt to paste the clipboard into the terminal
188    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
189        self.terminal.update(cx, |term, _| term.copy())
190    }
191
192    ///Attempt to paste the clipboard into the terminal
193    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
194        if let Some(item) = cx.read_from_clipboard() {
195            self.terminal.read(cx).paste(item.text());
196        }
197    }
198
199    ///Synthesize the keyboard event corresponding to 'up'
200    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
201        self.clear_bel(cx);
202        self.terminal
203            .read(cx)
204            .try_keystroke(&Keystroke::parse("up").unwrap());
205    }
206
207    ///Synthesize the keyboard event corresponding to 'down'
208    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
209        self.clear_bel(cx);
210        self.terminal
211            .read(cx)
212            .try_keystroke(&Keystroke::parse("down").unwrap());
213    }
214
215    ///Synthesize the keyboard event corresponding to 'ctrl-c'
216    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
217        self.clear_bel(cx);
218        self.terminal
219            .read(cx)
220            .try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
221    }
222
223    ///Synthesize the keyboard event corresponding to 'escape'
224    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
225        self.clear_bel(cx);
226        self.terminal
227            .read(cx)
228            .try_keystroke(&Keystroke::parse("escape").unwrap());
229    }
230
231    ///Synthesize the keyboard event corresponding to 'enter'
232    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
233        self.clear_bel(cx);
234        self.terminal
235            .read(cx)
236            .try_keystroke(&Keystroke::parse("enter").unwrap());
237    }
238}
239
240impl View for ConnectedView {
241    fn ui_name() -> &'static str {
242        "Terminal"
243    }
244
245    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
246        let terminal_handle = self.terminal.clone().downgrade();
247
248        let self_id = cx.view_id();
249        let focused = cx
250            .focused_view_id(cx.window_id())
251            .filter(|view_id| *view_id == self_id)
252            .is_some();
253
254        Stack::new()
255            .with_child(
256                TerminalEl::new(
257                    cx.handle(),
258                    terminal_handle,
259                    self.modal,
260                    focused,
261                    self.blink_show(),
262                )
263                .contained()
264                .boxed(),
265            )
266            .with_child(ChildView::new(&self.context_menu).boxed())
267            .boxed()
268    }
269
270    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
271        self.has_new_content = false;
272        self.terminal.read(cx).focus_in();
273        self.blink_cursors(self.blink_epoch, cx);
274        cx.notify();
275    }
276
277    fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
278        self.terminal.read(cx).focus_out();
279        cx.notify();
280    }
281
282    //IME stuff
283    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
284        if self
285            .terminal
286            .read(cx)
287            .last_mode
288            .contains(TermMode::ALT_SCREEN)
289        {
290            None
291        } else {
292            Some(0..0)
293        }
294    }
295
296    fn replace_text_in_range(
297        &mut self,
298        _: Option<std::ops::Range<usize>>,
299        text: &str,
300        cx: &mut ViewContext<Self>,
301    ) {
302        self.terminal
303            .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
304    }
305
306    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
307        let mut context = Self::default_keymap_context();
308        if self.modal {
309            context.set.insert("ModalTerminal".into());
310        }
311        context
312    }
313}