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 f1b2dd36cff0d698131cbf70b5ed82f8e19e4f1e..481ebce3a4912cbf21351e3ab08847a43daaf837 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 | 915 ++++++++++-------- crates/terminal/src/terminal_tab.rs | 490 ++++++++++ .../src/tests/terminal_test_context.rs | 17 +- 7 files changed, 1023 insertions(+), 1015 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 865bbc7de7fbfc3e570465674674db245a00cecc..f14e2f14b145bf83cfe767e017b3ed79ac3a8242 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 187b0182493295c84309a379e5e206d4f970cadd..1503c497a229d03ffbf26408759709daa3c94c54 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 ec5280befc7a767777010da8937397b0cff5eb51..a238f4cbc154c6b30e007a6f4d7b417bc2482ef1 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 481ebce3a4912cbf21351e3ab08847a43daaf837..0000000000000000000000000000000000000000 --- 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 4e5829559eb78abca44a5ed6a4bf41d1ecc17e4b..7062b046883d7c9c6eb0ba02d4485353acff68d4 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 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 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_view::ConnectedView; -use dirs::home_dir; use gpui::{ - actions, elements::*, geometry::vector::vec2f, AnyViewHandle, AppContext, Entity, ModelHandle, - MutableAppContext, View, ViewContext, ViewHandle, + geometry::vector::{vec2f, Vector2F}, + keymap::Keystroke, + ClipboardItem, CursorStyle, Entity, ModelContext, MutableAppContext, }; -use modal_view::deploy_modal; -use model::{Event, Terminal, TerminalBuilder, TerminalError}; - -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 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(); } } -pub struct TerminalView { - modal: bool, - content: TerminalContent, - associated_directory: Option, +#[derive(Clone, Copy, Debug)] +pub struct TermDimensions { + cell_width: f32, + line_height: f32, + height: f32, + width: f32, } -pub struct ErrorView { - error: TerminalError, -} +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(), + } + } -impl Entity for TerminalView { - type Event = Event; -} + pub fn num_lines(&self) -> usize { + (self.height / self.line_height).floor() as usize + } -impl Entity for ConnectedView { - type Event = Event; -} + pub fn num_columns(&self) -> usize { + (self.width / self.cell_width).floor() as usize + } -impl Entity for ErrorView { - type Event = Event; -} + pub fn height(&self) -> f32 { + self.height + } -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 width(&self) -> f32 { + self.width } - ///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 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), - ); - - 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, - } + ) } +} - 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 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 View for TerminalView { - fn ui_name() -> &'static str { - "Terminal View" +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 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), - }; + fn screen_lines(&self) -> usize { + self.num_lines() + } - 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 columns(&self) -> usize { + self.num_columns() } +} + +#[derive(Error, Debug)] +pub struct TerminalError { + pub directory: Option, + pub shell: Option, + pub source: std::io::Error, +} - fn on_focus(&mut self, cx: &mut ViewContext) { - cx.emit(Event::Activate); - cx.defer(|view, cx| { - cx.focus(view.content.handle()); - }); +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(), + } + }) } - 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 + 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(" ")), + }) } -} -impl View for ErrorView { - fn ui_name() -> &'static str { - "DisconnectedTerminal" + 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(); - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - let style = TerminalEl::make_text_style(cx.font_cache(), settings); + let shell = self.fmt_shell(); + + write!( + f, + "Working directory: {} Shell command: `{}`, IOError: {}", + dir_string, shell, self.source + ) + } +} - //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) +pub struct TerminalBuilder { + terminal: Terminal, + events_rx: UnboundedReceiver, +} - let program_text = { - match self.error.shell_to_string() { - Some(shell_txt) => format!("Shell Program: `{}`", shell_txt), - None => "No program specified".to_string(), +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 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 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() }; - let error_text = self.error.source.to_string(); + setup_env(&config); - 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() - } -} + //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)); -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() + //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, + }); } - TerminalContent::Error(_) => "Terminal".to_string(), }; - Flex::row() - .with_child( - Label::new(title, tab_theme.label.clone()) - .aligned() - .contained() - .boxed(), - ) - .boxed() - } + 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(" ")) + } + } + }; - 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(), + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, false, - cx, - )) - } + ); - fn project_path(&self, _cx: &gpui::AppContext) -> Option { - None - } + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } + let terminal = Terminal { + pty_tx: Notifier(pty_tx), + term, + title: shell_txt.to_string(), + }; - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false + Ok(TerminalBuilder { + terminal, + events_rx, + }) } - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + 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; + } + }) + .detach(); - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false + self.terminal } +} - fn save( +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, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); + 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"), + } } - 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"); + ///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 reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) + ///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 is_dirty(&self, cx: &gpui::AppContext) -> bool { - if let TerminalContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content() + ///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 } } - 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() + } + + ///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) } - fn should_close_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::CloseTerminal) + pub fn get_display_offset(&self) -> usize { + self.term.lock().renderable_content().display_offset } - fn should_activate_item_on_event(event: &Self::Event) -> bool { - matches!(event, &Event::Activate) + ///Scroll the terminal + pub fn scroll(&self, scroll: Scroll) { + self.term.lock().scroll_display(scroll) } -} -///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 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)); } - }; - 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) + pub fn mouse_down(&self, point: Point, side: Direction) { + self.set_selection(Some(Selection::new(SelectionType::Simple, point, side))); + } } -///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) +impl Drop for Terminal { + fn drop(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } } -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 - - ///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); - }); +//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; + + #[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 - - 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); - }); - } + /// 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() }; - //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; + if status < 0 { + bail!("getpwuid_r failed"); + } - //Test - cx.cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); + if res.is_null() { + bail!("pw not found"); + } - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); + // Sanity check. + assert_eq!(entry.pw_uid, uid); - 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())); - }); + // 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() }, + }) } - //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())); - }); + #[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, + } } - //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 0000000000000000000000000000000000000000..69bac7df1d60c2521ad6e86993b83a56d993a909 --- /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 e78939224b3c5c8011c121cf9f2a529830e770fa..06d45a5146a671e4bfc3f2f306a6266337717f11 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 7062b046883d7c9c6eb0ba02d4485353acff68d4..207ed6a69f4ce6912174692180603c6c77f341a5 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 0000000000000000000000000000000000000000..c1e64726b26f64054f9156aabf7132005098927a --- /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 a238f4cbc154c6b30e007a6f4d7b417bc2482ef1..2fbf7134951a8e60c68873085119c9e0cea11940 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 207ed6a69f4ce6912174692180603c6c77f341a5..4d6c7db64674e215830b7090904f865bb1f8684d 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 | 113 +++++++++--------- crates/terminal/src/connected_view.rs | 34 +++--- crates/terminal/src/terminal.rs | 59 +++++---- .../src/tests/terminal_test_context.rs | 21 ++-- 4 files changed, 125 insertions(+), 102 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index f14e2f14b145bf83cfe767e017b3ed79ac3a8242..856f5ba210aaae3e6e4c60fec65e6e1549fdc394 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, - ); - - //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(), - }, - )], - ) - }; + 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, + ); - 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(), + }, + )], + ) + }; + + 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 1503c497a229d03ffbf26408759709daa3c94c54..7bbc272a96d3427e48040b1ac600e9c7f4dc313d 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 4d6c7db64674e215830b7090904f865bb1f8684d..251e2e9cbaa73b7bae32217d01203a8e5055e548 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 06d45a5146a671e4bfc3f2f306a6266337717f11..d3bcf9102eb035b4a83943b447115419a1971e11 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 0000000000000000000000000000000000000000..481ebce3a4912cbf21351e3ab08847a43daaf837 --- /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 481ebce3a4912cbf21351e3ab08847a43daaf837..0000000000000000000000000000000000000000 --- 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 251e2e9cbaa73b7bae32217d01203a8e5055e548..621b90b4177f2fd9ecb5b8003e180fbf21a947fc 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 0000000000000000000000000000000000000000..69bac7df1d60c2521ad6e86993b83a56d993a909 --- /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 69bac7df1d60c2521ad6e86993b83a56d993a909..0000000000000000000000000000000000000000 --- 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 | 268 ++++++++++++++++---------- crates/terminal/src/terminal_view.rs | 4 +- 5 files changed, 190 insertions(+), 122 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 856f5ba210aaae3e6e4c60fec65e6e1549fdc394..26e3fc9665916c2a29d318964c950b55a98b432f 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 7bbc272a96d3427e48040b1ac600e9c7f4dc313d..c235f68e5891b482bd5e703699f80fd85052db0e 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 f88bfa927ac7f650251bd04c5762a71365562626..c353db57a06390da408e10cda94be0802410a280 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 621b90b4177f2fd9ecb5b8003e180fbf21a947fc..a91e22c892e2d7292949670229a74b43e9154312 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); + } + InternalEvent::Clear => { + 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); + } } - AlacTermEvent::CursorBlinkingChange => { - //TODO: Set a timer to blink the cursor on and off + 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")); + } } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); + InternalEvent::Copy => { + if let Some(txt) = term.selection_to_string() { + cx.write_to_clipboard(ClipboardItem::new(txt)) + } } - AlacTermEvent::Exit => cx.emit(Event::CloseTerminal), - AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"), + 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 - - //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) + 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) } + 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 69bac7df1d60c2521ad6e86993b83a56d993a909..655be38ad0395b79faf13c0475d2560c21219987 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 a91e22c892e2d7292949670229a74b43e9154312..3be4cb46751c701bcdb2a16ed012305f9e9496fb 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 | 127 ++++++++++++++------------ 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, 105 insertions(+), 109 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 26e3fc9665916c2a29d318964c950b55a98b432f..c42a0a94b05e488196b60007cf73bd2c864dcd46 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, - ); - - //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()), - ) - }, + 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(), + }, + )], ) }; - (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 c235f68e5891b482bd5e703699f80fd85052db0e..9cef85107902f01ddf5966769af8a2938c94c40b 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 c353db57a06390da408e10cda94be0802410a280..58d4b1e4c0a0992f6a79dbdc101acaeb61992c65 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 3be4cb46751c701bcdb2a16ed012305f9e9496fb..802d3e59be8a321bb2b685218dfe561fa0dfaca2 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 655be38ad0395b79faf13c0475d2560c21219987..14df31d9565d6222dfabae7bae4a39b517c37457 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 802d3e59be8a321bb2b685218dfe561fa0dfaca2..f1fd0d961b46aa910544c159d75b29289897059e 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 | 61 ++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f1fd0d961b46aa910544c159d75b29289897059e..12720ad7ea41f538f675148718e928136ac24ba4 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.)); - - 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, + //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![]; + + 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 8e9e976efd7edc431ea897a574c79885f2bf5f92..2c2f71b544907505cfaa7acb698d02313f40b395 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 03c6a26b7db643322c3a3cd9cc98bcce7cba7310..e6d25f495b7ac6f006a3e8d524c7d977c3df56d6 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 12720ad7ea41f538f675148718e928136ac24ba4..47093f41e9ce9f85c610d13edfd8a1e933607196 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 c42a0a94b05e488196b60007cf73bd2c864dcd46..edb50e7e6a6edd6b62399f7ad649817ca0e18750 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 47093f41e9ce9f85c610d13edfd8a1e933607196..d7efecc486e523d122a96ceaf2d67623579238e1 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 | 130 +++++++++++++++++----------- 1 file changed, 79 insertions(+), 51 deletions(-) diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index edb50e7e6a6edd6b62399f7ad649817ca0e18750..2b1239b52d0fd6c0d896f29fa2a251a341a163f9 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, - ); - - //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) + let mut cells = vec![]; + cells.extend(content.display_iter.map(|ic| IndexedCell { + point: ic.point.clone(), + cell: ic.cell.clone(), + })); + + ( + 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 2c2f71b544907505cfaa7acb698d02313f40b395..d6310d76db493129efc9cc2bd5c9beb1d245f569 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 e6d25f495b7ac6f006a3e8d524c7d977c3df56d6..354952481b59bbd281959ac4b1ac1d1fecc1b8a4 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 d7efecc486e523d122a96ceaf2d67623579238e1..d26f694abfd083ba4decc645159769c270ac645c 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 | 349 +++++++++++------- crates/terminal/src/terminal_view.rs | 4 +- .../src/tests/terminal_test_context.rs | 4 +- 7 files changed, 264 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6310d76db493129efc9cc2bd5c9beb1d245f569..7494cb67822db0480effc8e38fca35e239464014 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 354952481b59bbd281959ac4b1ac1d1fecc1b8a4..836555e963b4f463be5f7a2bdbe519f52742c550 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 2b1239b52d0fd6c0d896f29fa2a251a341a163f9..510d55fac202cec1663557ac2eb9d4789076be97 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 9cef85107902f01ddf5966769af8a2938c94c40b..d0d63b6f49db1580a38fd923bcd43789260c2df0 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 d26f694abfd083ba4decc645159769c270ac645c..e81d455536c0be079134955f7babeaf88998036d 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![]; - - 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, - } - } + use futures::StreamExt; + + //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.); - - //2nd power - let scaled_velocity = buffer_velocity * buffer_velocity; - - self.frames_to_skip = (scaled_velocity * (Self::default_fps() / 10.)).round() as usize; + self.utilization = Self::estimate_utilization(term.take_last_processed_bytes()); - 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 14df31d9565d6222dfabae7bae4a39b517c37457..1d2043b2eea65a0282a4888fb877253b64d6041b 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 d3bcf9102eb035b4a83943b447115419a1971e11..70ebce2cbecb87fabccd5f8097edc51cfb03f7da 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 | 136 ++++++-------------------------- 1 file changed, 24 insertions(+), 112 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e81d455536c0be079134955f7babeaf88998036d..cb016bd7f838fbd34aa6315eb4b1ab74acd46229 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, - // }; - - // 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(); + //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. - 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 } 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 cb016bd7f838fbd34aa6315eb4b1ab74acd46229..df0c0e59704f1b88c966d44c551028caf9b49bc6 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 bf8763da064e85975275197442ea8c103df62052..e6e10c84bf2873e46fd742040508078ff6e3e861 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 45e46ce37885a958e0bb8241cea9d3c47459f2dc..878e646c050723ce0ed598598724b442adaa482d 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 878e646c050723ce0ed598598724b442adaa482d..e873e7b48e74c2bd97531b29796a833d8bb5384e 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), ));