Skip to main content

freya_terminal/
pty.rs

1use std::{
2    cell::RefCell,
3    path::PathBuf,
4    rc::Rc,
5    time::Instant,
6};
7
8use alacritty_terminal::{
9    event::{
10        Event as AlacrittyEvent,
11        EventListener,
12    },
13    grid::Dimensions,
14    term::{
15        Config as TermConfig,
16        Term,
17    },
18    vte::{
19        Parser as VteParser,
20        ansi::{
21            Processor,
22            StdSyncHandler,
23        },
24    },
25};
26use freya_core::{
27    notify::ArcNotify,
28    prelude::{
29        Platform,
30        UserEvent,
31        spawn_forever,
32    },
33};
34use futures_lite::AsyncReadExt;
35use keyboard_types::Modifiers;
36use portable_pty::{
37    CommandBuilder,
38    PtySize,
39    native_pty_system,
40};
41
42use crate::{
43    handle::{
44        TerminalCleaner,
45        TerminalError,
46        TerminalHandle,
47        TerminalId,
48        TerminalInner,
49    },
50    osc7::{
51        CwdSink,
52        parse_cwd_url,
53    },
54};
55
56/// `Dimensions` impl passed to `Term::new` / `Term::resize`.
57#[derive(Clone, Copy)]
58pub(crate) struct TermSize {
59    pub screen_lines: usize,
60    pub columns: usize,
61}
62
63impl Dimensions for TermSize {
64    fn total_lines(&self) -> usize {
65        self.screen_lines
66    }
67
68    fn screen_lines(&self) -> usize {
69        self.screen_lines
70    }
71
72    fn columns(&self) -> usize {
73        self.columns
74    }
75}
76
77/// Listener proxy passed into alacritty's `Term`. Routes its side-effects
78/// (PtyWrite, Title, ClipboardStore) into the freya-side state.
79#[derive(Clone)]
80pub struct EventProxy {
81    pub(crate) writer: Rc<RefCell<Option<Box<dyn std::io::Write + Send>>>>,
82    pub(crate) title: Rc<RefCell<Option<String>>>,
83    pub(crate) title_notifier: ArcNotify,
84    pub(crate) clipboard_content: Rc<RefCell<Option<String>>>,
85    pub(crate) clipboard_notifier: ArcNotify,
86}
87
88impl EventListener for EventProxy {
89    fn send_event(&self, event: AlacrittyEvent) {
90        match event {
91            AlacrittyEvent::PtyWrite(text) => {
92                if let Some(writer) = &mut *self.writer.borrow_mut() {
93                    let _ = writer.write_all(text.as_bytes());
94                    let _ = writer.flush();
95                }
96            }
97            AlacrittyEvent::Title(t) => {
98                *self.title.borrow_mut() = Some(t);
99                self.title_notifier.notify();
100            }
101            AlacrittyEvent::ResetTitle => {
102                *self.title.borrow_mut() = None;
103                self.title_notifier.notify();
104            }
105            AlacrittyEvent::ClipboardStore(_, text) => {
106                *self.clipboard_content.borrow_mut() = Some(text);
107                self.clipboard_notifier.notify();
108            }
109            // Bell, MouseCursorDirty, ChildExit, ColorRequest, etc.
110            _ => {}
111        }
112    }
113}
114
115/// Spawn a PTY and return a `TerminalHandle`.
116pub(crate) fn spawn_pty(
117    id: TerminalId,
118    command: CommandBuilder,
119    scrollback_size: usize,
120) -> Result<TerminalHandle, TerminalError> {
121    let writer = Rc::new(RefCell::new(None::<Box<dyn std::io::Write + Send>>));
122    let closer_notifier = ArcNotify::new();
123    let output_notifier = ArcNotify::new();
124    let title_notifier = ArcNotify::new();
125    let cwd: Rc<RefCell<Option<PathBuf>>> = Rc::new(RefCell::new(None));
126    let title: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
127    let clipboard_content: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
128    let clipboard_notifier = ArcNotify::new();
129
130    let event_proxy = EventProxy {
131        writer: writer.clone(),
132        title: title.clone(),
133        title_notifier: title_notifier.clone(),
134        clipboard_content: clipboard_content.clone(),
135        clipboard_notifier: clipboard_notifier.clone(),
136    };
137
138    let term_config = TermConfig {
139        scrolling_history: scrollback_size,
140        ..TermConfig::default()
141    };
142    let initial_size = TermSize {
143        screen_lines: 24,
144        columns: 80,
145    };
146    let term = Rc::new(RefCell::new(Term::new(
147        term_config,
148        &initial_size,
149        event_proxy,
150    )));
151
152    let pty_system = native_pty_system();
153    let pair = pty_system
154        .openpty(PtySize::default())
155        .map_err(|_| TerminalError::NotInitialized)?;
156    let master_writer = pair
157        .master
158        .take_writer()
159        .map_err(|_| TerminalError::NotInitialized)?;
160    *writer.borrow_mut() = Some(master_writer);
161
162    pair.slave
163        .spawn_command(command)
164        .map_err(|_| TerminalError::NotInitialized)?;
165    let reader = pair
166        .master
167        .try_clone_reader()
168        .map_err(|_| TerminalError::NotInitialized)?;
169    let mut reader = blocking::Unblock::new(reader);
170
171    let inner = Rc::new(RefCell::new(TerminalInner {
172        master: pair.master,
173        last_write_time: Instant::now(),
174        pressed_button: None,
175        modifiers: Modifiers::empty(),
176    }));
177
178    let platform = Platform::get();
179    let pty_task = spawn_forever({
180        let term = term.clone();
181        let writer = writer.clone();
182        let closer_notifier = closer_notifier.clone();
183        let output_notifier = output_notifier.clone();
184        let cwd = cwd.clone();
185        async move {
186            let mut processor = Processor::<StdSyncHandler>::new();
187            // Side-channel parser for OSC 7 (cwd), which alacritty drops.
188            let mut cwd_parser = VteParser::new();
189            let mut cwd_sink = CwdSink::default();
190            loop {
191                let mut buf = [0u8; 4096];
192                match reader.read(&mut buf).await {
193                    Ok(0) => break,
194                    Ok(n) => {
195                        let data = &buf[..n];
196                        processor.advance(&mut *term.borrow_mut(), data);
197
198                        cwd_parser.advance(&mut cwd_sink, data);
199                        if let Some(url) = cwd_sink.take() {
200                            *cwd.borrow_mut() = Some(parse_cwd_url(&url));
201                        }
202
203                        output_notifier.notify();
204                        platform.send(UserEvent::RequestRedraw);
205                    }
206                    Err(_) => break,
207                }
208            }
209            // PTY closed: drop the writer and notify observers.
210            *writer.borrow_mut() = None;
211            closer_notifier.notify();
212            platform.send(UserEvent::RequestRedraw);
213        }
214    });
215
216    Ok(TerminalHandle {
217        closer_notifier: closer_notifier.clone(),
218        cleaner: Rc::new(TerminalCleaner {
219            writer: writer.clone(),
220            pty_task,
221            closer_notifier,
222        }),
223        id,
224        term,
225        writer,
226        inner,
227        cwd,
228        title,
229        title_notifier,
230        clipboard_content,
231        clipboard_notifier,
232        output_notifier,
233    })
234}