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