freya_terminal/
pty.rs

1use std::{
2    cell::RefCell,
3    rc::Rc,
4    time::Instant,
5};
6
7use freya_core::{
8    notify::ArcNotify,
9    prelude::{
10        Platform,
11        UserEvent,
12        spawn_forever,
13    },
14};
15use futures_lite::{
16    AsyncReadExt,
17    StreamExt,
18};
19use futures_util::FutureExt;
20use keyboard_types::Modifiers;
21use portable_pty::{
22    CommandBuilder,
23    PtySize,
24    native_pty_system,
25};
26use vt100::Parser;
27
28use crate::{
29    buffer::TerminalBuffer,
30    handle::{
31        TerminalCleaner,
32        TerminalError,
33        TerminalHandle,
34        TerminalId,
35    },
36    parser::check_for_terminal_queries,
37};
38
39/// Command to control terminal scrollback position.
40#[derive(Debug, Clone)]
41pub enum ScrollCommand {
42    /// Scroll by a relative number of lines (positive = up, negative = down)
43    Delta(i32),
44    /// Jump to the bottom (most recent output)
45    ToBottom,
46}
47
48/// Query the maximum scrollback available without disturbing the viewport.
49/// Saves current scrollback, queries max, and restores.
50fn query_max_scrollback(parser: &mut Parser) -> usize {
51    let saved = parser.screen().scrollback();
52    parser.screen_mut().set_scrollback(usize::MAX);
53    let max = parser.screen().scrollback();
54    parser.screen_mut().set_scrollback(saved);
55    max
56}
57
58/// Extract visible cells from the parser at the current scrollback position.
59fn extract_buffer(
60    parser: &Parser,
61    scroll_offset: usize,
62    total_scrollback: usize,
63) -> TerminalBuffer {
64    let (rows, cols) = parser.screen().size();
65    let rows_vec: Vec<Vec<vt100::Cell>> = (0..rows)
66        .map(|r| {
67            (0..cols)
68                .filter_map(|c| parser.screen().cell(r, c).cloned())
69                .collect()
70        })
71        .collect();
72    let (cur_r, cur_c) = parser.screen().cursor_position();
73    TerminalBuffer {
74        rows: rows_vec,
75        cursor_row: cur_r as usize,
76        cursor_col: cur_c as usize,
77        cols: cols as usize,
78        rows_count: rows as usize,
79        selection: None,
80        scroll_offset,
81        total_scrollback,
82    }
83}
84
85/// Spawn a PTY and return a TerminalHandle
86pub(crate) fn spawn_pty(
87    id: TerminalId,
88    command: CommandBuilder,
89    scrollback_size: usize,
90) -> Result<TerminalHandle, TerminalError> {
91    let (update_tx, mut update_rx) = futures_channel::mpsc::unbounded::<()>();
92    let (resize_tx, mut resize_rx) = futures_channel::mpsc::unbounded::<(u16, u16)>();
93    let (scroll_tx, mut scroll_rx) = futures_channel::mpsc::unbounded::<ScrollCommand>();
94
95    let buffer = Rc::new(RefCell::new(TerminalBuffer::default()));
96    let parser = Rc::new(RefCell::new(Parser::new(24, 80, scrollback_size)));
97    let writer = Rc::new(RefCell::new(None::<Box<dyn std::io::Write + Send>>));
98    let closer_notifier = ArcNotify::new();
99    let output_notifier = ArcNotify::new();
100
101    let pty_system = native_pty_system();
102    let pair = pty_system
103        .openpty(PtySize::default())
104        .map_err(|_| TerminalError::NotInitialized)?;
105    let master_writer = pair
106        .master
107        .take_writer()
108        .map_err(|_| TerminalError::NotInitialized)?;
109    *writer.borrow_mut() = Some(master_writer);
110
111    pair.slave
112        .spawn_command(command)
113        .map_err(|_| TerminalError::NotInitialized)?;
114    let reader = pair
115        .master
116        .try_clone_reader()
117        .map_err(|_| TerminalError::NotInitialized)?;
118    let mut reader = blocking::Unblock::new(reader);
119    let platform = Platform::get();
120    let reader_task = spawn_forever({
121        let parser = parser.clone();
122        let buffer = buffer.clone();
123        let closer_notifier = closer_notifier.clone();
124        let writer = writer.clone();
125        async move {
126            loop {
127                futures_util::select! {
128                     update = update_rx.next().fuse() => {
129                        if update.is_none() {
130                            *writer.borrow_mut() = None;
131                            closer_notifier.notify();
132                            platform.send(UserEvent::RequestRedraw);
133                            break;
134                        }
135                        let mut parser = parser.borrow_mut();
136                        let total_scrollback = query_max_scrollback(&mut parser);
137
138                        let mut buffer = buffer.borrow_mut();
139                        parser.screen_mut().set_scrollback(buffer.scroll_offset);
140                        let mut new_buffer = extract_buffer(&parser, buffer.scroll_offset, total_scrollback);
141                        parser.screen_mut().set_scrollback(0);
142
143                        new_buffer.selection = buffer.selection.take();
144                        *buffer = new_buffer;
145                        platform.send(UserEvent::RequestRedraw);
146                    }
147                    resize = resize_rx.next().fuse() => {
148                        if let Some((rows, cols)) = resize {
149                            let mut parser = parser.borrow_mut();
150                            parser.screen_mut().set_size(rows, cols);
151
152                            let total_scrollback = query_max_scrollback(&mut parser);
153                            let mut buffer = buffer.borrow_mut();
154                            buffer.scroll_offset = buffer.scroll_offset.min(total_scrollback);
155
156                            parser.screen_mut().set_scrollback(buffer.scroll_offset);
157                            let mut new_buffer = extract_buffer(&parser, buffer.scroll_offset, total_scrollback);
158                            parser.screen_mut().set_scrollback(0);
159
160                            new_buffer.selection = buffer.selection.take();
161                            *buffer = new_buffer;
162
163                            let (rows, cols) = parser.screen().size();
164                            let _ = pair.master.resize(PtySize {
165                                rows,
166                                cols,
167                                pixel_width: 0,
168                                pixel_height: 0,
169                            });
170                        }
171                    }
172                    scroll = scroll_rx.next().fuse() => {
173                        if let Some(cmd) = scroll {
174                            let mut parser = parser.borrow_mut();
175
176                            if parser.screen().alternate_screen() {
177                                continue;
178                            }
179
180                            let total_scrollback = query_max_scrollback(&mut parser);
181
182                            let mut buffer = buffer.borrow_mut();
183                            let offset = {
184                                match cmd {
185                                    ScrollCommand::Delta(_) => {
186                                        buffer.scroll_offset = buffer.scroll_offset.min(total_scrollback);
187                                    }
188                                    ScrollCommand::ToBottom => {
189                                        buffer.scroll_offset = 0;
190                                    }
191                                }
192                                buffer.scroll_offset
193                            };
194
195                            parser.screen_mut().set_scrollback(offset);
196                            let mut new_buffer = extract_buffer(&parser, offset, total_scrollback);
197                            parser.screen_mut().set_scrollback(0);
198
199                            new_buffer.selection = buffer.selection.take();
200                            *buffer = new_buffer;
201                            platform.send(UserEvent::RequestRedraw);
202                        }
203                    }
204                }
205            }
206        }
207    });
208
209    let pty_task = spawn_forever({
210        let writer = writer.clone();
211        let parser = parser.clone();
212        let output_notifier = output_notifier.clone();
213        async move {
214            loop {
215                let mut buf = [0u8; 4096];
216
217                match reader.read(&mut buf).await {
218                    Ok(0) => break,
219                    Ok(n) => {
220                        let data = &buf[..n];
221
222                        parser.borrow_mut().process(data);
223
224                        let responses = check_for_terminal_queries(data, &parser.borrow());
225                        if !responses.is_empty()
226                            && let Some(writer) = &mut *writer.borrow_mut()
227                        {
228                            for response in responses {
229                                let _ = writer.write_all(&response);
230                            }
231                            let _ = writer.flush();
232                        }
233
234                        let _ = update_tx.unbounded_send(());
235                        output_notifier.notify();
236                    }
237                    Err(_) => break,
238                }
239            }
240        }
241    });
242
243    Ok(TerminalHandle {
244        closer_notifier: closer_notifier.clone(),
245        cleaner: Rc::new(TerminalCleaner {
246            writer: writer.clone(),
247            reader_task,
248            pty_task,
249            closer_notifier,
250        }),
251        id,
252        buffer,
253        parser,
254        writer,
255        resize_sender: Rc::new(resize_tx),
256        scroll_sender: Rc::new(scroll_tx),
257        output_notifier,
258        last_write_time: Rc::new(RefCell::new(Instant::now())),
259        pressed_button: Rc::new(RefCell::new(None)),
260        modifiers: Rc::new(RefCell::new(Modifiers::empty())),
261    })
262}