connected_view.rs

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