1use std::{
2 cell::RefCell,
3 io::Write,
4 path::PathBuf,
5 rc::Rc,
6 time::{
7 Duration,
8 Instant,
9 },
10};
11
12use alacritty_terminal::{
13 grid::{
14 Dimensions,
15 Scroll,
16 },
17 index::{
18 Column,
19 Line,
20 Point,
21 Side,
22 },
23 selection::{
24 Selection,
25 SelectionType,
26 },
27 term::{
28 Term,
29 TermMode,
30 },
31};
32use freya_core::{
33 notify::ArcNotify,
34 prelude::{
35 Platform,
36 TaskHandle,
37 UseId,
38 UserEvent,
39 },
40};
41use keyboard_types::{
42 Key,
43 Modifiers,
44 NamedKey,
45};
46use portable_pty::{
47 MasterPty,
48 PtySize,
49};
50
51use crate::{
52 parser::{
53 TerminalMouseButton,
54 encode_mouse_move,
55 encode_mouse_press,
56 encode_mouse_release,
57 encode_wheel_event,
58 },
59 pty::{
60 EventProxy,
61 TermSize,
62 spawn_pty,
63 },
64 url::url_at,
65};
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct TerminalId(pub usize);
70
71impl TerminalId {
72 pub fn new() -> Self {
73 Self(UseId::<TerminalId>::get_in_hook())
74 }
75}
76
77impl Default for TerminalId {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83#[derive(Debug, thiserror::Error)]
85pub enum TerminalError {
86 #[error("Write error: {0}")]
87 WriteError(String),
88
89 #[error("Terminal not initialized")]
90 NotInitialized,
91}
92
93impl From<std::io::Error> for TerminalError {
94 fn from(e: std::io::Error) -> Self {
95 TerminalError::WriteError(e.to_string())
96 }
97}
98
99pub(crate) struct TerminalCleaner {
101 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
103 pub(crate) pty_task: TaskHandle,
105 pub(crate) closer_notifier: ArcNotify,
107}
108
109pub(crate) struct TerminalInner {
111 pub(crate) master: Box<dyn MasterPty + Send>,
112 pub(crate) last_write_time: Instant,
113 pub(crate) pressed_button: Option<TerminalMouseButton>,
114 pub(crate) modifiers: Modifiers,
115}
116
117impl Drop for TerminalCleaner {
118 fn drop(&mut self) {
119 *self.writer.borrow_mut() = None;
120 self.pty_task.try_cancel();
121 self.closer_notifier.notify();
122 }
123}
124
125#[derive(Clone)]
130pub struct TerminalHandle {
131 pub(crate) id: TerminalId,
133 pub(crate) term: Rc<RefCell<Term<EventProxy>>>,
136 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
138 pub(crate) inner: Rc<RefCell<TerminalInner>>,
140 pub(crate) cwd: Rc<RefCell<Option<PathBuf>>>,
142 pub(crate) title: Rc<RefCell<Option<String>>>,
144 pub(crate) closer_notifier: ArcNotify,
146 #[allow(dead_code)]
148 pub(crate) cleaner: Rc<TerminalCleaner>,
149 pub(crate) output_notifier: ArcNotify,
151 pub(crate) title_notifier: ArcNotify,
153 pub(crate) clipboard_content: Rc<RefCell<Option<String>>>,
155 pub(crate) clipboard_notifier: ArcNotify,
157}
158
159impl PartialEq for TerminalHandle {
160 fn eq(&self, other: &Self) -> bool {
161 self.id == other.id
162 }
163}
164
165impl TerminalHandle {
166 pub fn new(
181 id: TerminalId,
182 command: portable_pty::CommandBuilder,
183 scrollback_length: Option<usize>,
184 ) -> Result<Self, TerminalError> {
185 spawn_pty(id, command, scrollback_length.unwrap_or(1000))
186 }
187
188 pub fn write(&self, data: &[u8]) -> Result<(), TerminalError> {
190 self.write_raw(data)?;
191 let mut term = self.term.borrow_mut();
192 term.selection = None;
193 term.scroll_display(Scroll::Bottom);
194 self.inner.borrow_mut().last_write_time = Instant::now();
195 Ok(())
196 }
197
198 pub fn last_write_elapsed(&self) -> Duration {
200 self.inner.borrow().last_write_time.elapsed()
201 }
202
203 pub fn write_key(&self, key: &Key, modifiers: Modifiers) -> Result<bool, TerminalError> {
205 let shift = modifiers.contains(Modifiers::SHIFT);
206 let ctrl = modifiers.contains(Modifiers::CONTROL);
207 let alt = modifiers.contains(Modifiers::ALT);
208
209 let modifier = || 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
211
212 let seq: Vec<u8> = match key {
213 Key::Character(ch) if ctrl && ch.len() == 1 => vec![ch.as_bytes()[0] & 0x1f],
214 Key::Named(NamedKey::Enter) if shift || ctrl => {
215 format!("\x1b[13;{}u", modifier()).into_bytes()
216 }
217 Key::Named(NamedKey::Enter) => b"\r".to_vec(),
218 Key::Named(NamedKey::Backspace) if ctrl => vec![0x08],
219 Key::Named(NamedKey::Backspace) if alt => vec![0x1b, 0x7f],
220 Key::Named(NamedKey::Backspace) => vec![0x7f],
221 Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
222 format!("\x1b[3;{}~", modifier()).into_bytes()
223 }
224 Key::Named(NamedKey::Delete) => b"\x1b[3~".to_vec(),
225 Key::Named(NamedKey::Tab) if shift => b"\x1b[Z".to_vec(),
226 Key::Named(NamedKey::Tab) => b"\t".to_vec(),
227 Key::Named(NamedKey::Escape) => vec![0x1b],
228 Key::Named(
229 dir @ (NamedKey::ArrowUp
230 | NamedKey::ArrowDown
231 | NamedKey::ArrowLeft
232 | NamedKey::ArrowRight),
233 ) => {
234 let ch = match dir {
235 NamedKey::ArrowUp => 'A',
236 NamedKey::ArrowDown => 'B',
237 NamedKey::ArrowRight => 'C',
238 NamedKey::ArrowLeft => 'D',
239 _ => unreachable!(),
240 };
241 if shift || ctrl {
242 format!("\x1b[1;{}{ch}", modifier()).into_bytes()
243 } else {
244 vec![0x1b, b'[', ch as u8]
245 }
246 }
247 Key::Named(NamedKey::Home) => {
248 self.scroll(i32::MAX);
249 if self.term.borrow().grid().display_offset() != 0 {
250 return Ok(true);
251 }
252 if shift || ctrl {
253 format!("\x1b[1;{}H", modifier()).into_bytes()
254 } else {
255 b"\x1b[H".to_vec()
256 }
257 }
258 Key::Named(NamedKey::End) => {
259 if self.term.borrow().grid().display_offset() != 0 {
260 self.scroll_to_bottom();
261 return Ok(true);
262 }
263 if shift || ctrl {
264 format!("\x1b[1;{}F", modifier()).into_bytes()
265 } else {
266 b"\x1b[F".to_vec()
267 }
268 }
269 Key::Character(ch) => ch.as_bytes().to_vec(),
270 Key::Named(NamedKey::Shift) => {
271 self.shift_pressed(true);
272 return Ok(true);
273 }
274 _ => return Ok(false),
275 };
276
277 self.write(&seq)?;
278 Ok(true)
279 }
280
281 pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
283 let bracketed = self
284 .term
285 .borrow()
286 .mode()
287 .contains(TermMode::BRACKETED_PASTE);
288 if bracketed {
289 let filtered = text.replace(['\x1b', '\x03'], "");
290 self.write_raw(b"\x1b[200~")?;
291 self.write_raw(filtered.as_bytes())?;
292 self.write_raw(b"\x1b[201~")?;
293 } else {
294 let normalized = text.replace("\r\n", "\r").replace('\n', "\r");
295 self.write_raw(normalized.as_bytes())?;
296 }
297 Ok(())
298 }
299
300 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
302 let mut writer = self.writer.borrow_mut();
303 let writer = writer.as_mut().ok_or(TerminalError::NotInitialized)?;
304 writer.write_all(data)?;
305 writer.flush()?;
306 Ok(())
307 }
308
309 pub fn resize(&self, rows: u16, cols: u16) {
311 let _ = self.inner.borrow().master.resize(PtySize {
313 rows,
314 cols,
315 pixel_width: 0,
316 pixel_height: 0,
317 });
318
319 self.term.borrow_mut().resize(TermSize {
320 screen_lines: rows as usize,
321 columns: cols as usize,
322 });
323 }
324
325 pub fn scroll(&self, delta: i32) {
327 self.scroll_to(Scroll::Delta(delta));
328 }
329
330 pub fn scroll_to_bottom(&self) {
332 self.scroll_to(Scroll::Bottom);
333 }
334
335 fn scroll_to(&self, target: Scroll) {
336 let mut term = self.term.borrow_mut();
337 if term.mode().contains(TermMode::ALT_SCREEN) {
338 return;
339 }
340 term.scroll_display(target);
341 Platform::get().send(UserEvent::RequestRedraw);
342 }
343
344 pub fn cwd(&self) -> Option<PathBuf> {
346 self.cwd.borrow().clone()
347 }
348
349 pub fn title(&self) -> Option<String> {
351 self.title.borrow().clone()
352 }
353
354 pub fn clipboard_content(&self) -> Option<String> {
356 self.clipboard_content.borrow().clone()
357 }
358
359 fn mode(&self) -> TermMode {
361 *self.term.borrow().mode()
362 }
363
364 fn pressed_button(&self) -> Option<TerminalMouseButton> {
365 self.inner.borrow().pressed_button
366 }
367
368 fn set_pressed_button(&self, button: Option<TerminalMouseButton>) {
369 self.inner.borrow_mut().pressed_button = button;
370 }
371
372 fn is_shift_held(&self) -> bool {
373 self.inner.borrow().modifiers.contains(Modifiers::SHIFT)
374 }
375
376 pub fn mouse_move(&self, row: f32, col: f32) {
379 let held = self.pressed_button();
380
381 if self.is_shift_held() && held.is_some() {
382 self.update_selection(row, col);
383 return;
384 }
385
386 let mode = self.mode();
387 if mode.contains(TermMode::MOUSE_MOTION) {
388 let _ = self
390 .write_raw(encode_mouse_move(row as usize, col as usize, held, mode).as_bytes());
391 } else if mode.contains(TermMode::MOUSE_DRAG)
392 && let Some(button) = held
393 {
394 let _ = self.write_raw(
396 encode_mouse_move(row as usize, col as usize, Some(button), mode).as_bytes(),
397 );
398 } else if !mode.intersects(TermMode::MOUSE_MODE) && held.is_some() {
399 self.update_selection(row, col);
400 }
401 }
402
403 pub fn mouse_down(
407 &self,
408 row: f32,
409 col: f32,
410 button: TerminalMouseButton,
411 selection_type: SelectionType,
412 ) {
413 self.set_pressed_button(Some(button));
414
415 let mode = self.mode();
416 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
417 let _ = self
418 .write_raw(encode_mouse_press(row as usize, col as usize, button, mode).as_bytes());
419 } else {
420 self.start_selection(row, col, selection_type);
421 }
422 }
423
424 pub fn mouse_up(&self, row: f32, col: f32, button: TerminalMouseButton) {
426 self.set_pressed_button(None);
427
428 let mode = self.mode();
429 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
430 let _ = self.write_raw(
431 encode_mouse_release(row as usize, col as usize, button, mode).as_bytes(),
432 );
433 }
434 }
435
436 pub fn release(&self) {
438 self.set_pressed_button(None);
439 }
440
441 pub fn wheel(&self, delta_y: f64, row: f32, col: f32) {
444 let lines = (delta_y.abs().ceil() as i32).clamp(1, 10);
446 let scroll_delta = if delta_y > 0.0 { lines } else { -lines };
447
448 let mode = self.mode();
449 let scroll_offset = self.term.borrow().grid().display_offset();
450
451 if scroll_offset > 0 {
452 self.scroll(scroll_delta);
453 } else if mode.intersects(TermMode::MOUSE_MODE) {
454 let _ = self.write_raw(
455 encode_wheel_event(row as usize, col as usize, delta_y, mode).as_bytes(),
456 );
457 } else if mode.contains(TermMode::ALT_SCREEN) {
458 let app_cursor = mode.contains(TermMode::APP_CURSOR);
459 let key = match (delta_y > 0.0, app_cursor) {
460 (true, true) => "\x1bOA",
461 (true, false) => "\x1b[A",
462 (false, true) => "\x1bOB",
463 (false, false) => "\x1b[B",
464 };
465 for _ in 0..lines {
466 let _ = self.write_raw(key.as_bytes());
467 }
468 } else {
469 self.scroll(scroll_delta);
470 }
471 }
472
473 pub fn term(&self) -> std::cell::Ref<'_, Term<EventProxy>> {
475 self.term.borrow()
476 }
477
478 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
480 self.output_notifier.notified()
481 }
482
483 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
485 self.title_notifier.notified()
486 }
487
488 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
490 self.clipboard_notifier.notified()
491 }
492
493 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
495 self.closer_notifier.notified()
496 }
497
498 pub fn id(&self) -> TerminalId {
500 self.id
501 }
502
503 pub fn shift_pressed(&self, pressed: bool) {
505 let mods = &mut self.inner.borrow_mut().modifiers;
506 if pressed {
507 mods.insert(Modifiers::SHIFT);
508 } else {
509 mods.remove(Modifiers::SHIFT);
510 }
511 }
512
513 pub fn start_selection(&self, row: f32, col: f32, selection_type: SelectionType) {
515 let (point, side) = self.point_and_side_at(row, col);
516 self.term.borrow_mut().selection = Some(Selection::new(selection_type, point, side));
517 Platform::get().send(UserEvent::RequestRedraw);
518 }
519
520 pub fn update_selection(&self, row: f32, col: f32) {
522 let (point, side) = self.point_and_side_at(row, col);
523 if let Some(selection) = self.term.borrow_mut().selection.as_mut() {
524 selection.update(point, side);
525 Platform::get().send(UserEvent::RequestRedraw);
526 }
527 }
528
529 pub fn get_selected_text(&self) -> Option<String> {
531 self.term.borrow().selection_to_string()
532 }
533
534 pub fn hyperlink_at(&self, row: f32, col: f32) -> Option<String> {
538 let (point, _) = self.point_and_side_at(row, col);
539 let term = self.term.borrow();
540 let grid = term.grid();
541 if let Some(h) = grid[point].hyperlink() {
542 return Some(h.uri().to_owned());
543 }
544 url_at(&grid[point.line][..], point.column.0)
545 }
546
547 fn point_and_side_at(&self, row: f32, col: f32) -> (Point, Side) {
549 let term = self.term.borrow();
550 let col = col.max(0.0);
551 let side = if col.fract() < 0.5 {
552 Side::Left
553 } else {
554 Side::Right
555 };
556 let point = Point::new(
557 Line(row.max(0.0) as i32 - term.grid().display_offset() as i32),
558 Column((col as usize).min(term.columns().saturating_sub(1))),
559 );
560 (point, side)
561 }
562}