1use std::{
2 cell::{
3 Ref,
4 RefCell,
5 },
6 io::Write,
7 path::PathBuf,
8 rc::Rc,
9 time::Instant,
10};
11
12use freya_core::{
13 notify::ArcNotify,
14 prelude::{
15 Platform,
16 TaskHandle,
17 UseId,
18 UserEvent,
19 },
20};
21use keyboard_types::Modifiers;
22use portable_pty::{
23 MasterPty,
24 PtySize,
25};
26use vt100::Parser;
27
28use crate::{
29 buffer::{
30 TerminalBuffer,
31 TerminalSelection,
32 },
33 parser::{
34 TerminalMouseButton,
35 encode_mouse_move,
36 encode_mouse_press,
37 encode_mouse_release,
38 encode_wheel_event,
39 },
40 pty::{
41 extract_buffer,
42 query_max_scrollback,
43 spawn_pty,
44 },
45};
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct TerminalId(pub usize);
50
51impl TerminalId {
52 pub fn new() -> Self {
53 Self(UseId::<TerminalId>::get_in_hook())
54 }
55}
56
57impl Default for TerminalId {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63#[derive(Debug, thiserror::Error)]
65pub enum TerminalError {
66 #[error("PTY error: {0}")]
67 PtyError(String),
68
69 #[error("Write error: {0}")]
70 WriteError(String),
71
72 #[error("Terminal not initialized")]
73 NotInitialized,
74}
75
76pub(crate) struct TerminalCleaner {
78 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
80 pub(crate) reader_task: TaskHandle,
82 pub(crate) pty_task: TaskHandle,
83 pub(crate) closer_notifier: ArcNotify,
85}
86
87impl Drop for TerminalCleaner {
88 fn drop(&mut self) {
89 *self.writer.borrow_mut() = None;
90 self.reader_task.try_cancel();
91 self.pty_task.try_cancel();
92 self.closer_notifier.notify();
93 }
94}
95
96#[derive(Clone)]
103#[allow(dead_code)]
104pub struct TerminalHandle {
105 pub(crate) id: TerminalId,
107 pub(crate) buffer: Rc<RefCell<TerminalBuffer>>,
109 pub(crate) parser: Rc<RefCell<Parser>>,
111 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
113 pub(crate) master: Rc<RefCell<Box<dyn MasterPty + Send>>>,
115 pub(crate) cwd: Rc<RefCell<Option<PathBuf>>>,
117 pub(crate) title: Rc<RefCell<Option<String>>>,
119 pub(crate) closer_notifier: ArcNotify,
121 pub(crate) cleaner: Rc<TerminalCleaner>,
123 pub(crate) output_notifier: ArcNotify,
125 pub(crate) title_notifier: ArcNotify,
127 pub(crate) last_write_time: Rc<RefCell<Instant>>,
129 pub(crate) pressed_button: Rc<RefCell<Option<TerminalMouseButton>>>,
131 pub(crate) modifiers: Rc<RefCell<Modifiers>>,
133}
134
135impl PartialEq for TerminalHandle {
136 fn eq(&self, other: &Self) -> bool {
137 self.id == other.id
138 }
139}
140
141impl TerminalHandle {
142 pub fn new(
156 id: TerminalId,
157 command: portable_pty::CommandBuilder,
158 scrollback_length: Option<usize>,
159 ) -> Result<Self, TerminalError> {
160 spawn_pty(id, command, scrollback_length.unwrap_or(1000))
161 }
162
163 fn refresh_buffer(&self) {
165 let mut parser = self.parser.borrow_mut();
166 let total_scrollback = query_max_scrollback(&mut parser);
167
168 let mut buffer = self.buffer.borrow_mut();
169 buffer.scroll_offset = buffer.scroll_offset.min(total_scrollback);
170
171 parser.screen_mut().set_scrollback(buffer.scroll_offset);
172 let mut new_buffer = extract_buffer(&parser, buffer.scroll_offset, total_scrollback);
173 parser.screen_mut().set_scrollback(0);
174
175 new_buffer.selection = buffer.selection.take();
176 *buffer = new_buffer;
177 }
178
179 pub fn write(&self, data: &[u8]) -> Result<(), TerminalError> {
189 self.write_raw(data)?;
190 let mut buffer = self.buffer.borrow_mut();
191 buffer.selection = None;
192 buffer.scroll_offset = 0;
193 drop(buffer);
194 *self.last_write_time.borrow_mut() = Instant::now();
195 self.scroll_to_bottom();
196 Ok(())
197 }
198
199 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
201 match &mut *self.writer.borrow_mut() {
202 Some(w) => {
203 w.write_all(data)
204 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
205 w.flush()
206 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
207 Ok(())
208 }
209 None => Err(TerminalError::NotInitialized),
210 }
211 }
212
213 pub fn resize(&self, rows: u16, cols: u16) {
223 self.parser.borrow_mut().screen_mut().set_size(rows, cols);
224 self.refresh_buffer();
225 let _ = self.master.borrow().resize(PtySize {
226 rows,
227 cols,
228 pixel_width: 0,
229 pixel_height: 0,
230 });
231 }
232
233 pub fn scroll(&self, delta: i32) {
244 if self.parser.borrow().screen().alternate_screen() {
245 return;
246 }
247
248 {
249 let mut buffer = self.buffer.borrow_mut();
250 let new_offset = (buffer.scroll_offset as i64 + delta as i64).max(0) as usize;
251 buffer.scroll_offset = new_offset.min(buffer.total_scrollback);
252 }
253
254 self.refresh_buffer();
255 Platform::get().send(UserEvent::RequestRedraw);
256 }
257
258 pub fn scroll_to_bottom(&self) {
268 if self.parser.borrow().screen().alternate_screen() {
269 return;
270 }
271
272 self.buffer.borrow_mut().scroll_offset = 0;
273 self.refresh_buffer();
274 Platform::get().send(UserEvent::RequestRedraw);
275 }
276
277 pub fn scrollback_position(&self) -> usize {
287 self.buffer.borrow().scroll_offset
288 }
289
290 pub fn cwd(&self) -> Option<PathBuf> {
294 self.cwd.borrow().clone()
295 }
296
297 pub fn title(&self) -> Option<String> {
301 self.title.borrow().clone()
302 }
303
304 pub fn send_wheel_to_pty(&self, row: usize, col: usize, delta_y: f64) {
310 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
311 let seq = encode_wheel_event(row, col, delta_y, encoding);
312 let _ = self.write_raw(seq.as_bytes());
313 }
314
315 pub fn mouse_move(&self, row: usize, col: usize) {
326 let is_dragging = self.pressed_button.borrow().is_some();
327
328 if self.modifiers.borrow().contains(Modifiers::SHIFT) && is_dragging {
329 self.update_selection(row, col);
331 return;
332 }
333
334 let parser = self.parser.borrow();
335 let mouse_mode = parser.screen().mouse_protocol_mode();
336 let encoding = parser.screen().mouse_protocol_encoding();
337
338 let held = *self.pressed_button.borrow();
339
340 match mouse_mode {
341 vt100::MouseProtocolMode::AnyMotion => {
342 let seq = encode_mouse_move(row, col, held, encoding);
343 let _ = self.write_raw(seq.as_bytes());
344 }
345 vt100::MouseProtocolMode::ButtonMotion => {
346 if let Some(button) = held {
347 let seq = encode_mouse_move(row, col, Some(button), encoding);
348 let _ = self.write_raw(seq.as_bytes());
349 }
350 }
351 vt100::MouseProtocolMode::None => {
352 if is_dragging {
354 self.update_selection(row, col);
355 }
356 }
357 _ => {}
358 }
359 }
360
361 fn is_mouse_tracking_enabled(&self) -> bool {
363 let parser = self.parser.borrow();
364 parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None
365 }
366
367 pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
376 *self.pressed_button.borrow_mut() = Some(button);
377
378 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
379 self.start_selection(row, col);
381 } else if self.is_mouse_tracking_enabled() {
382 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
383 let seq = encode_mouse_press(row, col, button, encoding);
384 let _ = self.write_raw(seq.as_bytes());
385 } else {
386 self.start_selection(row, col);
387 }
388 }
389
390 pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
400 *self.pressed_button.borrow_mut() = None;
401
402 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
403 self.end_selection();
405 return;
406 }
407
408 let parser = self.parser.borrow();
409 let mouse_mode = parser.screen().mouse_protocol_mode();
410 let encoding = parser.screen().mouse_protocol_encoding();
411
412 match mouse_mode {
413 vt100::MouseProtocolMode::PressRelease
414 | vt100::MouseProtocolMode::ButtonMotion
415 | vt100::MouseProtocolMode::AnyMotion => {
416 let seq = encode_mouse_release(row, col, button, encoding);
417 let _ = self.write_raw(seq.as_bytes());
418 }
419 vt100::MouseProtocolMode::Press => {
420 }
422 vt100::MouseProtocolMode::None => {
423 self.end_selection();
424 }
425 }
426 }
427
428 const ALTERNATE_SCROLL_LINES: usize = 3;
430
431 pub fn release(&self) {
436 *self.pressed_button.borrow_mut() = None;
437 self.end_selection();
438 }
439
440 pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
451 let scroll_delta = if delta_y > 0.0 { 3 } else { -3 };
452 let scroll_offset = self.buffer.borrow().scroll_offset;
453 let (mouse_mode, alt_screen, app_cursor) = {
454 let parser = self.parser.borrow();
455 let screen = parser.screen();
456 (
457 screen.mouse_protocol_mode(),
458 screen.alternate_screen(),
459 screen.application_cursor(),
460 )
461 };
462
463 if scroll_offset > 0 {
464 let delta = scroll_delta;
466 self.scroll(delta);
467 } else if mouse_mode != vt100::MouseProtocolMode::None {
468 self.send_wheel_to_pty(row, col, delta_y);
470 } else if alt_screen {
471 let key = match (delta_y > 0.0, app_cursor) {
474 (true, true) => "\x1bOA",
475 (true, false) => "\x1b[A",
476 (false, true) => "\x1bOB",
477 (false, false) => "\x1b[B",
478 };
479 for _ in 0..Self::ALTERNATE_SCROLL_LINES {
480 let _ = self.write_raw(key.as_bytes());
481 }
482 } else {
483 let delta = scroll_delta;
485 self.scroll(delta);
486 }
487 }
488
489 pub fn read_buffer(&'_ self) -> Ref<'_, TerminalBuffer> {
491 self.buffer.borrow()
492 }
493
494 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
498 self.output_notifier.notified()
499 }
500
501 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
505 self.title_notifier.notified()
506 }
507
508 pub fn last_write_elapsed(&self) -> std::time::Duration {
509 self.last_write_time.borrow().elapsed()
510 }
511
512 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
525 self.closer_notifier.notified()
526 }
527
528 pub fn id(&self) -> TerminalId {
530 self.id
531 }
532
533 pub fn shift_pressed(&self, pressed: bool) {
538 let mut mods = self.modifiers.borrow_mut();
539 if pressed {
540 mods.insert(Modifiers::SHIFT);
541 } else {
542 mods.remove(Modifiers::SHIFT);
543 }
544 }
545
546 pub fn get_selection(&self) -> Option<TerminalSelection> {
548 self.buffer.borrow().selection.clone()
549 }
550
551 pub fn set_selection(&self, selection: Option<TerminalSelection>) {
553 self.buffer.borrow_mut().selection = selection;
554 }
555
556 pub fn start_selection(&self, row: usize, col: usize) {
557 let mut buffer = self.buffer.borrow_mut();
558 let scroll = buffer.scroll_offset;
559 buffer.selection = Some(TerminalSelection {
560 dragging: true,
561 start_row: row,
562 start_col: col,
563 start_scroll: scroll,
564 end_row: row,
565 end_col: col,
566 end_scroll: scroll,
567 });
568 Platform::get().send(UserEvent::RequestRedraw);
569 }
570
571 pub fn update_selection(&self, row: usize, col: usize) {
572 let mut buffer = self.buffer.borrow_mut();
573 let scroll = buffer.scroll_offset;
574 if let Some(selection) = &mut buffer.selection
575 && selection.dragging
576 {
577 selection.end_row = row;
578 selection.end_col = col;
579 selection.end_scroll = scroll;
580 Platform::get().send(UserEvent::RequestRedraw);
581 }
582 }
583
584 pub fn end_selection(&self) {
585 if let Some(selection) = &mut self.buffer.borrow_mut().selection {
586 selection.dragging = false;
587 Platform::get().send(UserEvent::RequestRedraw);
588 }
589 }
590
591 pub fn clear_selection(&self) {
593 self.buffer.borrow_mut().selection = None;
594 Platform::get().send(UserEvent::RequestRedraw);
595 }
596
597 pub fn get_selected_text(&self) -> Option<String> {
598 let buffer = self.buffer.borrow();
599 let selection = buffer.selection.clone()?;
600 if selection.is_empty() {
601 return None;
602 }
603
604 let scroll = buffer.scroll_offset;
605 let (display_start, start_col, display_end, end_col) = selection.display_positions(scroll);
606
607 let mut parser = self.parser.borrow_mut();
608 let saved_scrollback = parser.screen().scrollback();
609 let (_rows, cols) = parser.screen().size();
610
611 let mut lines = Vec::new();
612
613 for d in display_start..=display_end {
614 let cp = d - scroll as i64;
615 let needed_scrollback = (-cp).max(0) as usize;
616 let viewport_row = cp.max(0) as u16;
617
618 parser.screen_mut().set_scrollback(needed_scrollback);
619
620 let row_cells: Vec<_> = (0..cols)
621 .filter_map(|c| parser.screen().cell(viewport_row, c).cloned())
622 .collect();
623
624 let is_single = display_start == display_end;
625 let is_first = d == display_start;
626 let is_last = d == display_end;
627
628 let cells = if is_single {
629 let s = start_col.min(row_cells.len());
630 let e = end_col.min(row_cells.len());
631 &row_cells[s..e]
632 } else if is_first {
633 let s = start_col.min(row_cells.len());
634 &row_cells[s..]
635 } else if is_last {
636 &row_cells[..end_col.min(row_cells.len())]
637 } else {
638 &row_cells
639 };
640
641 let line: String = cells
642 .iter()
643 .map(|cell| {
644 if cell.has_contents() {
645 cell.contents()
646 } else {
647 " "
648 }
649 })
650 .collect::<String>();
651
652 lines.push(line);
653 }
654
655 parser.screen_mut().set_scrollback(saved_scrollback);
656
657 Some(lines.join("\n"))
658 }
659}