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};
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct TerminalId(pub usize);
69
70impl TerminalId {
71 pub fn new() -> Self {
72 Self(UseId::<TerminalId>::get_in_hook())
73 }
74}
75
76impl Default for TerminalId {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum TerminalError {
85 #[error("Write error: {0}")]
86 WriteError(String),
87
88 #[error("Terminal not initialized")]
89 NotInitialized,
90}
91
92impl From<std::io::Error> for TerminalError {
93 fn from(e: std::io::Error) -> Self {
94 TerminalError::WriteError(e.to_string())
95 }
96}
97
98pub(crate) struct TerminalCleaner {
100 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
102 pub(crate) pty_task: TaskHandle,
104 pub(crate) closer_notifier: ArcNotify,
106}
107
108pub(crate) struct TerminalInner {
110 pub(crate) master: Box<dyn MasterPty + Send>,
111 pub(crate) last_write_time: Instant,
112 pub(crate) pressed_button: Option<TerminalMouseButton>,
113 pub(crate) modifiers: Modifiers,
114}
115
116impl Drop for TerminalCleaner {
117 fn drop(&mut self) {
118 *self.writer.borrow_mut() = None;
119 self.pty_task.try_cancel();
120 self.closer_notifier.notify();
121 }
122}
123
124#[derive(Clone)]
129pub struct TerminalHandle {
130 pub(crate) id: TerminalId,
132 pub(crate) term: Rc<RefCell<Term<EventProxy>>>,
135 pub(crate) writer: Rc<RefCell<Option<Box<dyn Write + Send>>>>,
137 pub(crate) inner: Rc<RefCell<TerminalInner>>,
139 pub(crate) cwd: Rc<RefCell<Option<PathBuf>>>,
141 pub(crate) title: Rc<RefCell<Option<String>>>,
143 pub(crate) closer_notifier: ArcNotify,
145 #[allow(dead_code)]
147 pub(crate) cleaner: Rc<TerminalCleaner>,
148 pub(crate) output_notifier: ArcNotify,
150 pub(crate) title_notifier: ArcNotify,
152 pub(crate) clipboard_content: Rc<RefCell<Option<String>>>,
154 pub(crate) clipboard_notifier: ArcNotify,
156}
157
158impl PartialEq for TerminalHandle {
159 fn eq(&self, other: &Self) -> bool {
160 self.id == other.id
161 }
162}
163
164impl TerminalHandle {
165 pub fn new(
180 id: TerminalId,
181 command: portable_pty::CommandBuilder,
182 scrollback_length: Option<usize>,
183 ) -> Result<Self, TerminalError> {
184 spawn_pty(id, command, scrollback_length.unwrap_or(1000))
185 }
186
187 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> {
206 let shift = modifiers.contains(Modifiers::SHIFT);
207 let ctrl = modifiers.contains(Modifiers::CONTROL);
208 let alt = modifiers.contains(Modifiers::ALT);
209
210 let modifier = || 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
212
213 let seq: Vec<u8> = match key {
214 Key::Character(ch) if ctrl && ch.len() == 1 => vec![ch.as_bytes()[0] & 0x1f],
215 Key::Named(NamedKey::Enter) if shift || ctrl => {
216 format!("\x1b[13;{}u", modifier()).into_bytes()
217 }
218 Key::Named(NamedKey::Enter) => b"\r".to_vec(),
219 Key::Named(NamedKey::Backspace) if ctrl => vec![0x08],
220 Key::Named(NamedKey::Backspace) if alt => vec![0x1b, 0x7f],
221 Key::Named(NamedKey::Backspace) => vec![0x7f],
222 Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
223 format!("\x1b[3;{}~", modifier()).into_bytes()
224 }
225 Key::Named(NamedKey::Delete) => b"\x1b[3~".to_vec(),
226 Key::Named(NamedKey::Tab) if shift => b"\x1b[Z".to_vec(),
227 Key::Named(NamedKey::Tab) => b"\t".to_vec(),
228 Key::Named(NamedKey::Escape) => vec![0x1b],
229 Key::Named(
230 dir @ (NamedKey::ArrowUp
231 | NamedKey::ArrowDown
232 | NamedKey::ArrowLeft
233 | NamedKey::ArrowRight),
234 ) => {
235 let ch = match dir {
236 NamedKey::ArrowUp => 'A',
237 NamedKey::ArrowDown => 'B',
238 NamedKey::ArrowRight => 'C',
239 NamedKey::ArrowLeft => 'D',
240 _ => unreachable!(),
241 };
242 if shift || ctrl {
243 format!("\x1b[1;{}{ch}", modifier()).into_bytes()
244 } else {
245 vec![0x1b, b'[', ch as u8]
246 }
247 }
248 Key::Character(ch) => ch.as_bytes().to_vec(),
249 Key::Named(NamedKey::Shift) => {
250 self.shift_pressed(true);
251 return Ok(true);
252 }
253 _ => return Ok(false),
254 };
255
256 self.write(&seq)?;
257 Ok(true)
258 }
259
260 pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
263 let bracketed = self
264 .term
265 .borrow()
266 .mode()
267 .contains(TermMode::BRACKETED_PASTE);
268 if bracketed {
269 let filtered = text.replace(['\x1b', '\x03'], "");
270 self.write_raw(b"\x1b[200~")?;
271 self.write_raw(filtered.as_bytes())?;
272 self.write_raw(b"\x1b[201~")?;
273 } else {
274 let normalized = text.replace("\r\n", "\r").replace('\n', "\r");
275 self.write_raw(normalized.as_bytes())?;
276 }
277 Ok(())
278 }
279
280 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
282 let mut writer = self.writer.borrow_mut();
283 let writer = writer.as_mut().ok_or(TerminalError::NotInitialized)?;
284 writer.write_all(data)?;
285 writer.flush()?;
286 Ok(())
287 }
288
289 pub fn resize(&self, rows: u16, cols: u16) {
292 let _ = self.inner.borrow().master.resize(PtySize {
294 rows,
295 cols,
296 pixel_width: 0,
297 pixel_height: 0,
298 });
299
300 self.term.borrow_mut().resize(TermSize {
301 screen_lines: rows as usize,
302 columns: cols as usize,
303 });
304 }
305
306 pub fn scroll(&self, delta: i32) {
309 self.scroll_to(Scroll::Delta(delta));
310 }
311
312 pub fn scroll_to_bottom(&self) {
314 self.scroll_to(Scroll::Bottom);
315 }
316
317 fn scroll_to(&self, target: Scroll) {
318 let mut term = self.term.borrow_mut();
319 if term.mode().contains(TermMode::ALT_SCREEN) {
320 return;
321 }
322 term.scroll_display(target);
323 Platform::get().send(UserEvent::RequestRedraw);
324 }
325
326 pub fn cwd(&self) -> Option<PathBuf> {
328 self.cwd.borrow().clone()
329 }
330
331 pub fn title(&self) -> Option<String> {
333 self.title.borrow().clone()
334 }
335
336 pub fn clipboard_content(&self) -> Option<String> {
338 self.clipboard_content.borrow().clone()
339 }
340
341 fn mode(&self) -> TermMode {
343 *self.term.borrow().mode()
344 }
345
346 fn pressed_button(&self) -> Option<TerminalMouseButton> {
347 self.inner.borrow().pressed_button
348 }
349
350 fn set_pressed_button(&self, button: Option<TerminalMouseButton>) {
351 self.inner.borrow_mut().pressed_button = button;
352 }
353
354 fn is_shift_held(&self) -> bool {
355 self.inner.borrow().modifiers.contains(Modifiers::SHIFT)
356 }
357
358 pub fn mouse_move(&self, row: usize, col: usize) {
360 let held = self.pressed_button();
361
362 if self.is_shift_held() && held.is_some() {
363 self.update_selection(row, col);
364 return;
365 }
366
367 let mode = self.mode();
368 if mode.contains(TermMode::MOUSE_MOTION) {
369 let _ = self.write_raw(encode_mouse_move(row, col, held, mode).as_bytes());
371 } else if mode.contains(TermMode::MOUSE_DRAG)
372 && let Some(button) = held
373 {
374 let _ = self.write_raw(encode_mouse_move(row, col, Some(button), mode).as_bytes());
376 } else if !mode.intersects(TermMode::MOUSE_MODE) && held.is_some() {
377 self.update_selection(row, col);
378 }
379 }
380
381 pub fn mouse_down(&self, row: usize, col: usize, button: TerminalMouseButton) {
383 self.set_pressed_button(Some(button));
384
385 let mode = self.mode();
386 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
387 let _ = self.write_raw(encode_mouse_press(row, col, button, mode).as_bytes());
388 } else {
389 self.start_selection(row, col);
390 }
391 }
392
393 pub fn mouse_up(&self, row: usize, col: usize, button: TerminalMouseButton) {
395 self.set_pressed_button(None);
396
397 let mode = self.mode();
398 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
399 let _ = self.write_raw(encode_mouse_release(row, col, button, mode).as_bytes());
400 }
401 }
402
403 pub fn release(&self) {
405 self.set_pressed_button(None);
406 }
407
408 pub fn wheel(&self, delta_y: f64, row: usize, col: usize) {
412 let lines = (delta_y.abs().ceil() as i32).clamp(1, 10);
414 let scroll_delta = if delta_y > 0.0 { lines } else { -lines };
415
416 let mode = self.mode();
417 let scroll_offset = self.term.borrow().grid().display_offset();
418
419 if scroll_offset > 0 {
420 self.scroll(scroll_delta);
421 } else if mode.intersects(TermMode::MOUSE_MODE) {
422 let _ = self.write_raw(encode_wheel_event(row, col, delta_y, mode).as_bytes());
423 } else if mode.contains(TermMode::ALT_SCREEN) {
424 let app_cursor = mode.contains(TermMode::APP_CURSOR);
425 let key = match (delta_y > 0.0, app_cursor) {
426 (true, true) => "\x1bOA",
427 (true, false) => "\x1b[A",
428 (false, true) => "\x1bOB",
429 (false, false) => "\x1b[B",
430 };
431 for _ in 0..lines {
432 let _ = self.write_raw(key.as_bytes());
433 }
434 } else {
435 self.scroll(scroll_delta);
436 }
437 }
438
439 pub fn term(&self) -> std::cell::Ref<'_, Term<EventProxy>> {
443 self.term.borrow()
444 }
445
446 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
448 self.output_notifier.notified()
449 }
450
451 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
453 self.title_notifier.notified()
454 }
455
456 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
458 self.clipboard_notifier.notified()
459 }
460
461 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
463 self.closer_notifier.notified()
464 }
465
466 pub fn id(&self) -> TerminalId {
468 self.id
469 }
470
471 pub fn shift_pressed(&self, pressed: bool) {
473 let mods = &mut self.inner.borrow_mut().modifiers;
474 if pressed {
475 mods.insert(Modifiers::SHIFT);
476 } else {
477 mods.remove(Modifiers::SHIFT);
478 }
479 }
480
481 pub fn start_selection(&self, row: usize, col: usize) {
482 let mut term = self.term.borrow_mut();
483 let point = point_at(&term, row, col);
484 term.selection = Some(Selection::new(SelectionType::Simple, point, Side::Left));
485 Platform::get().send(UserEvent::RequestRedraw);
486 }
487
488 pub fn update_selection(&self, row: usize, col: usize) {
490 let mut term = self.term.borrow_mut();
491 let point = point_at(&term, row, col);
492 if let Some(selection) = term.selection.as_mut() {
493 selection.update(point, Side::Right);
494 Platform::get().send(UserEvent::RequestRedraw);
495 }
496 }
497
498 pub fn get_selected_text(&self) -> Option<String> {
500 self.term.borrow().selection_to_string()
501 }
502}
503
504fn point_at(term: &Term<EventProxy>, row: usize, col: usize) -> Point {
507 let offset = term.grid().display_offset() as i32;
508 let last_col = term.columns().saturating_sub(1);
509 Point::new(Line(row as i32 - offset), Column(col.min(last_col)))
510}