1use alacritty_terminal::{
2 ansi::{ClearMode, Handler},
3 config::{Config, PtyConfig},
4 event::{Event as AlacTermEvent, Notify},
5 event_loop::{EventLoop, Msg, Notifier},
6 grid::Scroll,
7 sync::FairMutex,
8 term::SizeInfo,
9 tty::{self, setup_env},
10 Term,
11};
12use futures::{channel::mpsc::unbounded, StreamExt};
13use settings::Settings;
14use std::{collections::HashMap, path::PathBuf, sync::Arc};
15
16use gpui::{ClipboardItem, CursorStyle, Entity, ModelContext};
17
18use crate::{
19 color_translation::{get_color_at_index, to_alac_rgb},
20 ZedListener,
21};
22
23const DEFAULT_TITLE: &str = "Terminal";
24
25///Upward flowing events, for changing the title and such
26#[derive(Copy, Clone, Debug)]
27pub enum Event {
28 TitleChanged,
29 CloseTerminal,
30 Activate,
31 Wakeup,
32 Bell,
33}
34
35pub struct TerminalConnection {
36 pub pty_tx: Notifier,
37 pub term: Arc<FairMutex<Term<ZedListener>>>,
38 pub title: String,
39 pub associated_directory: Option<PathBuf>,
40}
41
42impl TerminalConnection {
43 pub fn new(
44 working_directory: Option<PathBuf>,
45 initial_size: SizeInfo,
46 cx: &mut ModelContext<Self>,
47 ) -> TerminalConnection {
48 let pty_config = PtyConfig {
49 shell: None, //Use the users default shell
50 working_directory: working_directory.clone(),
51 hold: false,
52 };
53
54 let mut env: HashMap<String, String> = HashMap::new();
55 //TODO: Properly set the current locale,
56 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
57
58 let config = Config {
59 pty_config: pty_config.clone(),
60 env,
61 ..Default::default()
62 };
63
64 setup_env(&config);
65
66 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
67 let (events_tx, mut events_rx) = unbounded();
68
69 //Set up the terminal...
70 let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
71 let term = Arc::new(FairMutex::new(term));
72
73 //Setup the pty...
74 let pty = tty::new(&pty_config, &initial_size, None).expect("Could not create tty");
75
76 //And connect them together
77 let event_loop = EventLoop::new(
78 term.clone(),
79 ZedListener(events_tx.clone()),
80 pty,
81 pty_config.hold,
82 false,
83 );
84
85 //Kick things off
86 let pty_tx = event_loop.channel();
87 let _io_thread = event_loop.spawn();
88
89 cx.spawn_weak(|this, mut cx| async move {
90 //Listen for terminal events
91 while let Some(event) = events_rx.next().await {
92 match this.upgrade(&cx) {
93 Some(this) => {
94 this.update(&mut cx, |this, cx| {
95 this.process_terminal_event(event, cx);
96 cx.notify();
97 });
98 }
99 None => break,
100 }
101 }
102 })
103 .detach();
104
105 TerminalConnection {
106 pty_tx: Notifier(pty_tx),
107 term,
108 title: DEFAULT_TITLE.to_string(),
109 associated_directory: working_directory,
110 }
111 }
112
113 ///Takes events from Alacritty and translates them to behavior on this view
114 fn process_terminal_event(
115 &mut self,
116 event: alacritty_terminal::event::Event,
117 cx: &mut ModelContext<Self>,
118 ) {
119 match event {
120 // TODO: Handle is_self_focused in subscription on terminal view
121 AlacTermEvent::Wakeup => {
122 cx.emit(Event::Wakeup);
123 }
124 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
125 AlacTermEvent::MouseCursorDirty => {
126 //Calculate new cursor style.
127 //TODO: alacritty/src/input.rs:L922-L939
128 //Check on correctly handling mouse events for terminals
129 cx.platform().set_cursor_style(CursorStyle::Arrow); //???
130 }
131 AlacTermEvent::Title(title) => {
132 self.title = title;
133 cx.emit(Event::TitleChanged);
134 }
135 AlacTermEvent::ResetTitle => {
136 self.title = DEFAULT_TITLE.to_string();
137 cx.emit(Event::TitleChanged);
138 }
139 AlacTermEvent::ClipboardStore(_, data) => {
140 cx.write_to_clipboard(ClipboardItem::new(data))
141 }
142 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
143 &cx.read_from_clipboard()
144 .map(|ci| ci.text().to_string())
145 .unwrap_or("".to_string()),
146 )),
147 AlacTermEvent::ColorRequest(index, format) => {
148 let color = self.term.lock().colors()[index].unwrap_or_else(|| {
149 let term_style = &cx.global::<Settings>().theme.terminal;
150 to_alac_rgb(get_color_at_index(&index, &term_style.colors))
151 });
152 self.write_to_pty(format(color))
153 }
154 AlacTermEvent::CursorBlinkingChange => {
155 //TODO: Set a timer to blink the cursor on and off
156 }
157 AlacTermEvent::Bell => {
158 cx.emit(Event::Bell);
159 }
160 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
161 }
162 }
163
164 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
165 pub fn write_to_pty(&mut self, input: String) {
166 self.write_bytes_to_pty(input.into_bytes());
167 }
168
169 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
170 fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
171 self.term.lock().scroll_display(Scroll::Bottom);
172 self.pty_tx.notify(input);
173 }
174
175 ///Resize the terminal and the PTY. This locks the terminal.
176 pub fn set_size(&mut self, new_size: SizeInfo) {
177 self.pty_tx.0.send(Msg::Resize(new_size)).ok();
178 self.term.lock().resize(new_size);
179 }
180
181 pub fn clear(&mut self) {
182 self.write_to_pty("\x0c".into());
183 self.term.lock().clear_screen(ClearMode::Saved);
184 }
185}
186
187impl Drop for TerminalConnection {
188 fn drop(&mut self) {
189 self.pty_tx.0.send(Msg::Shutdown).ok();
190 }
191}
192
193impl Entity for TerminalConnection {
194 type Event = Event;
195}