1pub mod mappings;
2pub use alacritty_terminal;
3
4use alacritty_terminal::{
5 ansi::{ClearMode, Handler},
6 config::{Config, Program, PtyConfig, Scrolling},
7 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
8 event_loop::{EventLoop, Msg, Notifier},
9 grid::{Dimensions, Scroll as AlacScroll},
10 index::{Column, Direction as AlacDirection, Line, Point},
11 selection::{Selection, SelectionRange, SelectionType},
12 sync::FairMutex,
13 term::{
14 cell::Cell,
15 color::Rgb,
16 search::{Match, RegexIter, RegexSearch},
17 RenderableCursor, TermMode,
18 },
19 tty::{self, setup_env},
20 Term,
21};
22use anyhow::{bail, Result};
23
24use futures::{
25 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
26 FutureExt,
27};
28
29use mappings::mouse::{
30 alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
31};
32
33use procinfo::LocalProcessInfo;
34use schemars::JsonSchema;
35use serde::{Deserialize, Serialize};
36use util::truncate_and_trailoff;
37
38use std::{
39 cmp::min,
40 collections::{HashMap, VecDeque},
41 fmt::Display,
42 ops::{Deref, Index, RangeInclusive, Sub},
43 os::unix::prelude::AsRawFd,
44 path::PathBuf,
45 sync::Arc,
46 time::{Duration, Instant},
47};
48use thiserror::Error;
49
50use gpui::{
51 fonts,
52 geometry::vector::{vec2f, Vector2F},
53 keymap_matcher::Keystroke,
54 platform::{MouseButton, MouseMovedEvent, TouchPhase},
55 scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
56 AppContext, ClipboardItem, Entity, ModelContext, Task,
57};
58
59use crate::mappings::{
60 colors::{get_color_at_index, to_alac_rgb},
61 keys::to_esc_str,
62};
63use lazy_static::lazy_static;
64
65///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
66///Scroll multiplier that is set to 3 by default. This will be removed when I
67///Implement scroll bars.
68const SCROLL_MULTIPLIER: f32 = 4.;
69const MAX_SEARCH_LINES: usize = 100;
70const DEBUG_TERMINAL_WIDTH: f32 = 500.;
71const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
72const DEBUG_CELL_WIDTH: f32 = 5.;
73const DEBUG_LINE_HEIGHT: f32 = 5.;
74
75// Regex Copied from alacritty's ui_config.rs
76
77lazy_static! {
78 static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
79}
80
81///Upward flowing events, for changing the title and such
82#[derive(Clone, Copy, Debug)]
83pub enum Event {
84 TitleChanged,
85 BreadcrumbsChanged,
86 CloseTerminal,
87 Bell,
88 Wakeup,
89 BlinkChanged,
90 SelectionsChanged,
91}
92
93#[derive(Clone)]
94enum InternalEvent {
95 ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
96 Resize(TerminalSize),
97 Clear,
98 // FocusNextMatch,
99 Scroll(AlacScroll),
100 ScrollToPoint(Point),
101 SetSelection(Option<(Selection, Point)>),
102 UpdateSelection(Vector2F),
103 // Adjusted mouse position, should open
104 FindHyperlink(Vector2F, bool),
105 Copy,
106}
107
108///A translation struct for Alacritty to communicate with us from their event loop
109#[derive(Clone)]
110pub struct ZedListener(UnboundedSender<AlacTermEvent>);
111
112impl EventListener for ZedListener {
113 fn send_event(&self, event: AlacTermEvent) {
114 self.0.unbounded_send(event).ok();
115 }
116}
117
118pub fn init(cx: &mut AppContext) {
119 settings::register::<TerminalSettings>(cx);
120}
121
122#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
123#[serde(rename_all = "snake_case")]
124pub enum TerminalDockPosition {
125 Left,
126 Bottom,
127 Right,
128}
129
130#[derive(Deserialize)]
131pub struct TerminalSettings {
132 pub shell: Shell,
133 pub working_directory: WorkingDirectory,
134 font_size: Option<f32>,
135 pub font_family: Option<String>,
136 pub line_height: TerminalLineHeight,
137 pub font_features: Option<fonts::Features>,
138 pub env: HashMap<String, String>,
139 pub blinking: TerminalBlink,
140 pub alternate_scroll: AlternateScroll,
141 pub option_as_meta: bool,
142 pub copy_on_select: bool,
143 pub dock: TerminalDockPosition,
144 pub default_width: f32,
145 pub default_height: f32,
146}
147
148#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
149pub struct TerminalSettingsContent {
150 pub shell: Option<Shell>,
151 pub working_directory: Option<WorkingDirectory>,
152 pub font_size: Option<f32>,
153 pub font_family: Option<String>,
154 pub line_height: Option<TerminalLineHeight>,
155 pub font_features: Option<fonts::Features>,
156 pub env: Option<HashMap<String, String>>,
157 pub blinking: Option<TerminalBlink>,
158 pub alternate_scroll: Option<AlternateScroll>,
159 pub option_as_meta: Option<bool>,
160 pub copy_on_select: Option<bool>,
161 pub dock: Option<TerminalDockPosition>,
162 pub default_width: Option<f32>,
163 pub default_height: Option<f32>,
164}
165
166impl TerminalSettings {
167 pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
168 self.font_size
169 .map(|size| theme::adjusted_font_size(size, cx))
170 }
171}
172
173impl settings::Setting for TerminalSettings {
174 const KEY: Option<&'static str> = Some("terminal");
175
176 type FileContent = TerminalSettingsContent;
177
178 fn load(
179 default_value: &Self::FileContent,
180 user_values: &[&Self::FileContent],
181 _: &AppContext,
182 ) -> Result<Self> {
183 Self::load_via_json_merge(default_value, user_values)
184 }
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
188#[serde(rename_all = "snake_case")]
189pub enum TerminalLineHeight {
190 #[default]
191 Comfortable,
192 Standard,
193 Custom(f32),
194}
195
196impl TerminalLineHeight {
197 pub fn value(&self) -> f32 {
198 match self {
199 TerminalLineHeight::Comfortable => 1.618,
200 TerminalLineHeight::Standard => 1.3,
201 TerminalLineHeight::Custom(line_height) => *line_height,
202 }
203 }
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
207#[serde(rename_all = "snake_case")]
208pub enum TerminalBlink {
209 Off,
210 TerminalControlled,
211 On,
212}
213
214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
215#[serde(rename_all = "snake_case")]
216pub enum Shell {
217 System,
218 Program(String),
219 WithArguments { program: String, args: Vec<String> },
220}
221
222#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum AlternateScroll {
225 On,
226 Off,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
230#[serde(rename_all = "snake_case")]
231pub enum WorkingDirectory {
232 CurrentProjectDirectory,
233 FirstProjectDirectory,
234 AlwaysHome,
235 Always { directory: String },
236}
237
238#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
239pub struct TerminalSize {
240 pub cell_width: f32,
241 pub line_height: f32,
242 pub height: f32,
243 pub width: f32,
244}
245
246impl TerminalSize {
247 pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
248 TerminalSize {
249 cell_width,
250 line_height,
251 width: size.x(),
252 height: size.y(),
253 }
254 }
255
256 pub fn num_lines(&self) -> usize {
257 (self.height / self.line_height).floor() as usize
258 }
259
260 pub fn num_columns(&self) -> usize {
261 (self.width / self.cell_width).floor() as usize
262 }
263
264 pub fn height(&self) -> f32 {
265 self.height
266 }
267
268 pub fn width(&self) -> f32 {
269 self.width
270 }
271
272 pub fn cell_width(&self) -> f32 {
273 self.cell_width
274 }
275
276 pub fn line_height(&self) -> f32 {
277 self.line_height
278 }
279}
280impl Default for TerminalSize {
281 fn default() -> Self {
282 TerminalSize::new(
283 DEBUG_LINE_HEIGHT,
284 DEBUG_CELL_WIDTH,
285 vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
286 )
287 }
288}
289
290impl From<TerminalSize> for WindowSize {
291 fn from(val: TerminalSize) -> Self {
292 WindowSize {
293 num_lines: val.num_lines() as u16,
294 num_cols: val.num_columns() as u16,
295 cell_width: val.cell_width() as u16,
296 cell_height: val.line_height() as u16,
297 }
298 }
299}
300
301impl Dimensions for TerminalSize {
302 /// Note: this is supposed to be for the back buffer's length,
303 /// but we exclusively use it to resize the terminal, which does not
304 /// use this method. We still have to implement it for the trait though,
305 /// hence, this comment.
306 fn total_lines(&self) -> usize {
307 self.screen_lines()
308 }
309
310 fn screen_lines(&self) -> usize {
311 self.num_lines()
312 }
313
314 fn columns(&self) -> usize {
315 self.num_columns()
316 }
317}
318
319#[derive(Error, Debug)]
320pub struct TerminalError {
321 pub directory: Option<PathBuf>,
322 pub shell: Shell,
323 pub source: std::io::Error,
324}
325
326impl TerminalError {
327 pub fn fmt_directory(&self) -> String {
328 self.directory
329 .clone()
330 .map(|path| {
331 match path
332 .into_os_string()
333 .into_string()
334 .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
335 {
336 Ok(s) => s,
337 Err(s) => s,
338 }
339 })
340 .unwrap_or_else(|| {
341 let default_dir =
342 dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
343 match default_dir {
344 Some(dir) => format!("<none specified, using home directory> {}", dir),
345 None => "<none specified, could not find home directory>".to_string(),
346 }
347 })
348 }
349
350 pub fn shell_to_string(&self) -> String {
351 match &self.shell {
352 Shell::System => "<system shell>".to_string(),
353 Shell::Program(p) => p.to_string(),
354 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
355 }
356 }
357
358 pub fn fmt_shell(&self) -> String {
359 match &self.shell {
360 Shell::System => "<system defined shell>".to_string(),
361 Shell::Program(s) => s.to_string(),
362 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
363 }
364 }
365}
366
367impl Display for TerminalError {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 let dir_string: String = self.fmt_directory();
370 let shell = self.fmt_shell();
371
372 write!(
373 f,
374 "Working directory: {} Shell command: `{}`, IOError: {}",
375 dir_string, shell, self.source
376 )
377 }
378}
379
380pub struct TerminalBuilder {
381 terminal: Terminal,
382 events_rx: UnboundedReceiver<AlacTermEvent>,
383}
384
385impl TerminalBuilder {
386 pub fn new(
387 working_directory: Option<PathBuf>,
388 shell: Shell,
389 mut env: HashMap<String, String>,
390 blink_settings: Option<TerminalBlink>,
391 alternate_scroll: AlternateScroll,
392 window_id: usize,
393 ) -> Result<TerminalBuilder> {
394 let pty_config = {
395 let alac_shell = match shell.clone() {
396 Shell::System => None,
397 Shell::Program(program) => Some(Program::Just(program)),
398 Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
399 };
400
401 PtyConfig {
402 shell: alac_shell,
403 working_directory: working_directory.clone(),
404 hold: false,
405 }
406 };
407
408 //TODO: Properly set the current locale,
409 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
410 env.insert("ZED_TERM".to_string(), true.to_string());
411
412 let alac_scrolling = Scrolling::default();
413 // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
414
415 let config = Config {
416 pty_config: pty_config.clone(),
417 env,
418 scrolling: alac_scrolling,
419 ..Default::default()
420 };
421
422 setup_env(&config);
423
424 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
425 //TODO: Remove with a bounded sender which can be dispatched on &self
426 let (events_tx, events_rx) = unbounded();
427 //Set up the terminal...
428 let mut term = Term::new(
429 &config,
430 &TerminalSize::default(),
431 ZedListener(events_tx.clone()),
432 );
433
434 //Start off blinking if we need to
435 if let Some(TerminalBlink::On) = blink_settings {
436 term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
437 }
438
439 //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
440 if let AlternateScroll::Off = alternate_scroll {
441 term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
442 }
443
444 let term = Arc::new(FairMutex::new(term));
445
446 //Setup the pty...
447 let pty = match tty::new(
448 &pty_config,
449 TerminalSize::default().into(),
450 window_id as u64,
451 ) {
452 Ok(pty) => pty,
453 Err(error) => {
454 bail!(TerminalError {
455 directory: working_directory,
456 shell,
457 source: error,
458 });
459 }
460 };
461
462 let fd = pty.file().as_raw_fd();
463 let shell_pid = pty.child().id();
464
465 //And connect them together
466 let event_loop = EventLoop::new(
467 term.clone(),
468 ZedListener(events_tx.clone()),
469 pty,
470 pty_config.hold,
471 false,
472 );
473
474 //Kick things off
475 let pty_tx = event_loop.channel();
476 let _io_thread = event_loop.spawn();
477
478 let terminal = Terminal {
479 pty_tx: Notifier(pty_tx),
480 term,
481 events: VecDeque::with_capacity(10), //Should never get this high.
482 last_content: Default::default(),
483 last_mouse: None,
484 matches: Vec::new(),
485 last_synced: Instant::now(),
486 sync_task: None,
487 selection_head: None,
488 shell_fd: fd as u32,
489 shell_pid,
490 foreground_process_info: None,
491 breadcrumb_text: String::new(),
492 scroll_px: 0.,
493 last_mouse_position: None,
494 next_link_id: 0,
495 selection_phase: SelectionPhase::Ended,
496 };
497
498 Ok(TerminalBuilder {
499 terminal,
500 events_rx,
501 })
502 }
503
504 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
505 //Event loop
506 cx.spawn_weak(|this, mut cx| async move {
507 use futures::StreamExt;
508
509 while let Some(event) = self.events_rx.next().await {
510 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
511 //Process the first event immediately for lowered latency
512 this.process_event(&event, cx);
513 });
514
515 'outer: loop {
516 let mut events = vec![];
517 let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
518 let mut wakeup = false;
519 loop {
520 futures::select_biased! {
521 _ = timer => break,
522 event = self.events_rx.next() => {
523 if let Some(event) = event {
524 if matches!(event, AlacTermEvent::Wakeup) {
525 wakeup = true;
526 } else {
527 events.push(event);
528 }
529
530 if events.len() > 100 {
531 break;
532 }
533 } else {
534 break;
535 }
536 },
537 }
538 }
539
540 if events.is_empty() && wakeup == false {
541 smol::future::yield_now().await;
542 break 'outer;
543 } else {
544 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
545 if wakeup {
546 this.process_event(&AlacTermEvent::Wakeup, cx);
547 }
548
549 for event in events {
550 this.process_event(&event, cx);
551 }
552 });
553 smol::future::yield_now().await;
554 }
555 }
556 }
557
558 Some(())
559 })
560 .detach();
561
562 self.terminal
563 }
564}
565
566#[derive(Debug, Clone, Deserialize, Serialize)]
567pub struct IndexedCell {
568 pub point: Point,
569 pub cell: Cell,
570}
571
572impl Deref for IndexedCell {
573 type Target = Cell;
574
575 #[inline]
576 fn deref(&self) -> &Cell {
577 &self.cell
578 }
579}
580
581// TODO: Un-pub
582#[derive(Clone)]
583pub struct TerminalContent {
584 pub cells: Vec<IndexedCell>,
585 pub mode: TermMode,
586 pub display_offset: usize,
587 pub selection_text: Option<String>,
588 pub selection: Option<SelectionRange>,
589 pub cursor: RenderableCursor,
590 pub cursor_char: char,
591 pub size: TerminalSize,
592 pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
593}
594
595impl Default for TerminalContent {
596 fn default() -> Self {
597 TerminalContent {
598 cells: Default::default(),
599 mode: Default::default(),
600 display_offset: Default::default(),
601 selection_text: Default::default(),
602 selection: Default::default(),
603 cursor: RenderableCursor {
604 shape: alacritty_terminal::ansi::CursorShape::Block,
605 point: Point::new(Line(0), Column(0)),
606 },
607 cursor_char: Default::default(),
608 size: Default::default(),
609 last_hovered_hyperlink: None,
610 }
611 }
612}
613
614#[derive(PartialEq, Eq)]
615pub enum SelectionPhase {
616 Selecting,
617 Ended,
618}
619
620pub struct Terminal {
621 pty_tx: Notifier,
622 term: Arc<FairMutex<Term<ZedListener>>>,
623 events: VecDeque<InternalEvent>,
624 /// This is only used for mouse mode cell change detection
625 last_mouse: Option<(Point, AlacDirection)>,
626 /// This is only used for terminal hyperlink checking
627 last_mouse_position: Option<Vector2F>,
628 pub matches: Vec<RangeInclusive<Point>>,
629 pub last_content: TerminalContent,
630 last_synced: Instant,
631 sync_task: Option<Task<()>>,
632 pub selection_head: Option<Point>,
633 pub breadcrumb_text: String,
634 shell_pid: u32,
635 shell_fd: u32,
636 pub foreground_process_info: Option<LocalProcessInfo>,
637 scroll_px: f32,
638 next_link_id: usize,
639 selection_phase: SelectionPhase,
640}
641
642impl Terminal {
643 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
644 match event {
645 AlacTermEvent::Title(title) => {
646 self.breadcrumb_text = title.to_string();
647 cx.emit(Event::BreadcrumbsChanged);
648 }
649 AlacTermEvent::ResetTitle => {
650 self.breadcrumb_text = String::new();
651 cx.emit(Event::BreadcrumbsChanged);
652 }
653 AlacTermEvent::ClipboardStore(_, data) => {
654 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
655 }
656 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
657 &cx.read_from_clipboard()
658 .map(|ci| ci.text().to_string())
659 .unwrap_or_else(|| "".to_string()),
660 )),
661 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
662 AlacTermEvent::TextAreaSizeRequest(format) => {
663 self.write_to_pty(format(self.last_content.size.into()))
664 }
665 AlacTermEvent::CursorBlinkingChange => {
666 cx.emit(Event::BlinkChanged);
667 }
668 AlacTermEvent::Bell => {
669 cx.emit(Event::Bell);
670 }
671 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
672 AlacTermEvent::MouseCursorDirty => {
673 //NOOP, Handled in render
674 }
675 AlacTermEvent::Wakeup => {
676 cx.emit(Event::Wakeup);
677
678 if self.update_process_info() {
679 cx.emit(Event::TitleChanged);
680 }
681 }
682 AlacTermEvent::ColorRequest(idx, fun_ptr) => {
683 self.events
684 .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
685 }
686 }
687 }
688
689 /// Update the cached process info, returns whether the Zed-relevant info has changed
690 fn update_process_info(&mut self) -> bool {
691 let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
692 if pid < 0 {
693 pid = self.shell_pid as i32;
694 }
695
696 if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
697 let res = self
698 .foreground_process_info
699 .as_ref()
700 .map(|old_info| {
701 process_info.cwd != old_info.cwd || process_info.name != old_info.name
702 })
703 .unwrap_or(true);
704
705 self.foreground_process_info = Some(process_info.clone());
706
707 res
708 } else {
709 false
710 }
711 }
712
713 ///Takes events from Alacritty and translates them to behavior on this view
714 fn process_terminal_event(
715 &mut self,
716 event: &InternalEvent,
717 term: &mut Term<ZedListener>,
718 cx: &mut ModelContext<Self>,
719 ) {
720 match event {
721 InternalEvent::ColorRequest(index, format) => {
722 let color = term.colors()[*index].unwrap_or_else(|| {
723 let term_style = &theme::current(cx).terminal;
724 to_alac_rgb(get_color_at_index(index, &term_style))
725 });
726 self.write_to_pty(format(color))
727 }
728 InternalEvent::Resize(mut new_size) => {
729 new_size.height = f32::max(new_size.line_height, new_size.height);
730 new_size.width = f32::max(new_size.cell_width, new_size.width);
731
732 self.last_content.size = new_size.clone();
733
734 self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
735
736 term.resize(new_size);
737 }
738 InternalEvent::Clear => {
739 // Clear back buffer
740 term.clear_screen(ClearMode::Saved);
741
742 let cursor = term.grid().cursor.point;
743
744 // Clear the lines above
745 term.grid_mut().reset_region(..cursor.line);
746
747 // Copy the current line up
748 let line = term.grid()[cursor.line][..Column(term.grid().columns())]
749 .iter()
750 .cloned()
751 .enumerate()
752 .collect::<Vec<(usize, Cell)>>();
753
754 for (i, cell) in line {
755 term.grid_mut()[Line(0)][Column(i)] = cell;
756 }
757
758 // Reset the cursor
759 term.grid_mut().cursor.point =
760 Point::new(Line(0), term.grid_mut().cursor.point.column);
761 let new_cursor = term.grid().cursor.point;
762
763 // Clear the lines below the new cursor
764 if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
765 term.grid_mut().reset_region((new_cursor.line + 1)..);
766 }
767
768 cx.emit(Event::Wakeup);
769 }
770 InternalEvent::Scroll(scroll) => {
771 term.scroll_display(*scroll);
772 self.refresh_hyperlink();
773 }
774 InternalEvent::SetSelection(selection) => {
775 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
776
777 if let Some((_, head)) = selection {
778 self.selection_head = Some(*head);
779 }
780 cx.emit(Event::SelectionsChanged)
781 }
782 InternalEvent::UpdateSelection(position) => {
783 if let Some(mut selection) = term.selection.take() {
784 let point = grid_point(
785 *position,
786 self.last_content.size,
787 term.grid().display_offset(),
788 );
789
790 let side = mouse_side(*position, self.last_content.size);
791
792 selection.update(point, side);
793 term.selection = Some(selection);
794
795 self.selection_head = Some(point);
796 cx.emit(Event::SelectionsChanged)
797 }
798 }
799
800 InternalEvent::Copy => {
801 if let Some(txt) = term.selection_to_string() {
802 cx.write_to_clipboard(ClipboardItem::new(txt))
803 }
804 }
805 InternalEvent::ScrollToPoint(point) => {
806 term.scroll_to_point(*point);
807 self.refresh_hyperlink();
808 }
809 InternalEvent::FindHyperlink(position, open) => {
810 let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
811
812 let point = grid_point(
813 *position,
814 self.last_content.size,
815 term.grid().display_offset(),
816 )
817 .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
818
819 let link = term.grid().index(point).hyperlink();
820 let found_url = if link.is_some() {
821 let mut min_index = point;
822 loop {
823 let new_min_index =
824 min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1);
825 if new_min_index == min_index {
826 break;
827 } else if term.grid().index(new_min_index).hyperlink() != link {
828 break;
829 } else {
830 min_index = new_min_index
831 }
832 }
833
834 let mut max_index = point;
835 loop {
836 let new_max_index =
837 max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1);
838 if new_max_index == max_index {
839 break;
840 } else if term.grid().index(new_max_index).hyperlink() != link {
841 break;
842 } else {
843 max_index = new_max_index
844 }
845 }
846
847 let url = link.unwrap().uri().to_owned();
848 let url_match = min_index..=max_index;
849
850 Some((url, url_match))
851 } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
852 let url = term.bounds_to_string(*url_match.start(), *url_match.end());
853
854 Some((url, url_match))
855 } else {
856 None
857 };
858
859 if let Some((url, url_match)) = found_url {
860 if *open {
861 cx.platform().open_url(url.as_str());
862 } else {
863 self.update_hyperlink(prev_hyperlink, url, url_match);
864 }
865 }
866 }
867 }
868 }
869
870 fn update_hyperlink(
871 &mut self,
872 prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
873 url: String,
874 url_match: RangeInclusive<Point>,
875 ) {
876 if let Some(prev_hyperlink) = prev_hyperlink {
877 if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
878 self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
879 } else {
880 self.last_content.last_hovered_hyperlink =
881 Some((url, url_match, self.next_link_id()));
882 }
883 } else {
884 self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
885 }
886 }
887
888 fn next_link_id(&mut self) -> usize {
889 let res = self.next_link_id;
890 self.next_link_id = self.next_link_id.wrapping_add(1);
891 res
892 }
893
894 pub fn last_content(&self) -> &TerminalContent {
895 &self.last_content
896 }
897
898 //To test:
899 //- Activate match on terminal (scrolling and selection)
900 //- Editor search snapping behavior
901
902 pub fn activate_match(&mut self, index: usize) {
903 if let Some(search_match) = self.matches.get(index).cloned() {
904 self.set_selection(Some((make_selection(&search_match), *search_match.end())));
905
906 self.events
907 .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
908 }
909 }
910
911 fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
912 self.events
913 .push_back(InternalEvent::SetSelection(selection));
914 }
915
916 pub fn copy(&mut self) {
917 self.events.push_back(InternalEvent::Copy);
918 }
919
920 pub fn clear(&mut self) {
921 self.events.push_back(InternalEvent::Clear)
922 }
923
924 ///Resize the terminal and the PTY.
925 pub fn set_size(&mut self, new_size: TerminalSize) {
926 self.events.push_back(InternalEvent::Resize(new_size))
927 }
928
929 ///Write the Input payload to the tty.
930 fn write_to_pty(&self, input: String) {
931 self.pty_tx.notify(input.into_bytes());
932 }
933
934 pub fn input(&mut self, input: String) {
935 self.events
936 .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
937 self.events.push_back(InternalEvent::SetSelection(None));
938
939 self.write_to_pty(input);
940 }
941
942 pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
943 let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
944 if let Some(esc) = esc {
945 self.input(esc);
946 true
947 } else {
948 false
949 }
950 }
951
952 ///Paste text into the terminal
953 pub fn paste(&mut self, text: &str) {
954 let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
955 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
956 } else {
957 text.replace("\r\n", "\r").replace('\n', "\r")
958 };
959
960 self.input(paste_text);
961 }
962
963 pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
964 let term = self.term.clone();
965
966 let mut terminal = if let Some(term) = term.try_lock_unfair() {
967 term
968 } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
969 term.lock_unfair() //It's been too long, force block
970 } else if let None = self.sync_task {
971 //Skip this frame
972 let delay = cx.background().timer(Duration::from_millis(16));
973 self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
974 delay.await;
975 cx.update(|cx| {
976 if let Some(handle) = weak_handle.upgrade(cx) {
977 handle.update(cx, |terminal, cx| {
978 terminal.sync_task.take();
979 cx.notify();
980 });
981 }
982 });
983 }));
984 return;
985 } else {
986 //No lock and delayed rendering already scheduled, nothing to do
987 return;
988 };
989
990 //Note that the ordering of events matters for event processing
991 while let Some(e) = self.events.pop_front() {
992 self.process_terminal_event(&e, &mut terminal, cx)
993 }
994
995 self.last_content = Self::make_content(&terminal, &self.last_content);
996 self.last_synced = Instant::now();
997 }
998
999 fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1000 let content = term.renderable_content();
1001 TerminalContent {
1002 cells: content
1003 .display_iter
1004 //TODO: Add this once there's a way to retain empty lines
1005 // .filter(|ic| {
1006 // !ic.flags.contains(Flags::HIDDEN)
1007 // && !(ic.bg == Named(NamedColor::Background)
1008 // && ic.c == ' '
1009 // && !ic.flags.contains(Flags::INVERSE))
1010 // })
1011 .map(|ic| IndexedCell {
1012 point: ic.point,
1013 cell: ic.cell.clone(),
1014 })
1015 .collect::<Vec<IndexedCell>>(),
1016 mode: content.mode,
1017 display_offset: content.display_offset,
1018 selection_text: term.selection_to_string(),
1019 selection: content.selection,
1020 cursor: content.cursor,
1021 cursor_char: term.grid()[content.cursor.point].c,
1022 size: last_content.size,
1023 last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
1024 }
1025 }
1026
1027 pub fn focus_in(&self) {
1028 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1029 self.write_to_pty("\x1b[I".to_string());
1030 }
1031 }
1032
1033 pub fn focus_out(&mut self) {
1034 self.last_mouse_position = None;
1035 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1036 self.write_to_pty("\x1b[O".to_string());
1037 }
1038 }
1039
1040 pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
1041 match self.last_mouse {
1042 Some((old_point, old_side)) => {
1043 if old_point == point && old_side == side {
1044 false
1045 } else {
1046 self.last_mouse = Some((point, side));
1047 true
1048 }
1049 }
1050 None => {
1051 self.last_mouse = Some((point, side));
1052 true
1053 }
1054 }
1055 }
1056
1057 pub fn mouse_mode(&self, shift: bool) -> bool {
1058 self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1059 }
1060
1061 pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
1062 let position = e.position.sub(origin);
1063 self.last_mouse_position = Some(position);
1064 if self.mouse_mode(e.shift) {
1065 let point = grid_point(
1066 position,
1067 self.last_content.size,
1068 self.last_content.display_offset,
1069 );
1070 let side = mouse_side(position, self.last_content.size);
1071
1072 if self.mouse_changed(point, side) {
1073 if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
1074 self.pty_tx.notify(bytes);
1075 }
1076 }
1077 } else {
1078 self.hyperlink_from_position(Some(position));
1079 }
1080 }
1081
1082 fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
1083 if self.selection_phase == SelectionPhase::Selecting {
1084 self.last_content.last_hovered_hyperlink = None;
1085 } else if let Some(position) = position {
1086 self.events
1087 .push_back(InternalEvent::FindHyperlink(position, false));
1088 }
1089 }
1090
1091 pub fn mouse_drag(&mut self, e: MouseDrag, origin: Vector2F) {
1092 let position = e.position.sub(origin);
1093 self.last_mouse_position = Some(position);
1094
1095 if !self.mouse_mode(e.shift) {
1096 self.selection_phase = SelectionPhase::Selecting;
1097 // Alacritty has the same ordering, of first updating the selection
1098 // then scrolling 15ms later
1099 self.events
1100 .push_back(InternalEvent::UpdateSelection(position));
1101
1102 // Doesn't make sense to scroll the alt screen
1103 if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1104 let scroll_delta = match self.drag_line_delta(e) {
1105 Some(value) => value,
1106 None => return,
1107 };
1108
1109 let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
1110
1111 self.events
1112 .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1113 }
1114 }
1115 }
1116
1117 fn drag_line_delta(&mut self, e: MouseDrag) -> Option<f32> {
1118 //TODO: Why do these need to be doubled? Probably the same problem that the IME has
1119 let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
1120 let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
1121 let scroll_delta = if e.position.y() < top {
1122 (top - e.position.y()).powf(1.1)
1123 } else if e.position.y() > bottom {
1124 -((e.position.y() - bottom).powf(1.1))
1125 } else {
1126 return None; //Nothing to do
1127 };
1128 Some(scroll_delta)
1129 }
1130
1131 pub fn mouse_down(&mut self, e: &MouseDown, origin: Vector2F) {
1132 let position = e.position.sub(origin);
1133 let point = grid_point(
1134 position,
1135 self.last_content.size,
1136 self.last_content.display_offset,
1137 );
1138
1139 if self.mouse_mode(e.shift) {
1140 if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
1141 self.pty_tx.notify(bytes);
1142 }
1143 } else if e.button == MouseButton::Left {
1144 let position = e.position.sub(origin);
1145 let point = grid_point(
1146 position,
1147 self.last_content.size,
1148 self.last_content.display_offset,
1149 );
1150
1151 // Use .opposite so that selection is inclusive of the cell clicked.
1152 let side = mouse_side(position, self.last_content.size);
1153
1154 let selection_type = match e.click_count {
1155 0 => return, //This is a release
1156 1 => Some(SelectionType::Simple),
1157 2 => Some(SelectionType::Semantic),
1158 3 => Some(SelectionType::Lines),
1159 _ => None,
1160 };
1161
1162 let selection =
1163 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
1164
1165 if let Some(sel) = selection {
1166 self.events
1167 .push_back(InternalEvent::SetSelection(Some((sel, point))));
1168 }
1169 }
1170 }
1171
1172 pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext<Self>) {
1173 let setting = settings::get::<TerminalSettings>(cx);
1174
1175 let position = e.position.sub(origin);
1176 if self.mouse_mode(e.shift) {
1177 let point = grid_point(
1178 position,
1179 self.last_content.size,
1180 self.last_content.display_offset,
1181 );
1182
1183 if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
1184 self.pty_tx.notify(bytes);
1185 }
1186 } else {
1187 if e.button == MouseButton::Left && setting.copy_on_select {
1188 self.copy();
1189 }
1190
1191 //Hyperlinks
1192 if self.selection_phase == SelectionPhase::Ended {
1193 let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
1194 if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1195 cx.platform().open_url(link.uri());
1196 } else {
1197 self.events
1198 .push_back(InternalEvent::FindHyperlink(position, true));
1199 }
1200 }
1201 }
1202
1203 self.selection_phase = SelectionPhase::Ended;
1204 self.last_mouse = None;
1205 }
1206
1207 ///Scroll the terminal
1208 pub fn scroll_wheel(&mut self, e: MouseScrollWheel, origin: Vector2F) {
1209 let mouse_mode = self.mouse_mode(e.shift);
1210
1211 if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
1212 if mouse_mode {
1213 let point = grid_point(
1214 e.position.sub(origin),
1215 self.last_content.size,
1216 self.last_content.display_offset,
1217 );
1218
1219 if let Some(scrolls) =
1220 scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
1221 {
1222 for scroll in scrolls {
1223 self.pty_tx.notify(scroll);
1224 }
1225 };
1226 } else if self
1227 .last_content
1228 .mode
1229 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
1230 && !e.shift
1231 {
1232 self.pty_tx.notify(alt_scroll(scroll_lines))
1233 } else {
1234 if scroll_lines != 0 {
1235 let scroll = AlacScroll::Delta(scroll_lines);
1236
1237 self.events.push_back(InternalEvent::Scroll(scroll));
1238 }
1239 }
1240 }
1241 }
1242
1243 pub fn refresh_hyperlink(&mut self) {
1244 self.hyperlink_from_position(self.last_mouse_position);
1245 }
1246
1247 fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
1248 let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
1249 let line_height = self.last_content.size.line_height;
1250 match e.phase {
1251 /* Reset scroll state on started */
1252 Some(TouchPhase::Started) => {
1253 self.scroll_px = 0.;
1254 None
1255 }
1256 /* Calculate the appropriate scroll lines */
1257 Some(gpui::platform::TouchPhase::Moved) => {
1258 let old_offset = (self.scroll_px / line_height) as i32;
1259
1260 self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
1261
1262 let new_offset = (self.scroll_px / line_height) as i32;
1263
1264 // Whenever we hit the edges, reset our stored scroll to 0
1265 // so we can respond to changes in direction quickly
1266 self.scroll_px %= self.last_content.size.height;
1267
1268 Some(new_offset - old_offset)
1269 }
1270 /* Fall back to delta / line_height */
1271 None => Some(
1272 ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
1273 ),
1274 _ => None,
1275 }
1276 }
1277
1278 pub fn find_matches(
1279 &mut self,
1280 searcher: RegexSearch,
1281 cx: &mut ModelContext<Self>,
1282 ) -> Task<Vec<RangeInclusive<Point>>> {
1283 let term = self.term.clone();
1284 cx.background().spawn(async move {
1285 let term = term.lock();
1286
1287 all_search_matches(&term, &searcher).collect()
1288 })
1289 }
1290
1291 pub fn title(&self) -> String {
1292 self.foreground_process_info
1293 .as_ref()
1294 .map(|fpi| {
1295 format!(
1296 "{} — {}",
1297 truncate_and_trailoff(
1298 &fpi.cwd
1299 .file_name()
1300 .map(|name| name.to_string_lossy().to_string())
1301 .unwrap_or_default(),
1302 25
1303 ),
1304 truncate_and_trailoff(
1305 &{
1306 format!(
1307 "{}{}",
1308 fpi.name,
1309 if fpi.argv.len() >= 1 {
1310 format!(" {}", (&fpi.argv[1..]).join(" "))
1311 } else {
1312 "".to_string()
1313 }
1314 )
1315 },
1316 25
1317 )
1318 )
1319 })
1320 .unwrap_or_else(|| "Terminal".to_string())
1321 }
1322}
1323
1324impl Drop for Terminal {
1325 fn drop(&mut self) {
1326 self.pty_tx.0.send(Msg::Shutdown).ok();
1327 }
1328}
1329
1330impl Entity for Terminal {
1331 type Event = Event;
1332}
1333
1334/// Based on alacritty/src/display/hint.rs > regex_match_at
1335/// Retrieve the match, if the specified point is inside the content matching the regex.
1336fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
1337 visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
1338}
1339
1340/// Copied from alacritty/src/display/hint.rs:
1341/// Iterate over all visible regex matches.
1342pub fn visible_regex_match_iter<'a, T>(
1343 term: &'a Term<T>,
1344 regex: &'a RegexSearch,
1345) -> impl Iterator<Item = Match> + 'a {
1346 let viewport_start = Line(-(term.grid().display_offset() as i32));
1347 let viewport_end = viewport_start + term.bottommost_line();
1348 let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
1349 let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
1350 start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1351 end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1352
1353 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1354 .skip_while(move |rm| rm.end().line < viewport_start)
1355 .take_while(move |rm| rm.start().line <= viewport_end)
1356}
1357
1358fn make_selection(range: &RangeInclusive<Point>) -> Selection {
1359 let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1360 selection.update(*range.end(), AlacDirection::Right);
1361 selection
1362}
1363
1364fn all_search_matches<'a, T>(
1365 term: &'a Term<T>,
1366 regex: &'a RegexSearch,
1367) -> impl Iterator<Item = Match> + 'a {
1368 let start = Point::new(term.grid().topmost_line(), Column(0));
1369 let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
1370 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1371}
1372
1373fn content_index_for_mouse(pos: Vector2F, size: &TerminalSize) -> usize {
1374 let col = (pos.x() / size.cell_width()).round() as usize;
1375
1376 let clamped_col = min(col, size.columns() - 1);
1377
1378 let row = (pos.y() / size.line_height()).round() as usize;
1379
1380 let clamped_row = min(row, size.screen_lines() - 1);
1381
1382 clamped_row * size.columns() + clamped_col
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387 use alacritty_terminal::{
1388 index::{Column, Line, Point},
1389 term::cell::Cell,
1390 };
1391 use gpui::geometry::vector::vec2f;
1392 use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
1393
1394 use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
1395
1396 #[test]
1397 fn test_mouse_to_cell_test() {
1398 let mut rng = thread_rng();
1399 const ITERATIONS: usize = 10;
1400 const PRECISION: usize = 1000;
1401
1402 for _ in 0..ITERATIONS {
1403 let viewport_cells = rng.gen_range(15..20);
1404 let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
1405
1406 let size = crate::TerminalSize {
1407 cell_width: cell_size,
1408 line_height: cell_size,
1409 height: cell_size * (viewport_cells as f32),
1410 width: cell_size * (viewport_cells as f32),
1411 };
1412
1413 let cells = get_cells(size, &mut rng);
1414 let content = convert_cells_to_content(size, &cells);
1415
1416 for row in 0..(viewport_cells - 1) {
1417 let row = row as usize;
1418 for col in 0..(viewport_cells - 1) {
1419 let col = col as usize;
1420
1421 let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1422 let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1423
1424 let mouse_pos = vec2f(
1425 col as f32 * cell_size + col_offset,
1426 row as f32 * cell_size + row_offset,
1427 );
1428
1429 let content_index = content_index_for_mouse(mouse_pos, &content.size);
1430 let mouse_cell = content.cells[content_index].c;
1431 let real_cell = cells[row][col];
1432
1433 assert_eq!(mouse_cell, real_cell);
1434 }
1435 }
1436 }
1437 }
1438
1439 #[test]
1440 fn test_mouse_to_cell_clamp() {
1441 let mut rng = thread_rng();
1442
1443 let size = crate::TerminalSize {
1444 cell_width: 10.,
1445 line_height: 10.,
1446 height: 100.,
1447 width: 100.,
1448 };
1449
1450 let cells = get_cells(size, &mut rng);
1451 let content = convert_cells_to_content(size, &cells);
1452
1453 assert_eq!(
1454 content.cells[content_index_for_mouse(vec2f(-10., -10.), &content.size)].c,
1455 cells[0][0]
1456 );
1457 assert_eq!(
1458 content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content.size)].c,
1459 cells[9][9]
1460 );
1461 }
1462
1463 fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
1464 let mut cells = Vec::new();
1465
1466 for _ in 0..((size.height() / size.line_height()) as usize) {
1467 let mut row_vec = Vec::new();
1468 for _ in 0..((size.width() / size.cell_width()) as usize) {
1469 let cell_char = rng.sample(Alphanumeric) as char;
1470 row_vec.push(cell_char)
1471 }
1472 cells.push(row_vec)
1473 }
1474
1475 cells
1476 }
1477
1478 fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
1479 let mut ic = Vec::new();
1480
1481 for row in 0..cells.len() {
1482 for col in 0..cells[row].len() {
1483 let cell_char = cells[row][col];
1484 ic.push(IndexedCell {
1485 point: Point::new(Line(row as i32), Column(col)),
1486 cell: Cell {
1487 c: cell_char,
1488 ..Default::default()
1489 },
1490 });
1491 }
1492 }
1493
1494 TerminalContent {
1495 cells: ic,
1496 size,
1497 ..Default::default()
1498 }
1499 }
1500}