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> {
189 self.write_raw(data)?;
190 let mut term = self.term.borrow_mut();
191 term.selection = None;
192 term.scroll_display(Scroll::Bottom);
193 self.inner.borrow_mut().last_write_time = Instant::now();
194 Ok(())
195 }
196
197 pub fn last_write_elapsed(&self) -> Duration {
199 self.inner.borrow().last_write_time.elapsed()
200 }
201
202 pub fn write_key(&self, key: &Key, modifiers: Modifiers) -> Result<bool, TerminalError> {
204 let shift = modifiers.contains(Modifiers::SHIFT);
205 let ctrl = modifiers.contains(Modifiers::CONTROL);
206 let alt = modifiers.contains(Modifiers::ALT);
207
208 let modifier = || 1 + shift as u8 + (alt as u8) * 2 + (ctrl as u8) * 4;
210
211 let seq: Vec<u8> = match key {
212 Key::Character(ch) if ctrl && ch.len() == 1 => vec![ch.as_bytes()[0] & 0x1f],
213 Key::Named(NamedKey::Enter) if shift || ctrl => {
214 format!("\x1b[13;{}u", modifier()).into_bytes()
215 }
216 Key::Named(NamedKey::Enter) => b"\r".to_vec(),
217 Key::Named(NamedKey::Backspace) if ctrl => vec![0x08],
218 Key::Named(NamedKey::Backspace) if alt => vec![0x1b, 0x7f],
219 Key::Named(NamedKey::Backspace) => vec![0x7f],
220 Key::Named(NamedKey::Delete) if alt || ctrl || shift => {
221 format!("\x1b[3;{}~", modifier()).into_bytes()
222 }
223 Key::Named(NamedKey::Delete) => b"\x1b[3~".to_vec(),
224 Key::Named(NamedKey::Tab) if shift => b"\x1b[Z".to_vec(),
225 Key::Named(NamedKey::Tab) => b"\t".to_vec(),
226 Key::Named(NamedKey::Escape) => vec![0x1b],
227 Key::Named(
228 dir @ (NamedKey::ArrowUp
229 | NamedKey::ArrowDown
230 | NamedKey::ArrowLeft
231 | NamedKey::ArrowRight),
232 ) => {
233 let ch = match dir {
234 NamedKey::ArrowUp => 'A',
235 NamedKey::ArrowDown => 'B',
236 NamedKey::ArrowRight => 'C',
237 NamedKey::ArrowLeft => 'D',
238 _ => unreachable!(),
239 };
240 if shift || ctrl {
241 format!("\x1b[1;{}{ch}", modifier()).into_bytes()
242 } else {
243 vec![0x1b, b'[', ch as u8]
244 }
245 }
246 Key::Character(ch) => ch.as_bytes().to_vec(),
247 Key::Named(NamedKey::Shift) => {
248 self.shift_pressed(true);
249 return Ok(true);
250 }
251 _ => return Ok(false),
252 };
253
254 self.write(&seq)?;
255 Ok(true)
256 }
257
258 pub fn paste(&self, text: &str) -> Result<(), TerminalError> {
260 let bracketed = self
261 .term
262 .borrow()
263 .mode()
264 .contains(TermMode::BRACKETED_PASTE);
265 if bracketed {
266 let filtered = text.replace(['\x1b', '\x03'], "");
267 self.write_raw(b"\x1b[200~")?;
268 self.write_raw(filtered.as_bytes())?;
269 self.write_raw(b"\x1b[201~")?;
270 } else {
271 let normalized = text.replace("\r\n", "\r").replace('\n', "\r");
272 self.write_raw(normalized.as_bytes())?;
273 }
274 Ok(())
275 }
276
277 fn write_raw(&self, data: &[u8]) -> Result<(), TerminalError> {
279 let mut writer = self.writer.borrow_mut();
280 let writer = writer.as_mut().ok_or(TerminalError::NotInitialized)?;
281 writer.write_all(data)?;
282 writer.flush()?;
283 Ok(())
284 }
285
286 pub fn resize(&self, rows: u16, cols: u16) {
288 let _ = self.inner.borrow().master.resize(PtySize {
290 rows,
291 cols,
292 pixel_width: 0,
293 pixel_height: 0,
294 });
295
296 self.term.borrow_mut().resize(TermSize {
297 screen_lines: rows as usize,
298 columns: cols as usize,
299 });
300 }
301
302 pub fn scroll(&self, delta: i32) {
304 self.scroll_to(Scroll::Delta(delta));
305 }
306
307 pub fn scroll_to_bottom(&self) {
309 self.scroll_to(Scroll::Bottom);
310 }
311
312 fn scroll_to(&self, target: Scroll) {
313 let mut term = self.term.borrow_mut();
314 if term.mode().contains(TermMode::ALT_SCREEN) {
315 return;
316 }
317 term.scroll_display(target);
318 Platform::get().send(UserEvent::RequestRedraw);
319 }
320
321 pub fn cwd(&self) -> Option<PathBuf> {
323 self.cwd.borrow().clone()
324 }
325
326 pub fn title(&self) -> Option<String> {
328 self.title.borrow().clone()
329 }
330
331 pub fn clipboard_content(&self) -> Option<String> {
333 self.clipboard_content.borrow().clone()
334 }
335
336 fn mode(&self) -> TermMode {
338 *self.term.borrow().mode()
339 }
340
341 fn pressed_button(&self) -> Option<TerminalMouseButton> {
342 self.inner.borrow().pressed_button
343 }
344
345 fn set_pressed_button(&self, button: Option<TerminalMouseButton>) {
346 self.inner.borrow_mut().pressed_button = button;
347 }
348
349 fn is_shift_held(&self) -> bool {
350 self.inner.borrow().modifiers.contains(Modifiers::SHIFT)
351 }
352
353 pub fn mouse_move(&self, row: f32, col: f32) {
356 let held = self.pressed_button();
357
358 if self.is_shift_held() && held.is_some() {
359 self.update_selection(row, col);
360 return;
361 }
362
363 let mode = self.mode();
364 if mode.contains(TermMode::MOUSE_MOTION) {
365 let _ = self
367 .write_raw(encode_mouse_move(row as usize, col as usize, held, mode).as_bytes());
368 } else if mode.contains(TermMode::MOUSE_DRAG)
369 && let Some(button) = held
370 {
371 let _ = self.write_raw(
373 encode_mouse_move(row as usize, col as usize, Some(button), mode).as_bytes(),
374 );
375 } else if !mode.intersects(TermMode::MOUSE_MODE) && held.is_some() {
376 self.update_selection(row, col);
377 }
378 }
379
380 pub fn mouse_down(
384 &self,
385 row: f32,
386 col: f32,
387 button: TerminalMouseButton,
388 selection_type: SelectionType,
389 ) {
390 self.set_pressed_button(Some(button));
391
392 let mode = self.mode();
393 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
394 let _ = self
395 .write_raw(encode_mouse_press(row as usize, col as usize, button, mode).as_bytes());
396 } else {
397 self.start_selection(row, col, selection_type);
398 }
399 }
400
401 pub fn mouse_up(&self, row: f32, col: f32, button: TerminalMouseButton) {
403 self.set_pressed_button(None);
404
405 let mode = self.mode();
406 if !self.is_shift_held() && mode.intersects(TermMode::MOUSE_MODE) {
407 let _ = self.write_raw(
408 encode_mouse_release(row as usize, col as usize, button, mode).as_bytes(),
409 );
410 }
411 }
412
413 pub fn release(&self) {
415 self.set_pressed_button(None);
416 }
417
418 pub fn wheel(&self, delta_y: f64, row: f32, col: f32) {
421 let lines = (delta_y.abs().ceil() as i32).clamp(1, 10);
423 let scroll_delta = if delta_y > 0.0 { lines } else { -lines };
424
425 let mode = self.mode();
426 let scroll_offset = self.term.borrow().grid().display_offset();
427
428 if scroll_offset > 0 {
429 self.scroll(scroll_delta);
430 } else if mode.intersects(TermMode::MOUSE_MODE) {
431 let _ = self.write_raw(
432 encode_wheel_event(row as usize, col as usize, delta_y, mode).as_bytes(),
433 );
434 } else if mode.contains(TermMode::ALT_SCREEN) {
435 let app_cursor = mode.contains(TermMode::APP_CURSOR);
436 let key = match (delta_y > 0.0, app_cursor) {
437 (true, true) => "\x1bOA",
438 (true, false) => "\x1b[A",
439 (false, true) => "\x1bOB",
440 (false, false) => "\x1b[B",
441 };
442 for _ in 0..lines {
443 let _ = self.write_raw(key.as_bytes());
444 }
445 } else {
446 self.scroll(scroll_delta);
447 }
448 }
449
450 pub fn term(&self) -> std::cell::Ref<'_, Term<EventProxy>> {
452 self.term.borrow()
453 }
454
455 pub fn output_received(&self) -> impl std::future::Future<Output = ()> + '_ {
457 self.output_notifier.notified()
458 }
459
460 pub fn title_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
462 self.title_notifier.notified()
463 }
464
465 pub fn clipboard_changed(&self) -> impl std::future::Future<Output = ()> + '_ {
467 self.clipboard_notifier.notified()
468 }
469
470 pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
472 self.closer_notifier.notified()
473 }
474
475 pub fn id(&self) -> TerminalId {
477 self.id
478 }
479
480 pub fn shift_pressed(&self, pressed: bool) {
482 let mods = &mut self.inner.borrow_mut().modifiers;
483 if pressed {
484 mods.insert(Modifiers::SHIFT);
485 } else {
486 mods.remove(Modifiers::SHIFT);
487 }
488 }
489
490 pub fn start_selection(&self, row: f32, col: f32, selection_type: SelectionType) {
492 let (point, side) = self.point_and_side_at(row, col);
493 self.term.borrow_mut().selection = Some(Selection::new(selection_type, point, side));
494 Platform::get().send(UserEvent::RequestRedraw);
495 }
496
497 pub fn update_selection(&self, row: f32, col: f32) {
499 let (point, side) = self.point_and_side_at(row, col);
500 if let Some(selection) = self.term.borrow_mut().selection.as_mut() {
501 selection.update(point, side);
502 Platform::get().send(UserEvent::RequestRedraw);
503 }
504 }
505
506 pub fn get_selected_text(&self) -> Option<String> {
508 self.term.borrow().selection_to_string()
509 }
510
511 fn point_and_side_at(&self, row: f32, col: f32) -> (Point, Side) {
513 let term = self.term.borrow();
514 let col = col.max(0.0);
515 let side = if col.fract() < 0.5 {
516 Side::Left
517 } else {
518 Side::Right
519 };
520 let point = Point::new(
521 Line(row.max(0.0) as i32 - term.grid().display_offset() as i32),
522 Column((col as usize).min(term.columns().saturating_sub(1))),
523 );
524 (point, side)
525 }
526}