1pub mod mappings;
2
3pub use alacritty_terminal;
4
5pub mod terminal_settings;
6
7use alacritty_terminal::{
8 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
9 event_loop::{EventLoop, Msg, Notifier},
10 grid::{Dimensions, Scroll as AlacScroll},
11 index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
12 selection::{Selection, SelectionRange, SelectionType},
13 sync::FairMutex,
14 term::{
15 cell::Cell,
16 search::{Match, RegexIter, RegexSearch},
17 Config, RenderableCursor, TermMode,
18 },
19 tty::{self, setup_env},
20 vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode, Rgb},
21 Term,
22};
23use anyhow::{bail, Result};
24
25use futures::{
26 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
27 FutureExt,
28};
29
30use mappings::mouse::{
31 alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report,
32 scroll_report,
33};
34
35use collections::{HashMap, VecDeque};
36use futures::StreamExt;
37use procinfo::LocalProcessInfo;
38use serde::{Deserialize, Serialize};
39use settings::Settings;
40use smol::channel::{Receiver, Sender};
41#[cfg(target_os = "windows")]
42use std::num::NonZeroU32;
43use task::TaskId;
44use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
45use theme::{ActiveTheme, Theme};
46use util::truncate_and_trailoff;
47#[cfg(target_os = "windows")]
48use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
49
50use std::{
51 cmp::{self, min},
52 fmt::Display,
53 ops::{Deref, Index, RangeInclusive},
54 path::PathBuf,
55 sync::Arc,
56 time::Duration,
57};
58use thiserror::Error;
59
60#[cfg(unix)]
61use std::os::unix::prelude::AsRawFd;
62
63use gpui::{
64 actions, black, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla,
65 Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
66 Pixels, Point, Rgba, ScrollWheelEvent, Size, Task, TouchPhase,
67};
68
69use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
70
71actions!(
72 terminal,
73 [Clear, Copy, Paste, ShowCharacterPalette, SearchTest,]
74);
75
76///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
77///Scroll multiplier that is set to 3 by default. This will be removed when I
78///Implement scroll bars.
79const SCROLL_MULTIPLIER: f32 = 4.;
80const MAX_SEARCH_LINES: usize = 100;
81const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);
82const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
83const DEBUG_CELL_WIDTH: Pixels = px(5.);
84const DEBUG_LINE_HEIGHT: Pixels = px(5.);
85
86///Upward flowing events, for changing the title and such
87#[derive(Clone, Debug)]
88pub enum Event {
89 TitleChanged,
90 BreadcrumbsChanged,
91 CloseTerminal,
92 Bell,
93 Wakeup,
94 BlinkChanged,
95 SelectionsChanged,
96 NewNavigationTarget(Option<MaybeNavigationTarget>),
97 Open(MaybeNavigationTarget),
98}
99
100#[derive(Clone, Debug)]
101pub struct PathLikeTarget {
102 /// File system path, absolute or relative, existing or not.
103 /// Might have line and column number(s) attached as `file.rs:1:23`
104 pub maybe_path: String,
105 /// Current working directory of the terminal
106 pub terminal_dir: Option<PathBuf>,
107}
108
109/// A string inside terminal, potentially useful as a URI that can be opened.
110#[derive(Clone, Debug)]
111pub enum MaybeNavigationTarget {
112 /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
113 Url(String),
114 /// File system path, absolute or relative, existing or not.
115 /// Might have line and column number(s) attached as `file.rs:1:23`
116 PathLike(PathLikeTarget),
117}
118
119#[derive(Clone)]
120enum InternalEvent {
121 ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
122 Resize(TerminalSize),
123 Clear,
124 // FocusNextMatch,
125 Scroll(AlacScroll),
126 ScrollToAlacPoint(AlacPoint),
127 SetSelection(Option<(Selection, AlacPoint)>),
128 UpdateSelection(Point<Pixels>),
129 // Adjusted mouse position, should open
130 FindHyperlink(Point<Pixels>, bool),
131 Copy,
132}
133
134///A translation struct for Alacritty to communicate with us from their event loop
135#[derive(Clone)]
136pub struct ZedListener(UnboundedSender<AlacTermEvent>);
137
138impl EventListener for ZedListener {
139 fn send_event(&self, event: AlacTermEvent) {
140 self.0.unbounded_send(event).ok();
141 }
142}
143
144pub fn init(cx: &mut AppContext) {
145 TerminalSettings::register(cx);
146}
147
148#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
149pub struct TerminalSize {
150 pub cell_width: Pixels,
151 pub line_height: Pixels,
152 pub size: Size<Pixels>,
153}
154
155impl TerminalSize {
156 pub fn new(line_height: Pixels, cell_width: Pixels, size: Size<Pixels>) -> Self {
157 TerminalSize {
158 cell_width,
159 line_height,
160 size,
161 }
162 }
163
164 pub fn num_lines(&self) -> usize {
165 (self.size.height / self.line_height).floor() as usize
166 }
167
168 pub fn num_columns(&self) -> usize {
169 (self.size.width / self.cell_width).floor() as usize
170 }
171
172 pub fn height(&self) -> Pixels {
173 self.size.height
174 }
175
176 pub fn width(&self) -> Pixels {
177 self.size.width
178 }
179
180 pub fn cell_width(&self) -> Pixels {
181 self.cell_width
182 }
183
184 pub fn line_height(&self) -> Pixels {
185 self.line_height
186 }
187}
188
189impl Default for TerminalSize {
190 fn default() -> Self {
191 TerminalSize::new(
192 DEBUG_LINE_HEIGHT,
193 DEBUG_CELL_WIDTH,
194 Size {
195 width: DEBUG_TERMINAL_WIDTH,
196 height: DEBUG_TERMINAL_HEIGHT,
197 },
198 )
199 }
200}
201
202impl From<TerminalSize> for WindowSize {
203 fn from(val: TerminalSize) -> Self {
204 WindowSize {
205 num_lines: val.num_lines() as u16,
206 num_cols: val.num_columns() as u16,
207 cell_width: f32::from(val.cell_width()) as u16,
208 cell_height: f32::from(val.line_height()) as u16,
209 }
210 }
211}
212
213impl Dimensions for TerminalSize {
214 /// Note: this is supposed to be for the back buffer's length,
215 /// but we exclusively use it to resize the terminal, which does not
216 /// use this method. We still have to implement it for the trait though,
217 /// hence, this comment.
218 fn total_lines(&self) -> usize {
219 self.screen_lines()
220 }
221
222 fn screen_lines(&self) -> usize {
223 self.num_lines()
224 }
225
226 fn columns(&self) -> usize {
227 self.num_columns()
228 }
229}
230
231#[derive(Error, Debug)]
232pub struct TerminalError {
233 pub directory: Option<PathBuf>,
234 pub shell: Shell,
235 pub source: std::io::Error,
236}
237
238impl TerminalError {
239 pub fn fmt_directory(&self) -> String {
240 self.directory
241 .clone()
242 .map(|path| {
243 match path
244 .into_os_string()
245 .into_string()
246 .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
247 {
248 Ok(s) => s,
249 Err(s) => s,
250 }
251 })
252 .unwrap_or_else(|| {
253 let default_dir =
254 dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
255 match default_dir {
256 Some(dir) => format!("<none specified, using home directory> {}", dir),
257 None => "<none specified, could not find home directory>".to_string(),
258 }
259 })
260 }
261
262 pub fn shell_to_string(&self) -> String {
263 match &self.shell {
264 Shell::System => "<system shell>".to_string(),
265 Shell::Program(p) => p.to_string(),
266 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
267 }
268 }
269
270 pub fn fmt_shell(&self) -> String {
271 match &self.shell {
272 Shell::System => "<system defined shell>".to_string(),
273 Shell::Program(s) => s.to_string(),
274 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
275 }
276 }
277}
278
279impl Display for TerminalError {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 let dir_string: String = self.fmt_directory();
282 let shell = self.fmt_shell();
283
284 write!(
285 f,
286 "Working directory: {} Shell command: `{}`, IOError: {}",
287 dir_string, shell, self.source
288 )
289 }
290}
291
292pub struct SpawnTask {
293 pub id: TaskId,
294 pub label: String,
295 pub command: String,
296 pub args: Vec<String>,
297 pub env: HashMap<String, String>,
298}
299
300// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213
301const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000;
302const MAX_SCROLL_HISTORY_LINES: usize = 100_000;
303
304pub struct TerminalBuilder {
305 terminal: Terminal,
306 events_rx: UnboundedReceiver<AlacTermEvent>,
307}
308
309impl TerminalBuilder {
310 #[allow(clippy::too_many_arguments)]
311 pub fn new(
312 working_directory: Option<PathBuf>,
313 task: Option<TaskState>,
314 shell: Shell,
315 env: HashMap<String, String>,
316 blink_settings: Option<TerminalBlink>,
317 alternate_scroll: AlternateScroll,
318 max_scroll_history_lines: Option<usize>,
319 window: AnyWindowHandle,
320 completion_tx: Sender<()>,
321 ) -> Result<TerminalBuilder> {
322 let pty_options = {
323 let alac_shell = match shell.clone() {
324 Shell::System => None,
325 Shell::Program(program) => {
326 Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))
327 }
328 Shell::WithArguments { program, args } => {
329 Some(alacritty_terminal::tty::Shell::new(program, args))
330 }
331 };
332
333 alacritty_terminal::tty::Options {
334 shell: alac_shell,
335 working_directory: working_directory.clone(),
336 hold: false,
337 }
338 };
339
340 // First, setup Alacritty's env
341 setup_env();
342
343 // Then setup configured environment variables
344 for (key, value) in env {
345 std::env::set_var(key, value);
346 }
347 //TODO: Properly set the current locale,
348 std::env::set_var("LC_ALL", "en_US.UTF-8");
349 std::env::set_var("ZED_TERM", "true");
350
351 let scrolling_history = if task.is_some() {
352 // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
353 // After the task finishes, we do not allow appending to that terminal, so small tasks output should not
354 // cause excessive memory usage over time.
355 MAX_SCROLL_HISTORY_LINES
356 } else {
357 max_scroll_history_lines
358 .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)
359 .min(MAX_SCROLL_HISTORY_LINES)
360 };
361 let config = Config {
362 scrolling_history,
363 ..Config::default()
364 };
365
366 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
367 //TODO: Remove with a bounded sender which can be dispatched on &self
368 let (events_tx, events_rx) = unbounded();
369 //Set up the terminal...
370 let mut term = Term::new(
371 config,
372 &TerminalSize::default(),
373 ZedListener(events_tx.clone()),
374 );
375
376 //Start off blinking if we need to
377 if let Some(TerminalBlink::On) = blink_settings {
378 term.set_private_mode(PrivateMode::Named(NamedPrivateMode::BlinkingCursor));
379 }
380
381 //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
382 if let AlternateScroll::Off = alternate_scroll {
383 term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
384 }
385
386 let term = Arc::new(FairMutex::new(term));
387
388 //Setup the pty...
389 let pty = match tty::new(
390 &pty_options,
391 TerminalSize::default().into(),
392 window.window_id().as_u64(),
393 ) {
394 Ok(pty) => pty,
395 Err(error) => {
396 bail!(TerminalError {
397 directory: working_directory,
398 shell,
399 source: error,
400 });
401 }
402 };
403
404 #[cfg(unix)]
405 let (fd, shell_pid) = (pty.file().as_raw_fd(), pty.child().id());
406
407 // todo(windows)
408 #[cfg(windows)]
409 let (fd, shell_pid) = {
410 let child = pty.child_watcher();
411 let handle = child.raw_handle();
412 let pid = child.pid().unwrap_or_else(|| unsafe {
413 NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle)))
414 });
415 (handle, u32::from(pid))
416 };
417
418 //And connect them together
419 let event_loop = EventLoop::new(
420 term.clone(),
421 ZedListener(events_tx.clone()),
422 pty,
423 pty_options.hold,
424 false,
425 )?;
426
427 //Kick things off
428 let pty_tx = event_loop.channel();
429 let _io_thread = event_loop.spawn(); // DANGER
430
431 let url_regex = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
432 let word_regex = RegexSearch::new(r#"[\$\+\w.\[\]:/@\-~]+"#).unwrap();
433
434 let terminal = Terminal {
435 task,
436 pty_tx: Notifier(pty_tx),
437 completion_tx,
438 term,
439 events: VecDeque::with_capacity(10), //Should never get this high.
440 last_content: Default::default(),
441 last_mouse: None,
442 matches: Vec::new(),
443 selection_head: None,
444 shell_fd: fd as u32,
445 shell_pid,
446 foreground_process_info: None,
447 breadcrumb_text: String::new(),
448 scroll_px: px(0.),
449 last_mouse_position: None,
450 next_link_id: 0,
451 selection_phase: SelectionPhase::Ended,
452 cmd_pressed: false,
453 hovered_word: false,
454 url_regex,
455 word_regex,
456 };
457
458 Ok(TerminalBuilder {
459 terminal,
460 events_rx,
461 })
462 }
463
464 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
465 //Event loop
466 cx.spawn(|terminal, mut cx| async move {
467 while let Some(event) = self.events_rx.next().await {
468 terminal.update(&mut cx, |terminal, cx| {
469 //Process the first event immediately for lowered latency
470 terminal.process_event(&event, cx);
471 })?;
472
473 'outer: loop {
474 let mut events = Vec::new();
475 let mut timer = cx
476 .background_executor()
477 .timer(Duration::from_millis(4))
478 .fuse();
479 let mut wakeup = false;
480 loop {
481 futures::select_biased! {
482 _ = timer => break,
483 event = self.events_rx.next() => {
484 if let Some(event) = event {
485 if matches!(event, AlacTermEvent::Wakeup) {
486 wakeup = true;
487 } else {
488 events.push(event);
489 }
490
491 if events.len() > 100 {
492 break;
493 }
494 } else {
495 break;
496 }
497 },
498 }
499 }
500
501 if events.is_empty() && !wakeup {
502 smol::future::yield_now().await;
503 break 'outer;
504 }
505
506 terminal.update(&mut cx, |this, cx| {
507 if wakeup {
508 this.process_event(&AlacTermEvent::Wakeup, cx);
509 }
510
511 for event in events {
512 this.process_event(&event, cx);
513 }
514 })?;
515 smol::future::yield_now().await;
516 }
517 }
518
519 anyhow::Ok(())
520 })
521 .detach();
522
523 self.terminal
524 }
525}
526
527#[derive(Debug, Clone, Deserialize, Serialize)]
528pub struct IndexedCell {
529 pub point: AlacPoint,
530 pub cell: Cell,
531}
532
533impl Deref for IndexedCell {
534 type Target = Cell;
535
536 #[inline]
537 fn deref(&self) -> &Cell {
538 &self.cell
539 }
540}
541
542// TODO: Un-pub
543#[derive(Clone)]
544pub struct TerminalContent {
545 pub cells: Vec<IndexedCell>,
546 pub mode: TermMode,
547 pub display_offset: usize,
548 pub selection_text: Option<String>,
549 pub selection: Option<SelectionRange>,
550 pub cursor: RenderableCursor,
551 pub cursor_char: char,
552 pub size: TerminalSize,
553 pub last_hovered_word: Option<HoveredWord>,
554}
555
556#[derive(Clone)]
557pub struct HoveredWord {
558 pub word: String,
559 pub word_match: RangeInclusive<AlacPoint>,
560 pub id: usize,
561}
562
563impl Default for TerminalContent {
564 fn default() -> Self {
565 TerminalContent {
566 cells: Default::default(),
567 mode: Default::default(),
568 display_offset: Default::default(),
569 selection_text: Default::default(),
570 selection: Default::default(),
571 cursor: RenderableCursor {
572 shape: alacritty_terminal::vte::ansi::CursorShape::Block,
573 point: AlacPoint::new(Line(0), Column(0)),
574 },
575 cursor_char: Default::default(),
576 size: Default::default(),
577 last_hovered_word: None,
578 }
579 }
580}
581
582#[derive(PartialEq, Eq)]
583pub enum SelectionPhase {
584 Selecting,
585 Ended,
586}
587
588pub struct Terminal {
589 pty_tx: Notifier,
590 completion_tx: Sender<()>,
591 term: Arc<FairMutex<Term<ZedListener>>>,
592 events: VecDeque<InternalEvent>,
593 /// This is only used for mouse mode cell change detection
594 last_mouse: Option<(AlacPoint, AlacDirection)>,
595 /// This is only used for terminal hovered word checking
596 last_mouse_position: Option<Point<Pixels>>,
597 pub matches: Vec<RangeInclusive<AlacPoint>>,
598 pub last_content: TerminalContent,
599 pub selection_head: Option<AlacPoint>,
600 pub breadcrumb_text: String,
601 shell_pid: u32,
602 shell_fd: u32,
603 pub foreground_process_info: Option<LocalProcessInfo>,
604 scroll_px: Pixels,
605 next_link_id: usize,
606 selection_phase: SelectionPhase,
607 cmd_pressed: bool,
608 hovered_word: bool,
609 url_regex: RegexSearch,
610 word_regex: RegexSearch,
611 task: Option<TaskState>,
612}
613
614pub struct TaskState {
615 pub id: TaskId,
616 pub label: String,
617 pub completed: bool,
618 pub completion_rx: Receiver<()>,
619}
620
621impl Terminal {
622 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
623 match event {
624 AlacTermEvent::Title(title) => {
625 self.breadcrumb_text = title.to_string();
626 cx.emit(Event::BreadcrumbsChanged);
627 }
628 AlacTermEvent::ResetTitle => {
629 self.breadcrumb_text = String::new();
630 cx.emit(Event::BreadcrumbsChanged);
631 }
632 AlacTermEvent::ClipboardStore(_, data) => {
633 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
634 }
635 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
636 &cx.read_from_clipboard()
637 .map(|ci| ci.text().to_string())
638 .unwrap_or_else(|| "".to_string()),
639 )),
640 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
641 AlacTermEvent::TextAreaSizeRequest(format) => {
642 self.write_to_pty(format(self.last_content.size.into()))
643 }
644 AlacTermEvent::CursorBlinkingChange => {
645 cx.emit(Event::BlinkChanged);
646 }
647 AlacTermEvent::Bell => {
648 cx.emit(Event::Bell);
649 }
650 AlacTermEvent::Exit => match &mut self.task {
651 Some(task) => {
652 task.completed = true;
653 self.completion_tx.try_send(()).ok();
654 }
655 None => cx.emit(Event::CloseTerminal),
656 },
657 AlacTermEvent::MouseCursorDirty => {
658 //NOOP, Handled in render
659 }
660 AlacTermEvent::Wakeup => {
661 cx.emit(Event::Wakeup);
662
663 if self.update_process_info() {
664 cx.emit(Event::TitleChanged);
665 }
666 }
667 AlacTermEvent::ColorRequest(idx, fun_ptr) => {
668 self.events
669 .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
670 }
671 }
672 }
673
674 pub fn selection_started(&self) -> bool {
675 self.selection_phase == SelectionPhase::Selecting
676 }
677
678 /// Updates the cached process info, returns whether the Zed-relevant info has changed
679 fn update_process_info(&mut self) -> bool {
680 #[cfg(unix)]
681 let pid = {
682 let ret = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
683 if ret < 0 {
684 self.shell_pid as i32
685 } else {
686 ret
687 }
688 };
689
690 #[cfg(windows)]
691 let pid = {
692 let ret = unsafe { GetProcessId(HANDLE(self.shell_fd as _)) };
693 // the GetProcessId may fail and returns zero, which will lead to a stack overflow issue
694 if ret == 0 {
695 // in the builder process, there is a small chance, almost negligible,
696 // that this value could be zero, which means child_watcher returns None,
697 // GetProcessId returns 0.
698 if self.shell_pid == 0 {
699 return false;
700 }
701 self.shell_pid
702 } else {
703 ret
704 }
705 } as i32;
706
707 if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
708 let res = self
709 .foreground_process_info
710 .as_ref()
711 .map(|old_info| {
712 process_info.cwd != old_info.cwd || process_info.name != old_info.name
713 })
714 .unwrap_or(true);
715
716 self.foreground_process_info = Some(process_info.clone());
717
718 res
719 } else {
720 false
721 }
722 }
723
724 fn get_cwd(&self) -> Option<PathBuf> {
725 self.foreground_process_info
726 .as_ref()
727 .map(|info| info.cwd.clone())
728 }
729
730 ///Takes events from Alacritty and translates them to behavior on this view
731 fn process_terminal_event(
732 &mut self,
733 event: &InternalEvent,
734 term: &mut Term<ZedListener>,
735 cx: &mut ModelContext<Self>,
736 ) {
737 match event {
738 InternalEvent::ColorRequest(index, format) => {
739 let color = term.colors()[*index].unwrap_or_else(|| {
740 to_alac_rgb(get_color_at_index(*index, cx.theme().as_ref()))
741 });
742 self.write_to_pty(format(color))
743 }
744 InternalEvent::Resize(mut new_size) => {
745 new_size.size.height = cmp::max(new_size.line_height, new_size.height());
746 new_size.size.width = cmp::max(new_size.cell_width, new_size.width());
747
748 self.last_content.size = new_size;
749
750 self.pty_tx.0.send(Msg::Resize(new_size.into())).ok();
751
752 term.resize(new_size);
753 }
754 InternalEvent::Clear => {
755 // Clear back buffer
756 term.clear_screen(ClearMode::Saved);
757
758 let cursor = term.grid().cursor.point;
759
760 // Clear the lines above
761 term.grid_mut().reset_region(..cursor.line);
762
763 // Copy the current line up
764 let line = term.grid()[cursor.line][..Column(term.grid().columns())]
765 .iter()
766 .cloned()
767 .enumerate()
768 .collect::<Vec<(usize, Cell)>>();
769
770 for (i, cell) in line {
771 term.grid_mut()[Line(0)][Column(i)] = cell;
772 }
773
774 // Reset the cursor
775 term.grid_mut().cursor.point =
776 AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
777 let new_cursor = term.grid().cursor.point;
778
779 // Clear the lines below the new cursor
780 if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
781 term.grid_mut().reset_region((new_cursor.line + 1)..);
782 }
783
784 cx.emit(Event::Wakeup);
785 }
786 InternalEvent::Scroll(scroll) => {
787 term.scroll_display(*scroll);
788 self.refresh_hovered_word();
789 }
790 InternalEvent::SetSelection(selection) => {
791 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
792
793 if let Some((_, head)) = selection {
794 self.selection_head = Some(*head);
795 }
796 cx.emit(Event::SelectionsChanged)
797 }
798 InternalEvent::UpdateSelection(position) => {
799 if let Some(mut selection) = term.selection.take() {
800 let (point, side) = grid_point_and_side(
801 *position,
802 self.last_content.size,
803 term.grid().display_offset(),
804 );
805
806 selection.update(point, side);
807 term.selection = Some(selection);
808
809 self.selection_head = Some(point);
810 cx.emit(Event::SelectionsChanged)
811 }
812 }
813
814 InternalEvent::Copy => {
815 if let Some(txt) = term.selection_to_string() {
816 cx.write_to_clipboard(ClipboardItem::new(txt))
817 }
818 }
819 InternalEvent::ScrollToAlacPoint(point) => {
820 term.scroll_to_point(*point);
821 self.refresh_hovered_word();
822 }
823 InternalEvent::FindHyperlink(position, open) => {
824 let prev_hovered_word = self.last_content.last_hovered_word.take();
825
826 let point = grid_point(
827 *position,
828 self.last_content.size,
829 term.grid().display_offset(),
830 )
831 .grid_clamp(term, Boundary::Grid);
832
833 let link = term.grid().index(point).hyperlink();
834 let found_word = if link.is_some() {
835 let mut min_index = point;
836 loop {
837 let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
838 if new_min_index == min_index {
839 break;
840 } else if term.grid().index(new_min_index).hyperlink() != link {
841 break;
842 } else {
843 min_index = new_min_index
844 }
845 }
846
847 let mut max_index = point;
848 loop {
849 let new_max_index = max_index.add(term, Boundary::Cursor, 1);
850 if new_max_index == max_index {
851 break;
852 } else if term.grid().index(new_max_index).hyperlink() != link {
853 break;
854 } else {
855 max_index = new_max_index
856 }
857 }
858
859 let url = link.unwrap().uri().to_owned();
860 let url_match = min_index..=max_index;
861
862 Some((url, true, url_match))
863 } else if let Some(word_match) = regex_match_at(term, point, &mut self.word_regex) {
864 let maybe_url_or_path =
865 term.bounds_to_string(*word_match.start(), *word_match.end());
866 let original_match = word_match.clone();
867 let (sanitized_match, sanitized_word) =
868 if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') {
869 (
870 Match::new(
871 word_match.start().add(term, Boundary::Cursor, 1),
872 word_match.end().sub(term, Boundary::Cursor, 1),
873 ),
874 maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(),
875 )
876 } else {
877 (word_match, maybe_url_or_path)
878 };
879
880 let is_url = match regex_match_at(term, point, &mut self.url_regex) {
881 Some(url_match) => {
882 // `]` is a valid symbol in the `file://` URL, so the regex match will include it
883 // consider that when ensuring that the URL match is the same as the original word
884 if sanitized_match != original_match {
885 url_match.start() == sanitized_match.start()
886 && url_match.end() == original_match.end()
887 } else {
888 url_match == sanitized_match
889 }
890 }
891 None => false,
892 };
893 Some((sanitized_word, is_url, sanitized_match))
894 } else {
895 None
896 };
897
898 match found_word {
899 Some((maybe_url_or_path, is_url, url_match)) => {
900 if *open {
901 let target = if is_url {
902 MaybeNavigationTarget::Url(maybe_url_or_path)
903 } else {
904 MaybeNavigationTarget::PathLike(PathLikeTarget {
905 maybe_path: maybe_url_or_path,
906 terminal_dir: self.get_cwd(),
907 })
908 };
909 cx.emit(Event::Open(target));
910 } else {
911 self.update_selected_word(
912 prev_hovered_word,
913 url_match,
914 maybe_url_or_path,
915 is_url,
916 cx,
917 );
918 }
919 self.hovered_word = true;
920 }
921 None => {
922 if self.hovered_word {
923 cx.emit(Event::NewNavigationTarget(None));
924 }
925 self.hovered_word = false;
926 }
927 }
928 }
929 }
930 }
931
932 fn update_selected_word(
933 &mut self,
934 prev_word: Option<HoveredWord>,
935 word_match: RangeInclusive<AlacPoint>,
936 word: String,
937 is_url: bool,
938 cx: &mut ModelContext<Self>,
939 ) {
940 if let Some(prev_word) = prev_word {
941 if prev_word.word == word && prev_word.word_match == word_match {
942 self.last_content.last_hovered_word = Some(HoveredWord {
943 word,
944 word_match,
945 id: prev_word.id,
946 });
947 return;
948 }
949 }
950
951 self.last_content.last_hovered_word = Some(HoveredWord {
952 word: word.clone(),
953 word_match,
954 id: self.next_link_id(),
955 });
956 let navigation_target = if is_url {
957 MaybeNavigationTarget::Url(word)
958 } else {
959 MaybeNavigationTarget::PathLike(PathLikeTarget {
960 maybe_path: word,
961 terminal_dir: self.get_cwd(),
962 })
963 };
964 cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
965 }
966
967 fn next_link_id(&mut self) -> usize {
968 let res = self.next_link_id;
969 self.next_link_id = self.next_link_id.wrapping_add(1);
970 res
971 }
972
973 pub fn last_content(&self) -> &TerminalContent {
974 &self.last_content
975 }
976
977 //To test:
978 //- Activate match on terminal (scrolling and selection)
979 //- Editor search snapping behavior
980
981 pub fn activate_match(&mut self, index: usize) {
982 if let Some(search_match) = self.matches.get(index).cloned() {
983 self.set_selection(Some((make_selection(&search_match), *search_match.end())));
984
985 self.events
986 .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
987 }
988 }
989
990 pub fn select_matches(&mut self, matches: Vec<RangeInclusive<AlacPoint>>) {
991 let matches_to_select = self
992 .matches
993 .iter()
994 .filter(|self_match| matches.contains(self_match))
995 .cloned()
996 .collect::<Vec<_>>();
997 for match_to_select in matches_to_select {
998 self.set_selection(Some((
999 make_selection(&match_to_select),
1000 *match_to_select.end(),
1001 )));
1002 }
1003 }
1004
1005 pub fn select_all(&mut self) {
1006 let term = self.term.lock();
1007 let start = AlacPoint::new(term.topmost_line(), Column(0));
1008 let end = AlacPoint::new(term.bottommost_line(), term.last_column());
1009 drop(term);
1010 self.set_selection(Some((make_selection(&(start..=end)), end)));
1011 }
1012
1013 fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) {
1014 self.events
1015 .push_back(InternalEvent::SetSelection(selection));
1016 }
1017
1018 pub fn copy(&mut self) {
1019 self.events.push_back(InternalEvent::Copy);
1020 }
1021
1022 pub fn clear(&mut self) {
1023 self.events.push_back(InternalEvent::Clear)
1024 }
1025
1026 ///Resize the terminal and the PTY.
1027 pub fn set_size(&mut self, new_size: TerminalSize) {
1028 self.events.push_back(InternalEvent::Resize(new_size))
1029 }
1030
1031 ///Write the Input payload to the tty.
1032 fn write_to_pty(&self, input: String) {
1033 self.pty_tx.notify(input.into_bytes());
1034 }
1035
1036 fn write_bytes_to_pty(&self, input: Vec<u8>) {
1037 self.pty_tx.notify(input);
1038 }
1039
1040 pub fn input(&mut self, input: String) {
1041 self.events
1042 .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
1043 self.events.push_back(InternalEvent::SetSelection(None));
1044
1045 self.write_to_pty(input);
1046 }
1047
1048 pub fn input_bytes(&mut self, input: Vec<u8>) {
1049 self.events
1050 .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
1051 self.events.push_back(InternalEvent::SetSelection(None));
1052
1053 self.write_bytes_to_pty(input);
1054 }
1055
1056 pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
1057 let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
1058 if let Some(esc) = esc {
1059 self.input(esc);
1060 true
1061 } else {
1062 false
1063 }
1064 }
1065
1066 pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
1067 let changed = self.cmd_pressed != modifiers.command;
1068 if !self.cmd_pressed && modifiers.command {
1069 self.refresh_hovered_word();
1070 }
1071 self.cmd_pressed = modifiers.command;
1072 changed
1073 }
1074
1075 ///Paste text into the terminal
1076 pub fn paste(&mut self, text: &str) {
1077 let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
1078 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
1079 } else {
1080 text.replace("\r\n", "\r").replace('\n', "\r")
1081 };
1082
1083 self.input(paste_text);
1084 }
1085
1086 pub fn sync(&mut self, cx: &mut ModelContext<Self>) {
1087 let term = self.term.clone();
1088 let mut terminal = term.lock_unfair();
1089 //Note that the ordering of events matters for event processing
1090 while let Some(e) = self.events.pop_front() {
1091 self.process_terminal_event(&e, &mut terminal, cx)
1092 }
1093
1094 self.last_content = Self::make_content(&terminal, &self.last_content);
1095 }
1096
1097 fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1098 let content = term.renderable_content();
1099 TerminalContent {
1100 cells: content
1101 .display_iter
1102 //TODO: Add this once there's a way to retain empty lines
1103 // .filter(|ic| {
1104 // !ic.flags.contains(Flags::HIDDEN)
1105 // && !(ic.bg == Named(NamedColor::Background)
1106 // && ic.c == ' '
1107 // && !ic.flags.contains(Flags::INVERSE))
1108 // })
1109 .map(|ic| IndexedCell {
1110 point: ic.point,
1111 cell: ic.cell.clone(),
1112 })
1113 .collect::<Vec<IndexedCell>>(),
1114 mode: content.mode,
1115 display_offset: content.display_offset,
1116 selection_text: term.selection_to_string(),
1117 selection: content.selection,
1118 cursor: content.cursor,
1119 cursor_char: term.grid()[content.cursor.point].c,
1120 size: last_content.size,
1121 last_hovered_word: last_content.last_hovered_word.clone(),
1122 }
1123 }
1124
1125 pub fn focus_in(&self) {
1126 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1127 self.write_to_pty("\x1b[I".to_string());
1128 }
1129 }
1130
1131 pub fn focus_out(&mut self) {
1132 self.last_mouse_position = None;
1133 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1134 self.write_to_pty("\x1b[O".to_string());
1135 }
1136 }
1137
1138 pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool {
1139 match self.last_mouse {
1140 Some((old_point, old_side)) => {
1141 if old_point == point && old_side == side {
1142 false
1143 } else {
1144 self.last_mouse = Some((point, side));
1145 true
1146 }
1147 }
1148 None => {
1149 self.last_mouse = Some((point, side));
1150 true
1151 }
1152 }
1153 }
1154
1155 pub fn mouse_mode(&self, shift: bool) -> bool {
1156 self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1157 }
1158
1159 pub fn mouse_move(&mut self, e: &MouseMoveEvent, origin: Point<Pixels>) {
1160 let position = e.position - origin;
1161 self.last_mouse_position = Some(position);
1162 if self.mouse_mode(e.modifiers.shift) {
1163 let (point, side) = grid_point_and_side(
1164 position,
1165 self.last_content.size,
1166 self.last_content.display_offset,
1167 );
1168
1169 if self.mouse_changed(point, side) {
1170 if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
1171 self.pty_tx.notify(bytes);
1172 }
1173 }
1174 } else if self.cmd_pressed {
1175 self.word_from_position(Some(position));
1176 }
1177 }
1178
1179 fn word_from_position(&mut self, position: Option<Point<Pixels>>) {
1180 if self.selection_phase == SelectionPhase::Selecting {
1181 self.last_content.last_hovered_word = None;
1182 } else if let Some(position) = position {
1183 self.events
1184 .push_back(InternalEvent::FindHyperlink(position, false));
1185 }
1186 }
1187
1188 pub fn mouse_drag(
1189 &mut self,
1190 e: &MouseMoveEvent,
1191 origin: Point<Pixels>,
1192 region: Bounds<Pixels>,
1193 ) {
1194 let position = e.position - origin;
1195 self.last_mouse_position = Some(position);
1196
1197 if !self.mouse_mode(e.modifiers.shift) {
1198 self.selection_phase = SelectionPhase::Selecting;
1199 // Alacritty has the same ordering, of first updating the selection
1200 // then scrolling 15ms later
1201 self.events
1202 .push_back(InternalEvent::UpdateSelection(position));
1203
1204 // Doesn't make sense to scroll the alt screen
1205 if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1206 let scroll_delta = match self.drag_line_delta(e, region) {
1207 Some(value) => value,
1208 None => return,
1209 };
1210
1211 let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
1212
1213 self.events
1214 .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1215 }
1216 }
1217 }
1218
1219 fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
1220 //TODO: Why do these need to be doubled? Probably the same problem that the IME has
1221 let top = region.origin.y + (self.last_content.size.line_height * 2.);
1222 let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.);
1223 let scroll_delta = if e.position.y < top {
1224 (top - e.position.y).pow(1.1)
1225 } else if e.position.y > bottom {
1226 -((e.position.y - bottom).pow(1.1))
1227 } else {
1228 return None; //Nothing to do
1229 };
1230 Some(scroll_delta)
1231 }
1232
1233 pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
1234 let position = e.position - origin;
1235 let point = grid_point(
1236 position,
1237 self.last_content.size,
1238 self.last_content.display_offset,
1239 );
1240
1241 if self.mouse_mode(e.modifiers.shift) {
1242 if let Some(bytes) =
1243 mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode)
1244 {
1245 self.pty_tx.notify(bytes);
1246 }
1247 } else if e.button == MouseButton::Left {
1248 let position = e.position - origin;
1249 let (point, side) = grid_point_and_side(
1250 position,
1251 self.last_content.size,
1252 self.last_content.display_offset,
1253 );
1254
1255 let selection_type = match e.click_count {
1256 0 => return, //This is a release
1257 1 => Some(SelectionType::Simple),
1258 2 => Some(SelectionType::Semantic),
1259 3 => Some(SelectionType::Lines),
1260 _ => None,
1261 };
1262
1263 let selection =
1264 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
1265
1266 if let Some(sel) = selection {
1267 self.events
1268 .push_back(InternalEvent::SetSelection(Some((sel, point))));
1269 }
1270 }
1271 }
1272
1273 pub fn mouse_up(
1274 &mut self,
1275 e: &MouseUpEvent,
1276 origin: Point<Pixels>,
1277 cx: &mut ModelContext<Self>,
1278 ) {
1279 let setting = TerminalSettings::get_global(cx);
1280
1281 let position = e.position - origin;
1282 if self.mouse_mode(e.modifiers.shift) {
1283 let point = grid_point(
1284 position,
1285 self.last_content.size,
1286 self.last_content.display_offset,
1287 );
1288
1289 if let Some(bytes) =
1290 mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode)
1291 {
1292 self.pty_tx.notify(bytes);
1293 }
1294 } else {
1295 if e.button == MouseButton::Left && setting.copy_on_select {
1296 self.copy();
1297 }
1298
1299 //Hyperlinks
1300 if self.selection_phase == SelectionPhase::Ended {
1301 let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
1302 if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1303 cx.open_url(link.uri());
1304 } else if self.cmd_pressed {
1305 self.events
1306 .push_back(InternalEvent::FindHyperlink(position, true));
1307 }
1308 }
1309 }
1310
1311 self.selection_phase = SelectionPhase::Ended;
1312 self.last_mouse = None;
1313 }
1314
1315 ///Scroll the terminal
1316 pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point<Pixels>) {
1317 let mouse_mode = self.mouse_mode(e.shift);
1318
1319 if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
1320 if mouse_mode {
1321 let point = grid_point(
1322 e.position - origin,
1323 self.last_content.size,
1324 self.last_content.display_offset,
1325 );
1326
1327 if let Some(scrolls) = scroll_report(point, scroll_lines, e, self.last_content.mode)
1328 {
1329 for scroll in scrolls {
1330 self.pty_tx.notify(scroll);
1331 }
1332 };
1333 } else if self
1334 .last_content
1335 .mode
1336 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
1337 && !e.shift
1338 {
1339 self.pty_tx.notify(alt_scroll(scroll_lines))
1340 } else {
1341 if scroll_lines != 0 {
1342 let scroll = AlacScroll::Delta(scroll_lines);
1343
1344 self.events.push_back(InternalEvent::Scroll(scroll));
1345 }
1346 }
1347 }
1348 }
1349
1350 fn refresh_hovered_word(&mut self) {
1351 self.word_from_position(self.last_mouse_position);
1352 }
1353
1354 fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
1355 let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
1356 let line_height = self.last_content.size.line_height;
1357 match e.touch_phase {
1358 /* Reset scroll state on started */
1359 TouchPhase::Started => {
1360 self.scroll_px = px(0.);
1361 None
1362 }
1363 /* Calculate the appropriate scroll lines */
1364 TouchPhase::Moved => {
1365 let old_offset = (self.scroll_px / line_height) as i32;
1366
1367 self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier;
1368
1369 let new_offset = (self.scroll_px / line_height) as i32;
1370
1371 // Whenever we hit the edges, reset our stored scroll to 0
1372 // so we can respond to changes in direction quickly
1373 self.scroll_px %= self.last_content.size.height();
1374
1375 Some(new_offset - old_offset)
1376 }
1377 TouchPhase::Ended => None,
1378 }
1379 }
1380
1381 pub fn find_matches(
1382 &mut self,
1383 mut searcher: RegexSearch,
1384 cx: &mut ModelContext<Self>,
1385 ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
1386 let term = self.term.clone();
1387 cx.background_executor().spawn(async move {
1388 let term = term.lock();
1389
1390 all_search_matches(&term, &mut searcher).collect()
1391 })
1392 }
1393
1394 pub fn title(&self, truncate: bool) -> String {
1395 const MAX_CHARS: usize = 25;
1396 match &self.task {
1397 Some(task_state) => {
1398 if truncate {
1399 truncate_and_trailoff(&task_state.label, MAX_CHARS)
1400 } else {
1401 task_state.label.clone()
1402 }
1403 }
1404 None => self
1405 .foreground_process_info
1406 .as_ref()
1407 .map(|fpi| {
1408 let process_file = fpi
1409 .cwd
1410 .file_name()
1411 .map(|name| name.to_string_lossy().to_string())
1412 .unwrap_or_default();
1413 let process_name = format!(
1414 "{}{}",
1415 fpi.name,
1416 if fpi.argv.len() >= 1 {
1417 format!(" {}", (fpi.argv[1..]).join(" "))
1418 } else {
1419 "".to_string()
1420 }
1421 );
1422 let (process_file, process_name) = if truncate {
1423 (
1424 truncate_and_trailoff(&process_file, MAX_CHARS),
1425 truncate_and_trailoff(&process_name, MAX_CHARS),
1426 )
1427 } else {
1428 (process_file, process_name)
1429 };
1430 format!("{process_file} — {process_name}")
1431 })
1432 .unwrap_or_else(|| "Terminal".to_string()),
1433 }
1434 }
1435
1436 pub fn can_navigate_to_selected_word(&self) -> bool {
1437 self.cmd_pressed && self.hovered_word
1438 }
1439
1440 pub fn task(&self) -> Option<&TaskState> {
1441 self.task.as_ref()
1442 }
1443
1444 pub fn wait_for_completed_task(&self, cx: &mut AppContext) -> Task<()> {
1445 match self.task() {
1446 Some(task) => {
1447 if task.completed {
1448 Task::ready(())
1449 } else {
1450 let mut completion_receiver = task.completion_rx.clone();
1451 cx.spawn(|_| async move {
1452 completion_receiver.next().await;
1453 })
1454 }
1455 }
1456 None => Task::ready(()),
1457 }
1458 }
1459}
1460
1461impl Drop for Terminal {
1462 fn drop(&mut self) {
1463 self.pty_tx.0.send(Msg::Shutdown).ok();
1464 }
1465}
1466
1467impl EventEmitter<Event> for Terminal {}
1468
1469/// Based on alacritty/src/display/hint.rs > regex_match_at
1470/// Retrieve the match, if the specified point is inside the content matching the regex.
1471fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &mut RegexSearch) -> Option<Match> {
1472 visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
1473}
1474
1475/// Copied from alacritty/src/display/hint.rs:
1476/// Iterate over all visible regex matches.
1477pub fn visible_regex_match_iter<'a, T>(
1478 term: &'a Term<T>,
1479 regex: &'a mut RegexSearch,
1480) -> impl Iterator<Item = Match> + 'a {
1481 let viewport_start = Line(-(term.grid().display_offset() as i32));
1482 let viewport_end = viewport_start + term.bottommost_line();
1483 let mut start = term.line_search_left(AlacPoint::new(viewport_start, Column(0)));
1484 let mut end = term.line_search_right(AlacPoint::new(viewport_end, Column(0)));
1485 start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1486 end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1487
1488 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1489 .skip_while(move |rm| rm.end().line < viewport_start)
1490 .take_while(move |rm| rm.start().line <= viewport_end)
1491}
1492
1493fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
1494 let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1495 selection.update(*range.end(), AlacDirection::Right);
1496 selection
1497}
1498
1499fn all_search_matches<'a, T>(
1500 term: &'a Term<T>,
1501 regex: &'a mut RegexSearch,
1502) -> impl Iterator<Item = Match> + 'a {
1503 let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
1504 let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
1505 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1506}
1507
1508fn content_index_for_mouse(pos: Point<Pixels>, size: &TerminalSize) -> usize {
1509 let col = (pos.x / size.cell_width()).round() as usize;
1510 let clamped_col = min(col, size.columns() - 1);
1511 let row = (pos.y / size.line_height()).round() as usize;
1512 let clamped_row = min(row, size.screen_lines() - 1);
1513 clamped_row * size.columns() + clamped_col
1514}
1515
1516/// Converts an 8 bit ANSI color to its GPUI equivalent.
1517/// Accepts `usize` for compatibility with the `alacritty::Colors` interface,
1518/// Other than that use case, should only be called with values in the [0,255] range
1519pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla {
1520 let colors = theme.colors();
1521
1522 match index {
1523 // 0-15 are the same as the named colors above
1524 0 => colors.terminal_ansi_black,
1525 1 => colors.terminal_ansi_red,
1526 2 => colors.terminal_ansi_green,
1527 3 => colors.terminal_ansi_yellow,
1528 4 => colors.terminal_ansi_blue,
1529 5 => colors.terminal_ansi_magenta,
1530 6 => colors.terminal_ansi_cyan,
1531 7 => colors.terminal_ansi_white,
1532 8 => colors.terminal_ansi_bright_black,
1533 9 => colors.terminal_ansi_bright_red,
1534 10 => colors.terminal_ansi_bright_green,
1535 11 => colors.terminal_ansi_bright_yellow,
1536 12 => colors.terminal_ansi_bright_blue,
1537 13 => colors.terminal_ansi_bright_magenta,
1538 14 => colors.terminal_ansi_bright_cyan,
1539 15 => colors.terminal_ansi_bright_white,
1540 // 16-231 are mapped to their RGB colors on a 0-5 range per channel
1541 16..=231 => {
1542 let (r, g, b) = rgb_for_index(index as u8); // Split the index into its ANSI-RGB components
1543 let step = (u8::MAX as f32 / 5.).floor() as u8; // Split the RGB range into 5 chunks, with floor so no overflow
1544 rgba_color(r * step, g * step, b * step) // Map the ANSI-RGB components to an RGB color
1545 }
1546 // 232-255 are a 24 step grayscale from black to white
1547 232..=255 => {
1548 let i = index as u8 - 232; // Align index to 0..24
1549 let step = (u8::MAX as f32 / 24.).floor() as u8; // Split the RGB grayscale values into 24 chunks
1550 rgba_color(i * step, i * step, i * step) // Map the ANSI-grayscale components to the RGB-grayscale
1551 }
1552 // For compatibility with the alacritty::Colors interface
1553 256 => colors.text,
1554 257 => colors.background,
1555 258 => theme.players().local().cursor,
1556 259 => colors.terminal_ansi_dim_black,
1557 260 => colors.terminal_ansi_dim_red,
1558 261 => colors.terminal_ansi_dim_green,
1559 262 => colors.terminal_ansi_dim_yellow,
1560 263 => colors.terminal_ansi_dim_blue,
1561 264 => colors.terminal_ansi_dim_magenta,
1562 265 => colors.terminal_ansi_dim_cyan,
1563 266 => colors.terminal_ansi_dim_white,
1564 267 => colors.terminal_bright_foreground,
1565 268 => colors.terminal_ansi_black, // 'Dim Background', non-standard color
1566
1567 _ => black(),
1568 }
1569}
1570
1571/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube.
1572/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
1573///
1574/// Wikipedia gives a formula for calculating the index for a given color:
1575///
1576/// ```
1577/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
1578/// ```
1579///
1580/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index.
1581fn rgb_for_index(i: u8) -> (u8, u8, u8) {
1582 debug_assert!((16..=231).contains(&i));
1583 let i = i - 16;
1584 let r = (i - (i % 36)) / 36;
1585 let g = ((i % 36) - (i % 6)) / 6;
1586 let b = (i % 36) % 6;
1587 (r, g, b)
1588}
1589
1590pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla {
1591 Rgba {
1592 r: (r as f32 / 255.),
1593 g: (g as f32 / 255.),
1594 b: (b as f32 / 255.),
1595 a: 1.,
1596 }
1597 .into()
1598}
1599
1600#[cfg(test)]
1601mod tests {
1602 use alacritty_terminal::{
1603 index::{Column, Line, Point as AlacPoint},
1604 term::cell::Cell,
1605 };
1606 use gpui::{point, size, Pixels};
1607 use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
1608
1609 use crate::{
1610 content_index_for_mouse, rgb_for_index, IndexedCell, TerminalContent, TerminalSize,
1611 };
1612
1613 #[test]
1614 fn test_rgb_for_index() {
1615 // Test every possible value in the color cube.
1616 for i in 16..=231 {
1617 let (r, g, b) = rgb_for_index(i);
1618 assert_eq!(i, 16 + 36 * r + 6 * g + b);
1619 }
1620 }
1621
1622 #[test]
1623 fn test_mouse_to_cell_test() {
1624 let mut rng = thread_rng();
1625 const ITERATIONS: usize = 10;
1626 const PRECISION: usize = 1000;
1627
1628 for _ in 0..ITERATIONS {
1629 let viewport_cells = rng.gen_range(15..20);
1630 let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
1631
1632 let size = crate::TerminalSize {
1633 cell_width: Pixels::from(cell_size),
1634 line_height: Pixels::from(cell_size),
1635 size: size(
1636 Pixels::from(cell_size * (viewport_cells as f32)),
1637 Pixels::from(cell_size * (viewport_cells as f32)),
1638 ),
1639 };
1640
1641 let cells = get_cells(size, &mut rng);
1642 let content = convert_cells_to_content(size, &cells);
1643
1644 for row in 0..(viewport_cells - 1) {
1645 let row = row as usize;
1646 for col in 0..(viewport_cells - 1) {
1647 let col = col as usize;
1648
1649 let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1650 let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1651
1652 let mouse_pos = point(
1653 Pixels::from(col as f32 * cell_size + col_offset),
1654 Pixels::from(row as f32 * cell_size + row_offset),
1655 );
1656
1657 let content_index = content_index_for_mouse(mouse_pos, &content.size);
1658 let mouse_cell = content.cells[content_index].c;
1659 let real_cell = cells[row][col];
1660
1661 assert_eq!(mouse_cell, real_cell);
1662 }
1663 }
1664 }
1665 }
1666
1667 #[test]
1668 fn test_mouse_to_cell_clamp() {
1669 let mut rng = thread_rng();
1670
1671 let size = crate::TerminalSize {
1672 cell_width: Pixels::from(10.),
1673 line_height: Pixels::from(10.),
1674 size: size(Pixels::from(100.), Pixels::from(100.)),
1675 };
1676
1677 let cells = get_cells(size, &mut rng);
1678 let content = convert_cells_to_content(size, &cells);
1679
1680 assert_eq!(
1681 content.cells[content_index_for_mouse(
1682 point(Pixels::from(-10.), Pixels::from(-10.)),
1683 &content.size,
1684 )]
1685 .c,
1686 cells[0][0]
1687 );
1688 assert_eq!(
1689 content.cells[content_index_for_mouse(
1690 point(Pixels::from(1000.), Pixels::from(1000.)),
1691 &content.size,
1692 )]
1693 .c,
1694 cells[9][9]
1695 );
1696 }
1697
1698 fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
1699 let mut cells = Vec::new();
1700
1701 for _ in 0..((size.height() / size.line_height()) as usize) {
1702 let mut row_vec = Vec::new();
1703 for _ in 0..((size.width() / size.cell_width()) as usize) {
1704 let cell_char = rng.sample(Alphanumeric) as char;
1705 row_vec.push(cell_char)
1706 }
1707 cells.push(row_vec)
1708 }
1709
1710 cells
1711 }
1712
1713 fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
1714 let mut ic = Vec::new();
1715
1716 for row in 0..cells.len() {
1717 for col in 0..cells[row].len() {
1718 let cell_char = cells[row][col];
1719 ic.push(IndexedCell {
1720 point: AlacPoint::new(Line(row as i32), Column(col)),
1721 cell: Cell {
1722 c: cell_char,
1723 ..Default::default()
1724 },
1725 });
1726 }
1727 }
1728
1729 TerminalContent {
1730 cells: ic,
1731 size,
1732 ..Default::default()
1733 }
1734 }
1735}