From 4d8cd6d8eadc8dbfb749b0348086a0e4b61ed8b8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Jul 2022 17:51:15 -0700 Subject: [PATCH 01/22] Added event buffering, need to figure out a proper fix --- crates/terminal/src/model.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs index f1b2dd36cf..481ebce3a4 100644 --- a/crates/terminal/src/model.rs +++ b/crates/terminal/src/model.rs @@ -12,12 +12,9 @@ use alacritty_terminal::{ Term, }; use anyhow::{bail, Result}; -use futures::{ - channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, - StreamExt, -}; +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use settings::{Settings, Shell}; -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; use thiserror::Error; use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; @@ -234,18 +231,34 @@ impl TerminalBuilder { pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { - //Listen for terminal events - while let Some(event) = self.events_rx.next().await { + 'outer: loop { + let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); + + let mut events = vec![]; + + loop { + match self.events_rx.try_next() { + //Have a buffered event + Ok(Some(e)) => events.push(e), + //Ran out of buffered events + Ok(None) => break, + //Channel closed, exit + Err(_) => break 'outer, + } + } + match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - this.process_terminal_event(event, cx); - - cx.notify(); + for event in events { + this.process_terminal_event(event, cx); + } }); } - None => break, + None => break 'outer, } + + delay.await; } }) .detach(); From 889720d06d33cef3b1cc66141fc412cd40a332be Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 17:24:21 -0700 Subject: [PATCH 02/22] Fixed conflict --- crates/terminal/src/connected_el.rs | 73 +- crates/terminal/src/connected_view.rs | 5 +- .../terminal/src/{modal_view.rs => modal.rs} | 3 +- crates/terminal/src/model.rs | 535 ---------- crates/terminal/src/terminal.rs | 987 ++++++++++-------- crates/terminal/src/terminal_tab.rs | 490 +++++++++ .../src/tests/terminal_test_context.rs | 17 +- 7 files changed, 1059 insertions(+), 1051 deletions(-) rename crates/terminal/src/{modal_view.rs => modal.rs} (96%) delete mode 100644 crates/terminal/src/model.rs create mode 100644 crates/terminal/src/terminal_tab.rs diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 865bbc7de7..f14e2f14b1 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -1,6 +1,5 @@ use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, - event::WindowSize, grid::{Dimensions, GridIterator, Indexed, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, selection::SelectionRange, @@ -29,7 +28,9 @@ use util::ResultExt; use std::{cmp::min, ops::Range}; use std::{fmt::Debug, ops::Sub}; -use crate::{mappings::colors::convert_color, model::Terminal, ConnectedView}; +use crate::{ + connected_view::ConnectedView, mappings::colors::convert_color, TermDimensions, Terminal, +}; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -70,74 +71,6 @@ impl DisplayCursor { } } -#[derive(Clone, Copy, Debug)] -pub struct TermDimensions { - cell_width: f32, - line_height: f32, - height: f32, - width: f32, -} - -impl TermDimensions { - pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { - TermDimensions { - cell_width, - line_height, - width: size.x(), - height: size.y(), - } - } - - pub fn num_lines(&self) -> usize { - (self.height / self.line_height).floor() as usize - } - - pub fn num_columns(&self) -> usize { - (self.width / self.cell_width).floor() as usize - } - - pub fn height(&self) -> f32 { - self.height - } - - pub fn width(&self) -> f32 { - self.width - } - - pub fn cell_width(&self) -> f32 { - self.cell_width - } - - pub fn line_height(&self) -> f32 { - self.line_height - } -} - -impl Into for TermDimensions { - fn into(self) -> WindowSize { - WindowSize { - num_lines: self.num_lines() as u16, - num_cols: self.num_columns() as u16, - cell_width: self.cell_width() as u16, - cell_height: self.line_height() as u16, - } - } -} - -impl Dimensions for TermDimensions { - fn total_lines(&self) -> usize { - self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... - } - - fn screen_lines(&self) -> usize { - self.num_lines() - } - - fn columns(&self) -> usize { - self.num_columns() - } -} - #[derive(Clone, Debug, Default)] struct LayoutCell { point: Point, diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 187b018249..1503c497a2 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -3,10 +3,7 @@ use gpui::{ MutableAppContext, View, ViewContext, }; -use crate::{ - connected_el::TerminalEl, - model::{Event, Terminal}, -}; +use crate::{connected_el::TerminalEl, Event, Terminal}; ///Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] diff --git a/crates/terminal/src/modal_view.rs b/crates/terminal/src/modal.rs similarity index 96% rename from crates/terminal/src/modal_view.rs rename to crates/terminal/src/modal.rs index ec5280befc..a238f4cbc1 100644 --- a/crates/terminal/src/modal_view.rs +++ b/crates/terminal/src/modal.rs @@ -2,7 +2,8 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; use crate::{ - get_working_directory, model::Terminal, DeployModal, Event, TerminalContent, TerminalView, + terminal_tab::{get_working_directory, DeployModal, TerminalContent, TerminalView}, + Event, Terminal, }; #[derive(Debug)] diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs deleted file mode 100644 index 481ebce3a4..0000000000 --- a/crates/terminal/src/model.rs +++ /dev/null @@ -1,535 +0,0 @@ -use alacritty_terminal::{ - ansi::{ClearMode, Handler}, - config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, - grid::Scroll, - index::{Direction, Point}, - selection::{Selection, SelectionType}, - sync::FairMutex, - term::{test::TermSize, RenderableContent, TermMode}, - tty::{self, setup_env}, - Term, -}; -use anyhow::{bail, Result}; -use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use settings::{Settings, Shell}; -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; -use thiserror::Error; - -use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; - -use crate::{ - connected_el::TermDimensions, - mappings::{ - colors::{get_color_at_index, to_alac_rgb}, - keys::to_esc_str, - }, -}; - -const DEFAULT_TITLE: &str = "Terminal"; - -///Upward flowing events, for changing the title and such -#[derive(Copy, Clone, Debug)] -pub enum Event { - TitleChanged, - CloseTerminal, - Activate, - Wakeup, - Bell, - KeyInput, -} - -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(UnboundedSender); - -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); - } -} - -#[derive(Error, Debug)] -pub struct TerminalError { - pub directory: Option, - pub shell: Option, - pub source: std::io::Error, -} - -impl TerminalError { - pub fn fmt_directory(&self) -> String { - self.directory - .clone() - .map(|path| { - match path - .into_os_string() - .into_string() - .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) - { - Ok(s) => s, - Err(s) => s, - } - }) - .unwrap_or_else(|| { - let default_dir = - dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); - match default_dir { - Some(dir) => format!(" {}", dir), - None => "".to_string(), - } - }) - } - - pub fn shell_to_string(&self) -> Option { - self.shell.as_ref().map(|shell| match shell { - Shell::System => "".to_string(), - Shell::Program(p) => p.to_string(), - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - } - - pub fn fmt_shell(&self) -> String { - self.shell - .clone() - .map(|shell| match shell { - Shell::System => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - - match pw { - Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), - } - } - Shell::Program(s) => s, - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - .unwrap_or_else(|| { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - match pw { - Some(pw) => { - format!(" {}", pw.shell) - } - None => " {}".to_string(), - } - }) - } -} - -impl Display for TerminalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let dir_string: String = self.fmt_directory(); - - let shell = self.fmt_shell(); - - write!( - f, - "Working directory: {} Shell command: `{}`, IOError: {}", - dir_string, shell, self.source - ) - } -} - -pub struct TerminalBuilder { - terminal: Terminal, - events_rx: UnboundedReceiver, -} - -impl TerminalBuilder { - pub fn new( - working_directory: Option, - shell: Option, - env: Option>, - initial_size: TermDimensions, - ) -> Result { - let pty_config = { - let alac_shell = shell.clone().and_then(|shell| match shell { - Shell::System => None, - Shell::Program(program) => Some(Program::Just(program)), - Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), - }); - - PtyConfig { - shell: alac_shell, - working_directory: working_directory.clone(), - hold: false, - } - }; - - let mut env = env.unwrap_or_else(|| HashMap::new()); - - //TODO: Properly set the current locale, - env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); - - let config = Config { - pty_config: pty_config.clone(), - env, - ..Default::default() - }; - - setup_env(&config); - - //Spawn a task so the Alacritty EventLoop can communicate with us in a view context - let (events_tx, events_rx) = unbounded(); - - //Set up the terminal... - let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); - let term = Arc::new(FairMutex::new(term)); - - //Setup the pty... - let pty = match tty::new(&pty_config, initial_size.into(), None) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - shell, - source: error, - }); - } - }; - - let shell_txt = { - match shell { - Some(Shell::System) | None => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); - pw.shell.to_string() - } - Some(Shell::Program(program)) => program, - Some(Shell::WithArguments { program, args }) => { - format!("{} {}", program, args.join(" ")) - } - } - }; - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx.clone()), - pty, - pty_config.hold, - false, - ); - - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); - - let terminal = Terminal { - pty_tx: Notifier(pty_tx), - term, - title: shell_txt.to_string(), - }; - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - } - - pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { - cx.spawn_weak(|this, mut cx| async move { - 'outer: loop { - let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); - - let mut events = vec![]; - - loop { - match self.events_rx.try_next() { - //Have a buffered event - Ok(Some(e)) => events.push(e), - //Ran out of buffered events - Ok(None) => break, - //Channel closed, exit - Err(_) => break 'outer, - } - } - - match this.upgrade(&cx) { - Some(this) => { - this.update(&mut cx, |this, cx| { - for event in events { - this.process_terminal_event(event, cx); - } - }); - } - None => break 'outer, - } - - delay.await; - } - }) - .detach(); - - self.terminal - } -} - -pub struct Terminal { - pty_tx: Notifier, - term: Arc>>, - pub title: String, -} - -impl Terminal { - ///Takes events from Alacritty and translates them to behavior on this view - fn process_terminal_event( - &mut self, - event: alacritty_terminal::event::Event, - cx: &mut ModelContext, - ) { - match event { - // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title; - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = DEFAULT_TITLE.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), - AlacTermEvent::ColorRequest(index, format) => { - let color = self.term.lock().colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; - to_alac_rgb(get_color_at_index(&index, &term_style.colors)) - }); - self.write_to_pty(format(color)) - } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), - } - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - pub fn write_to_pty(&self, input: String) { - self.write_bytes_to_pty(input.into_bytes()); - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - fn write_bytes_to_pty(&self, input: Vec) { - self.term.lock().scroll_display(Scroll::Bottom); - self.pty_tx.notify(input); - } - - ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&self, new_size: WindowSize) { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - - let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); - self.term.lock().resize(term_size); - } - - pub fn clear(&self) { - self.write_to_pty("\x0c".into()); - self.term.lock().clear_screen(ClearMode::Saved); - } - - pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { - let guard = self.term.lock(); - let mode = guard.mode(); - let esc = to_esc_str(keystroke, mode); - drop(guard); - if esc.is_some() { - self.write_to_pty(esc.unwrap()); - true - } else { - false - } - } - - ///Paste text into the terminal - pub fn paste(&self, text: &str) { - if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { - self.write_to_pty("\x1b[200~".to_string()); - self.write_to_pty(text.replace('\x1b', "").to_string()); - self.write_to_pty("\x1b[201~".to_string()); - } else { - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } - } - - pub fn copy(&self) -> Option { - let term = self.term.lock(); - term.selection_to_string() - } - - ///Takes the selection out of the terminal - pub fn take_selection(&self) -> Option { - self.term.lock().selection.take() - } - ///Sets the selection object on the terminal - pub fn set_selection(&self, sel: Option) { - self.term.lock().selection = sel; - } - - pub fn render_lock(&self, new_size: Option, f: F) -> T - where - F: FnOnce(RenderableContent, char) -> T, - { - if let Some(new_size) = new_size { - self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size - //TODO: Is this bad for performance? - } - - let mut term = self.term.lock(); //Lock - - if let Some(new_size) = new_size { - term.resize(new_size); //Reflow - } - - let content = term.renderable_content(); - let cursor_text = term.grid()[content.cursor.point].c; - - f(content, cursor_text) - } - - pub fn get_display_offset(&self) -> usize { - self.term.lock().renderable_content().display_offset - } - - ///Scroll the terminal - pub fn scroll(&self, scroll: Scroll) { - self.term.lock().scroll_display(scroll) - } - - pub fn click(&self, point: Point, side: Direction, clicks: usize) { - let selection_type = match clicks { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; - - let selection = - selection_type.map(|selection_type| Selection::new(selection_type, point, side)); - - self.set_selection(selection); - } - - pub fn drag(&self, point: Point, side: Direction) { - if let Some(mut selection) = self.take_selection() { - selection.update(point, side); - self.set_selection(Some(selection)); - } - } - - pub fn mouse_down(&self, point: Point, side: Direction) { - self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); - } -} - -impl Drop for Terminal { - fn drop(&mut self) { - self.pty_tx.0.send(Msg::Shutdown).ok(); - } -} - -impl Entity for Terminal { - type Event = Event; -} - -//TODO Move this around -mod alacritty_unix { - use alacritty_terminal::config::Program; - use gpui::anyhow::{bail, Result}; - use libc; - use std::ffi::CStr; - use std::mem::MaybeUninit; - use std::ptr; - - #[derive(Debug)] - pub struct Passwd<'a> { - _name: &'a str, - _dir: &'a str, - pub shell: &'a str, - } - - /// Return a Passwd struct with pointers into the provided buf. - /// - /// # Unsafety - /// - /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. - pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { - // Create zeroed passwd struct. - let mut entry: MaybeUninit = MaybeUninit::uninit(); - - let mut res: *mut libc::passwd = ptr::null_mut(); - - // Try and read the pw file. - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - entry.as_mut_ptr(), - buf.as_mut_ptr() as *mut _, - buf.len(), - &mut res, - ) - }; - let entry = unsafe { entry.assume_init() }; - - if status < 0 { - bail!("getpwuid_r failed"); - } - - if res.is_null() { - bail!("pw not found"); - } - - // Sanity check. - assert_eq!(entry.pw_uid, uid); - - // Build a borrowed Passwd struct. - Ok(Passwd { - _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, - _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, - shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, - }) - } - - #[cfg(target_os = "macos")] - pub fn _default_shell(pw: &Passwd<'_>) -> Program { - let shell_name = pw.shell.rsplit('/').next().unwrap(); - let argv = vec![ - String::from("-c"), - format!("exec -a -{} {}", shell_name, pw.shell), - ]; - - Program::WithArgs { - program: "/bin/bash".to_owned(), - args: argv, - } - } - - #[cfg(not(target_os = "macos"))] - pub fn default_shell(pw: &Passwd<'_>) -> Program { - Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) - } -} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4e5829559e..7062b04688 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,33 +1,40 @@ pub mod connected_el; pub mod connected_view; pub mod mappings; -pub mod modal_view; -pub mod model; +pub mod modal; +pub mod terminal_tab; -use connected_view::ConnectedView; -use dirs::home_dir; -use gpui::{ - actions, elements::*, geometry::vector::vec2f, AnyViewHandle, AppContext, Entity, ModelHandle, - MutableAppContext, View, ViewContext, ViewHandle, +use alacritty_terminal::{ + ansi::{ClearMode, Handler}, + config::{Config, Program, PtyConfig}, + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, + event_loop::{EventLoop, Msg, Notifier}, + grid::{Dimensions, Scroll}, + index::{Direction, Point}, + selection::{Selection, SelectionType}, + sync::FairMutex, + term::{test::TermSize, RenderableContent, TermMode}, + tty::{self, setup_env}, + Term, }; -use modal_view::deploy_modal; -use model::{Event, Terminal, TerminalBuilder, TerminalError}; +use anyhow::{bail, Result}; +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use modal::deploy_modal; +use settings::{Settings, Shell}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; +use terminal_tab::TerminalView; +use thiserror::Error; -use connected_el::TermDimensions; -use project::{LocalWorktree, Project, ProjectPath}; -use settings::{Settings, WorkingDirectory}; -use smallvec::SmallVec; -use std::path::{Path, PathBuf}; -use workspace::{Item, Workspace}; +use gpui::{ + geometry::vector::{vec2f, Vector2F}, + keymap::Keystroke, + ClipboardItem, CursorStyle, Entity, ModelContext, MutableAppContext, +}; -use crate::connected_el::TerminalEl; - -const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. -const DEBUG_TERMINAL_HEIGHT: f32 = 200.; -const DEBUG_CELL_WIDTH: f32 = 5.; -const DEBUG_LINE_HEIGHT: f32 = 5.; - -actions!(terminal, [Deploy, DeployModal]); +use crate::mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, +}; ///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { @@ -37,359 +44,523 @@ pub fn init(cx: &mut MutableAppContext) { connected_view::init(cx); } -//Make terminal view an enum, that can give you views for the error and non-error states -//Take away all the result unwrapping in the current TerminalView by making it 'infallible' -//Bubble up to deploy(_modal)() calls +const DEFAULT_TITLE: &str = "Terminal"; +const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. +const DEBUG_TERMINAL_HEIGHT: f32 = 200.; +const DEBUG_CELL_WIDTH: f32 = 5.; +const DEBUG_LINE_HEIGHT: f32 = 5.; -enum TerminalContent { - Connected(ViewHandle), - Error(ViewHandle), +///Upward flowing events, for changing the title and such +#[derive(Copy, Clone, Debug)] +pub enum Event { + TitleChanged, + CloseTerminal, + Activate, + Wakeup, + Bell, + KeyInput, } -impl TerminalContent { - fn handle(&self) -> AnyViewHandle { - match self { - Self::Connected(handle) => handle.into(), - Self::Error(handle) => handle.into(), +///A translation struct for Alacritty to communicate with us from their event loop +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(event).ok(); + } +} + +#[derive(Clone, Copy, Debug)] +pub struct TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, +} + +impl TermDimensions { + pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { + TermDimensions { + cell_width, + line_height, + width: size.x(), + height: size.y(), } } -} -pub struct TerminalView { - modal: bool, - content: TerminalContent, - associated_directory: Option, -} - -pub struct ErrorView { - error: TerminalError, -} - -impl Entity for TerminalView { - type Event = Event; -} - -impl Entity for ConnectedView { - type Event = Event; -} - -impl Entity for ErrorView { - type Event = Event; -} - -impl TerminalView { - ///Create a new Terminal in the current working directory or the user's home directory - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let working_directory = get_working_directory(workspace, cx); - let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); - workspace.add_item(Box::new(view), cx); + pub fn num_lines(&self) -> usize { + (self.height / self.line_height).floor() as usize } - ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices - ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - fn new(working_directory: Option, modal: bool, cx: &mut ViewContext) -> Self { - //The details here don't matter, the terminal will be resized on the first layout - let size_info = TermDimensions::new( + pub fn num_columns(&self) -> usize { + (self.width / self.cell_width).floor() as usize + } + + pub fn height(&self) -> f32 { + self.height + } + + pub fn width(&self) -> f32 { + self.width + } + + pub fn cell_width(&self) -> f32 { + self.cell_width + } + + pub fn line_height(&self) -> f32 { + self.line_height + } +} +impl Default for TermDimensions { + fn default() -> Self { + TermDimensions::new( DEBUG_LINE_HEIGHT, DEBUG_CELL_WIDTH, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), + ) + } +} + +impl Into for TermDimensions { + fn into(self) -> WindowSize { + WindowSize { + num_lines: self.num_lines() as u16, + num_cols: self.num_columns() as u16, + cell_width: self.cell_width() as u16, + cell_height: self.line_height() as u16, + } + } +} + +impl Dimensions for TermDimensions { + fn total_lines(&self) -> usize { + self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... + } + + fn screen_lines(&self) -> usize { + self.num_lines() + } + + fn columns(&self) -> usize { + self.num_columns() + } +} + +#[derive(Error, Debug)] +pub struct TerminalError { + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, +} + +impl TerminalError { + pub fn fmt_directory(&self) -> String { + self.directory + .clone() + .map(|path| { + match path + .into_os_string() + .into_string() + .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) + { + Ok(s) => s, + Err(s) => s, + } + }) + .unwrap_or_else(|| { + let default_dir = + dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); + match default_dir { + Some(dir) => format!(" {}", dir), + None => "".to_string(), + } + }) + } + + pub fn shell_to_string(&self) -> Option { + self.shell.as_ref().map(|shell| match shell { + Shell::System => "".to_string(), + Shell::Program(p) => p.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + } + + pub fn fmt_shell(&self) -> String { + self.shell + .clone() + .map(|shell| match shell { + Shell::System => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + + match pw { + Some(pw) => format!(" {}", pw.shell), + None => "".to_string(), + } + } + Shell::Program(s) => s, + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + .unwrap_or_else(|| { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + match pw { + Some(pw) => { + format!(" {}", pw.shell) + } + None => " {}".to_string(), + } + }) + } +} + +impl Display for TerminalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dir_string: String = self.fmt_directory(); + + let shell = self.fmt_shell(); + + write!( + f, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} + +pub struct TerminalBuilder { + terminal: Terminal, + events_rx: UnboundedReceiver, +} + +impl TerminalBuilder { + pub fn new( + working_directory: Option, + shell: Option, + env: Option>, + initial_size: TermDimensions, + ) -> Result { + let pty_config = { + let alac_shell = shell.clone().and_then(|shell| match shell { + Shell::System => None, + Shell::Program(program) => Some(Program::Just(program)), + Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), + }); + + PtyConfig { + shell: alac_shell, + working_directory: working_directory.clone(), + hold: false, + } + }; + + let mut env = env.unwrap_or_else(|| HashMap::new()); + + //TODO: Properly set the current locale, + env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); + + let config = Config { + pty_config: pty_config.clone(), + env, + ..Default::default() + }; + + setup_env(&config); + + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context + let (events_tx, events_rx) = unbounded(); + + //Set up the terminal... + let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); + let term = Arc::new(FairMutex::new(term)); + + //Setup the pty... + let pty = match tty::new(&pty_config, initial_size.into(), None) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + shell, + source: error, + }); + } + }; + + let shell_txt = { + match shell { + Some(Shell::System) | None => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); + pw.shell.to_string() + } + Some(Shell::Program(program)) => program, + Some(Shell::WithArguments { program, args }) => { + format!("{} {}", program, args.join(" ")) + } + } + }; + + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, ); - let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); - let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) - { - Ok(terminal) => { - let terminal = cx.add_model(|cx| terminal.subscribe(cx)); - let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); - cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) - .detach(); - TerminalContent::Connected(view) + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: shell_txt.to_string(), + }; + + Ok(TerminalBuilder { + terminal, + events_rx, + }) + } + + pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { + cx.spawn_weak(|this, mut cx| async move { + 'outer: loop { + //Even as low as 45 locks zed up + let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); + + let mut events = vec![]; + + loop { + match self.events_rx.try_next() { + //Have a buffered event + Ok(Some(e)) => events.push(e), + //Channel closed, exit + Ok(None) => break 'outer, + //Ran out of buffered events + Err(_) => break, + } + } + + match this.upgrade(&cx) { + Some(this) => { + this.update(&mut cx, |this, cx| { + for event in events { + this.process_terminal_event(event, cx); + } + }); + } + None => break 'outer, + } + + delay.await; } - Err(error) => { - let view = cx.add_view(|_| ErrorView { - error: error.downcast::().unwrap(), + }) + .detach(); + + self.terminal + } +} + +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + pub title: String, +} + +impl Terminal { + ///Takes events from Alacritty and translates them to behavior on this view + fn process_terminal_event( + &mut self, + event: alacritty_terminal::event::Event, + cx: &mut ModelContext, + ) { + match event { + // TODO: Handle is_self_focused in subscription on terminal view + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO: alacritty/src/input.rs:L922-L939 + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + AlacTermEvent::ColorRequest(index, format) => { + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(&index, &term_style.colors)) }); - TerminalContent::Error(view) + self.write_to_pty(format(color)) } - }; - cx.focus(content.handle()); - - TerminalView { - modal, - content, - associated_directory: working_directory, + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), } } - fn from_terminal( - terminal: ModelHandle, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); - TerminalView { - modal, - content: TerminalContent::Connected(connected_view), - associated_directory: None, - } - } -} - -impl View for TerminalView { - fn ui_name() -> &'static str { - "Terminal View" + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + pub fn write_to_pty(&self, input: String) { + self.write_bytes_to_pty(input.into_bytes()); } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let child_view = match &self.content { - TerminalContent::Connected(connected) => ChildView::new(connected), - TerminalContent::Error(error) => ChildView::new(error), - }; - - if self.modal { - let settings = cx.global::(); - let container_style = settings.theme.terminal.modal_container; - child_view.contained().with_style(container_style).boxed() - } else { - child_view.boxed() - } + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + fn write_bytes_to_pty(&self, input: Vec) { + self.term.lock().scroll_display(Scroll::Bottom); + self.pty_tx.notify(input); } - fn on_focus(&mut self, cx: &mut ViewContext) { - cx.emit(Event::Activate); - cx.defer(|view, cx| { - cx.focus(view.content.handle()); - }); + ///Resize the terminal and the PTY. This locks the terminal. + pub fn set_size(&self, new_size: WindowSize) { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + + let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); + self.term.lock().resize(term_size); } - fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { - let mut context = Self::default_keymap_context(); - if self.modal { - context.set.insert("ModalTerminal".into()); - } - context - } -} - -impl View for ErrorView { - fn ui_name() -> &'static str { - "DisconnectedTerminal" + pub fn clear(&self) { + self.write_to_pty("\x0c".into()); + self.term.lock().clear_screen(ClearMode::Saved); } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - let style = TerminalEl::make_text_style(cx.font_cache(), settings); - - //TODO: - //We want markdown style highlighting so we can format the program and working directory with `` - //We want a max-width of 75% with word-wrap - //We want to be able to select the text - //Want to be able to scroll if the error message is massive somehow (resiliency) - - let program_text = { - match self.error.shell_to_string() { - Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), - None => "No program specified".to_string(), - } - }; - - let directory_text = { - match self.error.directory.as_ref() { - Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), - None => "No working directory specified".to_string(), - } - }; - - let error_text = self.error.source.to_string(); - - Flex::column() - .with_child( - Text::new("Failed to open the terminal.".to_string(), style.clone()) - .contained() - .boxed(), - ) - .with_child(Text::new(program_text, style.clone()).contained().boxed()) - .with_child(Text::new(directory_text, style.clone()).contained().boxed()) - .with_child(Text::new(error_text, style.clone()).contained().boxed()) - .aligned() - .boxed() - } -} - -impl Item for TerminalView { - fn tab_content( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &gpui::AppContext, - ) -> ElementBox { - let title = match &self.content { - TerminalContent::Connected(connected) => { - connected.read(cx).handle().read(cx).title.clone() - } - TerminalContent::Error(_) => "Terminal".to_string(), - }; - - Flex::row() - .with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() - } - - fn clone_on_split(&self, cx: &mut ViewContext) -> Option { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - Some(TerminalView::new( - self.associated_directory.clone(), - false, - cx, - )) - } - - fn project_path(&self, _cx: &gpui::AppContext) -> Option { - None - } - - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } - - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn save( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); - } - - fn save_as( - &mut self, - _project: gpui::ModelHandle, - _abs_path: std::path::PathBuf, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save_as should not have been called"); - } - - fn reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) - } - - fn is_dirty(&self, cx: &gpui::AppContext) -> bool { - if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content() + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + let guard = self.term.lock(); + let mode = guard.mode(); + let esc = to_esc_str(keystroke, mode); + drop(guard); + if esc.is_some() { + self.write_to_pty(esc.unwrap()); + true } else { false } } - fn has_conflict(&self, cx: &AppContext) -> bool { - if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_bell() + ///Paste text into the terminal + pub fn paste(&self, text: &str) { + if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { + self.write_to_pty("\x1b[200~".to_string()); + self.write_to_pty(text.replace('\x1b', "").to_string()); + self.write_to_pty("\x1b[201~".to_string()); } else { - false + self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); } } - fn should_update_tab_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::TitleChanged) + pub fn copy(&self) -> Option { + let term = self.term.lock(); + term.selection_to_string() } - fn should_close_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::CloseTerminal) + ///Takes the selection out of the terminal + pub fn take_selection(&self) -> Option { + self.term.lock().selection.take() + } + ///Sets the selection object on the terminal + pub fn set_selection(&self, sel: Option) { + self.term.lock().selection = sel; } - fn should_activate_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::Activate) - } -} - -///Get's the working directory for the given workspace, respecting the user's settings. -fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let wd_setting = cx - .global::() - .terminal_overrides - .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); - let res = match wd_setting { - WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) + pub fn render_lock(&self, new_size: Option, f: F) -> T + where + F: FnOnce(RenderableContent, char) -> T, + { + if let Some(new_size) = new_size { + self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size + //TODO: Is this bad for performance? } - }; - res.or_else(|| home_dir()) + + let mut term = self.term.lock(); //Lock + + if let Some(new_size) = new_size { + term.resize(new_size); //Reflow + } + + let content = term.renderable_content(); + let cursor_text = term.grid()[content.cursor.point].c; + + f(content, cursor_text) + } + + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset + } + + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) + } + + pub fn click(&self, point: Point, side: Direction, clicks: usize) { + let selection_type = match clicks { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + self.set_selection(selection); + } + + pub fn drag(&self, point: Point, side: Direction) { + if let Some(mut selection) = self.take_selection() { + selection.update(point, side); + self.set_selection(Some(selection)); + } + } + + pub fn mouse_down(&self, point: Point, side: Direction) { + self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + } } -///Get's the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - workspace - .worktrees(cx) - .next() - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) +impl Drop for Terminal { + fn drop(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } } -///Gets the intuitively correct working directory from the given workspace -///If there is an active entry for this project, returns that entry's worktree root. -///If there's no active entry but there is a worktree, returns that worktrees root. -///If either of these roots are files, or if there are any other query failures, -/// returns the user's home directory -fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let project = workspace.project().read(cx); - - project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| workspace.worktrees(cx).next()) - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -fn get_path_from_wt(wt: &LocalWorktree) -> Option { - wt.root_entry() - .filter(|re| re.is_dir()) - .map(|_| wt.abs_path().to_path_buf()) +impl Entity for Terminal { + type Event = Event; } #[cfg(test)] mod tests { + pub mod terminal_test_context; - use crate::tests::terminal_test_context::TerminalTestContext; - - use super::*; use gpui::TestAppContext; - use std::path::Path; - - mod terminal_test_context; + use crate::tests::terminal_test_context::TerminalTestContext; ///Basic integration test, can we get the terminal to show up, execute a command, //and produce noticable output? @@ -400,123 +571,83 @@ mod tests { cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7")) .await; } +} - ///Working directory calculation tests +//TODO Move this around and clean up the code +mod alacritty_unix { + use alacritty_terminal::config::Program; + use gpui::anyhow::{bail, Result}; + use libc; + use std::ffi::CStr; + use std::mem::MaybeUninit; + use std::ptr; - ///No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - //Test - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); + #[derive(Debug)] + pub struct Passwd<'a> { + _name: &'a str, + _dir: &'a str, + pub shell: &'a str, } - ///No active entry, but a worktree, worktree is a file -> home_dir() - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables + /// Return a Passwd struct with pointers into the provided buf. + /// + /// # Unsafety + /// + /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. + pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { + // Create zeroed passwd struct. + let mut entry: MaybeUninit = MaybeUninit::uninit(); - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - cx.create_file_wt(project.clone(), "/root.txt").await; + let mut res: *mut libc::passwd = ptr::null_mut(); - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r( + uid, + entry.as_mut_ptr(), + buf.as_mut_ptr() as *mut _, + buf.len(), + &mut res, + ) + }; + let entry = unsafe { entry.assume_init() }; - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); + if status < 0 { + bail!("getpwuid_r failed"); + } - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); + if res.is_null() { + bail!("pw not found"); + } + + // Sanity check. + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct. + Ok(Passwd { + _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + }) } - //No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; + #[cfg(target_os = "macos")] + pub fn _default_shell(pw: &Passwd<'_>) -> Program { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![ + String::from("-c"), + format!("exec -a -{} {}", shell_name, pw.shell), + ]; - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); + Program::WithArgs { + program: "/bin/bash".to_owned(), + args: argv, + } } - //Active entry with a work tree, worktree is a file -> home_dir() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - //Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); + #[cfg(not(target_os = "macos"))] + pub fn default_shell(pw: &Passwd<'_>) -> Program { + Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) } } diff --git a/crates/terminal/src/terminal_tab.rs b/crates/terminal/src/terminal_tab.rs new file mode 100644 index 0000000000..69bac7df1d --- /dev/null +++ b/crates/terminal/src/terminal_tab.rs @@ -0,0 +1,490 @@ +use crate::connected_view::ConnectedView; +use crate::{Event, Terminal, TerminalBuilder, TerminalError}; +use dirs::home_dir; +use gpui::{ + actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, + ViewHandle, +}; + +use crate::TermDimensions; +use project::{LocalWorktree, Project, ProjectPath}; +use settings::{Settings, WorkingDirectory}; +use smallvec::SmallVec; +use std::path::{Path, PathBuf}; +use workspace::{Item, Workspace}; + +use crate::connected_el::TerminalEl; + +actions!(terminal, [Deploy, DeployModal]); + +//Make terminal view an enum, that can give you views for the error and non-error states +//Take away all the result unwrapping in the current TerminalView by making it 'infallible' +//Bubble up to deploy(_modal)() calls + +pub enum TerminalContent { + Connected(ViewHandle), + Error(ViewHandle), +} + +impl TerminalContent { + fn handle(&self) -> AnyViewHandle { + match self { + Self::Connected(handle) => handle.into(), + Self::Error(handle) => handle.into(), + } + } +} + +pub struct TerminalView { + modal: bool, + pub content: TerminalContent, + associated_directory: Option, +} + +pub struct ErrorView { + error: TerminalError, +} + +impl Entity for TerminalView { + type Event = Event; +} + +impl Entity for ConnectedView { + type Event = Event; +} + +impl Entity for ErrorView { + type Event = Event; +} + +impl TerminalView { + ///Create a new Terminal in the current working directory or the user's home directory + pub fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices + ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` + pub fn new( + working_directory: Option, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + //The details here don't matter, the terminal will be resized on the first layout + let size_info = TermDimensions::default(); + + let settings = cx.global::(); + let shell = settings.terminal_overrides.shell.clone(); + let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. + + let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) + { + Ok(terminal) => { + let terminal = cx.add_model(|cx| terminal.subscribe(cx)); + let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) + .detach(); + TerminalContent::Connected(view) + } + Err(error) => { + let view = cx.add_view(|_| ErrorView { + error: error.downcast::().unwrap(), + }); + TerminalContent::Error(view) + } + }; + cx.focus(content.handle()); + + TerminalView { + modal, + content, + associated_directory: working_directory, + } + } + + pub fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + TerminalView { + modal, + content: TerminalContent::Connected(connected_view), + associated_directory: None, + } + } +} + +impl View for TerminalView { + fn ui_name() -> &'static str { + "Terminal" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let child_view = match &self.content { + TerminalContent::Connected(connected) => ChildView::new(connected), + TerminalContent::Error(error) => ChildView::new(error), + }; + + if self.modal { + let settings = cx.global::(); + let container_style = settings.theme.terminal.modal_container; + child_view.contained().with_style(container_style).boxed() + } else { + child_view.boxed() + } + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Activate); + cx.defer(|view, cx| { + cx.focus(view.content.handle()); + }); + } + + fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { + let mut context = Self::default_keymap_context(); + if self.modal { + context.set.insert("ModalTerminal".into()); + } + context + } +} + +impl View for ErrorView { + fn ui_name() -> &'static str { + "Terminal Error" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + let style = TerminalEl::make_text_style(cx.font_cache(), settings); + + //TODO: + //We want markdown style highlighting so we can format the program and working directory with `` + //We want a max-width of 75% with word-wrap + //We want to be able to select the text + //Want to be able to scroll if the error message is massive somehow (resiliency) + + let program_text = { + match self.error.shell_to_string() { + Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), + None => "No program specified".to_string(), + } + }; + + let directory_text = { + match self.error.directory.as_ref() { + Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), + None => "No working directory specified".to_string(), + } + }; + + let error_text = self.error.source.to_string(); + + Flex::column() + .with_child( + Text::new("Failed to open the terminal.".to_string(), style.clone()) + .contained() + .boxed(), + ) + .with_child(Text::new(program_text, style.clone()).contained().boxed()) + .with_child(Text::new(directory_text, style.clone()).contained().boxed()) + .with_child(Text::new(error_text, style.clone()).contained().boxed()) + .aligned() + .boxed() + } +} + +impl Item for TerminalView { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { + let title = match &self.content { + TerminalContent::Connected(connected) => { + connected.read(cx).handle().read(cx).title.clone() + } + TerminalContent::Error(_) => "Terminal".to_string(), + }; + + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option { + //From what I can tell, there's no way to tell the current working + //Directory of the terminal from outside the shell. There might be + //solutions to this, but they are non-trivial and require more IPC + Some(TerminalView::new( + self.associated_directory.clone(), + false, + cx, + )) + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, cx: &gpui::AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_new_content() + } else { + false + } + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_bell() + } else { + false + } + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::TitleChanged) + } + + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::CloseTerminal) + } + + fn should_activate_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::Activate) + } +} + +///Get's the working directory for the given workspace, respecting the user's settings. +pub fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let wd_setting = cx + .global::() + .terminal_overrides + .working_directory + .clone() + .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + let res = match wd_setting { + WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx), + WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), + WorkingDirectory::AlwaysHome => None, + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } + }; + res.or_else(|| home_dir()) +} + +///Get's the first project's home directory, or the home directory +fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + workspace + .worktrees(cx) + .next() + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +///Gets the intuitively correct working directory from the given workspace +///If there is an active entry for this project, returns that entry's worktree root. +///If there's no active entry but there is a worktree, returns that worktrees root. +///If either of these roots are files, or if there are any other query failures, +/// returns the user's home directory +fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let project = workspace.project().read(cx); + + project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .or_else(|| workspace.worktrees(cx).next()) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +fn get_path_from_wt(wt: &LocalWorktree) -> Option { + wt.root_entry() + .filter(|re| re.is_dir()) + .map(|_| wt.abs_path().to_path_buf()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use gpui::TestAppContext; + + use std::path::Path; + + use crate::tests::terminal_test_context::TerminalTestContext; + + ///Working directory calculation tests + + ///No Worktrees in project -> home_dir() + #[gpui::test] + async fn no_worktree(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + //Test + cx.cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_none()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + ///No active entry, but a worktree, worktree is a file -> home_dir() + #[gpui::test] + async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + cx.create_file_wt(project.clone(), "/root.txt").await; + + cx.cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + //No active entry, but a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + }); + } + + //Active entry with a work tree, worktree is a file -> home_dir() + #[gpui::test] + async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + //Active entry, with a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } +} diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index e78939224b..06d45a5146 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -1,17 +1,12 @@ use std::{path::Path, time::Duration}; -use gpui::{ - geometry::vector::vec2f, AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle, -}; +use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext, ViewHandle}; + use itertools::Itertools; use project::{Entry, Project, ProjectPath, Worktree}; use workspace::{AppState, Workspace}; -use crate::{ - connected_el::TermDimensions, - model::{Terminal, TerminalBuilder}, - DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT, DEBUG_TERMINAL_WIDTH, -}; +use crate::{TermDimensions, Terminal, TerminalBuilder}; pub struct TerminalTestContext<'a> { pub cx: &'a mut TestAppContext, @@ -22,11 +17,7 @@ impl<'a> TerminalTestContext<'a> { pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = TermDimensions::new( - DEBUG_CELL_WIDTH, - DEBUG_LINE_HEIGHT, - vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), - ); + let size_info = TermDimensions::default(); let connection = term.then(|| { cx.add_model(|cx| { From 27e76e3ca2e6f13d7a53d78adab5021daf423475 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 12:34:18 -0700 Subject: [PATCH 03/22] Retouched a test, should still be failing --- crates/terminal/src/terminal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7062b04688..207ed6a69f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -45,8 +45,8 @@ pub fn init(cx: &mut MutableAppContext) { } const DEFAULT_TITLE: &str = "Terminal"; -const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. -const DEBUG_TERMINAL_HEIGHT: f32 = 200.; +const DEBUG_TERMINAL_WIDTH: f32 = 100.; +const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; From be4873b92bffd2fcdd571e51a1cbf8c0e79e291d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 15:54:49 -0700 Subject: [PATCH 04/22] Checkpoint, build failing --- crates/terminal/README.md | 8 +++ crates/terminal/src/modal.rs | 2 +- crates/terminal/src/terminal.rs | 55 +++++++++++-------- .../src/{terminal_tab.rs => terminal_view.rs} | 0 4 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 crates/terminal/README.md rename crates/terminal/src/{terminal_tab.rs => terminal_view.rs} (100%) diff --git a/crates/terminal/README.md b/crates/terminal/README.md new file mode 100644 index 0000000000..c1e64726b2 --- /dev/null +++ b/crates/terminal/README.md @@ -0,0 +1,8 @@ +Design notes: + +This crate is split into two conceptual halves: +- The terminal.rs file and the ./src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. +- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. + +Terminals are created externally, and so can fail in unexpected ways However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split `Terminal` instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context. +The TerminalView struct abstracts over failed and successful terminals, and provides other constructs a standardized way of instantiating an always-successful terminal view. \ No newline at end of file diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index a238f4cbc1..2fbf713495 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -2,7 +2,7 @@ use gpui::{ModelHandle, ViewContext}; use workspace::Workspace; use crate::{ - terminal_tab::{get_working_directory, DeployModal, TerminalContent, TerminalView}, + terminal_view::{get_working_directory, DeployModal, TerminalContent, TerminalView}, Event, Terminal, }; diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 207ed6a69f..4d6c7db646 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -2,7 +2,7 @@ pub mod connected_el; pub mod connected_view; pub mod mappings; pub mod modal; -pub mod terminal_tab; +pub mod terminal_view; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -22,7 +22,7 @@ use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use modal::deploy_modal; use settings::{Settings, Shell}; use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; -use terminal_tab::TerminalView; +use terminal_view::TerminalView; use thiserror::Error; use gpui::{ @@ -319,6 +319,7 @@ impl TerminalBuilder { pty_tx: Notifier(pty_tx), term, title: shell_txt.to_string(), + event_stack: vec![], }; Ok(TerminalBuilder { @@ -330,8 +331,8 @@ impl TerminalBuilder { pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { 'outer: loop { - //Even as low as 45 locks zed up - let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); + //TODO: Pending GPUI updates, sync this to some higher, smarter system. + let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 60.)); let mut events = vec![]; @@ -349,9 +350,8 @@ impl TerminalBuilder { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - for event in events { - this.process_terminal_event(event, cx); - } + this.push_events(events); + cx.notify(); }); } None => break 'outer, @@ -370,13 +370,19 @@ pub struct Terminal { pty_tx: Notifier, term: Arc>>, pub title: String, + event_stack: Vec, } impl Terminal { + fn push_events(&mut self, events: Vec) { + self.event_stack.extend(events) + } + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, event: alacritty_terminal::event::Event, + term: &mut Term, cx: &mut ModelContext, ) { match event { @@ -384,7 +390,10 @@ impl Terminal { AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::PtyWrite(out) => { + term.scroll_display(Scroll::Bottom); + self.pty_tx.notify(out.into_bytes()) + } AlacTermEvent::MouseCursorDirty => { //Calculate new cursor style. //TODO: alacritty/src/input.rs:L922-L939 @@ -408,7 +417,7 @@ impl Terminal { .unwrap_or("".to_string()), )), AlacTermEvent::ColorRequest(index, format) => { - let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let color = term.colors()[index].unwrap_or_else(|| { let term_style = &cx.global::().theme.terminal; to_alac_rgb(get_color_at_index(&index, &term_style.colors)) }); @@ -427,13 +436,7 @@ impl Terminal { ///Write the Input payload to the tty. This locks the terminal so we can scroll it. pub fn write_to_pty(&self, input: String) { - self.write_bytes_to_pty(input.into_bytes()); - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - fn write_bytes_to_pty(&self, input: Vec) { - self.term.lock().scroll_display(Scroll::Bottom); - self.pty_tx.notify(input); + self.event_stack.push(AlacTermEvent::PtyWrite(input)) } ///Resize the terminal and the PTY. This locks the terminal. @@ -487,19 +490,23 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn render_lock(&self, new_size: Option, f: F) -> T + pub fn render_lock(&self, cx: &mut ModelContext, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { - if let Some(new_size) = new_size { - self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size - //TODO: Is this bad for performance? - } - let mut term = self.term.lock(); //Lock - if let Some(new_size) = new_size { - term.resize(new_size); //Reflow + //TODO, handle resizes + // if let Some(new_size) = new_size { + // self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); + // } + + // if let Some(new_size) = new_size { + // term.resize(new_size); //Reflow + // } + + for event in self.event_stack.drain(..) { + self.process_terminal_event(event, &mut term, cx) } let content = term.renderable_content(); diff --git a/crates/terminal/src/terminal_tab.rs b/crates/terminal/src/terminal_view.rs similarity index 100% rename from crates/terminal/src/terminal_tab.rs rename to crates/terminal/src/terminal_view.rs From bc2c8e0e05d1e74a129c51e75f77888e4e8bc827 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 17:04:51 -0700 Subject: [PATCH 05/22] Finished refactor for mutable terminal and long-single-lock style. Currently terminal is deadlocking instantly, need to just do the full refactor --- crates/terminal/src/connected_el.rs | 111 +++++++++--------- crates/terminal/src/connected_view.rs | 34 +++--- crates/terminal/src/terminal.rs | 59 ++++++---- .../src/tests/terminal_test_context.rs | 21 ++-- 4 files changed, 124 insertions(+), 101 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index f14e2f14b1..856f5ba210 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -512,10 +512,10 @@ impl Element for TerminalEl { cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { let settings = cx.global::(); - let font_cache = &cx.font_cache(); + let font_cache = cx.font_cache(); //Setup layout information - let terminal_theme = &settings.theme.terminal; + let terminal_theme = settings.theme.terminal.clone(); //-_- let text_style = TerminalEl::make_text_style(font_cache, &settings); let selection_color = settings.theme.editor.selection.selection; let dimensions = { @@ -524,62 +524,64 @@ impl Element for TerminalEl { TermDimensions::new(line_height, cell_width, constraint.max) }; - let terminal = self.terminal.upgrade(cx).unwrap().read(cx); + let background_color = if self.modal { + terminal_theme.colors.modal_background.clone() + } else { + terminal_theme.colors.background.clone() + }; let (cursor, cells, rects, highlights) = - terminal.render_lock(Some(dimensions.clone()), |content, cursor_text| { - let (cells, rects, highlights) = TerminalEl::layout_grid( - content.display_iter, - &text_style, - terminal_theme, - cx.text_layout_cache, - self.modal, - content.selection, - ); + self.terminal + .upgrade(cx) + .unwrap() + .update(cx.app, |terminal, mcx| { + terminal.render_lock(mcx, |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( + content.display_iter, + &text_style, + &terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); - //Layout cursor - let cursor = { - let cursor_point = - DisplayCursor::from(content.cursor.point, content.display_offset); - let cursor_text = { - let str_trxt = cursor_text.to_string(); - cx.text_layout_cache.layout_str( - &str_trxt, - text_style.font_size, - &[( - str_trxt.len(), - RunStyle { - font_id: text_style.font_id, - color: terminal_theme.colors.background, - underline: Default::default(), + //Layout cursor + let cursor = { + let cursor_point = + DisplayCursor::from(content.cursor.point, content.display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) + }; + + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) }, - )], - ) - }; - - TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - Cursor::new( - cursor_position, - block_width, - dimensions.line_height, - terminal_theme.colors.cursor, - CursorShape::Block, - Some(cursor_text.clone()), ) - }, - ) - }; + }; - (cursor, cells, rects, highlights) - }); - - //Select background color - let background_color = if self.modal { - terminal_theme.colors.modal_background - } else { - terminal_theme.colors.background - }; + (cursor, cells, rects, highlights) + }) + }); //Done! ( @@ -706,8 +708,9 @@ impl Element for TerminalEl { self.terminal .upgrade(cx.app) - .map(|model_handle| model_handle.read(cx.app)) - .map(|term| term.try_keystroke(keystroke)) + .map(|model_handle| { + model_handle.update(cx.app, |term, _| term.try_keystroke(keystroke)) + }) .unwrap_or(false) } _ => false, diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 1503c497a2..7bbc272a96 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -87,7 +87,7 @@ impl ConnectedView { } fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.terminal.read(cx).clear(); + self.terminal.update(cx, |term, _| term.clear()); } ///Attempt to paste the clipboard into the terminal @@ -101,43 +101,43 @@ impl ConnectedView { ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { cx.read_from_clipboard().map(|item| { - self.terminal.read(cx).paste(item.text()); + self.terminal.update(cx, |term, _| term.paste(item.text())); }); } ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("up").unwrap()); + self.terminal.update(cx, |term, _| { + term.try_keystroke(&Keystroke::parse("up").unwrap()); + }); } ///Synthesize the keyboard event corresponding to 'down' fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("down").unwrap()); + self.terminal.update(cx, |term, _| { + term.try_keystroke(&Keystroke::parse("down").unwrap()); + }); } ///Synthesize the keyboard event corresponding to 'ctrl-c' fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + self.terminal.update(cx, |term, _| { + term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); + }); } ///Synthesize the keyboard event corresponding to 'escape' fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("escape").unwrap()); + self.terminal.update(cx, |term, _| { + term.try_keystroke(&Keystroke::parse("escape").unwrap()); + }); } ///Synthesize the keyboard event corresponding to 'enter' fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.terminal - .read(cx) - .try_keystroke(&Keystroke::parse("enter").unwrap()); + self.terminal.update(cx, |term, _| { + term.try_keystroke(&Keystroke::parse("enter").unwrap()); + }); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4d6c7db646..251e2e9cba 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -4,6 +4,11 @@ pub mod mappings; pub mod modal; pub mod terminal_view; +#[cfg(test)] +use alacritty_terminal::term::cell::Cell; +#[cfg(test)] +use alacritty_terminal::Grid; + use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig}, @@ -383,13 +388,11 @@ impl Terminal { &mut self, event: alacritty_terminal::event::Event, term: &mut Term, - cx: &mut ModelContext, + cx: &mut ModelContext, ) { match event { // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } + AlacTermEvent::Wakeup => { /* Irrelevant, as we always notify on any event */ } AlacTermEvent::PtyWrite(out) => { term.scroll_display(Scroll::Bottom); self.pty_tx.notify(out.into_bytes()) @@ -411,17 +414,20 @@ impl Terminal { AlacTermEvent::ClipboardStore(_, data) => { cx.write_to_clipboard(ClipboardItem::new(data)) } - AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), + AlacTermEvent::ClipboardLoad(_, format) => self.pty_tx.notify( + format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + ) + .into_bytes(), + ), AlacTermEvent::ColorRequest(index, format) => { let color = term.colors()[index].unwrap_or_else(|| { let term_style = &cx.global::().theme.terminal; to_alac_rgb(get_color_at_index(&index, &term_style.colors)) }); - self.write_to_pty(format(color)) + self.pty_tx.notify(format(color).into_bytes()) } AlacTermEvent::CursorBlinkingChange => { //TODO: Set a timer to blink the cursor on and off @@ -435,7 +441,7 @@ impl Terminal { } ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - pub fn write_to_pty(&self, input: String) { + pub fn write_to_pty(&mut self, input: String) { self.event_stack.push(AlacTermEvent::PtyWrite(input)) } @@ -447,12 +453,12 @@ impl Terminal { self.term.lock().resize(term_size); } - pub fn clear(&self) { + pub fn clear(&mut self) { self.write_to_pty("\x0c".into()); self.term.lock().clear_screen(ClearMode::Saved); } - pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { let guard = self.term.lock(); let mode = guard.mode(); let esc = to_esc_str(keystroke, mode); @@ -466,7 +472,7 @@ impl Terminal { } ///Paste text into the terminal - pub fn paste(&self, text: &str) { + pub fn paste(&mut self, text: &str) { if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { self.write_to_pty("\x1b[200~".to_string()); self.write_to_pty(text.replace('\x1b', "").to_string()); @@ -490,11 +496,12 @@ impl Terminal { self.term.lock().selection = sel; } - pub fn render_lock(&self, cx: &mut ModelContext, f: F) -> T + pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { - let mut term = self.term.lock(); //Lock + let m = self.term.clone(); //TODO avoid clone? + let mut term = m.lock(); //Lock //TODO, handle resizes // if let Some(new_size) = new_size { @@ -505,7 +512,13 @@ impl Terminal { // term.resize(new_size); //Reflow // } - for event in self.event_stack.drain(..) { + for event in self + .event_stack + .iter() + .map(|event| event.clone()) + .collect::>() //TODO avoid copy + .drain(..) + { self.process_terminal_event(event, &mut term, cx) } @@ -516,12 +529,13 @@ impl Terminal { } pub fn get_display_offset(&self) -> usize { - self.term.lock().renderable_content().display_offset + 10 + // self.term.lock().renderable_content().display_offset } ///Scroll the terminal - pub fn scroll(&self, scroll: Scroll) { - self.term.lock().scroll_display(scroll) + pub fn scroll(&self, _scroll: Scroll) { + // self.term.lock().scroll_display(scroll) } pub fn click(&self, point: Point, side: Direction, clicks: usize) { @@ -549,6 +563,11 @@ impl Terminal { pub fn mouse_down(&self, point: Point, side: Direction) { self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); } + + #[cfg(test)] + fn grid(&self) -> Grid { + self.term.lock().grid().clone() + } } impl Drop for Terminal { diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 06d45a5146..d3bcf9102e 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -43,8 +43,9 @@ impl<'a> TerminalTestContext<'a> { }); connection - .condition(self.cx, |conn, cx| { - let content = Self::grid_as_str(conn); + .condition(self.cx, |term, cx| { + let content = Self::grid_as_str(term); + f(content, cx) }) .await; @@ -132,14 +133,14 @@ impl<'a> TerminalTestContext<'a> { } fn grid_as_str(connection: &Terminal) -> String { - connection.render_lock(None, |content, _| { - let lines = content.display_iter.group_by(|i| i.point.line.0); - lines - .into_iter() - .map(|(_, line)| line.map(|i| i.c).collect::()) - .collect::>() - .join("\n") - }) + let content = connection.grid(); + + let lines = content.display_iter().group_by(|i| i.point.line.0); + lines + .into_iter() + .map(|(_, line)| line.map(|i| i.c).collect::()) + .collect::>() + .join("\n") } } From aea3508b69d7ff5e0925505ec1ae80558ab9a767 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 17:15:20 -0700 Subject: [PATCH 06/22] keeping both... --- crates/terminal/src/model.rs | 535 +++++++++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 crates/terminal/src/model.rs diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs new file mode 100644 index 0000000000..481ebce3a4 --- /dev/null +++ b/crates/terminal/src/model.rs @@ -0,0 +1,535 @@ +use alacritty_terminal::{ + ansi::{ClearMode, Handler}, + config::{Config, Program, PtyConfig}, + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Scroll, + index::{Direction, Point}, + selection::{Selection, SelectionType}, + sync::FairMutex, + term::{test::TermSize, RenderableContent, TermMode}, + tty::{self, setup_env}, + Term, +}; +use anyhow::{bail, Result}; +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use settings::{Settings, Shell}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; +use thiserror::Error; + +use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; + +use crate::{ + connected_el::TermDimensions, + mappings::{ + colors::{get_color_at_index, to_alac_rgb}, + keys::to_esc_str, + }, +}; + +const DEFAULT_TITLE: &str = "Terminal"; + +///Upward flowing events, for changing the title and such +#[derive(Copy, Clone, Debug)] +pub enum Event { + TitleChanged, + CloseTerminal, + Activate, + Wakeup, + Bell, + KeyInput, +} + +///A translation struct for Alacritty to communicate with us from their event loop +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(event).ok(); + } +} + +#[derive(Error, Debug)] +pub struct TerminalError { + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, +} + +impl TerminalError { + pub fn fmt_directory(&self) -> String { + self.directory + .clone() + .map(|path| { + match path + .into_os_string() + .into_string() + .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) + { + Ok(s) => s, + Err(s) => s, + } + }) + .unwrap_or_else(|| { + let default_dir = + dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); + match default_dir { + Some(dir) => format!(" {}", dir), + None => "".to_string(), + } + }) + } + + pub fn shell_to_string(&self) -> Option { + self.shell.as_ref().map(|shell| match shell { + Shell::System => "".to_string(), + Shell::Program(p) => p.to_string(), + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + } + + pub fn fmt_shell(&self) -> String { + self.shell + .clone() + .map(|shell| match shell { + Shell::System => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + + match pw { + Some(pw) => format!(" {}", pw.shell), + None => "".to_string(), + } + } + Shell::Program(s) => s, + Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), + }) + .unwrap_or_else(|| { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + match pw { + Some(pw) => { + format!(" {}", pw.shell) + } + None => " {}".to_string(), + } + }) + } +} + +impl Display for TerminalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dir_string: String = self.fmt_directory(); + + let shell = self.fmt_shell(); + + write!( + f, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} + +pub struct TerminalBuilder { + terminal: Terminal, + events_rx: UnboundedReceiver, +} + +impl TerminalBuilder { + pub fn new( + working_directory: Option, + shell: Option, + env: Option>, + initial_size: TermDimensions, + ) -> Result { + let pty_config = { + let alac_shell = shell.clone().and_then(|shell| match shell { + Shell::System => None, + Shell::Program(program) => Some(Program::Just(program)), + Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), + }); + + PtyConfig { + shell: alac_shell, + working_directory: working_directory.clone(), + hold: false, + } + }; + + let mut env = env.unwrap_or_else(|| HashMap::new()); + + //TODO: Properly set the current locale, + env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); + + let config = Config { + pty_config: pty_config.clone(), + env, + ..Default::default() + }; + + setup_env(&config); + + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context + let (events_tx, events_rx) = unbounded(); + + //Set up the terminal... + let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); + let term = Arc::new(FairMutex::new(term)); + + //Setup the pty... + let pty = match tty::new(&pty_config, initial_size.into(), None) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + shell, + source: error, + }); + } + }; + + let shell_txt = { + match shell { + Some(Shell::System) | None => { + let mut buf = [0; 1024]; + let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); + pw.shell.to_string() + } + Some(Shell::Program(program)) => program, + Some(Shell::WithArguments { program, args }) => { + format!("{} {}", program, args.join(" ")) + } + } + }; + + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, + ); + + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); + + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: shell_txt.to_string(), + }; + + Ok(TerminalBuilder { + terminal, + events_rx, + }) + } + + pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { + cx.spawn_weak(|this, mut cx| async move { + 'outer: loop { + let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); + + let mut events = vec![]; + + loop { + match self.events_rx.try_next() { + //Have a buffered event + Ok(Some(e)) => events.push(e), + //Ran out of buffered events + Ok(None) => break, + //Channel closed, exit + Err(_) => break 'outer, + } + } + + match this.upgrade(&cx) { + Some(this) => { + this.update(&mut cx, |this, cx| { + for event in events { + this.process_terminal_event(event, cx); + } + }); + } + None => break 'outer, + } + + delay.await; + } + }) + .detach(); + + self.terminal + } +} + +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + pub title: String, +} + +impl Terminal { + ///Takes events from Alacritty and translates them to behavior on this view + fn process_terminal_event( + &mut self, + event: alacritty_terminal::event::Event, + cx: &mut ModelContext, + ) { + match event { + // TODO: Handle is_self_focused in subscription on terminal view + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO: alacritty/src/input.rs:L922-L939 + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + AlacTermEvent::ColorRequest(index, format) => { + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(&index, &term_style.colors)) + }); + self.write_to_pty(format(color)) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), + } + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + pub fn write_to_pty(&self, input: String) { + self.write_bytes_to_pty(input.into_bytes()); + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + fn write_bytes_to_pty(&self, input: Vec) { + self.term.lock().scroll_display(Scroll::Bottom); + self.pty_tx.notify(input); + } + + ///Resize the terminal and the PTY. This locks the terminal. + pub fn set_size(&self, new_size: WindowSize) { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + + let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); + self.term.lock().resize(term_size); + } + + pub fn clear(&self) { + self.write_to_pty("\x0c".into()); + self.term.lock().clear_screen(ClearMode::Saved); + } + + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + let guard = self.term.lock(); + let mode = guard.mode(); + let esc = to_esc_str(keystroke, mode); + drop(guard); + if esc.is_some() { + self.write_to_pty(esc.unwrap()); + true + } else { + false + } + } + + ///Paste text into the terminal + pub fn paste(&self, text: &str) { + if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { + self.write_to_pty("\x1b[200~".to_string()); + self.write_to_pty(text.replace('\x1b', "").to_string()); + self.write_to_pty("\x1b[201~".to_string()); + } else { + self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); + } + } + + pub fn copy(&self) -> Option { + let term = self.term.lock(); + term.selection_to_string() + } + + ///Takes the selection out of the terminal + pub fn take_selection(&self) -> Option { + self.term.lock().selection.take() + } + ///Sets the selection object on the terminal + pub fn set_selection(&self, sel: Option) { + self.term.lock().selection = sel; + } + + pub fn render_lock(&self, new_size: Option, f: F) -> T + where + F: FnOnce(RenderableContent, char) -> T, + { + if let Some(new_size) = new_size { + self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size + //TODO: Is this bad for performance? + } + + let mut term = self.term.lock(); //Lock + + if let Some(new_size) = new_size { + term.resize(new_size); //Reflow + } + + let content = term.renderable_content(); + let cursor_text = term.grid()[content.cursor.point].c; + + f(content, cursor_text) + } + + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset + } + + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) + } + + pub fn click(&self, point: Point, side: Direction, clicks: usize) { + let selection_type = match clicks { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + self.set_selection(selection); + } + + pub fn drag(&self, point: Point, side: Direction) { + if let Some(mut selection) = self.take_selection() { + selection.update(point, side); + self.set_selection(Some(selection)); + } + } + + pub fn mouse_down(&self, point: Point, side: Direction) { + self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } +} + +impl Entity for Terminal { + type Event = Event; +} + +//TODO Move this around +mod alacritty_unix { + use alacritty_terminal::config::Program; + use gpui::anyhow::{bail, Result}; + use libc; + use std::ffi::CStr; + use std::mem::MaybeUninit; + use std::ptr; + + #[derive(Debug)] + pub struct Passwd<'a> { + _name: &'a str, + _dir: &'a str, + pub shell: &'a str, + } + + /// Return a Passwd struct with pointers into the provided buf. + /// + /// # Unsafety + /// + /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. + pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { + // Create zeroed passwd struct. + let mut entry: MaybeUninit = MaybeUninit::uninit(); + + let mut res: *mut libc::passwd = ptr::null_mut(); + + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r( + uid, + entry.as_mut_ptr(), + buf.as_mut_ptr() as *mut _, + buf.len(), + &mut res, + ) + }; + let entry = unsafe { entry.assume_init() }; + + if status < 0 { + bail!("getpwuid_r failed"); + } + + if res.is_null() { + bail!("pw not found"); + } + + // Sanity check. + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct. + Ok(Passwd { + _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + }) + } + + #[cfg(target_os = "macos")] + pub fn _default_shell(pw: &Passwd<'_>) -> Program { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![ + String::from("-c"), + format!("exec -a -{} {}", shell_name, pw.shell), + ]; + + Program::WithArgs { + program: "/bin/bash".to_owned(), + args: argv, + } + } + + #[cfg(not(target_os = "macos"))] + pub fn default_shell(pw: &Passwd<'_>) -> Program { + Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) + } +} From 67e650b0e00a5e6b5fd8f8911a416eddbaccf1f7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Jul 2022 17:19:11 -0700 Subject: [PATCH 07/22] Fixed conflicts --- crates/terminal/src/model.rs | 535 ---------------------------- crates/terminal/src/terminal.rs | 12 +- crates/terminal/src/terminal_tab.rs | 490 +++++++++++++++++++++++++ 3 files changed, 496 insertions(+), 541 deletions(-) delete mode 100644 crates/terminal/src/model.rs create mode 100644 crates/terminal/src/terminal_tab.rs diff --git a/crates/terminal/src/model.rs b/crates/terminal/src/model.rs deleted file mode 100644 index 481ebce3a4..0000000000 --- a/crates/terminal/src/model.rs +++ /dev/null @@ -1,535 +0,0 @@ -use alacritty_terminal::{ - ansi::{ClearMode, Handler}, - config::{Config, Program, PtyConfig}, - event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, - grid::Scroll, - index::{Direction, Point}, - selection::{Selection, SelectionType}, - sync::FairMutex, - term::{test::TermSize, RenderableContent, TermMode}, - tty::{self, setup_env}, - Term, -}; -use anyhow::{bail, Result}; -use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use settings::{Settings, Shell}; -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; -use thiserror::Error; - -use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext}; - -use crate::{ - connected_el::TermDimensions, - mappings::{ - colors::{get_color_at_index, to_alac_rgb}, - keys::to_esc_str, - }, -}; - -const DEFAULT_TITLE: &str = "Terminal"; - -///Upward flowing events, for changing the title and such -#[derive(Copy, Clone, Debug)] -pub enum Event { - TitleChanged, - CloseTerminal, - Activate, - Wakeup, - Bell, - KeyInput, -} - -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(UnboundedSender); - -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); - } -} - -#[derive(Error, Debug)] -pub struct TerminalError { - pub directory: Option, - pub shell: Option, - pub source: std::io::Error, -} - -impl TerminalError { - pub fn fmt_directory(&self) -> String { - self.directory - .clone() - .map(|path| { - match path - .into_os_string() - .into_string() - .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) - { - Ok(s) => s, - Err(s) => s, - } - }) - .unwrap_or_else(|| { - let default_dir = - dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string()); - match default_dir { - Some(dir) => format!(" {}", dir), - None => "".to_string(), - } - }) - } - - pub fn shell_to_string(&self) -> Option { - self.shell.as_ref().map(|shell| match shell { - Shell::System => "".to_string(), - Shell::Program(p) => p.to_string(), - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - } - - pub fn fmt_shell(&self) -> String { - self.shell - .clone() - .map(|shell| match shell { - Shell::System => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - - match pw { - Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), - } - } - Shell::Program(s) => s, - Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), - }) - .unwrap_or_else(|| { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - match pw { - Some(pw) => { - format!(" {}", pw.shell) - } - None => " {}".to_string(), - } - }) - } -} - -impl Display for TerminalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let dir_string: String = self.fmt_directory(); - - let shell = self.fmt_shell(); - - write!( - f, - "Working directory: {} Shell command: `{}`, IOError: {}", - dir_string, shell, self.source - ) - } -} - -pub struct TerminalBuilder { - terminal: Terminal, - events_rx: UnboundedReceiver, -} - -impl TerminalBuilder { - pub fn new( - working_directory: Option, - shell: Option, - env: Option>, - initial_size: TermDimensions, - ) -> Result { - let pty_config = { - let alac_shell = shell.clone().and_then(|shell| match shell { - Shell::System => None, - Shell::Program(program) => Some(Program::Just(program)), - Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), - }); - - PtyConfig { - shell: alac_shell, - working_directory: working_directory.clone(), - hold: false, - } - }; - - let mut env = env.unwrap_or_else(|| HashMap::new()); - - //TODO: Properly set the current locale, - env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); - - let config = Config { - pty_config: pty_config.clone(), - env, - ..Default::default() - }; - - setup_env(&config); - - //Spawn a task so the Alacritty EventLoop can communicate with us in a view context - let (events_tx, events_rx) = unbounded(); - - //Set up the terminal... - let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); - let term = Arc::new(FairMutex::new(term)); - - //Setup the pty... - let pty = match tty::new(&pty_config, initial_size.into(), None) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - shell, - source: error, - }); - } - }; - - let shell_txt = { - match shell { - Some(Shell::System) | None => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap(); - pw.shell.to_string() - } - Some(Shell::Program(program)) => program, - Some(Shell::WithArguments { program, args }) => { - format!("{} {}", program, args.join(" ")) - } - } - }; - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx.clone()), - pty, - pty_config.hold, - false, - ); - - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); - - let terminal = Terminal { - pty_tx: Notifier(pty_tx), - term, - title: shell_txt.to_string(), - }; - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - } - - pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { - cx.spawn_weak(|this, mut cx| async move { - 'outer: loop { - let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); - - let mut events = vec![]; - - loop { - match self.events_rx.try_next() { - //Have a buffered event - Ok(Some(e)) => events.push(e), - //Ran out of buffered events - Ok(None) => break, - //Channel closed, exit - Err(_) => break 'outer, - } - } - - match this.upgrade(&cx) { - Some(this) => { - this.update(&mut cx, |this, cx| { - for event in events { - this.process_terminal_event(event, cx); - } - }); - } - None => break 'outer, - } - - delay.await; - } - }) - .detach(); - - self.terminal - } -} - -pub struct Terminal { - pty_tx: Notifier, - term: Arc>>, - pub title: String, -} - -impl Terminal { - ///Takes events from Alacritty and translates them to behavior on this view - fn process_terminal_event( - &mut self, - event: alacritty_terminal::event::Event, - cx: &mut ModelContext, - ) { - match event { - // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title; - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = DEFAULT_TITLE.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), - AlacTermEvent::ColorRequest(index, format) => { - let color = self.term.lock().colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; - to_alac_rgb(get_color_at_index(&index, &term_style.colors)) - }); - self.write_to_pty(format(color)) - } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), - } - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - pub fn write_to_pty(&self, input: String) { - self.write_bytes_to_pty(input.into_bytes()); - } - - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. - fn write_bytes_to_pty(&self, input: Vec) { - self.term.lock().scroll_display(Scroll::Bottom); - self.pty_tx.notify(input); - } - - ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&self, new_size: WindowSize) { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - - let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); - self.term.lock().resize(term_size); - } - - pub fn clear(&self) { - self.write_to_pty("\x0c".into()); - self.term.lock().clear_screen(ClearMode::Saved); - } - - pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { - let guard = self.term.lock(); - let mode = guard.mode(); - let esc = to_esc_str(keystroke, mode); - drop(guard); - if esc.is_some() { - self.write_to_pty(esc.unwrap()); - true - } else { - false - } - } - - ///Paste text into the terminal - pub fn paste(&self, text: &str) { - if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { - self.write_to_pty("\x1b[200~".to_string()); - self.write_to_pty(text.replace('\x1b', "").to_string()); - self.write_to_pty("\x1b[201~".to_string()); - } else { - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } - } - - pub fn copy(&self) -> Option { - let term = self.term.lock(); - term.selection_to_string() - } - - ///Takes the selection out of the terminal - pub fn take_selection(&self) -> Option { - self.term.lock().selection.take() - } - ///Sets the selection object on the terminal - pub fn set_selection(&self, sel: Option) { - self.term.lock().selection = sel; - } - - pub fn render_lock(&self, new_size: Option, f: F) -> T - where - F: FnOnce(RenderableContent, char) -> T, - { - if let Some(new_size) = new_size { - self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size - //TODO: Is this bad for performance? - } - - let mut term = self.term.lock(); //Lock - - if let Some(new_size) = new_size { - term.resize(new_size); //Reflow - } - - let content = term.renderable_content(); - let cursor_text = term.grid()[content.cursor.point].c; - - f(content, cursor_text) - } - - pub fn get_display_offset(&self) -> usize { - self.term.lock().renderable_content().display_offset - } - - ///Scroll the terminal - pub fn scroll(&self, scroll: Scroll) { - self.term.lock().scroll_display(scroll) - } - - pub fn click(&self, point: Point, side: Direction, clicks: usize) { - let selection_type = match clicks { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; - - let selection = - selection_type.map(|selection_type| Selection::new(selection_type, point, side)); - - self.set_selection(selection); - } - - pub fn drag(&self, point: Point, side: Direction) { - if let Some(mut selection) = self.take_selection() { - selection.update(point, side); - self.set_selection(Some(selection)); - } - } - - pub fn mouse_down(&self, point: Point, side: Direction) { - self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); - } -} - -impl Drop for Terminal { - fn drop(&mut self) { - self.pty_tx.0.send(Msg::Shutdown).ok(); - } -} - -impl Entity for Terminal { - type Event = Event; -} - -//TODO Move this around -mod alacritty_unix { - use alacritty_terminal::config::Program; - use gpui::anyhow::{bail, Result}; - use libc; - use std::ffi::CStr; - use std::mem::MaybeUninit; - use std::ptr; - - #[derive(Debug)] - pub struct Passwd<'a> { - _name: &'a str, - _dir: &'a str, - pub shell: &'a str, - } - - /// Return a Passwd struct with pointers into the provided buf. - /// - /// # Unsafety - /// - /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. - pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { - // Create zeroed passwd struct. - let mut entry: MaybeUninit = MaybeUninit::uninit(); - - let mut res: *mut libc::passwd = ptr::null_mut(); - - // Try and read the pw file. - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - entry.as_mut_ptr(), - buf.as_mut_ptr() as *mut _, - buf.len(), - &mut res, - ) - }; - let entry = unsafe { entry.assume_init() }; - - if status < 0 { - bail!("getpwuid_r failed"); - } - - if res.is_null() { - bail!("pw not found"); - } - - // Sanity check. - assert_eq!(entry.pw_uid, uid); - - // Build a borrowed Passwd struct. - Ok(Passwd { - _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, - _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, - shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, - }) - } - - #[cfg(target_os = "macos")] - pub fn _default_shell(pw: &Passwd<'_>) -> Program { - let shell_name = pw.shell.rsplit('/').next().unwrap(); - let argv = vec![ - String::from("-c"), - format!("exec -a -{} {}", shell_name, pw.shell), - ]; - - Program::WithArgs { - program: "/bin/bash".to_owned(), - args: argv, - } - } - - #[cfg(not(target_os = "macos"))] - pub fn default_shell(pw: &Passwd<'_>) -> Program { - Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) - } -} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 251e2e9cba..621b90b417 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -50,6 +50,7 @@ pub fn init(cx: &mut MutableAppContext) { } const DEFAULT_TITLE: &str = "Terminal"; + const DEBUG_TERMINAL_WIDTH: f32 = 100.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; @@ -224,7 +225,6 @@ impl TerminalError { impl Display for TerminalError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let dir_string: String = self.fmt_directory(); - let shell = self.fmt_shell(); write!( @@ -276,7 +276,6 @@ impl TerminalBuilder { //Spawn a task so the Alacritty EventLoop can communicate with us in a view context let (events_tx, events_rx) = unbounded(); - //Set up the terminal... let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone())); let term = Arc::new(FairMutex::new(term)); @@ -392,11 +391,11 @@ impl Terminal { ) { match event { // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { /* Irrelevant, as we always notify on any event */ } - AlacTermEvent::PtyWrite(out) => { - term.scroll_display(Scroll::Bottom); - self.pty_tx.notify(out.into_bytes()) + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), + AlacTermEvent::MouseCursorDirty => { //Calculate new cursor style. //TODO: alacritty/src/input.rs:L922-L939 @@ -414,6 +413,7 @@ impl Terminal { AlacTermEvent::ClipboardStore(_, data) => { cx.write_to_clipboard(ClipboardItem::new(data)) } + AlacTermEvent::ClipboardLoad(_, format) => self.pty_tx.notify( format( &cx.read_from_clipboard() diff --git a/crates/terminal/src/terminal_tab.rs b/crates/terminal/src/terminal_tab.rs new file mode 100644 index 0000000000..69bac7df1d --- /dev/null +++ b/crates/terminal/src/terminal_tab.rs @@ -0,0 +1,490 @@ +use crate::connected_view::ConnectedView; +use crate::{Event, Terminal, TerminalBuilder, TerminalError}; +use dirs::home_dir; +use gpui::{ + actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, + ViewHandle, +}; + +use crate::TermDimensions; +use project::{LocalWorktree, Project, ProjectPath}; +use settings::{Settings, WorkingDirectory}; +use smallvec::SmallVec; +use std::path::{Path, PathBuf}; +use workspace::{Item, Workspace}; + +use crate::connected_el::TerminalEl; + +actions!(terminal, [Deploy, DeployModal]); + +//Make terminal view an enum, that can give you views for the error and non-error states +//Take away all the result unwrapping in the current TerminalView by making it 'infallible' +//Bubble up to deploy(_modal)() calls + +pub enum TerminalContent { + Connected(ViewHandle), + Error(ViewHandle), +} + +impl TerminalContent { + fn handle(&self) -> AnyViewHandle { + match self { + Self::Connected(handle) => handle.into(), + Self::Error(handle) => handle.into(), + } + } +} + +pub struct TerminalView { + modal: bool, + pub content: TerminalContent, + associated_directory: Option, +} + +pub struct ErrorView { + error: TerminalError, +} + +impl Entity for TerminalView { + type Event = Event; +} + +impl Entity for ConnectedView { + type Event = Event; +} + +impl Entity for ErrorView { + type Event = Event; +} + +impl TerminalView { + ///Create a new Terminal in the current working directory or the user's home directory + pub fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let working_directory = get_working_directory(workspace, cx); + let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); + workspace.add_item(Box::new(view), cx); + } + + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices + ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` + pub fn new( + working_directory: Option, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + //The details here don't matter, the terminal will be resized on the first layout + let size_info = TermDimensions::default(); + + let settings = cx.global::(); + let shell = settings.terminal_overrides.shell.clone(); + let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. + + let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) + { + Ok(terminal) => { + let terminal = cx.add_model(|cx| terminal.subscribe(cx)); + let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) + .detach(); + TerminalContent::Connected(view) + } + Err(error) => { + let view = cx.add_view(|_| ErrorView { + error: error.downcast::().unwrap(), + }); + TerminalContent::Error(view) + } + }; + cx.focus(content.handle()); + + TerminalView { + modal, + content, + associated_directory: working_directory, + } + } + + pub fn from_terminal( + terminal: ModelHandle, + modal: bool, + cx: &mut ViewContext, + ) -> Self { + let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); + TerminalView { + modal, + content: TerminalContent::Connected(connected_view), + associated_directory: None, + } + } +} + +impl View for TerminalView { + fn ui_name() -> &'static str { + "Terminal" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let child_view = match &self.content { + TerminalContent::Connected(connected) => ChildView::new(connected), + TerminalContent::Error(error) => ChildView::new(error), + }; + + if self.modal { + let settings = cx.global::(); + let container_style = settings.theme.terminal.modal_container; + child_view.contained().with_style(container_style).boxed() + } else { + child_view.boxed() + } + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Activate); + cx.defer(|view, cx| { + cx.focus(view.content.handle()); + }); + } + + fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { + let mut context = Self::default_keymap_context(); + if self.modal { + context.set.insert("ModalTerminal".into()); + } + context + } +} + +impl View for ErrorView { + fn ui_name() -> &'static str { + "Terminal Error" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + let style = TerminalEl::make_text_style(cx.font_cache(), settings); + + //TODO: + //We want markdown style highlighting so we can format the program and working directory with `` + //We want a max-width of 75% with word-wrap + //We want to be able to select the text + //Want to be able to scroll if the error message is massive somehow (resiliency) + + let program_text = { + match self.error.shell_to_string() { + Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), + None => "No program specified".to_string(), + } + }; + + let directory_text = { + match self.error.directory.as_ref() { + Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), + None => "No working directory specified".to_string(), + } + }; + + let error_text = self.error.source.to_string(); + + Flex::column() + .with_child( + Text::new("Failed to open the terminal.".to_string(), style.clone()) + .contained() + .boxed(), + ) + .with_child(Text::new(program_text, style.clone()).contained().boxed()) + .with_child(Text::new(directory_text, style.clone()).contained().boxed()) + .with_child(Text::new(error_text, style.clone()).contained().boxed()) + .aligned() + .boxed() + } +} + +impl Item for TerminalView { + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &gpui::AppContext, + ) -> ElementBox { + let title = match &self.content { + TerminalContent::Connected(connected) => { + connected.read(cx).handle().read(cx).title.clone() + } + TerminalContent::Error(_) => "Terminal".to_string(), + }; + + Flex::row() + .with_child( + Label::new(title, tab_theme.label.clone()) + .aligned() + .contained() + .boxed(), + ) + .boxed() + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option { + //From what I can tell, there's no way to tell the current working + //Directory of the terminal from outside the shell. There might be + //solutions to this, but they are non-trivial and require more IPC + Some(TerminalView::new( + self.associated_directory.clone(), + false, + cx, + )) + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, cx: &gpui::AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_new_content() + } else { + false + } + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + if let TerminalContent::Connected(connected) = &self.content { + connected.read(cx).has_bell() + } else { + false + } + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::TitleChanged) + } + + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::CloseTerminal) + } + + fn should_activate_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::Activate) + } +} + +///Get's the working directory for the given workspace, respecting the user's settings. +pub fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let wd_setting = cx + .global::() + .terminal_overrides + .working_directory + .clone() + .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + let res = match wd_setting { + WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx), + WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), + WorkingDirectory::AlwaysHome => None, + WorkingDirectory::Always { directory } => { + shellexpand::full(&directory) //TODO handle this better + .ok() + .map(|dir| Path::new(&dir.to_string()).to_path_buf()) + .filter(|dir| dir.is_dir()) + } + }; + res.or_else(|| home_dir()) +} + +///Get's the first project's home directory, or the home directory +fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + workspace + .worktrees(cx) + .next() + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +///Gets the intuitively correct working directory from the given workspace +///If there is an active entry for this project, returns that entry's worktree root. +///If there's no active entry but there is a worktree, returns that worktrees root. +///If either of these roots are files, or if there are any other query failures, +/// returns the user's home directory +fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { + let project = workspace.project().read(cx); + + project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .or_else(|| workspace.worktrees(cx).next()) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .and_then(get_path_from_wt) +} + +fn get_path_from_wt(wt: &LocalWorktree) -> Option { + wt.root_entry() + .filter(|re| re.is_dir()) + .map(|_| wt.abs_path().to_path_buf()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use gpui::TestAppContext; + + use std::path::Path; + + use crate::tests::terminal_test_context::TerminalTestContext; + + ///Working directory calculation tests + + ///No Worktrees in project -> home_dir() + #[gpui::test] + async fn no_worktree(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + //Test + cx.cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_none()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + ///No active entry, but a worktree, worktree is a file -> home_dir() + #[gpui::test] + async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + cx.create_file_wt(project.clone(), "/root.txt").await; + + cx.cx.read(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + //Make sure enviroment is as expeted + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, None); + }); + } + + //No active entry, but a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_none()); + assert!(workspace.worktrees(cx).next().is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); + }); + } + + //Active entry with a work tree, worktree is a file -> home_dir() + #[gpui::test] + async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, None); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } + + //Active entry, with a worktree, worktree is a folder -> worktree_folder + #[gpui::test] + async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { + //Setup variables + let mut cx = TerminalTestContext::new(cx, true); + let (project, workspace) = cx.blank_workspace().await; + let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; + let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; + cx.insert_active_entry_for(wt2, entry2, project.clone()); + + //Test + cx.cx.update(|cx| { + let workspace = workspace.read(cx); + let active_entry = project.read(cx).active_entry(); + + assert!(active_entry.is_some()); + + let res = current_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); + let res = first_project_directory(workspace, cx); + assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); + }); + } +} From 71af8764890fd86de34715f25264743fac0dc62a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 26 Jul 2022 13:10:04 -0700 Subject: [PATCH 08/22] removed stray file --- crates/terminal/src/terminal_tab.rs | 490 ---------------------------- 1 file changed, 490 deletions(-) delete mode 100644 crates/terminal/src/terminal_tab.rs diff --git a/crates/terminal/src/terminal_tab.rs b/crates/terminal/src/terminal_tab.rs deleted file mode 100644 index 69bac7df1d..0000000000 --- a/crates/terminal/src/terminal_tab.rs +++ /dev/null @@ -1,490 +0,0 @@ -use crate::connected_view::ConnectedView; -use crate::{Event, Terminal, TerminalBuilder, TerminalError}; -use dirs::home_dir; -use gpui::{ - actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, - ViewHandle, -}; - -use crate::TermDimensions; -use project::{LocalWorktree, Project, ProjectPath}; -use settings::{Settings, WorkingDirectory}; -use smallvec::SmallVec; -use std::path::{Path, PathBuf}; -use workspace::{Item, Workspace}; - -use crate::connected_el::TerminalEl; - -actions!(terminal, [Deploy, DeployModal]); - -//Make terminal view an enum, that can give you views for the error and non-error states -//Take away all the result unwrapping in the current TerminalView by making it 'infallible' -//Bubble up to deploy(_modal)() calls - -pub enum TerminalContent { - Connected(ViewHandle), - Error(ViewHandle), -} - -impl TerminalContent { - fn handle(&self) -> AnyViewHandle { - match self { - Self::Connected(handle) => handle.into(), - Self::Error(handle) => handle.into(), - } - } -} - -pub struct TerminalView { - modal: bool, - pub content: TerminalContent, - associated_directory: Option, -} - -pub struct ErrorView { - error: TerminalError, -} - -impl Entity for TerminalView { - type Event = Event; -} - -impl Entity for ConnectedView { - type Event = Event; -} - -impl Entity for ErrorView { - type Event = Event; -} - -impl TerminalView { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let working_directory = get_working_directory(workspace, cx); - let view = cx.add_view(|cx| TerminalView::new(working_directory, false, cx)); - workspace.add_item(Box::new(view), cx); - } - - ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices - ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` - pub fn new( - working_directory: Option, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - //The details here don't matter, the terminal will be resized on the first layout - let size_info = TermDimensions::default(); - - let settings = cx.global::(); - let shell = settings.terminal_overrides.shell.clone(); - let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap. - - let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info) - { - Ok(terminal) => { - let terminal = cx.add_model(|cx| terminal.subscribe(cx)); - let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); - cx.subscribe(&view, |_this, _content, event, cx| cx.emit(event.clone())) - .detach(); - TerminalContent::Connected(view) - } - Err(error) => { - let view = cx.add_view(|_| ErrorView { - error: error.downcast::().unwrap(), - }); - TerminalContent::Error(view) - } - }; - cx.focus(content.handle()); - - TerminalView { - modal, - content, - associated_directory: working_directory, - } - } - - pub fn from_terminal( - terminal: ModelHandle, - modal: bool, - cx: &mut ViewContext, - ) -> Self { - let connected_view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx)); - TerminalView { - modal, - content: TerminalContent::Connected(connected_view), - associated_directory: None, - } - } -} - -impl View for TerminalView { - fn ui_name() -> &'static str { - "Terminal" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let child_view = match &self.content { - TerminalContent::Connected(connected) => ChildView::new(connected), - TerminalContent::Error(error) => ChildView::new(error), - }; - - if self.modal { - let settings = cx.global::(); - let container_style = settings.theme.terminal.modal_container; - child_view.contained().with_style(container_style).boxed() - } else { - child_view.boxed() - } - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - cx.emit(Event::Activate); - cx.defer(|view, cx| { - cx.focus(view.content.handle()); - }); - } - - fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { - let mut context = Self::default_keymap_context(); - if self.modal { - context.set.insert("ModalTerminal".into()); - } - context - } -} - -impl View for ErrorView { - fn ui_name() -> &'static str { - "Terminal Error" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - let style = TerminalEl::make_text_style(cx.font_cache(), settings); - - //TODO: - //We want markdown style highlighting so we can format the program and working directory with `` - //We want a max-width of 75% with word-wrap - //We want to be able to select the text - //Want to be able to scroll if the error message is massive somehow (resiliency) - - let program_text = { - match self.error.shell_to_string() { - Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), - None => "No program specified".to_string(), - } - }; - - let directory_text = { - match self.error.directory.as_ref() { - Some(path) => format!("Working directory: `{}`", path.to_string_lossy()), - None => "No working directory specified".to_string(), - } - }; - - let error_text = self.error.source.to_string(); - - Flex::column() - .with_child( - Text::new("Failed to open the terminal.".to_string(), style.clone()) - .contained() - .boxed(), - ) - .with_child(Text::new(program_text, style.clone()).contained().boxed()) - .with_child(Text::new(directory_text, style.clone()).contained().boxed()) - .with_child(Text::new(error_text, style.clone()).contained().boxed()) - .aligned() - .boxed() - } -} - -impl Item for TerminalView { - fn tab_content( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &gpui::AppContext, - ) -> ElementBox { - let title = match &self.content { - TerminalContent::Connected(connected) => { - connected.read(cx).handle().read(cx).title.clone() - } - TerminalContent::Error(_) => "Terminal".to_string(), - }; - - Flex::row() - .with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() - } - - fn clone_on_split(&self, cx: &mut ViewContext) -> Option { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - Some(TerminalView::new( - self.associated_directory.clone(), - false, - cx, - )) - } - - fn project_path(&self, _cx: &gpui::AppContext) -> Option { - None - } - - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } - - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn save( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); - } - - fn save_as( - &mut self, - _project: gpui::ModelHandle, - _abs_path: std::path::PathBuf, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save_as should not have been called"); - } - - fn reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) - } - - fn is_dirty(&self, cx: &gpui::AppContext) -> bool { - if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content() - } else { - false - } - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_bell() - } else { - false - } - } - - fn should_update_tab_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::TitleChanged) - } - - fn should_close_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::CloseTerminal) - } - - fn should_activate_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::Activate) - } -} - -///Get's the working directory for the given workspace, respecting the user's settings. -pub fn get_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let wd_setting = cx - .global::() - .terminal_overrides - .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); - let res = match wd_setting { - WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - }; - res.or_else(|| home_dir()) -} - -///Get's the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - workspace - .worktrees(cx) - .next() - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -///Gets the intuitively correct working directory from the given workspace -///If there is an active entry for this project, returns that entry's worktree root. -///If there's no active entry but there is a worktree, returns that worktrees root. -///If either of these roots are files, or if there are any other query failures, -/// returns the user's home directory -fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let project = workspace.project().read(cx); - - project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| workspace.worktrees(cx).next()) - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -fn get_path_from_wt(wt: &LocalWorktree) -> Option { - wt.root_entry() - .filter(|re| re.is_dir()) - .map(|_| wt.abs_path().to_path_buf()) -} - -#[cfg(test)] -mod tests { - - use super::*; - use gpui::TestAppContext; - - use std::path::Path; - - use crate::tests::terminal_test_context::TerminalTestContext; - - ///Working directory calculation tests - - ///No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - //Test - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - ///No active entry, but a worktree, worktree is a file -> home_dir() - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - cx.create_file_wt(project.clone(), "/root.txt").await; - - cx.cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure enviroment is as expeted - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - //No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root/").await; - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - //Active entry with a work tree, worktree is a file -> home_dir() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_file_wt(project.clone(), "/root2.txt").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - //Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let mut cx = TerminalTestContext::new(cx, true); - let (project, workspace) = cx.blank_workspace().await; - let (_wt, _entry) = cx.create_folder_wt(project.clone(), "/root1/").await; - let (wt2, entry2) = cx.create_folder_wt(project.clone(), "/root2/").await; - cx.insert_active_entry_for(wt2, entry2, project.clone()); - - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } -} From ace16b63a9035cb42e10e1fee06de1e1f2c61a84 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 26 Jul 2022 16:17:26 -0700 Subject: [PATCH 09/22] Checkpoint, still converting terminal to events. Not compiling --- crates/terminal/src/connected_el.rs | 6 +- crates/terminal/src/connected_view.rs | 22 +-- crates/terminal/src/mappings/keys.rs | 12 ++ crates/terminal/src/terminal.rs | 266 ++++++++++++++++---------- crates/terminal/src/terminal_view.rs | 4 +- 5 files changed, 189 insertions(+), 121 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 856f5ba210..26e3fc9665 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -514,8 +514,12 @@ impl Element for TerminalEl { let settings = cx.global::(); let font_cache = cx.font_cache(); + //First step, make all methods take mut and update internal event queue to take new actions + //Update process terminal event to handle all actions correctly + //And it's done. + //Setup layout information - let terminal_theme = settings.theme.terminal.clone(); //-_- + let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. let text_style = TerminalEl::make_text_style(font_cache, &settings); let selection_color = settings.theme.editor.selection.selection; let dimensions = { diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 7bbc272a96..c235f68e58 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -1,6 +1,6 @@ use gpui::{ - actions, keymap::Keystroke, AppContext, ClipboardItem, Element, ElementBox, ModelHandle, - MutableAppContext, View, ViewContext, + actions, keymap::Keystroke, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, + View, ViewContext, }; use crate::{connected_el::TerminalEl, Event, Terminal}; @@ -43,20 +43,19 @@ impl ConnectedView { modal: bool, cx: &mut ViewContext, ) -> Self { - cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); + // cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); //Terminal notifies for us cx.subscribe(&terminal, |this, _, event, cx| match event { Event::Wakeup => { - if cx.is_self_focused() { - cx.notify() - } else { + if !cx.is_self_focused() { this.has_new_content = true; - cx.emit(Event::TitleChanged); + cx.emit(Event::Wakeup); } } Event::Bell => { this.has_bell = true; - cx.emit(Event::TitleChanged); + cx.emit(Event::Wakeup); } + _ => cx.emit(*event), }) .detach(); @@ -83,7 +82,7 @@ impl ConnectedView { pub fn clear_bel(&mut self, cx: &mut ViewContext) { self.has_bell = false; - cx.emit(Event::TitleChanged); + cx.emit(Event::Wakeup); } fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { @@ -92,10 +91,7 @@ impl ConnectedView { ///Attempt to paste the clipboard into the terminal fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal - .read(cx) - .copy() - .map(|text| cx.write_to_clipboard(ClipboardItem::new(text))); + self.terminal.read(cx).copy() } ///Attempt to paste the clipboard into the terminal diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index f88bfa927a..c353db57a0 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -35,6 +35,18 @@ impl Modifiers { } } +///This function checks if to_esc_str would work, assuming all terminal settings are off. +///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds. +///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal +///lock to determine whether we could actually send the keystroke with the current settings. Therefore, +///This conservative guess is used instead. Note that in practice the case where this method +///Returns false when the actual terminal would consume the keystroke never actually happens. All +///keystrokes that depend on terminal modes also have a mapping that doesn't depend on the terminal mode. +///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough +pub fn might_convert(keystroke: &Keystroke) -> bool { + to_esc_str(keystroke, &TermMode::NONE).is_some() +} + pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option { let modifiers = Modifiers::new(&keystroke); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 621b90b417..a91e22c892 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -18,15 +18,19 @@ use alacritty_terminal::{ index::{Direction, Point}, selection::{Selection, SelectionType}, sync::FairMutex, - term::{test::TermSize, RenderableContent, TermMode}, + term::{RenderableContent, TermMode}, tty::{self, setup_env}, Term, }; use anyhow::{bail, Result}; use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use itertools::Itertools; +use mappings::keys::might_convert; use modal::deploy_modal; use settings::{Settings, Shell}; -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + cmp::Ordering, collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration, +}; use terminal_view::TerminalView; use thiserror::Error; @@ -49,22 +53,62 @@ pub fn init(cx: &mut MutableAppContext) { connected_view::init(cx); } -const DEFAULT_TITLE: &str = "Terminal"; - const DEBUG_TERMINAL_WIDTH: f32 = 100.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; ///Upward flowing events, for changing the title and such -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Event { TitleChanged, CloseTerminal, Activate, - Wakeup, Bell, - KeyInput, + Wakeup, +} + +#[derive(Clone, Debug)] +enum InternalEvent { + TermEvent(AlacTermEvent), + Resize(TermDimensions), + Clear, + Keystroke(Keystroke), + Paste(String), + SetSelection(Option), + Copy, +} + +impl PartialEq for InternalEvent { + fn eq(&self, other: &Self) -> bool { + if matches!(self, other) { + true + } else { + false + } + } +} + +impl Eq for InternalEvent {} + +impl PartialOrd for InternalEvent { + fn partial_cmp(&self, other: &Self) -> Option { + if self.eq(other) { + Some(Ordering::Equal) + } else if matches!(other, InternalEvent::Copy) { + Some(Ordering::Less) + } else if matches!(self, InternalEvent::Copy) { + Some(Ordering::Greater) + } else { + Some(Ordering::Equal) + } + } +} + +impl Ord for InternalEvent { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } } ///A translation struct for Alacritty to communicate with us from their event loop @@ -322,8 +366,10 @@ impl TerminalBuilder { let terminal = Terminal { pty_tx: Notifier(pty_tx), term, - title: shell_txt.to_string(), + event_stack: vec![], + title: shell_txt.clone(), + default_title: shell_txt, }; Ok(TerminalBuilder { @@ -373,98 +419,136 @@ impl TerminalBuilder { pub struct Terminal { pty_tx: Notifier, term: Arc>>, - pub title: String, - event_stack: Vec, + event_stack: Vec, + default_title: String, + title: String, } impl Terminal { fn push_events(&mut self, events: Vec) { - self.event_stack.extend(events) + self.event_stack + .extend(events.into_iter().map(|e| InternalEvent::TermEvent(e))) } ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, - event: alacritty_terminal::event::Event, + event: &InternalEvent, term: &mut Term, cx: &mut ModelContext, ) { + // TODO: Handle is_self_focused in subscription on terminal view match event { - // TODO: Handle is_self_focused in subscription on terminal view - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out), - - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title; - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = DEFAULT_TITLE.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data)) - } - - AlacTermEvent::ClipboardLoad(_, format) => self.pty_tx.notify( - format( + InternalEvent::TermEvent(term_event) => match term_event { + AlacTermEvent::Wakeup => { + cx.emit(Event::Wakeup); + } + AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO: alacritty/src/input.rs:L922-L939 + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = self.default_title.clone(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data.to_string())) + } + AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format( &cx.read_from_clipboard() .map(|ci| ci.text().to_string()) .unwrap_or("".to_string()), - ) - .into_bytes(), - ), - AlacTermEvent::ColorRequest(index, format) => { - let color = term.colors()[index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; - to_alac_rgb(get_color_at_index(&index, &term_style.colors)) - }); - self.pty_tx.notify(format(color).into_bytes()) + )), + AlacTermEvent::ColorRequest(index, format) => { + let color = term.colors()[*index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + to_alac_rgb(get_color_at_index(index, &term_style.colors)) + }); + self.notify_pty(format(color)) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::TextAreaSizeRequest(_) => { + println!("Received text area resize request") + } + }, + InternalEvent::Resize(new_size) => { + self.pty_tx + .0 + .send(Msg::Resize(new_size.clone().into())) + .ok(); + + term.resize(*new_size); } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off + InternalEvent::Clear => { + self.notify_pty("\x0c".to_string()); + term.clear_screen(ClearMode::Saved); } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); + InternalEvent::Keystroke(keystroke) => { + let esc = to_esc_str(keystroke, term.mode()); + if let Some(esc) = esc { + self.notify_pty(esc); + } } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), + InternalEvent::Paste(text) => { + if term.mode().contains(TermMode::BRACKETED_PASTE) { + self.notify_pty("\x1b[200~".to_string()); + self.notify_pty(text.replace('\x1b', "").to_string()); + self.notify_pty("\x1b[201~".to_string()); + } else { + self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r")); + } + } + InternalEvent::Copy => { + if let Some(txt) = term.selection_to_string() { + cx.write_to_clipboard(ClipboardItem::new(txt)) + } + } + InternalEvent::SetSelection(sel) => {} } } + fn notify_pty(&self, txt: String) { + self.pty_tx.notify(txt.into_bytes()); + } + + //TODO: + // - Continue refactor into event system + // - Fix PTYWrite call to not be circular and messy + // - Change title to be emitted and maintained externally + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. pub fn write_to_pty(&mut self, input: String) { - self.event_stack.push(AlacTermEvent::PtyWrite(input)) + self.event_stack + .push(InternalEvent::TermEvent(AlacTermEvent::PtyWrite(input))) } ///Resize the terminal and the PTY. This locks the terminal. - pub fn set_size(&self, new_size: WindowSize) { - self.pty_tx.0.send(Msg::Resize(new_size)).ok(); - - let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize); - self.term.lock().resize(term_size); + pub fn set_size(&mut self, new_size: TermDimensions) { + self.event_stack + .push(InternalEvent::Resize(new_size.into())) } pub fn clear(&mut self) { - self.write_to_pty("\x0c".into()); - self.term.lock().clear_screen(ClearMode::Saved); + self.event_stack.push(InternalEvent::Clear) } pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { - let guard = self.term.lock(); - let mode = guard.mode(); - let esc = to_esc_str(keystroke, mode); - drop(guard); - if esc.is_some() { - self.write_to_pty(esc.unwrap()); + if might_convert(keystroke) { + self.event_stack + .push(InternalEvent::Keystroke(keystroke.clone())); true } else { false @@ -473,55 +557,27 @@ impl Terminal { ///Paste text into the terminal pub fn paste(&mut self, text: &str) { - if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) { - self.write_to_pty("\x1b[200~".to_string()); - self.write_to_pty(text.replace('\x1b', "").to_string()); - self.write_to_pty("\x1b[201~".to_string()); - } else { - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } + self.event_stack + .push(InternalEvent::Paste(text.to_string())); } - pub fn copy(&self) -> Option { - let term = self.term.lock(); - term.selection_to_string() - } - - ///Takes the selection out of the terminal - pub fn take_selection(&self) -> Option { - self.term.lock().selection.take() - } - ///Sets the selection object on the terminal - pub fn set_selection(&self, sel: Option) { - self.term.lock().selection = sel; + pub fn copy(&self) { + self.event_stack.push(InternalEvent::Copy); } pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { - let m = self.term.clone(); //TODO avoid clone? - let mut term = m.lock(); //Lock + let m = self.term.clone(); //Arc clone + let mut term = m.lock(); - //TODO, handle resizes - // if let Some(new_size) = new_size { - // self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); - // } - - // if let Some(new_size) = new_size { - // term.resize(new_size); //Reflow - // } - - for event in self - .event_stack - .iter() - .map(|event| event.clone()) - .collect::>() //TODO avoid copy - .drain(..) - { - self.process_terminal_event(event, &mut term, cx) + for event in self.event_stack.clone().into_iter().sorted() { + self.process_terminal_event(&event, &mut term, cx) } + self.event_stack.clear(); + let content = term.renderable_content(); let cursor_text = term.grid()[content.cursor.point].c; diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 69bac7df1d..655be38ad0 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -208,7 +208,7 @@ impl Item for TerminalView { ) -> ElementBox { let title = match &self.content { TerminalContent::Connected(connected) => { - connected.read(cx).handle().read(cx).title.clone() + connected.read(cx).handle().read(cx).title.to_string() } TerminalContent::Error(_) => "Terminal".to_string(), }; @@ -294,7 +294,7 @@ impl Item for TerminalView { } fn should_update_tab_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::TitleChanged) + matches!(event, &Event::TitleChanged | &Event::Wakeup) } fn should_close_item_on_event(event: &Self::Event) -> bool { From d1e878f0c6f654b064d7e9848f4b28d95bdb86ca Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 26 Jul 2022 16:58:14 -0700 Subject: [PATCH 10/22] Checkpoint, still not compiling --- crates/terminal/src/terminal.rs | 71 +++++++++++---------------------- 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a91e22c892..3be4cb4675 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -75,42 +75,12 @@ enum InternalEvent { Clear, Keystroke(Keystroke), Paste(String), + Scroll(Scroll), SetSelection(Option), + UpdateSelection(Point), Copy, } -impl PartialEq for InternalEvent { - fn eq(&self, other: &Self) -> bool { - if matches!(self, other) { - true - } else { - false - } - } -} - -impl Eq for InternalEvent {} - -impl PartialOrd for InternalEvent { - fn partial_cmp(&self, other: &Self) -> Option { - if self.eq(other) { - Some(Ordering::Equal) - } else if matches!(other, InternalEvent::Copy) { - Some(Ordering::Less) - } else if matches!(self, InternalEvent::Copy) { - Some(Ordering::Greater) - } else { - Some(Ordering::Equal) - } - } -} - -impl Ord for InternalEvent { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - ///A translation struct for Alacritty to communicate with us from their event loop #[derive(Clone)] pub struct ZedListener(UnboundedSender); @@ -511,12 +481,20 @@ impl Terminal { self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r")); } } + InternalEvent::Scroll(scroll) => term.scroll_display(*scroll), + InternalEvent::SetSelection(sel) => term.selection = sel, + InternalEvent::UpdateSelection(point) => { + if let Some(mut selection) = term.selection.take() { + selection.update(*point, side); + term.selection = Some(selection); + } + } + InternalEvent::Copy => { if let Some(txt) = term.selection_to_string() { cx.write_to_clipboard(ClipboardItem::new(txt)) } } - InternalEvent::SetSelection(sel) => {} } } @@ -529,13 +507,13 @@ impl Terminal { // - Fix PTYWrite call to not be circular and messy // - Change title to be emitted and maintained externally - ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + ///Write the Input payload to the tty. pub fn write_to_pty(&mut self, input: String) { self.event_stack .push(InternalEvent::TermEvent(AlacTermEvent::PtyWrite(input))) } - ///Resize the terminal and the PTY. This locks the terminal. + ///Resize the terminal and the PTY. pub fn set_size(&mut self, new_size: TermDimensions) { self.event_stack .push(InternalEvent::Resize(new_size.into())) @@ -584,14 +562,9 @@ impl Terminal { f(content, cursor_text) } - pub fn get_display_offset(&self) -> usize { - 10 - // self.term.lock().renderable_content().display_offset - } - ///Scroll the terminal pub fn scroll(&self, _scroll: Scroll) { - // self.term.lock().scroll_display(scroll) + self.event_stack.push(InternalEvent::Scroll(scroll)); } pub fn click(&self, point: Point, side: Direction, clicks: usize) { @@ -606,18 +579,22 @@ impl Terminal { let selection = selection_type.map(|selection_type| Selection::new(selection_type, point, side)); - self.set_selection(selection); + self.event_stack + .push(InternalEvent::SetSelection(selection)); } pub fn drag(&self, point: Point, side: Direction) { - if let Some(mut selection) = self.take_selection() { - selection.update(point, side); - self.set_selection(Some(selection)); - } + self.event_stack.push(InternalEvent::UpdateSelection(point)); } + ///TODO: Check if the mouse_down-then-click assumption holds, so this code works as expected pub fn mouse_down(&self, point: Point, side: Direction) { - self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + self.event_stack + .push(InternalEvent::SetSelection(Some(Selection::new( + SelectionType::Simple, + point, + side, + )))); } #[cfg(test)] From 9dfdaae94d4564e32059e6ee166497e6ceef0a10 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Jul 2022 10:11:10 -0700 Subject: [PATCH 11/22] Nearly done, not scheduling our own re-render yet --- crates/terminal/src/connected_el.rs | 125 ++++++++++++++------------ crates/terminal/src/connected_view.rs | 5 +- crates/terminal/src/mappings/keys.rs | 4 +- crates/terminal/src/terminal.rs | 73 +++++++-------- crates/terminal/src/terminal_view.rs | 5 +- 5 files changed, 104 insertions(+), 108 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 26e3fc9665..c42a0a94b0 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -46,6 +46,7 @@ pub struct LayoutState { background_color: Color, selection_color: Color, size: TermDimensions, + display_offset: usize, } ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points @@ -355,6 +356,7 @@ impl TerminalEl { view_id: usize, visible_bounds: RectF, cur_size: TermDimensions, + display_offset: usize, cx: &mut PaintContext, ) { let mouse_down_connection = self.terminal.clone(); @@ -371,7 +373,7 @@ impl TerminalEl { position, origin, cur_size, - terminal.get_display_offset(), + display_offset, ); terminal.mouse_down(point, side); @@ -396,7 +398,7 @@ impl TerminalEl { position, origin, cur_size, - terminal.get_display_offset(), + display_offset, ); terminal.click(point, side, click_count); @@ -415,7 +417,7 @@ impl TerminalEl { position, origin, cur_size, - terminal.get_display_offset(), + display_offset, ); terminal.drag(point, side); @@ -514,10 +516,6 @@ impl Element for TerminalEl { let settings = cx.global::(); let font_cache = cx.font_cache(); - //First step, make all methods take mut and update internal event queue to take new actions - //Update process terminal event to handle all actions correctly - //And it's done. - //Setup layout information let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. let text_style = TerminalEl::make_text_style(font_cache, &settings); @@ -534,58 +532,59 @@ impl Element for TerminalEl { terminal_theme.colors.background.clone() }; - let (cursor, cells, rects, highlights) = - self.terminal - .upgrade(cx) - .unwrap() - .update(cx.app, |terminal, mcx| { - terminal.render_lock(mcx, |content, cursor_text| { - let (cells, rects, highlights) = TerminalEl::layout_grid( - content.display_iter, - &text_style, - &terminal_theme, - cx.text_layout_cache, - self.modal, - content.selection, - ); + let (cursor, cells, rects, highlights, display_offset) = self + .terminal + .upgrade(cx) + .unwrap() + .update(cx.app, |terminal, mcx| { + terminal.set_size(dimensions); + terminal.render_lock(mcx, |content, cursor_text| { + let (cells, rects, highlights) = TerminalEl::layout_grid( + content.display_iter, + &text_style, + &terminal_theme, + cx.text_layout_cache, + self.modal, + content.selection, + ); - //Layout cursor - let cursor = { - let cursor_point = - DisplayCursor::from(content.cursor.point, content.display_offset); - let cursor_text = { - let str_trxt = cursor_text.to_string(); - cx.text_layout_cache.layout_str( - &str_trxt, - text_style.font_size, - &[( - str_trxt.len(), - RunStyle { - font_id: text_style.font_id, - color: terminal_theme.colors.background, - underline: Default::default(), - }, - )], - ) - }; - - TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - Cursor::new( - cursor_position, - block_width, - dimensions.line_height, - terminal_theme.colors.cursor, - CursorShape::Block, - Some(cursor_text.clone()), - ) - }, + //Layout cursor + let cursor = { + let cursor_point = + DisplayCursor::from(content.cursor.point, content.display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], ) }; - (cursor, cells, rects, highlights) - }) - }); + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }, + ) + }; + + (cursor, cells, rects, highlights, content.display_offset) + }) + }); //Done! ( @@ -598,6 +597,7 @@ impl Element for TerminalEl { size: dimensions, rects, highlights, + display_offset, }, ) } @@ -616,7 +616,14 @@ impl Element for TerminalEl { let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.size, cx); + self.attach_mouse_handlers( + origin, + self.view.id(), + visible_bounds, + layout.size, + layout.display_offset, + cx, + ); cx.paint_layer(clip_bounds, |cx| { //Start with a background color @@ -694,9 +701,9 @@ impl Element for TerminalEl { (delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER; self.terminal.upgrade(cx.app).map(|terminal| { - terminal - .read(cx.app) - .scroll(Scroll::Delta(vertical_scroll.round() as i32)); + terminal.update(cx.app, |term, _| { + term.scroll(Scroll::Delta(vertical_scroll.round() as i32)) + }); }); }) .is_some(), diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index c235f68e58..9cef851079 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -43,11 +43,12 @@ impl ConnectedView { modal: bool, cx: &mut ViewContext, ) -> Self { - // cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); //Terminal notifies for us + cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); cx.subscribe(&terminal, |this, _, event, cx| match event { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; + cx.notify(); cx.emit(Event::Wakeup); } } @@ -91,7 +92,7 @@ impl ConnectedView { ///Attempt to paste the clipboard into the terminal fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal.read(cx).copy() + self.terminal.update(cx, |term, _| term.copy()) } ///Attempt to paste the clipboard into the terminal diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index c353db57a0..58d4b1e4c0 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -40,8 +40,8 @@ impl Modifiers { ///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal ///lock to determine whether we could actually send the keystroke with the current settings. Therefore, ///This conservative guess is used instead. Note that in practice the case where this method -///Returns false when the actual terminal would consume the keystroke never actually happens. All -///keystrokes that depend on terminal modes also have a mapping that doesn't depend on the terminal mode. +///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes +///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode. ///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough pub fn might_convert(keystroke: &Keystroke) -> bool { to_esc_str(keystroke, &TermMode::NONE).is_some() diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3be4cb4675..802d3e59be 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -24,13 +24,10 @@ use alacritty_terminal::{ }; use anyhow::{bail, Result}; use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use itertools::Itertools; use mappings::keys::might_convert; use modal::deploy_modal; use settings::{Settings, Shell}; -use std::{ - cmp::Ordering, collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration, -}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; use terminal_view::TerminalView; use thiserror::Error; @@ -53,7 +50,7 @@ pub fn init(cx: &mut MutableAppContext) { connected_view::init(cx); } -const DEBUG_TERMINAL_WIDTH: f32 = 100.; +const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; @@ -77,7 +74,7 @@ enum InternalEvent { Paste(String), Scroll(Scroll), SetSelection(Option), - UpdateSelection(Point), + UpdateSelection((Point, Direction)), Copy, } @@ -337,7 +334,7 @@ impl TerminalBuilder { pty_tx: Notifier(pty_tx), term, - event_stack: vec![], + events: vec![], title: shell_txt.clone(), default_title: shell_txt, }; @@ -366,12 +363,10 @@ impl TerminalBuilder { Err(_) => break, } } - match this.upgrade(&cx) { Some(this) => { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _cx| { this.push_events(events); - cx.notify(); }); } None => break 'outer, @@ -389,14 +384,14 @@ impl TerminalBuilder { pub struct Terminal { pty_tx: Notifier, term: Arc>>, - event_stack: Vec, + events: Vec, default_title: String, title: String, } impl Terminal { fn push_events(&mut self, events: Vec) { - self.event_stack + self.events .extend(events.into_iter().map(|e| InternalEvent::TermEvent(e))) } @@ -407,6 +402,7 @@ impl Terminal { term: &mut Term, cx: &mut ModelContext, ) { + dbg!(event); // TODO: Handle is_self_focused in subscription on terminal view match event { InternalEvent::TermEvent(term_event) => match term_event { @@ -467,6 +463,7 @@ impl Terminal { term.clear_screen(ClearMode::Saved); } InternalEvent::Keystroke(keystroke) => { + println!("Trying keystroke: {}", keystroke); let esc = to_esc_str(keystroke, term.mode()); if let Some(esc) = esc { self.notify_pty(esc); @@ -482,10 +479,10 @@ impl Terminal { } } InternalEvent::Scroll(scroll) => term.scroll_display(*scroll), - InternalEvent::SetSelection(sel) => term.selection = sel, - InternalEvent::UpdateSelection(point) => { + InternalEvent::SetSelection(sel) => term.selection = sel.clone(), + InternalEvent::UpdateSelection((point, side)) => { if let Some(mut selection) = term.selection.take() { - selection.update(*point, side); + selection.update(*point, *side); term.selection = Some(selection); } } @@ -502,30 +499,24 @@ impl Terminal { self.pty_tx.notify(txt.into_bytes()); } - //TODO: - // - Continue refactor into event system - // - Fix PTYWrite call to not be circular and messy - // - Change title to be emitted and maintained externally - ///Write the Input payload to the tty. pub fn write_to_pty(&mut self, input: String) { - self.event_stack + self.events .push(InternalEvent::TermEvent(AlacTermEvent::PtyWrite(input))) } ///Resize the terminal and the PTY. pub fn set_size(&mut self, new_size: TermDimensions) { - self.event_stack - .push(InternalEvent::Resize(new_size.into())) + self.events.push(InternalEvent::Resize(new_size.into())) } pub fn clear(&mut self) { - self.event_stack.push(InternalEvent::Clear) + self.events.push(InternalEvent::Clear) } pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { if might_convert(keystroke) { - self.event_stack + self.events .push(InternalEvent::Keystroke(keystroke.clone())); true } else { @@ -535,27 +526,25 @@ impl Terminal { ///Paste text into the terminal pub fn paste(&mut self, text: &str) { - self.event_stack - .push(InternalEvent::Paste(text.to_string())); + self.events.push(InternalEvent::Paste(text.to_string())); } - pub fn copy(&self) { - self.event_stack.push(InternalEvent::Copy); + pub fn copy(&mut self) { + self.events.push(InternalEvent::Copy); } pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where F: FnOnce(RenderableContent, char) -> T, { + println!("RENDER LOCK!"); let m = self.term.clone(); //Arc clone let mut term = m.lock(); - for event in self.event_stack.clone().into_iter().sorted() { - self.process_terminal_event(&event, &mut term, cx) + while let Some(e) = self.events.pop() { + self.process_terminal_event(&e, &mut term, cx) } - self.event_stack.clear(); - let content = term.renderable_content(); let cursor_text = term.grid()[content.cursor.point].c; @@ -563,11 +552,11 @@ impl Terminal { } ///Scroll the terminal - pub fn scroll(&self, _scroll: Scroll) { - self.event_stack.push(InternalEvent::Scroll(scroll)); + pub fn scroll(&mut self, scroll: Scroll) { + self.events.push(InternalEvent::Scroll(scroll)); } - pub fn click(&self, point: Point, side: Direction, clicks: usize) { + pub fn click(&mut self, point: Point, side: Direction, clicks: usize) { let selection_type = match clicks { 0 => return, //This is a release 1 => Some(SelectionType::Simple), @@ -579,17 +568,17 @@ impl Terminal { let selection = selection_type.map(|selection_type| Selection::new(selection_type, point, side)); - self.event_stack - .push(InternalEvent::SetSelection(selection)); + self.events.push(InternalEvent::SetSelection(selection)); } - pub fn drag(&self, point: Point, side: Direction) { - self.event_stack.push(InternalEvent::UpdateSelection(point)); + pub fn drag(&mut self, point: Point, side: Direction) { + self.events + .push(InternalEvent::UpdateSelection((point, side))); } ///TODO: Check if the mouse_down-then-click assumption holds, so this code works as expected - pub fn mouse_down(&self, point: Point, side: Direction) { - self.event_stack + pub fn mouse_down(&mut self, point: Point, side: Direction) { + self.events .push(InternalEvent::SetSelection(Some(Selection::new( SelectionType::Simple, point, diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 655be38ad0..14df31d956 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -65,14 +65,13 @@ impl TerminalView { workspace.add_item(Box::new(view), cx); } - ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices - ///To get the right working directory from a workspace, use: `get_wd_for_workspace()` + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices pub fn new( working_directory: Option, modal: bool, cx: &mut ViewContext, ) -> Self { - //The details here don't matter, the terminal will be resized on the first layout + //The exact size here doesn't matter, the terminal will be resized on the first layout let size_info = TermDimensions::default(); let settings = cx.global::(); From 153305f5e45bbccd9083f8228fe436729f0eba34 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Jul 2022 10:21:50 -0700 Subject: [PATCH 12/22] Finished long-lock style rendering. Need to dynamically adjust the notification rate to handle high throughput scenarios --- crates/terminal/src/terminal.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 802d3e59be..f1fd0d961b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -345,11 +345,14 @@ impl TerminalBuilder { }) } + //TODO: Adaptive framerate mechanism for high throughput times? + //Probably nescessary... + pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { cx.spawn_weak(|this, mut cx| async move { 'outer: loop { //TODO: Pending GPUI updates, sync this to some higher, smarter system. - let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 60.)); + let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); let mut events = vec![]; @@ -365,8 +368,9 @@ impl TerminalBuilder { } match this.upgrade(&cx) { Some(this) => { - this.update(&mut cx, |this, _cx| { + this.update(&mut cx, |this, cx| { this.push_events(events); + cx.notify(); }); } None => break 'outer, @@ -402,7 +406,6 @@ impl Terminal { term: &mut Term, cx: &mut ModelContext, ) { - dbg!(event); // TODO: Handle is_self_focused in subscription on terminal view match event { InternalEvent::TermEvent(term_event) => match term_event { @@ -463,7 +466,6 @@ impl Terminal { term.clear_screen(ClearMode::Saved); } InternalEvent::Keystroke(keystroke) => { - println!("Trying keystroke: {}", keystroke); let esc = to_esc_str(keystroke, term.mode()); if let Some(esc) = esc { self.notify_pty(esc); @@ -537,7 +539,6 @@ impl Terminal { where F: FnOnce(RenderableContent, char) -> T, { - println!("RENDER LOCK!"); let m = self.term.clone(); //Arc clone let mut term = m.lock(); From 0ccdc64668262612bfdff0e37b1ee1f63f007401 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Jul 2022 10:58:23 -0700 Subject: [PATCH 13/22] Working on finding a way of estimating throughput --- crates/terminal/src/terminal.rs | 59 ++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f1fd0d961b..12720ad7ea 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -54,6 +54,7 @@ const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; +const TERM_FRAME_RATE: f32 = 120.; ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] @@ -349,31 +350,43 @@ impl TerminalBuilder { //Probably nescessary... pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { + let mut frames_to_skip = 0; cx.spawn_weak(|this, mut cx| async move { 'outer: loop { //TODO: Pending GPUI updates, sync this to some higher, smarter system. - let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.)); + //Note: This sampling interval is too high on really hammering programs like + //`yes`. Zed is just usable enough to cancel the command at this interval. + //To properly fix this, I'll need to do some dynamic reworking of this number + //Based on the current throughput. See what iterm2 does: + //https://news.ycombinator.com/item?id=17634547#17635856 + let delay = cx + .background() + .timer(Duration::from_secs_f32(1.0 / TERM_FRAME_RATE)); + if frames_to_skip == 0 { + let mut events = vec![]; - let mut events = vec![]; - - loop { - match self.events_rx.try_next() { - //Have a buffered event - Ok(Some(e)) => events.push(e), - //Channel closed, exit - Ok(None) => break 'outer, - //Ran out of buffered events - Err(_) => break, + loop { + match self.events_rx.try_next() { + //Have a buffered event + Ok(Some(e)) => events.push(e), + //Channel closed, exit + Ok(None) => break 'outer, + //Ran out of buffered events + Err(_) => break, + } } - } - match this.upgrade(&cx) { - Some(this) => { - this.update(&mut cx, |this, cx| { - this.push_events(events); - cx.notify(); - }); + match this.upgrade(&cx) { + Some(this) => { + this.update(&mut cx, |this, cx| { + this.push_events(events); + frames_to_skip = this.frames_to_skip(); + cx.notify(); + }); + } + None => break 'outer, } - None => break 'outer, + } else { + frames_to_skip = frames_to_skip - 1; } delay.await; @@ -394,6 +407,11 @@ pub struct Terminal { } impl Terminal { + ///Tells the render loop how many frames to skip before reading from the terminal. + fn frames_to_skip(&self) -> usize { + 4 + } + fn push_events(&mut self, events: Vec) { self.events .extend(events.into_iter().map(|e| InternalEvent::TermEvent(e))) @@ -547,6 +565,9 @@ impl Terminal { } let content = term.renderable_content(); + + println!("Offset: {}", term.grid().history_size()); + let cursor_text = term.grid()[content.cursor.point].c; f(content, cursor_text) From 8a48a11a004dcb0418a8682d1b4537b3f50511c5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Jul 2022 16:33:15 -0700 Subject: [PATCH 14/22] Implemcargo --- Cargo.lock | 4 +-- crates/terminal/Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 43 ++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e9e976efd..2c2f71b544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" +source = "git+https://github.com/zed-industries/alacritty?rev=382e1892c4eaae7d45ec6ea580db47242ec66e31#382e1892c4eaae7d45ec6ea580db47242ec66e31" dependencies = [ "proc-macro2", "quote", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.17.0-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=e9b864860ec79cc1b70042aafce100cdd6985a0a#e9b864860ec79cc1b70042aafce100cdd6985a0a" +source = "git+https://github.com/zed-industries/alacritty?rev=382e1892c4eaae7d45ec6ea580db47242ec66e31#382e1892c4eaae7d45ec6ea580db47242ec66e31" dependencies = [ "alacritty_config_derive", "base64 0.13.0", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 03c6a26b7d..e6d25f495b 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "e9b864860ec79cc1b70042aafce100cdd6985a0a"} +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "382e1892c4eaae7d45ec6ea580db47242ec66e31"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 12720ad7ea..47093f41e9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -54,7 +54,7 @@ const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -const TERM_FRAME_RATE: f32 = 120.; +const MAX_FRAME_RATE: f32 = 120.; ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] @@ -361,7 +361,7 @@ impl TerminalBuilder { //https://news.ycombinator.com/item?id=17634547#17635856 let delay = cx .background() - .timer(Duration::from_secs_f32(1.0 / TERM_FRAME_RATE)); + .timer(Duration::from_secs_f32(1.0 / Terminal::default_fps())); if frames_to_skip == 0 { let mut events = vec![]; @@ -404,12 +404,17 @@ pub struct Terminal { events: Vec, default_title: String, title: String, + frames_to_skip: usize, } impl Terminal { + fn default_fps() -> f32 { + MAX_FRAME_RATE + } + ///Tells the render loop how many frames to skip before reading from the terminal. fn frames_to_skip(&self) -> usize { - 4 + self.frames_to_skip } fn push_events(&mut self, events: Vec) { @@ -430,6 +435,7 @@ impl Terminal { AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); } + //TODO: Does not need to be in lock context AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()), AlacTermEvent::MouseCursorDirty => { //Calculate new cursor style. @@ -445,9 +451,11 @@ impl Terminal { self.title = self.default_title.clone(); cx.emit(Event::TitleChanged); } + //TODO: Does not need to be in lock context AlacTermEvent::ClipboardStore(_, data) => { cx.write_to_clipboard(ClipboardItem::new(data.to_string())) } + //TODO: Does not need to be in lock context AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format( &cx.read_from_clipboard() .map(|ci| ci.text().to_string()) @@ -557,6 +565,8 @@ impl Terminal { where F: FnOnce(RenderableContent, char) -> T, { + let back_buffer_size = 5000; + let m = self.term.clone(); //Arc clone let mut term = m.lock(); @@ -564,9 +574,34 @@ impl Terminal { self.process_terminal_event(&e, &mut term, cx) } + // let overflow_size = term.grid().total_lines() + + //We need something that starts at 0, and grows upward. + + //Concept: Set storage twice as long as is actually available. + //Alacritty Default is 10,000, so for now hardcode 5,000 lines for history + //and 5,000 for measurement (Later, put this in configuration 😤) + //Measure the number of lines over 5,000 and the time since last frame + //divide for velocity + //This is the velocity of lines. map linearly to [0..10] (with .round()) + //(Later, perhaps make this an exponential backoff) + //Report that number as the skip frames. + + let velocity = term.grid().history_size().saturating_sub(back_buffer_size); + + let fractional_velocity = velocity as f32 / back_buffer_size as f32; + + //3rd power + let scaled_fraction = fractional_velocity * fractional_velocity * fractional_velocity; + + self.frames_to_skip = (scaled_fraction * Self::default_fps() / 10.).round() as usize; + + term.grid_mut().update_history(back_buffer_size); //Clear out the measurement space + term.grid_mut().update_history(back_buffer_size * 2); //Extra space for measuring + let content = term.renderable_content(); - println!("Offset: {}", term.grid().history_size()); + println!("Offset: {}", term.grid().total_lines()); let cursor_text = term.grid()[content.cursor.point].c; From 57146b6e398e1fefe1eb74210152b9781bd9e953 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Jul 2022 16:46:15 -0700 Subject: [PATCH 15/22] Added variable rate refreshing based on terminal throughput. Should be the last of the performance improvements for now --- crates/terminal/src/connected_el.rs | 2 +- crates/terminal/src/terminal.rs | 49 +++++++++-------------------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index c42a0a94b0..edb50e7e6a 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -35,7 +35,7 @@ use crate::{ ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. -const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; ///The information generated during layout that is nescessary for painting pub struct LayoutState { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 47093f41e9..d7efecc486 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -11,7 +11,7 @@ use alacritty_terminal::Grid; use alacritty_terminal::{ ansi::{ClearMode, Handler}, - config::{Config, Program, PtyConfig}, + config::{Config, Program, PtyConfig, Scrolling}, event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll}, @@ -23,6 +23,7 @@ use alacritty_terminal::{ Term, }; use anyhow::{bail, Result}; + use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use mappings::keys::might_convert; use modal::deploy_modal; @@ -55,6 +56,7 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; const MAX_FRAME_RATE: f32 = 120.; +const BACK_BUFFER_SIZE: usize = 5000; ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] @@ -278,9 +280,13 @@ impl TerminalBuilder { //TODO: Properly set the current locale, env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); + let mut alac_scrolling = Scrolling::default(); + alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32); + let config = Config { pty_config: pty_config.clone(), env, + scrolling: alac_scrolling, ..Default::default() }; @@ -338,6 +344,7 @@ impl TerminalBuilder { events: vec![], title: shell_txt.clone(), default_title: shell_txt, + frames_to_skip: 0, }; Ok(TerminalBuilder { @@ -346,19 +353,10 @@ impl TerminalBuilder { }) } - //TODO: Adaptive framerate mechanism for high throughput times? - //Probably nescessary... - pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { let mut frames_to_skip = 0; cx.spawn_weak(|this, mut cx| async move { 'outer: loop { - //TODO: Pending GPUI updates, sync this to some higher, smarter system. - //Note: This sampling interval is too high on really hammering programs like - //`yes`. Zed is just usable enough to cancel the command at this interval. - //To properly fix this, I'll need to do some dynamic reworking of this number - //Based on the current throughput. See what iterm2 does: - //https://news.ycombinator.com/item?id=17634547#17635856 let delay = cx .background() .timer(Duration::from_secs_f32(1.0 / Terminal::default_fps())); @@ -565,8 +563,6 @@ impl Terminal { where F: FnOnce(RenderableContent, char) -> T, { - let back_buffer_size = 5000; - let m = self.term.clone(); //Arc clone let mut term = m.lock(); @@ -574,35 +570,20 @@ impl Terminal { self.process_terminal_event(&e, &mut term, cx) } - // let overflow_size = term.grid().total_lines() + let buffer_velocity = term.grid().history_size().saturating_sub(BACK_BUFFER_SIZE); - //We need something that starts at 0, and grows upward. + let fractional_velocity = buffer_velocity as f32 / BACK_BUFFER_SIZE as f32; - //Concept: Set storage twice as long as is actually available. - //Alacritty Default is 10,000, so for now hardcode 5,000 lines for history - //and 5,000 for measurement (Later, put this in configuration 😤) - //Measure the number of lines over 5,000 and the time since last frame - //divide for velocity - //This is the velocity of lines. map linearly to [0..10] (with .round()) - //(Later, perhaps make this an exponential backoff) - //Report that number as the skip frames. + //2nd power + let scaled_fraction = fractional_velocity * fractional_velocity; - let velocity = term.grid().history_size().saturating_sub(back_buffer_size); + self.frames_to_skip = (scaled_fraction * (Self::default_fps() / 10.)).round() as usize; - let fractional_velocity = velocity as f32 / back_buffer_size as f32; - - //3rd power - let scaled_fraction = fractional_velocity * fractional_velocity * fractional_velocity; - - self.frames_to_skip = (scaled_fraction * Self::default_fps() / 10.).round() as usize; - - term.grid_mut().update_history(back_buffer_size); //Clear out the measurement space - term.grid_mut().update_history(back_buffer_size * 2); //Extra space for measuring + term.grid_mut().update_history(BACK_BUFFER_SIZE); //Clear out the measurement space + term.grid_mut().update_history(BACK_BUFFER_SIZE * 2); //Extra space for measuring let content = term.renderable_content(); - println!("Offset: {}", term.grid().total_lines()); - let cursor_text = term.grid()[content.cursor.point].c; f(content, cursor_text) From 81cbdcfd11459a1aad6735a7dc4eafd55f0f1a82 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 28 Jul 2022 14:58:19 -0700 Subject: [PATCH 16/22] Reduced time holding lock even more --- crates/terminal/src/connected_el.rs | 128 +++++++++++++++++----------- 1 file changed, 78 insertions(+), 50 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index edb50e7e6a..2b1239b52d 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -1,6 +1,6 @@ use alacritty_terminal::{ ansi::{Color::Named, NamedColor}, - grid::{Dimensions, GridIterator, Indexed, Scroll}, + grid::{Dimensions, Scroll}, index::{Column as GridCol, Line as GridLine, Point, Side}, selection::SelectionRange, term::cell::{Cell, Flags}, @@ -25,7 +25,10 @@ use settings::Settings; use theme::TerminalStyle; use util::ResultExt; -use std::{cmp::min, ops::Range}; +use std::{ + cmp::min, + ops::{Deref, Range}, +}; use std::{fmt::Debug, ops::Sub}; use crate::{ @@ -49,6 +52,20 @@ pub struct LayoutState { display_offset: usize, } +struct IndexedCell { + point: Point, + cell: Cell, +} + +impl Deref for IndexedCell { + type Target = Cell; + + #[inline] + fn deref(&self) -> &Cell { + &self.cell + } +} + ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points struct DisplayCursor { line: i32, @@ -190,7 +207,7 @@ impl TerminalEl { } fn layout_grid( - grid: GridIterator, + grid: Vec, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, @@ -209,7 +226,7 @@ impl TerminalEl { let mut cur_alac_color = None; let mut highlighted_range = None; - let linegroups = grid.group_by(|i| i.point.line); + let linegroups = grid.into_iter().group_by(|i| i.point.line); for (line_index, (_, line)) in linegroups.into_iter().enumerate() { for (x_index, cell) in line.enumerate() { //Increase selection range @@ -236,7 +253,7 @@ impl TerminalEl { } else { match cur_alac_color { Some(cur_color) => { - if cell.bg == cur_color { + if cell.cell.bg == cur_color { cur_rect = cur_rect.take().map(|rect| rect.extend()); } else { cur_alac_color = Some(cell.bg); @@ -326,7 +343,7 @@ impl TerminalEl { ///Convert the Alacritty cell styles to GPUI text styles and background color fn cell_style( - indexed: &Indexed<&Cell>, + indexed: &IndexedCell, style: &TerminalStyle, text_style: &TextStyle, modal: bool, @@ -532,60 +549,71 @@ impl Element for TerminalEl { terminal_theme.colors.background.clone() }; - let (cursor, cells, rects, highlights, display_offset) = self + let (cells, selection, cursor, display_offset, cursor_text) = self .terminal .upgrade(cx) .unwrap() .update(cx.app, |terminal, mcx| { terminal.set_size(dimensions); terminal.render_lock(mcx, |content, cursor_text| { - let (cells, rects, highlights) = TerminalEl::layout_grid( - content.display_iter, - &text_style, - &terminal_theme, - cx.text_layout_cache, - self.modal, - content.selection, - ); + let mut cells = vec![]; + cells.extend(content.display_iter.map(|ic| IndexedCell { + point: ic.point.clone(), + cell: ic.cell.clone(), + })); - //Layout cursor - let cursor = { - let cursor_point = - DisplayCursor::from(content.cursor.point, content.display_offset); - let cursor_text = { - let str_trxt = cursor_text.to_string(); - cx.text_layout_cache.layout_str( - &str_trxt, - text_style.font_size, - &[( - str_trxt.len(), - RunStyle { - font_id: text_style.font_id, - color: terminal_theme.colors.background, - underline: Default::default(), - }, - )], - ) - }; - - TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - Cursor::new( - cursor_position, - block_width, - dimensions.line_height, - terminal_theme.colors.cursor, - CursorShape::Block, - Some(cursor_text.clone()), - ) - }, - ) - }; - - (cursor, cells, rects, highlights, content.display_offset) + ( + cells, + content.selection.clone(), + content.cursor.clone(), + content.display_offset.clone(), + cursor_text.clone(), + ) }) }); + let (cells, rects, highlights) = TerminalEl::layout_grid( + cells, + &text_style, + &terminal_theme, + cx.text_layout_cache, + self.modal, + selection, + ); + + //Layout cursor + let cursor = { + let cursor_point = DisplayCursor::from(cursor.point, display_offset); + let cursor_text = { + let str_trxt = cursor_text.to_string(); + cx.text_layout_cache.layout_str( + &str_trxt, + text_style.font_size, + &[( + str_trxt.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.colors.background, + underline: Default::default(), + }, + )], + ) + }; + + TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.colors.cursor, + CursorShape::Block, + Some(cursor_text.clone()), + ) + }, + ) + }; + //Done! ( constraint.max, From 8471af5a7d092c9280cba5427f3a550bf0b4de32 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 28 Jul 2022 16:03:00 -0700 Subject: [PATCH 17/22] Improved render performance implementation to use a fork of alacritty which includes the last # of bytes processed as a way of estimating throughput in cases where the terminal output is chanegd in place --- Cargo.lock | 4 ++-- crates/terminal/Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 19 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c2f71b544..d6310d76db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "git+https://github.com/zed-industries/alacritty?rev=382e1892c4eaae7d45ec6ea580db47242ec66e31#382e1892c4eaae7d45ec6ea580db47242ec66e31" +source = "git+https://github.com/zed-industries/alacritty?rev=ba56f545e3e3606af0112c6bdfe998baf7faab50#ba56f545e3e3606af0112c6bdfe998baf7faab50" dependencies = [ "proc-macro2", "quote", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.17.0-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=382e1892c4eaae7d45ec6ea580db47242ec66e31#382e1892c4eaae7d45ec6ea580db47242ec66e31" +source = "git+https://github.com/zed-industries/alacritty?rev=ba56f545e3e3606af0112c6bdfe998baf7faab50#ba56f545e3e3606af0112c6bdfe998baf7faab50" dependencies = [ "alacritty_config_derive", "base64 0.13.0", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index e6d25f495b..354952481b 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "382e1892c4eaae7d45ec6ea580db47242ec66e31"} +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "ba56f545e3e3606af0112c6bdfe998baf7faab50"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d7efecc486..d26f694abf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -13,7 +13,7 @@ use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig, Scrolling}, event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, + event_loop::{EventLoop, Msg, Notifier, READ_BUFFER_SIZE}, grid::{Dimensions, Scroll}, index::{Direction, Point}, selection::{Selection, SelectionType}, @@ -55,7 +55,7 @@ const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space. const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -const MAX_FRAME_RATE: f32 = 120.; +const MAX_FRAME_RATE: f32 = 60.; const BACK_BUFFER_SIZE: usize = 5000; ///Upward flowing events, for changing the title and such @@ -412,7 +412,7 @@ impl Terminal { ///Tells the render loop how many frames to skip before reading from the terminal. fn frames_to_skip(&self) -> usize { - self.frames_to_skip + 0 //self.frames_to_skip } fn push_events(&mut self, events: Vec) { @@ -570,17 +570,16 @@ impl Terminal { self.process_terminal_event(&e, &mut term, cx) } - let buffer_velocity = term.grid().history_size().saturating_sub(BACK_BUFFER_SIZE); - - let fractional_velocity = buffer_velocity as f32 / BACK_BUFFER_SIZE as f32; + //TODO: determine a better metric for this + let buffer_velocity = + (term.last_processed_bytes() as f32 / (READ_BUFFER_SIZE as f32 / 4.)).clamp(0., 1.); //2nd power - let scaled_fraction = fractional_velocity * fractional_velocity; + let scaled_velocity = buffer_velocity * buffer_velocity; - self.frames_to_skip = (scaled_fraction * (Self::default_fps() / 10.)).round() as usize; + self.frames_to_skip = (scaled_velocity * (Self::default_fps() / 10.)).round() as usize; - term.grid_mut().update_history(BACK_BUFFER_SIZE); //Clear out the measurement space - term.grid_mut().update_history(BACK_BUFFER_SIZE * 2); //Extra space for measuring + term.set_last_processed_bytes(0); //Clear it in case no reads between this lock and the next. let content = term.renderable_content(); From 05cc78d929d8ffac6b32f5e4c6a11d1d8428ed71 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 1 Aug 2022 16:47:16 -0700 Subject: [PATCH 18/22] Abandoning this attempt, nto good enough at async --- Cargo.lock | 4 +- crates/terminal/Cargo.toml | 2 +- crates/terminal/src/connected_el.rs | 14 +- crates/terminal/src/connected_view.rs | 46 ++- crates/terminal/src/terminal.rs | 347 +++++++++++------- crates/terminal/src/terminal_view.rs | 4 +- .../src/tests/terminal_test_context.rs | 4 +- 7 files changed, 263 insertions(+), 158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6310d76db..7494cb6782 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.1.0" -source = "git+https://github.com/zed-industries/alacritty?rev=ba56f545e3e3606af0112c6bdfe998baf7faab50#ba56f545e3e3606af0112c6bdfe998baf7faab50" +source = "git+https://github.com/zed-industries/alacritty?rev=a274ff43c38c76f9766a908ff86a4e10a8998a6f#a274ff43c38c76f9766a908ff86a4e10a8998a6f" dependencies = [ "proc-macro2", "quote", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.17.0-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=ba56f545e3e3606af0112c6bdfe998baf7faab50#ba56f545e3e3606af0112c6bdfe998baf7faab50" +source = "git+https://github.com/zed-industries/alacritty?rev=a274ff43c38c76f9766a908ff86a4e10a8998a6f#a274ff43c38c76f9766a908ff86a4e10a8998a6f" dependencies = [ "alacritty_config_derive", "base64 0.13.0", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 354952481b..836555e963 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -8,7 +8,7 @@ path = "src/terminal.rs" doctest = false [dependencies] -alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "ba56f545e3e3606af0112c6bdfe998baf7faab50"} +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a274ff43c38c76f9766a908ff86a4e10a8998a6f"} editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 2b1239b52d..510d55fac2 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -32,7 +32,7 @@ use std::{ use std::{fmt::Debug, ops::Sub}; use crate::{ - connected_view::ConnectedView, mappings::colors::convert_color, TermDimensions, Terminal, + connected_view::ConnectedView, mappings::colors::convert_color, Terminal, TerminalSize, }; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable @@ -48,7 +48,7 @@ pub struct LayoutState { cursor: Option, background_color: Color, selection_color: Color, - size: TermDimensions, + size: TerminalSize, display_offset: usize, } @@ -319,7 +319,7 @@ impl TerminalEl { // the same position for sequential indexes. Use em_width instead fn shape_cursor( cursor_point: DisplayCursor, - size: TermDimensions, + size: TerminalSize, text_fragment: &Line, ) -> Option<(Vector2F, f32)> { if cursor_point.line() < size.total_lines() as i32 { @@ -372,7 +372,7 @@ impl TerminalEl { origin: Vector2F, view_id: usize, visible_bounds: RectF, - cur_size: TermDimensions, + cur_size: TerminalSize, display_offset: usize, cx: &mut PaintContext, ) { @@ -482,7 +482,7 @@ impl TerminalEl { pub fn mouse_to_cell_data( pos: Vector2F, origin: Vector2F, - cur_size: TermDimensions, + cur_size: TerminalSize, display_offset: usize, ) -> (Point, alacritty_terminal::index::Direction) { let pos = pos.sub(origin); @@ -540,7 +540,7 @@ impl Element for TerminalEl { let dimensions = { let line_height = font_cache.line_height(text_style.font_size); let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); - TermDimensions::new(line_height, cell_width, constraint.max) + TerminalSize::new(line_height, cell_width, constraint.max) }; let background_color = if self.modal { @@ -807,7 +807,7 @@ mod test { let origin_x = 10.; let origin_y = 20.; - let cur_size = crate::connected_el::TermDimensions::new( + let cur_size = crate::connected_el::TerminalSize::new( line_height, cell_width, gpui::geometry::vector::vec2f(term_width, term_height), diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 9cef851079..d0d63b6f49 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -1,3 +1,4 @@ +use alacritty_terminal::term::TermMode; use gpui::{ actions, keymap::Keystroke, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View, ViewContext, @@ -98,43 +99,43 @@ impl ConnectedView { ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { cx.read_from_clipboard().map(|item| { - self.terminal.update(cx, |term, _| term.paste(item.text())); + self.terminal.read(cx).paste(item.text()); }); } ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("up").unwrap()); - }); + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("up").unwrap()); } ///Synthesize the keyboard event corresponding to 'down' fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("down").unwrap()); - }); + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("down").unwrap()); } ///Synthesize the keyboard event corresponding to 'ctrl-c' fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); - }); + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); } ///Synthesize the keyboard event corresponding to 'escape' fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("escape").unwrap()); - }); + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("escape").unwrap()); } ///Synthesize the keyboard event corresponding to 'enter' fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("enter").unwrap()); - }); + self.terminal + .read(cx) + .try_keystroke(&Keystroke::parse("enter").unwrap()); } } @@ -154,8 +155,17 @@ impl View for ConnectedView { self.has_new_content = false; } - fn selected_text_range(&self, _: &AppContext) -> Option> { - Some(0..0) + fn selected_text_range(&self, cx: &AppContext) -> Option> { + if self + .terminal + .read(cx) + .last_mode + .contains(TermMode::ALT_SCREEN) + { + None + } else { + Some(0..0) + } } fn replace_text_in_range( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d26f694abf..e81d455536 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -24,18 +24,27 @@ use alacritty_terminal::{ }; use anyhow::{bail, Result}; -use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use mappings::keys::might_convert; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, + future, +}; + use modal::deploy_modal; use settings::{Settings, Shell}; -use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + fmt::Display, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; use terminal_view::TerminalView; use thiserror::Error; use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, - ClipboardItem, CursorStyle, Entity, ModelContext, MutableAppContext, + AsyncAppContext, ClipboardItem, Entity, ModelContext, MutableAppContext, WeakModelHandle, }; use crate::mappings::{ @@ -71,10 +80,8 @@ pub enum Event { #[derive(Clone, Debug)] enum InternalEvent { TermEvent(AlacTermEvent), - Resize(TermDimensions), + Resize(TerminalSize), Clear, - Keystroke(Keystroke), - Paste(String), Scroll(Scroll), SetSelection(Option), UpdateSelection((Point, Direction)), @@ -92,16 +99,16 @@ impl EventListener for ZedListener { } #[derive(Clone, Copy, Debug)] -pub struct TermDimensions { +pub struct TerminalSize { cell_width: f32, line_height: f32, height: f32, width: f32, } -impl TermDimensions { +impl TerminalSize { pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self { - TermDimensions { + TerminalSize { cell_width, line_height, width: size.x(), @@ -133,9 +140,9 @@ impl TermDimensions { self.line_height } } -impl Default for TermDimensions { +impl Default for TerminalSize { fn default() -> Self { - TermDimensions::new( + TerminalSize::new( DEBUG_LINE_HEIGHT, DEBUG_CELL_WIDTH, vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT), @@ -143,7 +150,7 @@ impl Default for TermDimensions { } } -impl Into for TermDimensions { +impl Into for TerminalSize { fn into(self) -> WindowSize { WindowSize { num_lines: self.num_lines() as u16, @@ -154,7 +161,7 @@ impl Into for TermDimensions { } } -impl Dimensions for TermDimensions { +impl Dimensions for TerminalSize { fn total_lines(&self) -> usize { self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer... } @@ -249,6 +256,46 @@ impl Display for TerminalError { } } +///This is a helper struct that represents a block on a +struct ThrottleGuard { + should_complete: bool, + length: Duration, + start: Instant, +} + +impl ThrottleGuard { + fn new(duration: Duration, cx: &mut ModelContext, on_completion: F) -> Arc + where + T: Entity, + F: FnOnce(WeakModelHandle, AsyncAppContext) -> () + 'static, + { + let selff = Arc::new(Self { + should_complete: false, + start: Instant::now(), + length: duration, + }); + + let moved_self = selff.clone(); + cx.spawn_weak(|w, cx| async move { + cx.background().timer(duration).await; + + if moved_self.should_complete { + on_completion(w, cx); + } + }); + + selff + } + + fn activate(&mut self) { + self.should_complete = true; + } + + fn is_done(&self) -> bool { + self.start.elapsed() > self.length + } +} + pub struct TerminalBuilder { terminal: Terminal, events_rx: UnboundedReceiver, @@ -259,7 +306,7 @@ impl TerminalBuilder { working_directory: Option, shell: Option, env: Option>, - initial_size: TermDimensions, + initial_size: TerminalSize, ) -> Result { let pty_config = { let alac_shell = shell.clone().and_then(|shell| match shell { @@ -299,7 +346,7 @@ impl TerminalBuilder { let term = Arc::new(FairMutex::new(term)); //Setup the pty... - let pty = match tty::new(&pty_config, initial_size.into(), None) { + let pty = match tty::new(&pty_config, initial_size.clone().into(), None) { Ok(pty) => pty, Err(error) => { bail!(TerminalError { @@ -340,11 +387,12 @@ impl TerminalBuilder { let terminal = Terminal { pty_tx: Notifier(pty_tx), term, - events: vec![], title: shell_txt.clone(), default_title: shell_txt, - frames_to_skip: 0, + last_mode: TermMode::NONE, + cur_size: initial_size, + utilization: 0., }; Ok(TerminalBuilder { @@ -353,45 +401,93 @@ impl TerminalBuilder { }) } - pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { - let mut frames_to_skip = 0; + pub fn subscribe(self, cx: &mut ModelContext) -> Terminal { + //Event loop cx.spawn_weak(|this, mut cx| async move { - 'outer: loop { - let delay = cx - .background() - .timer(Duration::from_secs_f32(1.0 / Terminal::default_fps())); - if frames_to_skip == 0 { - let mut events = vec![]; + use futures::StreamExt; - loop { - match self.events_rx.try_next() { - //Have a buffered event - Ok(Some(e)) => events.push(e), - //Channel closed, exit - Ok(None) => break 'outer, - //Ran out of buffered events - Err(_) => break, - } - } + //Throttle guard + let mut guard: Option> = None; + + self.events_rx + .for_each(|event| { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - this.push_events(events); - frames_to_skip = this.frames_to_skip(); - cx.notify(); + //Process the event + this.process_event(&event, cx); + + //Clean up the guard if it's expired + guard = match guard.take() { + Some(guard) => { + if guard.is_done() { + None + } else { + Some(guard) + } + } + None => None, + }; + + //Figure out whether to render or not. + if matches!(event, AlacTermEvent::Wakeup) { + if guard.is_none() { + cx.emit(Event::Wakeup); + cx.notify(); + + let dur = Duration::from_secs_f32( + 1.0 / (Terminal::default_fps() + * (1. - this.utilization()).clamp(0.1, 1.)), + ); + + guard = Some(ThrottleGuard::new(dur, cx, |this, mut cx| { + match this.upgrade(&cx) { + Some(handle) => handle.update(&mut cx, |_, cx| { + cx.emit(Event::Wakeup); + cx.notify(); + }), + None => {} + } + })) + } else { + let taken = guard.take().unwrap(); + taken.activate(); + guard = Some(taken); + } + } }); } - None => break 'outer, + None => {} } - } else { - frames_to_skip = frames_to_skip - 1; - } - - delay.await; - } + future::ready(()) + }) + .await; }) .detach(); + // //Render loop + // cx.spawn_weak(|this, mut cx| async move { + // loop { + // let utilization = match this.upgrade(&cx) { + // Some(this) => this.update(&mut cx, |this, cx| { + // cx.emit(Event::Wakeup); + // cx.notify(); + // this.utilization() + // }), + // None => break, + // }; + + // let utilization = (1. - this.utilization()).clamp(0.1, 1.); + + // let delay = cx.background().timer(Duration::from_secs_f32( + // 1.0 / (Terminal::default_fps() * utilization), + // )); + + // delay.await; + // } + // }) + // .detach(); + self.terminal } } @@ -402,7 +498,10 @@ pub struct Terminal { events: Vec, default_title: String, title: String, - frames_to_skip: usize, + cur_size: TerminalSize, + last_mode: TermMode, + //Percentage, between 0 and 1 + utilization: f32, } impl Terminal { @@ -410,16 +509,57 @@ impl Terminal { MAX_FRAME_RATE } - ///Tells the render loop how many frames to skip before reading from the terminal. - fn frames_to_skip(&self) -> usize { - 0 //self.frames_to_skip + fn utilization(&self) -> f32 { + self.utilization } - fn push_events(&mut self, events: Vec) { - self.events - .extend(events.into_iter().map(|e| InternalEvent::TermEvent(e))) + fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext) { + match event { + AlacTermEvent::Title(title) => { + self.title = title.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = self.default_title.clone(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data.to_string())) + } + AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()), + AlacTermEvent::TextAreaSizeRequest(format) => { + self.notify_pty(format(self.cur_size.clone().into())) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO whatever state we need to set to get the cursor blinking + } + AlacTermEvent::Bell => { + cx.emit(Event::Bell); + } + AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), + AlacTermEvent::MouseCursorDirty => { + //NOOP, Handled in render + } + AlacTermEvent::Wakeup => { + //NOOP, Handled elsewhere + } + AlacTermEvent::ColorRequest(_, _) => { + self.events.push(InternalEvent::TermEvent(event.clone())) + } + } } + // fn process_events(&mut self, events: Vec, cx: &mut ModelContext) { + // for event in events.into_iter() { + // self.process_event(&event, cx); + // } + // } + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, @@ -430,35 +570,7 @@ impl Terminal { // TODO: Handle is_self_focused in subscription on terminal view match event { InternalEvent::TermEvent(term_event) => match term_event { - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - } - //TODO: Does not need to be in lock context - AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()), - AlacTermEvent::MouseCursorDirty => { - //Calculate new cursor style. - //TODO: alacritty/src/input.rs:L922-L939 - //Check on correctly handling mouse events for terminals - cx.platform().set_cursor_style(CursorStyle::Arrow); //??? - } - AlacTermEvent::Title(title) => { - self.title = title.to_string(); - cx.emit(Event::TitleChanged); - } - AlacTermEvent::ResetTitle => { - self.title = self.default_title.clone(); - cx.emit(Event::TitleChanged); - } - //TODO: Does not need to be in lock context - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new(data.to_string())) - } - //TODO: Does not need to be in lock context - AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format( - &cx.read_from_clipboard() - .map(|ci| ci.text().to_string()) - .unwrap_or("".to_string()), - )), + //Needs to lock AlacTermEvent::ColorRequest(index, format) => { let color = term.colors()[*index].unwrap_or_else(|| { let term_style = &cx.global::().theme.terminal; @@ -466,18 +578,11 @@ impl Terminal { }); self.notify_pty(format(color)) } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - AlacTermEvent::TextAreaSizeRequest(_) => { - println!("Received text area resize request") - } + _ => {} //Other events are handled in the event loop }, InternalEvent::Resize(new_size) => { + self.cur_size = new_size.clone(); + self.pty_tx .0 .send(Msg::Resize(new_size.clone().into())) @@ -489,21 +594,6 @@ impl Terminal { self.notify_pty("\x0c".to_string()); term.clear_screen(ClearMode::Saved); } - InternalEvent::Keystroke(keystroke) => { - let esc = to_esc_str(keystroke, term.mode()); - if let Some(esc) = esc { - self.notify_pty(esc); - } - } - InternalEvent::Paste(text) => { - if term.mode().contains(TermMode::BRACKETED_PASTE) { - self.notify_pty("\x1b[200~".to_string()); - self.notify_pty(text.replace('\x1b', "").to_string()); - self.notify_pty("\x1b[201~".to_string()); - } else { - self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r")); - } - } InternalEvent::Scroll(scroll) => term.scroll_display(*scroll), InternalEvent::SetSelection(sel) => term.selection = sel.clone(), InternalEvent::UpdateSelection((point, side)) => { @@ -521,18 +611,17 @@ impl Terminal { } } - fn notify_pty(&self, txt: String) { + pub fn notify_pty(&self, txt: String) { self.pty_tx.notify(txt.into_bytes()); } ///Write the Input payload to the tty. pub fn write_to_pty(&mut self, input: String) { - self.events - .push(InternalEvent::TermEvent(AlacTermEvent::PtyWrite(input))) + self.pty_tx.notify(input.into_bytes()); } ///Resize the terminal and the PTY. - pub fn set_size(&mut self, new_size: TermDimensions) { + pub fn set_size(&mut self, new_size: TerminalSize) { self.events.push(InternalEvent::Resize(new_size.into())) } @@ -540,10 +629,10 @@ impl Terminal { self.events.push(InternalEvent::Clear) } - pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool { - if might_convert(keystroke) { - self.events - .push(InternalEvent::Keystroke(keystroke.clone())); + pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool { + let esc = to_esc_str(keystroke, &self.last_mode); + if let Some(esc) = esc { + self.notify_pty(esc); true } else { false @@ -551,8 +640,14 @@ impl Terminal { } ///Paste text into the terminal - pub fn paste(&mut self, text: &str) { - self.events.push(InternalEvent::Paste(text.to_string())); + pub fn paste(&self, text: &str) { + if self.last_mode.contains(TermMode::BRACKETED_PASTE) { + self.notify_pty("\x1b[200~".to_string()); + self.notify_pty(text.replace('\x1b', "").to_string()); + self.notify_pty("\x1b[201~".to_string()); + } else { + self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r")); + } } pub fn copy(&mut self) { @@ -570,16 +665,9 @@ impl Terminal { self.process_terminal_event(&e, &mut term, cx) } - //TODO: determine a better metric for this - let buffer_velocity = - (term.last_processed_bytes() as f32 / (READ_BUFFER_SIZE as f32 / 4.)).clamp(0., 1.); + self.utilization = Self::estimate_utilization(term.take_last_processed_bytes()); - //2nd power - let scaled_velocity = buffer_velocity * buffer_velocity; - - self.frames_to_skip = (scaled_velocity * (Self::default_fps() / 10.)).round() as usize; - - term.set_last_processed_bytes(0); //Clear it in case no reads between this lock and the next. + self.last_mode = term.mode().clone(); let content = term.renderable_content(); @@ -588,6 +676,13 @@ impl Terminal { f(content, cursor_text) } + fn estimate_utilization(last_processed: usize) -> f32 { + let buffer_utilization = (last_processed as f32 / (READ_BUFFER_SIZE as f32)).clamp(0., 1.); + + //Scale result to bias low, then high + buffer_utilization * buffer_utilization + } + ///Scroll the terminal pub fn scroll(&mut self, scroll: Scroll) { self.events.push(InternalEvent::Scroll(scroll)); diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 14df31d956..1d2043b2ee 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -6,7 +6,7 @@ use gpui::{ ViewHandle, }; -use crate::TermDimensions; +use crate::TerminalSize; use project::{LocalWorktree, Project, ProjectPath}; use settings::{Settings, WorkingDirectory}; use smallvec::SmallVec; @@ -72,7 +72,7 @@ impl TerminalView { cx: &mut ViewContext, ) -> Self { //The exact size here doesn't matter, the terminal will be resized on the first layout - let size_info = TermDimensions::default(); + let size_info = TerminalSize::default(); let settings = cx.global::(); let shell = settings.terminal_overrides.shell.clone(); diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index d3bcf9102e..70ebce2cbe 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use project::{Entry, Project, ProjectPath, Worktree}; use workspace::{AppState, Workspace}; -use crate::{TermDimensions, Terminal, TerminalBuilder}; +use crate::{Terminal, TerminalBuilder, TerminalSize}; pub struct TerminalTestContext<'a> { pub cx: &'a mut TestAppContext, @@ -17,7 +17,7 @@ impl<'a> TerminalTestContext<'a> { pub fn new(cx: &'a mut TestAppContext, term: bool) -> Self { cx.set_condition_duration(Some(Duration::from_secs(5))); - let size_info = TermDimensions::default(); + let size_info = TerminalSize::default(); let connection = term.then(|| { cx.add_model(|cx| { From 59ba9da24773e5d0af26739bc3eb7bf1b0afcf07 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 1 Aug 2022 16:52:21 -0700 Subject: [PATCH 19/22] Probably good enough using the two thread solution, latency is low for most things, and it feels good --- crates/terminal/src/terminal.rs | 130 ++++++-------------------------- 1 file changed, 21 insertions(+), 109 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e81d455536..cb016bd7f8 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -31,20 +31,14 @@ use futures::{ use modal::deploy_modal; use settings::{Settings, Shell}; -use std::{ - collections::HashMap, - fmt::Display, - path::PathBuf, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration}; use terminal_view::TerminalView; use thiserror::Error; use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, - AsyncAppContext, ClipboardItem, Entity, ModelContext, MutableAppContext, WeakModelHandle, + ClipboardItem, Entity, ModelContext, MutableAppContext, }; use crate::mappings::{ @@ -256,46 +250,6 @@ impl Display for TerminalError { } } -///This is a helper struct that represents a block on a -struct ThrottleGuard { - should_complete: bool, - length: Duration, - start: Instant, -} - -impl ThrottleGuard { - fn new(duration: Duration, cx: &mut ModelContext, on_completion: F) -> Arc - where - T: Entity, - F: FnOnce(WeakModelHandle, AsyncAppContext) -> () + 'static, - { - let selff = Arc::new(Self { - should_complete: false, - start: Instant::now(), - length: duration, - }); - - let moved_self = selff.clone(); - cx.spawn_weak(|w, cx| async move { - cx.background().timer(duration).await; - - if moved_self.should_complete { - on_completion(w, cx); - } - }); - - selff - } - - fn activate(&mut self) { - self.should_complete = true; - } - - fn is_done(&self) -> bool { - self.start.elapsed() > self.length - } -} - pub struct TerminalBuilder { terminal: Terminal, events_rx: UnboundedReceiver, @@ -406,9 +360,6 @@ impl TerminalBuilder { cx.spawn_weak(|this, mut cx| async move { use futures::StreamExt; - //Throttle guard - let mut guard: Option> = None; - self.events_rx .for_each(|event| { match this.upgrade(&cx) { @@ -416,45 +367,6 @@ impl TerminalBuilder { this.update(&mut cx, |this, cx| { //Process the event this.process_event(&event, cx); - - //Clean up the guard if it's expired - guard = match guard.take() { - Some(guard) => { - if guard.is_done() { - None - } else { - Some(guard) - } - } - None => None, - }; - - //Figure out whether to render or not. - if matches!(event, AlacTermEvent::Wakeup) { - if guard.is_none() { - cx.emit(Event::Wakeup); - cx.notify(); - - let dur = Duration::from_secs_f32( - 1.0 / (Terminal::default_fps() - * (1. - this.utilization()).clamp(0.1, 1.)), - ); - - guard = Some(ThrottleGuard::new(dur, cx, |this, mut cx| { - match this.upgrade(&cx) { - Some(handle) => handle.update(&mut cx, |_, cx| { - cx.emit(Event::Wakeup); - cx.notify(); - }), - None => {} - } - })) - } else { - let taken = guard.take().unwrap(); - taken.activate(); - guard = Some(taken); - } - } }); } None => {} @@ -465,28 +377,28 @@ impl TerminalBuilder { }) .detach(); - // //Render loop - // cx.spawn_weak(|this, mut cx| async move { - // loop { - // let utilization = match this.upgrade(&cx) { - // Some(this) => this.update(&mut cx, |this, cx| { - // cx.emit(Event::Wakeup); - // cx.notify(); - // this.utilization() - // }), - // None => break, - // }; + //Render loop + cx.spawn_weak(|this, mut cx| async move { + loop { + let utilization = match this.upgrade(&cx) { + Some(this) => this.update(&mut cx, |this, cx| { + cx.emit(Event::Wakeup); + cx.notify(); + this.utilization() + }), + None => break, + }; - // let utilization = (1. - this.utilization()).clamp(0.1, 1.); + let utilization = (1. - utilization).clamp(0.1, 1.); - // let delay = cx.background().timer(Duration::from_secs_f32( - // 1.0 / (Terminal::default_fps() * utilization), - // )); + let delay = cx.background().timer(Duration::from_secs_f32( + 1.0 / (Terminal::default_fps() * utilization), + )); - // delay.await; - // } - // }) - // .detach(); + delay.await; + } + }) + .detach(); self.terminal } From ca00128794555b2995a24df5051448327ef88d24 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 1 Aug 2022 17:13:06 -0700 Subject: [PATCH 20/22] End of day --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index cb016bd7f8..df0c0e5970 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -365,12 +365,12 @@ impl TerminalBuilder { match this.upgrade(&cx) { Some(this) => { this.update(&mut cx, |this, cx| { - //Process the event this.process_event(&event, cx); }); } None => {} } + future::ready(()) }) .await; From 8277b981042b58e095683b5781756599c40cc0a0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 2 Aug 2022 11:58:24 -0700 Subject: [PATCH 21/22] Fixed bel bug --- crates/terminal/src/connected_view.rs | 5 +++++ crates/terminal/src/terminal.rs | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index bf8763da06..e6e10c84bf 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -105,6 +105,7 @@ impl ConnectedView { ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.clear_bel(cx); self.terminal .read(cx) .try_keystroke(&Keystroke::parse("up").unwrap()); @@ -112,6 +113,7 @@ impl ConnectedView { ///Synthesize the keyboard event corresponding to 'down' fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.clear_bel(cx); self.terminal .read(cx) .try_keystroke(&Keystroke::parse("down").unwrap()); @@ -119,6 +121,7 @@ impl ConnectedView { ///Synthesize the keyboard event corresponding to 'ctrl-c' fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { + self.clear_bel(cx); self.terminal .read(cx) .try_keystroke(&Keystroke::parse("ctrl-c").unwrap()); @@ -126,6 +129,7 @@ impl ConnectedView { ///Synthesize the keyboard event corresponding to 'escape' fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.clear_bel(cx); self.terminal .read(cx) .try_keystroke(&Keystroke::parse("escape").unwrap()); @@ -133,6 +137,7 @@ impl ConnectedView { ///Synthesize the keyboard event corresponding to 'enter' fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { + self.clear_bel(cx); self.terminal .read(cx) .try_keystroke(&Keystroke::parse("enter").unwrap()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 45e46ce378..878e646c05 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -377,7 +377,6 @@ impl TerminalBuilder { loop { let utilization = match this.upgrade(&cx) { Some(this) => this.update(&mut cx, |this, cx| { - cx.emit(Event::Wakeup); cx.notify(); this.utilization() }), @@ -453,7 +452,7 @@ impl Terminal { //NOOP, Handled in render } AlacTermEvent::Wakeup => { - //NOOP, Handled elsewhere + cx.emit(Event::Wakeup); } AlacTermEvent::ColorRequest(_, _) => { self.events.push(InternalEvent::TermEvent(event.clone())) From ffffe7890fcacb0581816a3ba379f7489f20d802 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 2 Aug 2022 12:15:04 -0700 Subject: [PATCH 22/22] Attempting to do throttling again --- crates/terminal/src/terminal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 878e646c05..e873e7b48e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -384,7 +384,6 @@ impl TerminalBuilder { }; let utilization = (1. - utilization).clamp(0.1, 1.); - let delay = cx.background().timer(Duration::from_secs_f32( 1.0 / (Terminal::default_fps() * utilization), ));