connection.rs

  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}