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::{
22 Key,
23 Modifiers,
24 NamedKey,
25};
26use portable_pty::{
27 MasterPty,
28 PtySize,
29};
30use vt100::Parser;
31
32use crate::{
33 buffer::{
34 TerminalBuffer,
35 TerminalSelection,
36 },
37 parser::{
38 TerminalMouseButton,
39 encode_mouse_move,
40 encode_mouse_press,
41 encode_mouse_release,
42 encode_wheel_event,
43 },
44 pty::{
45 extract_buffer,
46 query_max_scrollback,
47 spawn_pty,
48 },
49};
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct TerminalId(pub usize);
54
55impl TerminalId {
56 pub fn new() -> Self {
57 Self(UseId::<TerminalId>::get_in_hook())
58 }
59}
60
61impl Default for TerminalId {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum TerminalError {
70 #[error("PTY error: {0}")]
71 PtyError(String),
72
73 #[error("Write error: {0}")]
74 WriteError(String),
75
76 #[error("Terminal not initialized")]
77 NotInitialized,
78}
79
80pub(crate) struct TerminalCleaner {
82 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
84 pub(crate) reader_task: TaskHandle,
86 pub(crate) pty_task: TaskHandle,
87 pub(crate) closer_notifier: ArcNotify,
89}
90
91impl Drop for TerminalCleaner {
92 fn drop(&mut self) {
93 *self.writer.borrow_mut() = None;
94 self.reader_task.try_cancel();
95 self.pty_task.try_cancel();
96 self.closer_notifier.notify();
97 }
98}
99
100#[derive(Clone)]
107#[allow(dead_code)]
108pub struct TerminalHandle {
109 pub(crate) id: TerminalId,
111 pub(crate) buffer: Rc<RefCell<TerminalBuffer>>,
113 pub(crate) parser: Rc<RefCell<Parser>>,
115 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
117 pub(crate) master: Rc<RefCell<Box<dyn MasterPty + Send>>>,
119 pub(crate) cwd: Rc<RefCell<Option<PathBuf>>>,
121 pub(crate) title: Rc<RefCell<Option<String>>>,
123 pub(crate) closer_notifier: ArcNotify,
125 pub(crate) cleaner: Rc<TerminalCleaner>,
127 pub(crate) output_notifier: ArcNotify,
129 pub(crate) title_notifier: ArcNotify,
131 pub(crate) clipboard_content: Rc<RefCell<Option<String>>>,
133 pub(crate) clipboard_notifier: ArcNotify,
135 pub(crate) last_write_time: Rc<RefCell<Instant>>,
137 pub(crate) pressed_button: Rc<RefCell<Option<TerminalMouseButton>>>,
139 pub(crate) modifiers: Rc<RefCell<Modifiers>>,
141}
142
143impl PartialEq for TerminalHandle {
144 fn eq(&self, other: &Self) -> bool {
145 self.id == other.id
146 }
147}
148
149impl TerminalHandle {
150 pub fn new(
164 id: TerminalId,
165 command: portable_pty::CommandBuilder,
166 scrollback_length: Option<usize>,
167 ) -> Result<Self, TerminalError> {
168 spawn_pty(id, command, scrollback_length.unwrap_or(1000))
169 }
170
171 fn refresh_buffer(&self) {
173 let mut parser = self.parser.borrow_mut();
174 let total_scrollback = query_max_scrollback(&mut parser);
175
176 let mut buffer = self.buffer.borrow_mut();
177 buffer.scroll_offset = buffer.scroll_offset.min(total_scrollback);
178
179 parser.screen_mut().set_scrollback(buffer.scroll_offset);
180 let mut new_buffer = extract_buffer(&parser, buffer.scroll_offset, total_scrollback);
181 parser.screen_mut().set_scrollback(0);
182
183 new_buffer.selection = buffer.selection.take();
184 *buffer = new_buffer;
185 }
186
187 pub fn write(&self, data: &[u8]) -> Result<(), TerminalError> {
197 self.write_raw(data)?;
198 let mut buffer = self.buffer.borrow_mut();
199 buffer.selection = None;
200 buffer.scroll_offset = 0;
201 drop(buffer);
202 *self.last_write_time.borrow_mut() = Instant::now();
203 self.scroll_to_bottom();
204 Ok(())
205 }
206
207 pub fn write_key(&self, key: &Key, modifiers: Modifiers) -> Result<bool, TerminalError> {
229 let shift = modifiers.contains(Modifiers::SHIFT);
230 let ctrl = modifiers.contains(Modifiers::CONTROL);
231 let alt = modifiers.contains(Modifiers::ALT);
232
233 match key {
234 Key::Character(ch) if ctrl && ch.len() == 1 => {
235 self.write(&[ch.as_bytes()[0] & 0x1f])?;
236 Ok(true)
237 }
238 Key::Named(NamedKey::Enter) if shift || ctrl => {
239 let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
240 let seq = format!("\x1b[13;{m}u");
241 self.write(seq.as_bytes())?;
242 Ok(true)
243 }
244 Key::Named(NamedKey::Enter) => {
245 self.write(b"\r")?;
246 Ok(true)
247 }
248 Key::Named(NamedKey::Backspace) if ctrl => {
249 self.write(&[0x08])?;
250 Ok(true)
251 }
252 Key::Named(NamedKey::Backspace) if alt => {
253 self.write(&[0x1b, 0x7f])?;
254 Ok(true)
255 }
256 Key::Named(NamedKey::Backspace) => {
257 self.write(&[0x7f])?;
258 Ok(true)
259 }
260 Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
261 let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
262 let seq = format!("\x1b[3;{m}~");
263 self.write(seq.as_bytes())?;
264 Ok(true)
265 }
266 Key::Named(NamedKey::Delete) => {
267 self.write(b"\x1b[3~")?;
268 Ok(true)
269 }
270 Key::Named(NamedKey::Shift) => {
271 self.shift_pressed(true);
272 Ok(true)
273 }
274 Key::Named(NamedKey::Tab) if shift => {
275 self.write(b"\x1b[Z")?;
276 Ok(true)
277 }
278 Key::Named(NamedKey::Tab) => {
279 self.write(b"\t")?;
280 Ok(true)
281 }
282 Key::Named(NamedKey::Escape) => {
283 self.write(&[0x1b])?;
284 Ok(true)
285 }
286 Key::Named(
287 dir @ (NamedKey::ArrowUp
288 | NamedKey::ArrowDown
289 | NamedKey::ArrowLeft
290 | NamedKey::ArrowRight),
291 ) => {
292 let ch = match dir {
293 NamedKey::ArrowUp => 'A',
294 NamedKey::ArrowDown => 'B',
295 NamedKey::ArrowRight => 'C',
296 NamedKey::ArrowLeft => 'D',
297 _ => unreachable!(),
298 };
299 if shift || ctrl {
300 let m = 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
301 let seq = format!("\x1b[1;{m}{ch}");
302 self.write(seq.as_bytes())?;
303 } else {
304 self.write(&[0x1b, b'[', ch as u8])?;
305 }
306 Ok(true)
307 }
308 Key::Character(ch) => {
309 self.write(ch.as_bytes())?;
310 Ok(true)
311 }
312 _ => Ok(false),
313 }
314 }
315
316 pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
320 let bracketed = self.parser.borrow().screen().bracketed_paste();
321
322 let mut data = Vec::with_capacity(text.len() + 12);
323 if bracketed {
324 data.extend_from_slice(b"\x1b[200~");
325 }
326 data.extend_from_slice(text.as_bytes());
327 if bracketed {
328 data.extend_from_slice(b"\x1b[201~");
329 }
330
331 self.write(&data)
332 }
333
334 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
336 match &mut *self.writer.borrow_mut() {
337 Some(w) => {
338 w.write_all(data)
339 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
340 w.flush()
341 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
342 Ok(())
343 }
344 None => Err(TerminalError::NotInitialized),
345 }
346 }
347
348 pub fn resize(&self, rows: u16, cols: u16) {
358 self.parser.borrow_mut().screen_mut().set_size(rows, cols);
359 self.refresh_buffer();
360 let _ = self.master.borrow().resize(PtySize {
361 rows,
362 cols,
363 pixel_width: 0,
364 pixel_height: 0,
365 });
366 }
367
368 pub fn scroll(&self, delta: i32) {
379 if self.parser.borrow().screen().alternate_screen() {
380 return;
381 }
382
383 {
384 let mut buffer = self.buffer.borrow_mut();
385 let new_offset = (buffer.scroll_offset as i64 + delta as i64).max(0) as usize;
386 buffer.scroll_offset = new_offset.min(buffer.total_scrollback);
387 }
388
389 self.refresh_buffer();
390 Platform::get().send(UserEvent::RequestRedraw);
391 }
392
393 pub fn scroll_to_bottom(&self) {
403 if self.parser.borrow().screen().alternate_screen() {
404 return;
405 }
406
407 self.buffer.borrow_mut().scroll_offset = 0;
408 self.refresh_buffer();
409 Platform::get().send(UserEvent::RequestRedraw);
410 }
411
412 pub fn scrollback_position(&self) -> usize {
422 self.buffer.borrow().scroll_offset
423 }
424
425 pub fn cwd(&self) -> Option<PathBuf> {
429 self.cwd.borrow().clone()
430 }
431
432 pub fn title(&self) -> Option<String> {
436 self.title.borrow().clone()
437 }
438
439 pub fn clipboard_content(&self) -> Option<String> {
441 self.clipboard_content.borrow().clone()
442 }
443
444 pub fn send_wheel_to_pty(&self, row: usize, col: usize, delta_y: f64) {
450 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
451 let seq = encode_wheel_event(row, col, delta_y, encoding);
452 let _ = self.write_raw(seq.as_bytes());
453 }
454
455 pub fn mouse_move(&self, row: usize, col: usize) {
466 let is_dragging = self.pressed_button.borrow().is_some();
467
468 if self.modifiers.borrow().contains(Modifiers::SHIFT) && is_dragging {
469 self.update_selection(row, col);
471 return;
472 }
473
474 let parser = self.parser.borrow();
475 let mouse_mode = parser.screen().mouse_protocol_mode();
476 let encoding = parser.screen().mouse_protocol_encoding();
477
478 let held = *self.pressed_button.borrow();
479
480 match mouse_mode {
481 vt100::MouseProtocolMode::AnyMotion => {
482 let seq = encode_mouse_move(row, col, held, encoding);
483 let _ = self.write_raw(seq.as_bytes());
484 }
485 vt100::MouseProtocolMode::ButtonMotion => {
486 if let Some(button) = held {
487 let seq = encode_mouse_move(row, col, Some(button), encoding);
488 let _ = self.write_raw(seq.as_bytes());
489 }
490 }
491 vt100::MouseProtocolMode::None => {
492 if is_dragging {
494 self.update_selection(row, col);
495 }
496 }
497 _ => {}
498 }
499 }
500
501 fn is_mouse_tracking_enabled(&self) -> bool {
503 let parser = self.parser.borrow();
504 parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None
505 }
506
507 pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
516 *self.pressed_button.borrow_mut() = Some(button);
517
518 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
519 self.start_selection(row, col);
521 } else if self.is_mouse_tracking_enabled() {
522 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
523 let seq = encode_mouse_press(row, col, button, encoding);
524 let _ = self.write_raw(seq.as_bytes());
525 } else {
526 self.start_selection(row, col);
527 }
528 }
529
530 pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
540 *self.pressed_button.borrow_mut() = None;
541
542 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
543 self.end_selection();
545 return;
546 }
547
548 let parser = self.parser.borrow();
549 let mouse_mode = parser.screen().mouse_protocol_mode();
550 let encoding = parser.screen().mouse_protocol_encoding();
551
552 match mouse_mode {
553 vt100::MouseProtocolMode::PressRelease
554 | vt100::MouseProtocolMode::ButtonMotion
555 | vt100::MouseProtocolMode::AnyMotion => {
556 let seq = encode_mouse_release(row, col, button, encoding);
557 let _ = self.write_raw(seq.as_bytes());
558 }
559 vt100::MouseProtocolMode::Press => {
560 }
562 vt100::MouseProtocolMode::None => {
563 self.end_selection();
564 }
565 }
566 }
567
568 const ALTERNATE_SCROLL_LINES: usize = 3;
570
571 pub fn release(&self) {
576 *self.pressed_button.borrow_mut() = None;
577 self.end_selection();
578 }
579
580 pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
591 let scroll_delta = if delta_y > 0.0 { 3 } else { -3 };
592 let scroll_offset = self.buffer.borrow().scroll_offset;
593 let (mouse_mode, alt_screen, app_cursor) = {
594 let parser = self.parser.borrow();
595 let screen = parser.screen();
596 (
597 screen.mouse_protocol_mode(),
598 screen.alternate_screen(),
599 screen.application_cursor(),
600 )
601 };
602
603 if scroll_offset > 0 {
604 let delta = scroll_delta;
606 self.scroll(delta);
607 } else if mouse_mode != vt100::MouseProtocolMode::None {
608 self.send_wheel_to_pty(row, col, delta_y);
610 } else if alt_screen {
611 let key = match (delta_y > 0.0, app_cursor) {
614 (true, true) => "\x1bOA",
615 (true, false) => "\x1b[A",
616 (false, true) => "\x1bOB",
617 (false, false) => "\x1b[B",
618 };
619 for _ in 0..Self::ALTERNATE_SCROLL_LINES {
620 let _ = self.write_raw(key.as_bytes());
621 }
622 } else {
623 let delta = scroll_delta;
625 self.scroll(delta);
626 }
627 }
628
629 pub fn read_buffer(&'_ self) -> Ref<'_, TerminalBuffer> {
631 self.buffer.borrow()
632 }
633
634 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
638 self.output_notifier.notified()
639 }
640
641 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
645 self.title_notifier.notified()
646 }
647
648 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
650 self.clipboard_notifier.notified()
651 }
652
653 pub fn last_write_elapsed(&self) -> std::time::Duration {
654 self.last_write_time.borrow().elapsed()
655 }
656
657 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
670 self.closer_notifier.notified()
671 }
672
673 pub fn id(&self) -> TerminalId {
675 self.id
676 }
677
678 pub fn shift_pressed(&self, pressed: bool) {
683 let mut mods = self.modifiers.borrow_mut();
684 if pressed {
685 mods.insert(Modifiers::SHIFT);
686 } else {
687 mods.remove(Modifiers::SHIFT);
688 }
689 }
690
691 pub fn get_selection(&self) -> Option<TerminalSelection> {
693 self.buffer.borrow().selection.clone()
694 }
695
696 pub fn set_selection(&self, selection: Option<TerminalSelection>) {
698 self.buffer.borrow_mut().selection = selection;
699 }
700
701 pub fn start_selection(&self, row: usize, col: usize) {
702 let mut buffer = self.buffer.borrow_mut();
703 let scroll = buffer.scroll_offset;
704 buffer.selection = Some(TerminalSelection {
705 dragging: true,
706 start_row: row,
707 start_col: col,
708 start_scroll: scroll,
709 end_row: row,
710 end_col: col,
711 end_scroll: scroll,
712 });
713 Platform::get().send(UserEvent::RequestRedraw);
714 }
715
716 pub fn update_selection(&self, row: usize, col: usize) {
717 let mut buffer = self.buffer.borrow_mut();
718 let scroll = buffer.scroll_offset;
719 if let Some(selection) = &mut buffer.selection
720 && selection.dragging
721 {
722 selection.end_row = row;
723 selection.end_col = col;
724 selection.end_scroll = scroll;
725 Platform::get().send(UserEvent::RequestRedraw);
726 }
727 }
728
729 pub fn end_selection(&self) {
730 if let Some(selection) = &mut self.buffer.borrow_mut().selection {
731 selection.dragging = false;
732 Platform::get().send(UserEvent::RequestRedraw);
733 }
734 }
735
736 pub fn clear_selection(&self) {
738 self.buffer.borrow_mut().selection = None;
739 Platform::get().send(UserEvent::RequestRedraw);
740 }
741
742 pub fn get_selected_text(&self) -> Option<String> {
743 let buffer = self.buffer.borrow();
744 let selection = buffer.selection.clone()?;
745 if selection.is_empty() {
746 return None;
747 }
748
749 let scroll = buffer.scroll_offset;
750 let (display_start, start_col, display_end, end_col) = selection.display_positions(scroll);
751
752 let mut parser = self.parser.borrow_mut();
753 let saved_scrollback = parser.screen().scrollback();
754 let (_rows, cols) = parser.screen().size();
755
756 let mut lines = Vec::new();
757
758 for d in display_start..=display_end {
759 let cp = d - scroll as i64;
760 let needed_scrollback = (-cp).max(0) as usize;
761 let viewport_row = cp.max(0) as u16;
762
763 parser.screen_mut().set_scrollback(needed_scrollback);
764
765 let row_cells: Vec<_> = (0..cols)
766 .filter_map(|c| parser.screen().cell(viewport_row, c).cloned())
767 .collect();
768
769 let is_single = display_start == display_end;
770 let is_first = d == display_start;
771 let is_last = d == display_end;
772
773 let cells = if is_single {
774 let s = start_col.min(row_cells.len());
775 let e = end_col.min(row_cells.len());
776 &row_cells[s..e]
777 } else if is_first {
778 let s = start_col.min(row_cells.len());
779 &row_cells[s..]
780 } else if is_last {
781 &row_cells[..end_col.min(row_cells.len())]
782 } else {
783 &row_cells
784 };
785
786 let line: String = cells
787 .iter()
788 .map(|cell| {
789 if cell.has_contents() {
790 cell.contents()
791 } else {
792 " "
793 }
794 })
795 .collect::<String>();
796
797 lines.push(line);
798 }
799
800 parser.screen_mut().set_scrollback(saved_scrollback);
801
802 Some(lines.join("\n"))
803 }
804}