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