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> {
321 if self.parser.borrow().screen().bracketed_paste() {
322 let filtered = text.replace(['\x1b', '\x03'], "");
323 self.write_raw(b"\x1b[200~")?;
324 self.write_raw(filtered.as_bytes())?;
325 self.write_raw(b"\x1b[201~")?;
326 } else {
327 let normalized = text.replace("\r\n", "\r").replace('\n', "\r");
328 self.write_raw(normalized.as_bytes())?;
329 }
330 Ok(())
331 }
332
333 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
335 match &mut *self.writer.borrow_mut() {
336 Some(w) => {
337 w.write_all(data)
338 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
339 w.flush()
340 .map_err(|e| TerminalError::WriteError(e.to_string()))?;
341 Ok(())
342 }
343 None => Err(TerminalError::NotInitialized),
344 }
345 }
346
347 pub fn resize(&self, rows: u16, cols: u16) {
357 self.parser.borrow_mut().screen_mut().set_size(rows, cols);
358 self.refresh_buffer();
359 let _ = self.master.borrow().resize(PtySize {
360 rows,
361 cols,
362 pixel_width: 0,
363 pixel_height: 0,
364 });
365 }
366
367 pub fn scroll(&self, delta: i32) {
378 if self.parser.borrow().screen().alternate_screen() {
379 return;
380 }
381
382 {
383 let mut buffer = self.buffer.borrow_mut();
384 let new_offset = (buffer.scroll_offset as i64 + delta as i64).max(0) as usize;
385 buffer.scroll_offset = new_offset.min(buffer.total_scrollback);
386 }
387
388 self.refresh_buffer();
389 Platform::get().send(UserEvent::RequestRedraw);
390 }
391
392 pub fn scroll_to_bottom(&self) {
402 if self.parser.borrow().screen().alternate_screen() {
403 return;
404 }
405
406 self.buffer.borrow_mut().scroll_offset = 0;
407 self.refresh_buffer();
408 Platform::get().send(UserEvent::RequestRedraw);
409 }
410
411 pub fn scrollback_position(&self) -> usize {
421 self.buffer.borrow().scroll_offset
422 }
423
424 pub fn cwd(&self) -> Option<PathBuf> {
428 self.cwd.borrow().clone()
429 }
430
431 pub fn title(&self) -> Option<String> {
435 self.title.borrow().clone()
436 }
437
438 pub fn clipboard_content(&self) -> Option<String> {
440 self.clipboard_content.borrow().clone()
441 }
442
443 pub fn send_wheel_to_pty(&self, row: usize, col: usize, delta_y: f64) {
449 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
450 let seq = encode_wheel_event(row, col, delta_y, encoding);
451 let _ = self.write_raw(seq.as_bytes());
452 }
453
454 pub fn mouse_move(&self, row: usize, col: usize) {
465 let is_dragging = self.pressed_button.borrow().is_some();
466
467 if self.modifiers.borrow().contains(Modifiers::SHIFT) && is_dragging {
468 self.update_selection(row, col);
470 return;
471 }
472
473 let parser = self.parser.borrow();
474 let mouse_mode = parser.screen().mouse_protocol_mode();
475 let encoding = parser.screen().mouse_protocol_encoding();
476
477 let held = *self.pressed_button.borrow();
478
479 match mouse_mode {
480 vt100::MouseProtocolMode::AnyMotion => {
481 let seq = encode_mouse_move(row, col, held, encoding);
482 let _ = self.write_raw(seq.as_bytes());
483 }
484 vt100::MouseProtocolMode::ButtonMotion => {
485 if let Some(button) = held {
486 let seq = encode_mouse_move(row, col, Some(button), encoding);
487 let _ = self.write_raw(seq.as_bytes());
488 }
489 }
490 vt100::MouseProtocolMode::None => {
491 if is_dragging {
493 self.update_selection(row, col);
494 }
495 }
496 _ => {}
497 }
498 }
499
500 fn is_mouse_tracking_enabled(&self) -> bool {
502 let parser = self.parser.borrow();
503 parser.screen().mouse_protocol_mode() != vt100::MouseProtocolMode::None
504 }
505
506 pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
515 *self.pressed_button.borrow_mut() = Some(button);
516
517 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
518 self.start_selection(row, col);
520 } else if self.is_mouse_tracking_enabled() {
521 let encoding = self.parser.borrow().screen().mouse_protocol_encoding();
522 let seq = encode_mouse_press(row, col, button, encoding);
523 let _ = self.write_raw(seq.as_bytes());
524 } else {
525 self.start_selection(row, col);
526 }
527 }
528
529 pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
539 *self.pressed_button.borrow_mut() = None;
540
541 if self.modifiers.borrow().contains(Modifiers::SHIFT) {
542 self.end_selection();
544 return;
545 }
546
547 let parser = self.parser.borrow();
548 let mouse_mode = parser.screen().mouse_protocol_mode();
549 let encoding = parser.screen().mouse_protocol_encoding();
550
551 match mouse_mode {
552 vt100::MouseProtocolMode::PressRelease
553 | vt100::MouseProtocolMode::ButtonMotion
554 | vt100::MouseProtocolMode::AnyMotion => {
555 let seq = encode_mouse_release(row, col, button, encoding);
556 let _ = self.write_raw(seq.as_bytes());
557 }
558 vt100::MouseProtocolMode::Press => {
559 }
561 vt100::MouseProtocolMode::None => {
562 self.end_selection();
563 }
564 }
565 }
566
567 const ALTERNATE_SCROLL_LINES: usize = 3;
569
570 pub fn release(&self) {
575 *self.pressed_button.borrow_mut() = None;
576 self.end_selection();
577 }
578
579 pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
590 let scroll_delta = if delta_y > 0.0 { 3 } else { -3 };
591 let scroll_offset = self.buffer.borrow().scroll_offset;
592 let (mouse_mode, alt_screen, app_cursor) = {
593 let parser = self.parser.borrow();
594 let screen = parser.screen();
595 (
596 screen.mouse_protocol_mode(),
597 screen.alternate_screen(),
598 screen.application_cursor(),
599 )
600 };
601
602 if scroll_offset > 0 {
603 let delta = scroll_delta;
605 self.scroll(delta);
606 } else if mouse_mode != vt100::MouseProtocolMode::None {
607 self.send_wheel_to_pty(row, col, delta_y);
609 } else if alt_screen {
610 let key = match (delta_y > 0.0, app_cursor) {
613 (true, true) => "\x1bOA",
614 (true, false) => "\x1b[A",
615 (false, true) => "\x1bOB",
616 (false, false) => "\x1b[B",
617 };
618 for _ in 0..Self::ALTERNATE_SCROLL_LINES {
619 let _ = self.write_raw(key.as_bytes());
620 }
621 } else {
622 let delta = scroll_delta;
624 self.scroll(delta);
625 }
626 }
627
628 pub fn read_buffer(&'_ self) -> Ref<'_, TerminalBuffer> {
630 self.buffer.borrow()
631 }
632
633 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
637 self.output_notifier.notified()
638 }
639
640 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
644 self.title_notifier.notified()
645 }
646
647 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
649 self.clipboard_notifier.notified()
650 }
651
652 pub fn last_write_elapsed(&self) -> std::time::Duration {
653 self.last_write_time.borrow().elapsed()
654 }
655
656 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
669 self.closer_notifier.notified()
670 }
671
672 pub fn id(&self) -> TerminalId {
674 self.id
675 }
676
677 pub fn shift_pressed(&self, pressed: bool) {
682 let mut mods = self.modifiers.borrow_mut();
683 if pressed {
684 mods.insert(Modifiers::SHIFT);
685 } else {
686 mods.remove(Modifiers::SHIFT);
687 }
688 }
689
690 pub fn get_selection(&self) -> Option<TerminalSelection> {
692 self.buffer.borrow().selection.clone()
693 }
694
695 pub fn set_selection(&self, selection: Option<TerminalSelection>) {
697 self.buffer.borrow_mut().selection = selection;
698 }
699
700 pub fn start_selection(&self, row: usize, col: usize) {
701 let mut buffer = self.buffer.borrow_mut();
702 let scroll = buffer.scroll_offset;
703 buffer.selection = Some(TerminalSelection {
704 dragging: true,
705 start_row: row,
706 start_col: col,
707 start_scroll: scroll,
708 end_row: row,
709 end_col: col,
710 end_scroll: scroll,
711 });
712 Platform::get().send(UserEvent::RequestRedraw);
713 }
714
715 pub fn update_selection(&self, row: usize, col: usize) {
716 let mut buffer = self.buffer.borrow_mut();
717 let scroll = buffer.scroll_offset;
718 if let Some(selection) = &mut buffer.selection
719 && selection.dragging
720 {
721 selection.end_row = row;
722 selection.end_col = col;
723 selection.end_scroll = scroll;
724 Platform::get().send(UserEvent::RequestRedraw);
725 }
726 }
727
728 pub fn end_selection(&self) {
729 if let Some(selection) = &mut self.buffer.borrow_mut().selection {
730 selection.dragging = false;
731 Platform::get().send(UserEvent::RequestRedraw);
732 }
733 }
734
735 pub fn clear_selection(&self) {
737 self.buffer.borrow_mut().selection = None;
738 Platform::get().send(UserEvent::RequestRedraw);
739 }
740
741 pub fn get_selected_text(&self) -> Option<String> {
742 let buffer = self.buffer.borrow();
743 let selection = buffer.selection.clone()?;
744 if selection.is_empty() {
745 return None;
746 }
747
748 let scroll = buffer.scroll_offset;
749 let (display_start, start_col, display_end, end_col) = selection.display_positions(scroll);
750
751 let mut parser = self.parser.borrow_mut();
752 let saved_scrollback = parser.screen().scrollback();
753 let (_rows, cols) = parser.screen().size();
754
755 let mut lines = Vec::new();
756
757 for d in display_start..=display_end {
758 let cp = d - scroll as i64;
759 let needed_scrollback = (-cp).max(0) as usize;
760 let viewport_row = cp.max(0) as u16;
761
762 parser.screen_mut().set_scrollback(needed_scrollback);
763
764 let row_cells: Vec<_> = (0..cols)
765 .filter_map(|c| parser.screen().cell(viewport_row, c).cloned())
766 .collect();
767
768 let is_single = display_start == display_end;
769 let is_first = d == display_start;
770 let is_last = d == display_end;
771
772 let cells = if is_single {
773 let s = start_col.min(row_cells.len());
774 let e = end_col.min(row_cells.len());
775 &row_cells[s..e]
776 } else if is_first {
777 let s = start_col.min(row_cells.len());
778 &row_cells[s..]
779 } else if is_last {
780 &row_cells[..end_col.min(row_cells.len())]
781 } else {
782 &row_cells
783 };
784
785 let mut line: String = cells
786 .iter()
787 .map(|cell| {
788 if cell.has_contents() {
789 cell.contents()
790 } else {
791 " "
792 }
793 })
794 .collect();
795
796 if !is_single && !is_last {
798 line.truncate(line.trim_end_matches(' ').len());
799 }
800
801 lines.push(line);
802 }
803
804 parser.screen_mut().set_scrollback(saved_scrollback);
805
806 Some(lines.join("\n"))
807 }
808}