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