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