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) => f32::max(*line_height, 1.),
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 pub fn select_matches(&mut self, matches: Vec<RangeInclusive<Point>>) {
912 let matches_to_select = self
913 .matches
914 .iter()
915 .filter(|self_match| matches.contains(self_match))
916 .cloned()
917 .collect::<Vec<_>>();
918 for match_to_select in matches_to_select {
919 self.set_selection(Some((
920 make_selection(&match_to_select),
921 *match_to_select.end(),
922 )));
923 }
924 }
925
926 fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
927 self.events
928 .push_back(InternalEvent::SetSelection(selection));
929 }
930
931 pub fn copy(&mut self) {
932 self.events.push_back(InternalEvent::Copy);
933 }
934
935 pub fn clear(&mut self) {
936 self.events.push_back(InternalEvent::Clear)
937 }
938
939 ///Resize the terminal and the PTY.
940 pub fn set_size(&mut self, new_size: TerminalSize) {
941 self.events.push_back(InternalEvent::Resize(new_size))
942 }
943
944 ///Write the Input payload to the tty.
945 fn write_to_pty(&self, input: String) {
946 self.pty_tx.notify(input.into_bytes());
947 }
948
949 pub fn input(&mut self, input: String) {
950 self.events
951 .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
952 self.events.push_back(InternalEvent::SetSelection(None));
953
954 self.write_to_pty(input);
955 }
956
957 pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
958 let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
959 if let Some(esc) = esc {
960 self.input(esc);
961 true
962 } else {
963 false
964 }
965 }
966
967 ///Paste text into the terminal
968 pub fn paste(&mut self, text: &str) {
969 let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
970 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
971 } else {
972 text.replace("\r\n", "\r").replace('\n', "\r")
973 };
974
975 self.input(paste_text);
976 }
977
978 pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
979 let term = self.term.clone();
980
981 let mut terminal = if let Some(term) = term.try_lock_unfair() {
982 term
983 } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
984 term.lock_unfair() //It's been too long, force block
985 } else if let None = self.sync_task {
986 //Skip this frame
987 let delay = cx.background().timer(Duration::from_millis(16));
988 self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
989 delay.await;
990 cx.update(|cx| {
991 if let Some(handle) = weak_handle.upgrade(cx) {
992 handle.update(cx, |terminal, cx| {
993 terminal.sync_task.take();
994 cx.notify();
995 });
996 }
997 });
998 }));
999 return;
1000 } else {
1001 //No lock and delayed rendering already scheduled, nothing to do
1002 return;
1003 };
1004
1005 //Note that the ordering of events matters for event processing
1006 while let Some(e) = self.events.pop_front() {
1007 self.process_terminal_event(&e, &mut terminal, cx)
1008 }
1009
1010 self.last_content = Self::make_content(&terminal, &self.last_content);
1011 self.last_synced = Instant::now();
1012 }
1013
1014 fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1015 let content = term.renderable_content();
1016 TerminalContent {
1017 cells: content
1018 .display_iter
1019 //TODO: Add this once there's a way to retain empty lines
1020 // .filter(|ic| {
1021 // !ic.flags.contains(Flags::HIDDEN)
1022 // && !(ic.bg == Named(NamedColor::Background)
1023 // && ic.c == ' '
1024 // && !ic.flags.contains(Flags::INVERSE))
1025 // })
1026 .map(|ic| IndexedCell {
1027 point: ic.point,
1028 cell: ic.cell.clone(),
1029 })
1030 .collect::<Vec<IndexedCell>>(),
1031 mode: content.mode,
1032 display_offset: content.display_offset,
1033 selection_text: term.selection_to_string(),
1034 selection: content.selection,
1035 cursor: content.cursor,
1036 cursor_char: term.grid()[content.cursor.point].c,
1037 size: last_content.size,
1038 last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
1039 }
1040 }
1041
1042 pub fn focus_in(&self) {
1043 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1044 self.write_to_pty("\x1b[I".to_string());
1045 }
1046 }
1047
1048 pub fn focus_out(&mut self) {
1049 self.last_mouse_position = None;
1050 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1051 self.write_to_pty("\x1b[O".to_string());
1052 }
1053 }
1054
1055 pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
1056 match self.last_mouse {
1057 Some((old_point, old_side)) => {
1058 if old_point == point && old_side == side {
1059 false
1060 } else {
1061 self.last_mouse = Some((point, side));
1062 true
1063 }
1064 }
1065 None => {
1066 self.last_mouse = Some((point, side));
1067 true
1068 }
1069 }
1070 }
1071
1072 pub fn mouse_mode(&self, shift: bool) -> bool {
1073 self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1074 }
1075
1076 pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
1077 let position = e.position.sub(origin);
1078 self.last_mouse_position = Some(position);
1079 if self.mouse_mode(e.shift) {
1080 let point = grid_point(
1081 position,
1082 self.last_content.size,
1083 self.last_content.display_offset,
1084 );
1085 let side = mouse_side(position, self.last_content.size);
1086
1087 if self.mouse_changed(point, side) {
1088 if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
1089 self.pty_tx.notify(bytes);
1090 }
1091 }
1092 } else {
1093 self.hyperlink_from_position(Some(position));
1094 }
1095 }
1096
1097 fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
1098 if self.selection_phase == SelectionPhase::Selecting {
1099 self.last_content.last_hovered_hyperlink = None;
1100 } else if let Some(position) = position {
1101 self.events
1102 .push_back(InternalEvent::FindHyperlink(position, false));
1103 }
1104 }
1105
1106 pub fn mouse_drag(&mut self, e: MouseDrag, origin: Vector2F) {
1107 let position = e.position.sub(origin);
1108 self.last_mouse_position = Some(position);
1109
1110 if !self.mouse_mode(e.shift) {
1111 self.selection_phase = SelectionPhase::Selecting;
1112 // Alacritty has the same ordering, of first updating the selection
1113 // then scrolling 15ms later
1114 self.events
1115 .push_back(InternalEvent::UpdateSelection(position));
1116
1117 // Doesn't make sense to scroll the alt screen
1118 if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1119 let scroll_delta = match self.drag_line_delta(e) {
1120 Some(value) => value,
1121 None => return,
1122 };
1123
1124 let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
1125
1126 self.events
1127 .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1128 }
1129 }
1130 }
1131
1132 fn drag_line_delta(&mut self, e: MouseDrag) -> Option<f32> {
1133 //TODO: Why do these need to be doubled? Probably the same problem that the IME has
1134 let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
1135 let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
1136 let scroll_delta = if e.position.y() < top {
1137 (top - e.position.y()).powf(1.1)
1138 } else if e.position.y() > bottom {
1139 -((e.position.y() - bottom).powf(1.1))
1140 } else {
1141 return None; //Nothing to do
1142 };
1143 Some(scroll_delta)
1144 }
1145
1146 pub fn mouse_down(&mut self, e: &MouseDown, origin: Vector2F) {
1147 let position = e.position.sub(origin);
1148 let point = grid_point(
1149 position,
1150 self.last_content.size,
1151 self.last_content.display_offset,
1152 );
1153
1154 if self.mouse_mode(e.shift) {
1155 if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
1156 self.pty_tx.notify(bytes);
1157 }
1158 } else if e.button == MouseButton::Left {
1159 let position = e.position.sub(origin);
1160 let point = grid_point(
1161 position,
1162 self.last_content.size,
1163 self.last_content.display_offset,
1164 );
1165
1166 // Use .opposite so that selection is inclusive of the cell clicked.
1167 let side = mouse_side(position, self.last_content.size);
1168
1169 let selection_type = match e.click_count {
1170 0 => return, //This is a release
1171 1 => Some(SelectionType::Simple),
1172 2 => Some(SelectionType::Semantic),
1173 3 => Some(SelectionType::Lines),
1174 _ => None,
1175 };
1176
1177 let selection =
1178 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
1179
1180 if let Some(sel) = selection {
1181 self.events
1182 .push_back(InternalEvent::SetSelection(Some((sel, point))));
1183 }
1184 }
1185 }
1186
1187 pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext<Self>) {
1188 let setting = settings::get::<TerminalSettings>(cx);
1189
1190 let position = e.position.sub(origin);
1191 if self.mouse_mode(e.shift) {
1192 let point = grid_point(
1193 position,
1194 self.last_content.size,
1195 self.last_content.display_offset,
1196 );
1197
1198 if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
1199 self.pty_tx.notify(bytes);
1200 }
1201 } else {
1202 if e.button == MouseButton::Left && setting.copy_on_select {
1203 self.copy();
1204 }
1205
1206 //Hyperlinks
1207 if self.selection_phase == SelectionPhase::Ended {
1208 let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
1209 if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1210 cx.platform().open_url(link.uri());
1211 } else {
1212 self.events
1213 .push_back(InternalEvent::FindHyperlink(position, true));
1214 }
1215 }
1216 }
1217
1218 self.selection_phase = SelectionPhase::Ended;
1219 self.last_mouse = None;
1220 }
1221
1222 ///Scroll the terminal
1223 pub fn scroll_wheel(&mut self, e: MouseScrollWheel, origin: Vector2F) {
1224 let mouse_mode = self.mouse_mode(e.shift);
1225
1226 if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
1227 if mouse_mode {
1228 let point = grid_point(
1229 e.position.sub(origin),
1230 self.last_content.size,
1231 self.last_content.display_offset,
1232 );
1233
1234 if let Some(scrolls) =
1235 scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
1236 {
1237 for scroll in scrolls {
1238 self.pty_tx.notify(scroll);
1239 }
1240 };
1241 } else if self
1242 .last_content
1243 .mode
1244 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
1245 && !e.shift
1246 {
1247 self.pty_tx.notify(alt_scroll(scroll_lines))
1248 } else {
1249 if scroll_lines != 0 {
1250 let scroll = AlacScroll::Delta(scroll_lines);
1251
1252 self.events.push_back(InternalEvent::Scroll(scroll));
1253 }
1254 }
1255 }
1256 }
1257
1258 pub fn refresh_hyperlink(&mut self) {
1259 self.hyperlink_from_position(self.last_mouse_position);
1260 }
1261
1262 fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
1263 let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
1264 let line_height = self.last_content.size.line_height;
1265 match e.phase {
1266 /* Reset scroll state on started */
1267 Some(TouchPhase::Started) => {
1268 self.scroll_px = 0.;
1269 None
1270 }
1271 /* Calculate the appropriate scroll lines */
1272 Some(gpui::platform::TouchPhase::Moved) => {
1273 let old_offset = (self.scroll_px / line_height) as i32;
1274
1275 self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
1276
1277 let new_offset = (self.scroll_px / line_height) as i32;
1278
1279 // Whenever we hit the edges, reset our stored scroll to 0
1280 // so we can respond to changes in direction quickly
1281 self.scroll_px %= self.last_content.size.height;
1282
1283 Some(new_offset - old_offset)
1284 }
1285 /* Fall back to delta / line_height */
1286 None => Some(
1287 ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
1288 ),
1289 _ => None,
1290 }
1291 }
1292
1293 pub fn find_matches(
1294 &mut self,
1295 searcher: RegexSearch,
1296 cx: &mut ModelContext<Self>,
1297 ) -> Task<Vec<RangeInclusive<Point>>> {
1298 let term = self.term.clone();
1299 cx.background().spawn(async move {
1300 let term = term.lock();
1301
1302 all_search_matches(&term, &searcher).collect()
1303 })
1304 }
1305
1306 pub fn title(&self) -> String {
1307 self.foreground_process_info
1308 .as_ref()
1309 .map(|fpi| {
1310 format!(
1311 "{} — {}",
1312 truncate_and_trailoff(
1313 &fpi.cwd
1314 .file_name()
1315 .map(|name| name.to_string_lossy().to_string())
1316 .unwrap_or_default(),
1317 25
1318 ),
1319 truncate_and_trailoff(
1320 &{
1321 format!(
1322 "{}{}",
1323 fpi.name,
1324 if fpi.argv.len() >= 1 {
1325 format!(" {}", (&fpi.argv[1..]).join(" "))
1326 } else {
1327 "".to_string()
1328 }
1329 )
1330 },
1331 25
1332 )
1333 )
1334 })
1335 .unwrap_or_else(|| "Terminal".to_string())
1336 }
1337}
1338
1339impl Drop for Terminal {
1340 fn drop(&mut self) {
1341 self.pty_tx.0.send(Msg::Shutdown).ok();
1342 }
1343}
1344
1345impl Entity for Terminal {
1346 type Event = Event;
1347}
1348
1349/// Based on alacritty/src/display/hint.rs > regex_match_at
1350/// Retrieve the match, if the specified point is inside the content matching the regex.
1351fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
1352 visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
1353}
1354
1355/// Copied from alacritty/src/display/hint.rs:
1356/// Iterate over all visible regex matches.
1357pub fn visible_regex_match_iter<'a, T>(
1358 term: &'a Term<T>,
1359 regex: &'a RegexSearch,
1360) -> impl Iterator<Item = Match> + 'a {
1361 let viewport_start = Line(-(term.grid().display_offset() as i32));
1362 let viewport_end = viewport_start + term.bottommost_line();
1363 let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
1364 let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
1365 start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1366 end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1367
1368 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1369 .skip_while(move |rm| rm.end().line < viewport_start)
1370 .take_while(move |rm| rm.start().line <= viewport_end)
1371}
1372
1373fn make_selection(range: &RangeInclusive<Point>) -> Selection {
1374 let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1375 selection.update(*range.end(), AlacDirection::Right);
1376 selection
1377}
1378
1379fn all_search_matches<'a, T>(
1380 term: &'a Term<T>,
1381 regex: &'a RegexSearch,
1382) -> impl Iterator<Item = Match> + 'a {
1383 let start = Point::new(term.grid().topmost_line(), Column(0));
1384 let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
1385 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1386}
1387
1388fn content_index_for_mouse(pos: Vector2F, size: &TerminalSize) -> usize {
1389 let col = (pos.x() / size.cell_width()).round() as usize;
1390
1391 let clamped_col = min(col, size.columns() - 1);
1392
1393 let row = (pos.y() / size.line_height()).round() as usize;
1394
1395 let clamped_row = min(row, size.screen_lines() - 1);
1396
1397 clamped_row * size.columns() + clamped_col
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 use alacritty_terminal::{
1403 index::{Column, Line, Point},
1404 term::cell::Cell,
1405 };
1406 use gpui::geometry::vector::vec2f;
1407 use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
1408
1409 use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
1410
1411 #[test]
1412 fn test_mouse_to_cell_test() {
1413 let mut rng = thread_rng();
1414 const ITERATIONS: usize = 10;
1415 const PRECISION: usize = 1000;
1416
1417 for _ in 0..ITERATIONS {
1418 let viewport_cells = rng.gen_range(15..20);
1419 let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
1420
1421 let size = crate::TerminalSize {
1422 cell_width: cell_size,
1423 line_height: cell_size,
1424 height: cell_size * (viewport_cells as f32),
1425 width: cell_size * (viewport_cells as f32),
1426 };
1427
1428 let cells = get_cells(size, &mut rng);
1429 let content = convert_cells_to_content(size, &cells);
1430
1431 for row in 0..(viewport_cells - 1) {
1432 let row = row as usize;
1433 for col in 0..(viewport_cells - 1) {
1434 let col = col as usize;
1435
1436 let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1437 let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1438
1439 let mouse_pos = vec2f(
1440 col as f32 * cell_size + col_offset,
1441 row as f32 * cell_size + row_offset,
1442 );
1443
1444 let content_index = content_index_for_mouse(mouse_pos, &content.size);
1445 let mouse_cell = content.cells[content_index].c;
1446 let real_cell = cells[row][col];
1447
1448 assert_eq!(mouse_cell, real_cell);
1449 }
1450 }
1451 }
1452 }
1453
1454 #[test]
1455 fn test_mouse_to_cell_clamp() {
1456 let mut rng = thread_rng();
1457
1458 let size = crate::TerminalSize {
1459 cell_width: 10.,
1460 line_height: 10.,
1461 height: 100.,
1462 width: 100.,
1463 };
1464
1465 let cells = get_cells(size, &mut rng);
1466 let content = convert_cells_to_content(size, &cells);
1467
1468 assert_eq!(
1469 content.cells[content_index_for_mouse(vec2f(-10., -10.), &content.size)].c,
1470 cells[0][0]
1471 );
1472 assert_eq!(
1473 content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content.size)].c,
1474 cells[9][9]
1475 );
1476 }
1477
1478 fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
1479 let mut cells = Vec::new();
1480
1481 for _ in 0..((size.height() / size.line_height()) as usize) {
1482 let mut row_vec = Vec::new();
1483 for _ in 0..((size.width() / size.cell_width()) as usize) {
1484 let cell_char = rng.sample(Alphanumeric) as char;
1485 row_vec.push(cell_char)
1486 }
1487 cells.push(row_vec)
1488 }
1489
1490 cells
1491 }
1492
1493 fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
1494 let mut ic = Vec::new();
1495
1496 for row in 0..cells.len() {
1497 for col in 0..cells[row].len() {
1498 let cell_char = cells[row][col];
1499 ic.push(IndexedCell {
1500 point: Point::new(Line(row as i32), Column(col)),
1501 cell: Cell {
1502 c: cell_char,
1503 ..Default::default()
1504 },
1505 });
1506 }
1507 }
1508
1509 TerminalContent {
1510 cells: ic,
1511 size,
1512 ..Default::default()
1513 }
1514 }
1515}