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::Character(ch) => ch.as_bytes().to_vec(),
248 Key::Named(NamedKey::Shift) => {
249 self.shift_pressed(true);
250 return Ok(true);
251 }
252 _ => return Ok(false),
253 };
254
255 self.write(&seq)?;
256 Ok(true)
257 }
258
259 pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
261 let bracketed = self
262 .term
263 .borrow()
264 .mode()
265 .contains(TermMode::BRACKETED_PASTE);
266 if bracketed {
267 let filtered = text.replace(['\x1b', '\x03'], "");
268 self.write_raw(b"\x1b[200~")?;
269 self.write_raw(filtered.as_bytes())?;
270 self.write_raw(b"\x1b[201~")?;
271 } else {
272 let normalized = text.replace("\r\n", "\r").replace('\n', "\r");
273 self.write_raw(normalized.as_bytes())?;
274 }
275 Ok(())
276 }
277
278 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
280 let mut writer = self.writer.borrow_mut();
281 let writer = writer.as_mut().ok_or(TerminalError::NotInitialized)?;
282 writer.write_all(data)?;
283 writer.flush()?;
284 Ok(())
285 }
286
287 pub fn resize(&self, rows: u16, cols: u16) {
289 let _ = self.inner.borrow().master.resize(PtySize {
291 rows,
292 cols,
293 pixel_width: 0,
294 pixel_height: 0,
295 });
296
297 self.term.borrow_mut().resize(TermSize {
298 screen_lines: rows as usize,
299 columns: cols as usize,
300 });
301 }
302
303 pub fn scroll(&self, delta: i32) {
305 self.scroll_to(Scroll::Delta(delta));
306 }
307
308 pub fn scroll_to_bottom(&self) {
310 self.scroll_to(Scroll::Bottom);
311 }
312
313 fn scroll_to(&self, target: Scroll) {
314 let mut term = self.term.borrow_mut();
315 if term.mode().contains(TermMode::ALT_SCREEN) {
316 return;
317 }
318 term.scroll_display(target);
319 Platform::get().send(UserEvent::RequestRedraw);
320 }
321
322 pub fn cwd(&self) -> Option<PathBuf> {
324 self.cwd.borrow().clone()
325 }
326
327 pub fn title(&self) -> Option<String> {
329 self.title.borrow().clone()
330 }
331
332 pub fn clipboard_content(&self) -> Option<String> {
334 self.clipboard_content.borrow().clone()
335 }
336
337 fn mode(&self) -> TermMode {
339 *self.term.borrow().mode()
340 }
341
342 fn pressed_button(&self) -> Option<TerminalMouseButton> {
343 self.inner.borrow().pressed_button
344 }
345
346 fn set_pressed_button(&self, button: Option<TerminalMouseButton>) {
347 self.inner.borrow_mut().pressed_button = button;
348 }
349
350 fn is_shift_held(&self) -> bool {
351 self.inner.borrow().modifiers.contains(Modifiers::SHIFT)
352 }
353
354 pub fn mouse_move(&self, row: f32, col: f32) {
357 let held = self.pressed_button();
358
359 if self.is_shift_held() && held.is_some() {
360 self.update_selection(row, col);
361 return;
362 }
363
364 let mode = self.mode();
365 if mode.contains(TermMode::MOUSE_MOTION) {
366 let _ = self
368 .write_raw(encode_mouse_move(row as usize, col as usize, held, mode).as_bytes());
369 } else if mode.contains(TermMode::MOUSE_DRAG)
370 && let Some(button) = held
371 {
372 let _ = self.write_raw(
374 encode_mouse_move(row as usize, col as usize, Some(button), mode).as_bytes(),
375 );
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(
385 &self,
386 row: f32,
387 col: f32,
388 button: TerminalMouseButton,
389 selection_type: SelectionType,
390 ) {
391 self.set_pressed_button(Some(button));
392
393 let mode = self.mode();
394 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
395 let _ = self
396 .write_raw(encode_mouse_press(row as usize, col as usize, button, mode).as_bytes());
397 } else {
398 self.start_selection(row, col, selection_type);
399 }
400 }
401
402 pub fn mouse_up(&self, row: f32, col: f32, button: TerminalMouseButton) {
404 self.set_pressed_button(None);
405
406 let mode = self.mode();
407 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
408 let _ = self.write_raw(
409 encode_mouse_release(row as usize, col as usize, button, mode).as_bytes(),
410 );
411 }
412 }
413
414 pub fn release(&self) {
416 self.set_pressed_button(None);
417 }
418
419 pub fn wheel(&self, delta_y: f64, row: f32, col: f32) {
422 let lines = (delta_y.abs().ceil() as i32).clamp(1, 10);
424 let scroll_delta = if delta_y > 0.0 { lines } else { -lines };
425
426 let mode = self.mode();
427 let scroll_offset = self.term.borrow().grid().display_offset();
428
429 if scroll_offset > 0 {
430 self.scroll(scroll_delta);
431 } else if mode.intersects(TermMode::MOUSE_MODE) {
432 let _ = self.write_raw(
433 encode_wheel_event(row as usize, col as usize, delta_y, mode).as_bytes(),
434 );
435 } else if mode.contains(TermMode::ALT_SCREEN) {
436 let app_cursor = mode.contains(TermMode::APP_CURSOR);
437 let key = match (delta_y > 0.0, app_cursor) {
438 (true, true) => "\x1bOA",
439 (true, false) => "\x1b[A",
440 (false, true) => "\x1bOB",
441 (false, false) => "\x1b[B",
442 };
443 for _ in 0..lines {
444 let _ = self.write_raw(key.as_bytes());
445 }
446 } else {
447 self.scroll(scroll_delta);
448 }
449 }
450
451 pub fn term(&self) -> std::cell::Ref<'_, Term<EventProxy>> {
453 self.term.borrow()
454 }
455
456 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
458 self.output_notifier.notified()
459 }
460
461 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
463 self.title_notifier.notified()
464 }
465
466 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
468 self.clipboard_notifier.notified()
469 }
470
471 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
473 self.closer_notifier.notified()
474 }
475
476 pub fn id(&self) -> TerminalId {
478 self.id
479 }
480
481 pub fn shift_pressed(&self, pressed: bool) {
483 let mods = &mut self.inner.borrow_mut().modifiers;
484 if pressed {
485 mods.insert(Modifiers::SHIFT);
486 } else {
487 mods.remove(Modifiers::SHIFT);
488 }
489 }
490
491 pub fn start_selection(&self, row: f32, col: f32, selection_type: SelectionType) {
493 let (point, side) = self.point_and_side_at(row, col);
494 self.term.borrow_mut().selection = Some(Selection::new(selection_type, point, side));
495 Platform::get().send(UserEvent::RequestRedraw);
496 }
497
498 pub fn update_selection(&self, row: f32, col: f32) {
500 let (point, side) = self.point_and_side_at(row, col);
501 if let Some(selection) = self.term.borrow_mut().selection.as_mut() {
502 selection.update(point, side);
503 Platform::get().send(UserEvent::RequestRedraw);
504 }
505 }
506
507 pub fn get_selected_text(&self) -> Option<String> {
509 self.term.borrow().selection_to_string()
510 }
511
512 pub fn hyperlink_at(&self, row: f32, col: f32) -> Option<String> {
516 let (point, _) = self.point_and_side_at(row, col);
517 let term = self.term.borrow();
518 let grid = term.grid();
519 if let Some(h) = grid[point].hyperlink() {
520 return Some(h.uri().to_owned());
521 }
522 url_at(&grid[point.line][..], point.column.0)
523 }
524
525 fn point_and_side_at(&self, row: f32, col: f32) -> (Point, Side) {
527 let term = self.term.borrow();
528 let col = col.max(0.0);
529 let side = if col.fract() < 0.5 {
530 Side::Left
531 } else {
532 Side::Right
533 };
534 let point = Point::new(
535 Line(row.max(0.0) as i32 - term.grid().display_offset() as i32),
536 Column((col as usize).min(term.columns().saturating_sub(1))),
537 );
538 (point, side)
539 }
540}