WIP

Antonio Scandurra created

Change summary

Cargo.lock                                |   29 
Cargo.toml                                |    1 
crates/gpui2/src/app.rs                   |   18 
crates/gpui2/src/geometry.rs              |   23 
crates/gpui2/src/window.rs                |   12 
crates/project2/src/worktree.rs           |    2 
crates/terminal2/Cargo.toml               |   38 
crates/terminal2/src/mappings/colors.rs   |  130 ++
crates/terminal2/src/mappings/keys.rs     |  457 +++++++
crates/terminal2/src/mappings/mod.rs      |    3 
crates/terminal2/src/mappings/mouse.rs    |  336 +++++
crates/terminal2/src/terminal2.rs         | 1515 +++++++++++++++++++++++++
crates/terminal2/src/terminal_settings.rs |  164 ++
13 files changed, 2,720 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8374,6 +8374,35 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "terminal2"
+version = "0.1.0"
+dependencies = [
+ "alacritty_terminal",
+ "anyhow",
+ "db2",
+ "dirs 4.0.0",
+ "futures 0.3.28",
+ "gpui2",
+ "itertools 0.10.5",
+ "lazy_static",
+ "libc",
+ "mio-extras",
+ "ordered-float 2.10.0",
+ "procinfo",
+ "rand 0.8.5",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "shellexpand",
+ "smallvec",
+ "smol",
+ "theme2",
+ "thiserror",
+ "util",
+]
+
 [[package]]
 name = "terminal_view"
 version = "0.1.0"

Cargo.toml 🔗

@@ -78,6 +78,7 @@ members = [
     "crates/storybook2",
     "crates/sum_tree",
     "crates/terminal",
+    "crates/terminal2",
     "crates/text",
     "crates/theme",
     "crates/theme2",

crates/gpui2/src/app.rs 🔗

@@ -9,11 +9,11 @@ use refineable::Refineable;
 use smallvec::SmallVec;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AppMetadata, AssetSource, Context,
-    DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap,
-    LayoutId, MainThread, MainThreadOnly, Platform, SharedString, SubscriberSet, Subscription,
-    SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window, WindowContext,
-    WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, AppMetadata, AssetSource, ClipboardItem,
+    Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, KeyBinding,
+    Keymap, LayoutId, MainThread, MainThreadOnly, Platform, SharedString, SubscriberSet,
+    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window,
+    WindowContext, WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -697,6 +697,14 @@ impl MainThread<AppContext> {
         self.platform().activate(ignoring_other_apps);
     }
 
+    pub fn write_to_clipboard(&self, item: ClipboardItem) {
+        self.platform().write_to_clipboard(item)
+    }
+
+    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+        self.platform().read_from_clipboard()
+    }
+
     pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
         self.platform().write_credentials(url, username, password)
     }

crates/gpui2/src/geometry.rs 🔗

@@ -1,6 +1,7 @@
 use core::fmt::Debug;
 use derive_more::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
 use refineable::Refineable;
+use serde_derive::{Deserialize, Serialize};
 use std::{
     cmp::{self, PartialOrd},
     fmt,
@@ -647,7 +648,21 @@ where
 
 impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
 
-#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, Neg, PartialEq, PartialOrd)]
+#[derive(
+    Clone,
+    Copy,
+    Default,
+    Add,
+    AddAssign,
+    Sub,
+    SubAssign,
+    Div,
+    Neg,
+    PartialEq,
+    PartialOrd,
+    Serialize,
+    Deserialize,
+)]
 #[repr(transparent)]
 pub struct Pixels(pub(crate) f32);
 
@@ -684,6 +699,10 @@ impl MulAssign<f32> for Pixels {
 impl Pixels {
     pub const MAX: Pixels = Pixels(f32::MAX);
 
+    pub fn floor(&self) -> Self {
+        Self(self.0.floor())
+    }
+
     pub fn round(&self) -> Self {
         Self(self.0.round())
     }
@@ -1008,7 +1027,7 @@ pub fn rems(rems: f32) -> Rems {
     Rems(rems)
 }
 
-pub fn px(pixels: f32) -> Pixels {
+pub const fn px(pixels: f32) -> Pixels {
     Pixels(pixels)
 }
 

crates/gpui2/src/window.rs 🔗

@@ -1653,6 +1653,12 @@ impl<'a, 'w, S: 'static> std::ops::DerefMut for ViewContext<'a, 'w, S> {
 // #[derive(Clone, Copy, Eq, PartialEq, Hash)]
 slotmap::new_key_type! { pub struct WindowId; }
 
+impl WindowId {
+    pub fn as_u64(&self) -> u64 {
+        self.0.as_ffi()
+    }
+}
+
 #[derive(PartialEq, Eq)]
 pub struct WindowHandle<S> {
     id: WindowId,
@@ -1694,6 +1700,12 @@ pub struct AnyWindowHandle {
     state_type: TypeId,
 }
 
+impl AnyWindowHandle {
+    pub fn window_id(&self) -> WindowId {
+        self.id
+    }
+}
+
 #[cfg(any(test, feature = "test"))]
 impl From<SmallVec<[u32; 16]>> for StackingOrder {
     fn from(small_vec: SmallVec<[u32; 16]>) -> Self {

crates/project2/src/worktree.rs 🔗

@@ -1245,7 +1245,7 @@ impl LocalWorktree {
             .unbounded_send((self.snapshot(), Arc::from([]), Arc::from([])))
             .ok();
 
-        let worktree_id = cx.entity_id().as_u64();
+        let worktree_id = cx.entity_id().;
         let _maintain_remote_snapshot = cx.executor().spawn(async move {
             let mut is_first = true;
             while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await {

crates/terminal2/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "terminal2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/terminal2.rs"
+doctest = false
+
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+settings2 = { path = "../settings2" }
+db2 = { path = "../db2" }
+theme2 = { path = "../theme2" }
+util = { path = "../util" }
+
+alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" }
+procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
+smallvec.workspace = true
+smol.workspace = true
+mio-extras = "2.0.6"
+futures.workspace = true
+ordered-float.workspace = true
+itertools = "0.10"
+dirs = "4.0.0"
+shellexpand = "2.1.0"
+libc = "0.2"
+anyhow.workspace = true
+schemars.workspace = true
+thiserror.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+
+[dev-dependencies]
+rand.workspace = true

crates/terminal2/src/mappings/colors.rs 🔗

@@ -0,0 +1,130 @@
+use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb};
+use gpui2::color::Color;
+use theme2::TerminalStyle;
+
+///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
+pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
+    match alac_color {
+        //Named and theme defined colors
+        alacritty_terminal::ansi::Color::Named(n) => match n {
+            alacritty_terminal::ansi::NamedColor::Black => style.black,
+            alacritty_terminal::ansi::NamedColor::Red => style.red,
+            alacritty_terminal::ansi::NamedColor::Green => style.green,
+            alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
+            alacritty_terminal::ansi::NamedColor::Blue => style.blue,
+            alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
+            alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
+            alacritty_terminal::ansi::NamedColor::White => style.white,
+            alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
+            alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
+            alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
+            alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
+            alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
+            alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
+            alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
+            alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
+            alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
+            alacritty_terminal::ansi::NamedColor::Background => style.background,
+            alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
+            alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
+            alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
+            alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
+            alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
+            alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
+            alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
+            alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
+            alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
+            alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
+            alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
+        },
+        //'True' colors
+        alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
+        //8 bit, indexed colors
+        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
+    }
+}
+
+///Converts an 8 bit ANSI color to it's GPUI equivalent.
+///Accepts usize for compatibility with the alacritty::Colors interface,
+///Other than that use case, should only be called with values in the [0,255] range
+pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
+    match index {
+        //0-15 are the same as the named colors above
+        0 => style.black,
+        1 => style.red,
+        2 => style.green,
+        3 => style.yellow,
+        4 => style.blue,
+        5 => style.magenta,
+        6 => style.cyan,
+        7 => style.white,
+        8 => style.bright_black,
+        9 => style.bright_red,
+        10 => style.bright_green,
+        11 => style.bright_yellow,
+        12 => style.bright_blue,
+        13 => style.bright_magenta,
+        14 => style.bright_cyan,
+        15 => style.bright_white,
+        //16-231 are mapped to their RGB colors on a 0-5 range per channel
+        16..=231 => {
+            let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
+            let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
+            Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
+        }
+        //232-255 are a 24 step grayscale from black to white
+        232..=255 => {
+            let i = *index as u8 - 232; //Align index to 0..24
+            let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
+            Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
+        }
+        //For compatibility with the alacritty::Colors interface
+        256 => style.foreground,
+        257 => style.background,
+        258 => style.cursor,
+        259 => style.dim_black,
+        260 => style.dim_red,
+        261 => style.dim_green,
+        262 => style.dim_yellow,
+        263 => style.dim_blue,
+        264 => style.dim_magenta,
+        265 => style.dim_cyan,
+        266 => style.dim_white,
+        267 => style.bright_foreground,
+        268 => style.black, //'Dim Background', non-standard color
+        _ => Color::new(0, 0, 0, 255),
+    }
+}
+///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
+///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
+///
+///Wikipedia gives a formula for calculating the index for a given color:
+///
+///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
+///
+///This function does the reverse, calculating the r, g, and b components from a given index.
+fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
+    debug_assert!((&16..=&231).contains(&i));
+    let i = i - 16;
+    let r = (i - (i % 36)) / 36;
+    let g = ((i % 36) - (i % 6)) / 6;
+    let b = (i % 36) % 6;
+    (r, g, b)
+}
+
+//Convenience method to convert from a GPUI color to an alacritty Rgb
+pub fn to_alac_rgb(color: Color) -> AlacRgb {
+    AlacRgb::new(color.r, color.g, color.g)
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_rgb_for_index() {
+        //Test every possible value in the color cube
+        for i in 16..=231 {
+            let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8));
+            assert_eq!(i, 16 + 36 * r + 6 * g + b);
+        }
+    }
+}

crates/terminal2/src/mappings/keys.rs 🔗

@@ -0,0 +1,457 @@
+/// The mappings defined in this file where created from reading the alacritty source
+use alacritty_terminal::term::TermMode;
+use gpui2::keymap_matcher::Keystroke;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Modifiers {
+    None,
+    Alt,
+    Ctrl,
+    Shift,
+    CtrlShift,
+    Other,
+}
+
+impl Modifiers {
+    fn new(ks: &Keystroke) -> Self {
+        match (ks.alt, ks.ctrl, ks.shift, ks.cmd) {
+            (false, false, false, false) => Modifiers::None,
+            (true, false, false, false) => Modifiers::Alt,
+            (false, true, false, false) => Modifiers::Ctrl,
+            (false, false, true, false) => Modifiers::Shift,
+            (false, true, true, false) => Modifiers::CtrlShift,
+            _ => Modifiers::Other,
+        }
+    }
+
+    fn any(&self) -> bool {
+        match &self {
+            Modifiers::None => false,
+            Modifiers::Alt => true,
+            Modifiers::Ctrl => true,
+            Modifiers::Shift => true,
+            Modifiers::CtrlShift => true,
+            Modifiers::Other => true,
+        }
+    }
+}
+
+pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
+    let modifiers = Modifiers::new(keystroke);
+
+    // Manual Bindings including modifiers
+    let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
+        //Basic special keys
+        ("tab", Modifiers::None) => Some("\x09".to_string()),
+        ("escape", Modifiers::None) => Some("\x1b".to_string()),
+        ("enter", Modifiers::None) => Some("\x0d".to_string()),
+        ("enter", Modifiers::Shift) => Some("\x0d".to_string()),
+        ("backspace", Modifiers::None) => Some("\x7f".to_string()),
+        //Interesting escape codes
+        ("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
+        ("backspace", Modifiers::Alt) => Some("\x1b\x7f".to_string()),
+        ("backspace", Modifiers::Shift) => Some("\x7f".to_string()),
+        ("home", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2H".to_string())
+        }
+        ("end", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2F".to_string())
+        }
+        ("pageup", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[5;2~".to_string())
+        }
+        ("pagedown", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[6;2~".to_string())
+        }
+        ("home", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOH".to_string())
+        }
+        ("home", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[H".to_string())
+        }
+        ("end", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOF".to_string())
+        }
+        ("end", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[F".to_string())
+        }
+        ("up", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOA".to_string())
+        }
+        ("up", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[A".to_string())
+        }
+        ("down", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOB".to_string())
+        }
+        ("down", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[B".to_string())
+        }
+        ("right", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOC".to_string())
+        }
+        ("right", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[C".to_string())
+        }
+        ("left", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOD".to_string())
+        }
+        ("left", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[D".to_string())
+        }
+        ("back", Modifiers::None) => Some("\x7f".to_string()),
+        ("insert", Modifiers::None) => Some("\x1b[2~".to_string()),
+        ("delete", Modifiers::None) => Some("\x1b[3~".to_string()),
+        ("pageup", Modifiers::None) => Some("\x1b[5~".to_string()),
+        ("pagedown", Modifiers::None) => Some("\x1b[6~".to_string()),
+        ("f1", Modifiers::None) => Some("\x1bOP".to_string()),
+        ("f2", Modifiers::None) => Some("\x1bOQ".to_string()),
+        ("f3", Modifiers::None) => Some("\x1bOR".to_string()),
+        ("f4", Modifiers::None) => Some("\x1bOS".to_string()),
+        ("f5", Modifiers::None) => Some("\x1b[15~".to_string()),
+        ("f6", Modifiers::None) => Some("\x1b[17~".to_string()),
+        ("f7", Modifiers::None) => Some("\x1b[18~".to_string()),
+        ("f8", Modifiers::None) => Some("\x1b[19~".to_string()),
+        ("f9", Modifiers::None) => Some("\x1b[20~".to_string()),
+        ("f10", Modifiers::None) => Some("\x1b[21~".to_string()),
+        ("f11", Modifiers::None) => Some("\x1b[23~".to_string()),
+        ("f12", Modifiers::None) => Some("\x1b[24~".to_string()),
+        ("f13", Modifiers::None) => Some("\x1b[25~".to_string()),
+        ("f14", Modifiers::None) => Some("\x1b[26~".to_string()),
+        ("f15", Modifiers::None) => Some("\x1b[28~".to_string()),
+        ("f16", Modifiers::None) => Some("\x1b[29~".to_string()),
+        ("f17", Modifiers::None) => Some("\x1b[31~".to_string()),
+        ("f18", Modifiers::None) => Some("\x1b[32~".to_string()),
+        ("f19", Modifiers::None) => Some("\x1b[33~".to_string()),
+        ("f20", Modifiers::None) => Some("\x1b[34~".to_string()),
+        // NumpadEnter, Action::Esc("\n".into());
+        //Mappings for caret notation keys
+        ("a", Modifiers::Ctrl) => Some("\x01".to_string()), //1
+        ("A", Modifiers::CtrlShift) => Some("\x01".to_string()), //1
+        ("b", Modifiers::Ctrl) => Some("\x02".to_string()), //2
+        ("B", Modifiers::CtrlShift) => Some("\x02".to_string()), //2
+        ("c", Modifiers::Ctrl) => Some("\x03".to_string()), //3
+        ("C", Modifiers::CtrlShift) => Some("\x03".to_string()), //3
+        ("d", Modifiers::Ctrl) => Some("\x04".to_string()), //4
+        ("D", Modifiers::CtrlShift) => Some("\x04".to_string()), //4
+        ("e", Modifiers::Ctrl) => Some("\x05".to_string()), //5
+        ("E", Modifiers::CtrlShift) => Some("\x05".to_string()), //5
+        ("f", Modifiers::Ctrl) => Some("\x06".to_string()), //6
+        ("F", Modifiers::CtrlShift) => Some("\x06".to_string()), //6
+        ("g", Modifiers::Ctrl) => Some("\x07".to_string()), //7
+        ("G", Modifiers::CtrlShift) => Some("\x07".to_string()), //7
+        ("h", Modifiers::Ctrl) => Some("\x08".to_string()), //8
+        ("H", Modifiers::CtrlShift) => Some("\x08".to_string()), //8
+        ("i", Modifiers::Ctrl) => Some("\x09".to_string()), //9
+        ("I", Modifiers::CtrlShift) => Some("\x09".to_string()), //9
+        ("j", Modifiers::Ctrl) => Some("\x0a".to_string()), //10
+        ("J", Modifiers::CtrlShift) => Some("\x0a".to_string()), //10
+        ("k", Modifiers::Ctrl) => Some("\x0b".to_string()), //11
+        ("K", Modifiers::CtrlShift) => Some("\x0b".to_string()), //11
+        ("l", Modifiers::Ctrl) => Some("\x0c".to_string()), //12
+        ("L", Modifiers::CtrlShift) => Some("\x0c".to_string()), //12
+        ("m", Modifiers::Ctrl) => Some("\x0d".to_string()), //13
+        ("M", Modifiers::CtrlShift) => Some("\x0d".to_string()), //13
+        ("n", Modifiers::Ctrl) => Some("\x0e".to_string()), //14
+        ("N", Modifiers::CtrlShift) => Some("\x0e".to_string()), //14
+        ("o", Modifiers::Ctrl) => Some("\x0f".to_string()), //15
+        ("O", Modifiers::CtrlShift) => Some("\x0f".to_string()), //15
+        ("p", Modifiers::Ctrl) => Some("\x10".to_string()), //16
+        ("P", Modifiers::CtrlShift) => Some("\x10".to_string()), //16
+        ("q", Modifiers::Ctrl) => Some("\x11".to_string()), //17
+        ("Q", Modifiers::CtrlShift) => Some("\x11".to_string()), //17
+        ("r", Modifiers::Ctrl) => Some("\x12".to_string()), //18
+        ("R", Modifiers::CtrlShift) => Some("\x12".to_string()), //18
+        ("s", Modifiers::Ctrl) => Some("\x13".to_string()), //19
+        ("S", Modifiers::CtrlShift) => Some("\x13".to_string()), //19
+        ("t", Modifiers::Ctrl) => Some("\x14".to_string()), //20
+        ("T", Modifiers::CtrlShift) => Some("\x14".to_string()), //20
+        ("u", Modifiers::Ctrl) => Some("\x15".to_string()), //21
+        ("U", Modifiers::CtrlShift) => Some("\x15".to_string()), //21
+        ("v", Modifiers::Ctrl) => Some("\x16".to_string()), //22
+        ("V", Modifiers::CtrlShift) => Some("\x16".to_string()), //22
+        ("w", Modifiers::Ctrl) => Some("\x17".to_string()), //23
+        ("W", Modifiers::CtrlShift) => Some("\x17".to_string()), //23
+        ("x", Modifiers::Ctrl) => Some("\x18".to_string()), //24
+        ("X", Modifiers::CtrlShift) => Some("\x18".to_string()), //24
+        ("y", Modifiers::Ctrl) => Some("\x19".to_string()), //25
+        ("Y", Modifiers::CtrlShift) => Some("\x19".to_string()), //25
+        ("z", Modifiers::Ctrl) => Some("\x1a".to_string()), //26
+        ("Z", Modifiers::CtrlShift) => Some("\x1a".to_string()), //26
+        ("@", Modifiers::Ctrl) => Some("\x00".to_string()), //0
+        ("[", Modifiers::Ctrl) => Some("\x1b".to_string()), //27
+        ("\\", Modifiers::Ctrl) => Some("\x1c".to_string()), //28
+        ("]", Modifiers::Ctrl) => Some("\x1d".to_string()), //29
+        ("^", Modifiers::Ctrl) => Some("\x1e".to_string()), //30
+        ("_", Modifiers::Ctrl) => Some("\x1f".to_string()), //31
+        ("?", Modifiers::Ctrl) => Some("\x7f".to_string()), //127
+        _ => None,
+    };
+    if manual_esc_str.is_some() {
+        return manual_esc_str;
+    }
+
+    // Automated bindings applying modifiers
+    if modifiers.any() {
+        let modifier_code = modifier_code(keystroke);
+        let modified_esc_str = match keystroke.key.as_ref() {
+            "up" => Some(format!("\x1b[1;{}A", modifier_code)),
+            "down" => Some(format!("\x1b[1;{}B", modifier_code)),
+            "right" => Some(format!("\x1b[1;{}C", modifier_code)),
+            "left" => Some(format!("\x1b[1;{}D", modifier_code)),
+            "f1" => Some(format!("\x1b[1;{}P", modifier_code)),
+            "f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
+            "f3" => Some(format!("\x1b[1;{}R", modifier_code)),
+            "f4" => Some(format!("\x1b[1;{}S", modifier_code)),
+            "F5" => Some(format!("\x1b[15;{}~", modifier_code)),
+            "f6" => Some(format!("\x1b[17;{}~", modifier_code)),
+            "f7" => Some(format!("\x1b[18;{}~", modifier_code)),
+            "f8" => Some(format!("\x1b[19;{}~", modifier_code)),
+            "f9" => Some(format!("\x1b[20;{}~", modifier_code)),
+            "f10" => Some(format!("\x1b[21;{}~", modifier_code)),
+            "f11" => Some(format!("\x1b[23;{}~", modifier_code)),
+            "f12" => Some(format!("\x1b[24;{}~", modifier_code)),
+            "f13" => Some(format!("\x1b[25;{}~", modifier_code)),
+            "f14" => Some(format!("\x1b[26;{}~", modifier_code)),
+            "f15" => Some(format!("\x1b[28;{}~", modifier_code)),
+            "f16" => Some(format!("\x1b[29;{}~", modifier_code)),
+            "f17" => Some(format!("\x1b[31;{}~", modifier_code)),
+            "f18" => Some(format!("\x1b[32;{}~", modifier_code)),
+            "f19" => Some(format!("\x1b[33;{}~", modifier_code)),
+            "f20" => Some(format!("\x1b[34;{}~", modifier_code)),
+            _ if modifier_code == 2 => None,
+            "insert" => Some(format!("\x1b[2;{}~", modifier_code)),
+            "pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
+            "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
+            "end" => Some(format!("\x1b[1;{}F", modifier_code)),
+            "home" => Some(format!("\x1b[1;{}H", modifier_code)),
+            _ => None,
+        };
+        if modified_esc_str.is_some() {
+            return modified_esc_str;
+        }
+    }
+
+    let alt_meta_binding = if alt_is_meta && modifiers == Modifiers::Alt && keystroke.key.is_ascii()
+    {
+        Some(format!("\x1b{}", keystroke.key))
+    } else {
+        None
+    };
+
+    if alt_meta_binding.is_some() {
+        return alt_meta_binding;
+    }
+
+    None
+}
+
+///   Code     Modifiers
+/// ---------+---------------------------
+///    2     | Shift
+///    3     | Alt
+///    4     | Shift + Alt
+///    5     | Control
+///    6     | Shift + Control
+///    7     | Alt + Control
+///    8     | Shift + Alt + Control
+/// ---------+---------------------------
+/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
+fn modifier_code(keystroke: &Keystroke) -> u32 {
+    let mut modifier_code = 0;
+    if keystroke.shift {
+        modifier_code |= 1;
+    }
+    if keystroke.alt {
+        modifier_code |= 1 << 1;
+    }
+    if keystroke.ctrl {
+        modifier_code |= 1 << 2;
+    }
+    modifier_code + 1
+}
+
+#[cfg(test)]
+mod test {
+    use gpui2::keymap_matcher::Keystroke;
+
+    use super::*;
+
+    #[test]
+    fn test_scroll_keys() {
+        //These keys should be handled by the scrolling element directly
+        //Need to signify this by returning 'None'
+        let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
+        let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
+        let shift_home = Keystroke::parse("shift-home").unwrap();
+        let shift_end = Keystroke::parse("shift-end").unwrap();
+
+        let none = TermMode::NONE;
+        assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
+        assert_eq!(to_esc_str(&shift_pagedown, &none, false), None);
+        assert_eq!(to_esc_str(&shift_home, &none, false), None);
+        assert_eq!(to_esc_str(&shift_end, &none, false), None);
+
+        let alt_screen = TermMode::ALT_SCREEN;
+        assert_eq!(
+            to_esc_str(&shift_pageup, &alt_screen, false),
+            Some("\x1b[5;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_pagedown, &alt_screen, false),
+            Some("\x1b[6;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_home, &alt_screen, false),
+            Some("\x1b[1;2H".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_end, &alt_screen, false),
+            Some("\x1b[1;2F".to_string())
+        );
+
+        let pageup = Keystroke::parse("pageup").unwrap();
+        let pagedown = Keystroke::parse("pagedown").unwrap();
+        let any = TermMode::ANY;
+
+        assert_eq!(
+            to_esc_str(&pageup, &any, false),
+            Some("\x1b[5~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&pagedown, &any, false),
+            Some("\x1b[6~".to_string())
+        );
+    }
+
+    #[test]
+    fn test_plain_inputs() {
+        let ks = Keystroke {
+            ctrl: false,
+            alt: false,
+            shift: false,
+            cmd: false,
+            function: false,
+            key: "🖖🏻".to_string(), //2 char string
+            ime_key: None,
+        };
+        assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
+    }
+
+    #[test]
+    fn test_application_mode() {
+        let app_cursor = TermMode::APP_CURSOR;
+        let none = TermMode::NONE;
+
+        let up = Keystroke::parse("up").unwrap();
+        let down = Keystroke::parse("down").unwrap();
+        let left = Keystroke::parse("left").unwrap();
+        let right = Keystroke::parse("right").unwrap();
+
+        assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".to_string()));
+        assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".to_string()));
+        assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".to_string()));
+        assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".to_string()));
+
+        assert_eq!(
+            to_esc_str(&up, &app_cursor, false),
+            Some("\x1bOA".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&down, &app_cursor, false),
+            Some("\x1bOB".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&right, &app_cursor, false),
+            Some("\x1bOC".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&left, &app_cursor, false),
+            Some("\x1bOD".to_string())
+        );
+    }
+
+    #[test]
+    fn test_ctrl_codes() {
+        let letters_lower = 'a'..='z';
+        let letters_upper = 'A'..='Z';
+        let mode = TermMode::ANY;
+
+        for (lower, upper) in letters_lower.zip(letters_upper) {
+            assert_eq!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
+                    &mode,
+                    false
+                ),
+                to_esc_str(
+                    &Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
+                    &mode,
+                    false
+                ),
+                "On letter: {}/{}",
+                lower,
+                upper
+            )
+        }
+    }
+
+    #[test]
+    fn alt_is_meta() {
+        let ascii_printable = ' '..='~';
+        for character in ascii_printable {
+            assert_eq!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("alt-{}", character)).unwrap(),
+                    &TermMode::NONE,
+                    true
+                )
+                .unwrap(),
+                format!("\x1b{}", character)
+            );
+        }
+
+        let gpui_keys = [
+            "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9",
+            "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert",
+            "pageup", "pagedown", "end", "home",
+        ];
+
+        for key in gpui_keys {
+            assert_ne!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("alt-{}", key)).unwrap(),
+                    &TermMode::NONE,
+                    true
+                )
+                .unwrap(),
+                format!("\x1b{}", key)
+            );
+        }
+    }
+
+    #[test]
+    fn test_modifier_code_calc() {
+        //   Code     Modifiers
+        // ---------+---------------------------
+        //    2     | Shift
+        //    3     | Alt
+        //    4     | Shift + Alt
+        //    5     | Control
+        //    6     | Shift + Control
+        //    7     | Alt + Control
+        //    8     | Shift + Alt + Control
+        // ---------+---------------------------
+        // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
+        assert_eq!(2, modifier_code(&Keystroke::parse("shift-A").unwrap()));
+        assert_eq!(3, modifier_code(&Keystroke::parse("alt-A").unwrap()));
+        assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-A").unwrap()));
+        assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-A").unwrap()));
+        assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-A").unwrap()));
+        assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-A").unwrap()));
+        assert_eq!(
+            8,
+            modifier_code(&Keystroke::parse("shift-ctrl-alt-A").unwrap())
+        );
+    }
+}

crates/terminal2/src/mappings/mouse.rs 🔗

@@ -0,0 +1,336 @@
+use std::cmp::{max, min};
+use std::iter::repeat;
+
+use alacritty_terminal::grid::Dimensions;
+/// Most of the code, and specifically the constants, in this are copied from Alacritty,
+/// with modifications for our circumstances
+use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
+use alacritty_terminal::term::TermMode;
+use gpui2::platform;
+use gpui2::scene::MouseScrollWheel;
+use gpui2::{
+    geometry::vector::Vector2F,
+    platform::{MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent},
+};
+
+use crate::TerminalSize;
+
+struct Modifiers {
+    ctrl: bool,
+    shift: bool,
+    alt: bool,
+}
+
+impl Modifiers {
+    fn from_moved(e: &MouseMovedEvent) -> Self {
+        Modifiers {
+            ctrl: e.ctrl,
+            shift: e.shift,
+            alt: e.alt,
+        }
+    }
+
+    fn from_button(e: &MouseButtonEvent) -> Self {
+        Modifiers {
+            ctrl: e.ctrl,
+            shift: e.shift,
+            alt: e.alt,
+        }
+    }
+
+    fn from_scroll(scroll: &ScrollWheelEvent) -> Self {
+        Modifiers {
+            ctrl: scroll.ctrl,
+            shift: scroll.shift,
+            alt: scroll.alt,
+        }
+    }
+}
+
+enum MouseFormat {
+    SGR,
+    Normal(bool),
+}
+
+impl MouseFormat {
+    fn from_mode(mode: TermMode) -> Self {
+        if mode.contains(TermMode::SGR_MOUSE) {
+            MouseFormat::SGR
+        } else if mode.contains(TermMode::UTF8_MOUSE) {
+            MouseFormat::Normal(true)
+        } else {
+            MouseFormat::Normal(false)
+        }
+    }
+}
+
+#[derive(Debug)]
+enum MouseButton {
+    LeftButton = 0,
+    MiddleButton = 1,
+    RightButton = 2,
+    LeftMove = 32,
+    MiddleMove = 33,
+    RightMove = 34,
+    NoneMove = 35,
+    ScrollUp = 64,
+    ScrollDown = 65,
+    Other = 99,
+}
+
+impl MouseButton {
+    fn from_move(e: &MouseMovedEvent) -> Self {
+        match e.pressed_button {
+            Some(b) => match b {
+                platform::MouseButton::Left => MouseButton::LeftMove,
+                platform::MouseButton::Middle => MouseButton::MiddleMove,
+                platform::MouseButton::Right => MouseButton::RightMove,
+                platform::MouseButton::Navigate(_) => MouseButton::Other,
+            },
+            None => MouseButton::NoneMove,
+        }
+    }
+
+    fn from_button(e: &MouseButtonEvent) -> Self {
+        match e.button {
+            platform::MouseButton::Left => MouseButton::LeftButton,
+            platform::MouseButton::Right => MouseButton::MiddleButton,
+            platform::MouseButton::Middle => MouseButton::RightButton,
+            platform::MouseButton::Navigate(_) => MouseButton::Other,
+        }
+    }
+
+    fn from_scroll(e: &ScrollWheelEvent) -> Self {
+        if e.delta.raw().y() > 0. {
+            MouseButton::ScrollUp
+        } else {
+            MouseButton::ScrollDown
+        }
+    }
+
+    fn is_other(&self) -> bool {
+        match self {
+            MouseButton::Other => true,
+            _ => false,
+        }
+    }
+}
+
+pub fn scroll_report(
+    point: Point,
+    scroll_lines: i32,
+    e: &MouseScrollWheel,
+    mode: TermMode,
+) -> Option<impl Iterator<Item = Vec<u8>>> {
+    if mode.intersects(TermMode::MOUSE_MODE) {
+        mouse_report(
+            point,
+            MouseButton::from_scroll(e),
+            true,
+            Modifiers::from_scroll(e),
+            MouseFormat::from_mode(mode),
+        )
+        .map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
+    } else {
+        None
+    }
+}
+
+pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
+    let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
+
+    let mut content = Vec::with_capacity(scroll_lines.abs() as usize * 3);
+    for _ in 0..scroll_lines.abs() {
+        content.push(0x1b);
+        content.push(b'O');
+        content.push(cmd);
+    }
+    content
+}
+
+pub fn mouse_button_report(
+    point: Point,
+    e: &MouseButtonEvent,
+    pressed: bool,
+    mode: TermMode,
+) -> Option<Vec<u8>> {
+    let button = MouseButton::from_button(e);
+    if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
+        mouse_report(
+            point,
+            button,
+            pressed,
+            Modifiers::from_button(e),
+            MouseFormat::from_mode(mode),
+        )
+    } else {
+        None
+    }
+}
+
+pub fn mouse_moved_report(point: Point, e: &MouseMovedEvent, mode: TermMode) -> Option<Vec<u8>> {
+    let button = MouseButton::from_move(e);
+
+    if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
+        //Only drags are reported in drag mode, so block NoneMove.
+        if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, MouseButton::NoneMove) {
+            None
+        } else {
+            mouse_report(
+                point,
+                button,
+                true,
+                Modifiers::from_moved(e),
+                MouseFormat::from_mode(mode),
+            )
+        }
+    } else {
+        None
+    }
+}
+
+pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
+    if cur_size.cell_width as usize == 0 {
+        return Side::Right;
+    }
+    let x = pos.0.x() as usize;
+    let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
+    let half_cell_width = (cur_size.cell_width / 2.0) as usize;
+    let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
+    let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
+    //Width: Pixels or columns?
+    if cell_x > half_cell_width
+    // Edge case when mouse leaves the window.
+    || x as f32 >= end_of_grid
+    {
+        Side::Right
+    } else {
+        Side::Left
+    }
+}
+
+pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
+    let col = pos.x() / cur_size.cell_width;
+    let col = min(GridCol(col as usize), cur_size.last_column());
+    let line = pos.y() / cur_size.line_height;
+    let line = min(line as i32, cur_size.bottommost_line().0);
+    Point::new(GridLine(line - display_offset as i32), col)
+}
+
+///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
+fn mouse_report(
+    point: Point,
+    button: MouseButton,
+    pressed: bool,
+    modifiers: Modifiers,
+    format: MouseFormat,
+) -> Option<Vec<u8>> {
+    if point.line < 0 {
+        return None;
+    }
+
+    let mut mods = 0;
+    if modifiers.shift {
+        mods += 4;
+    }
+    if modifiers.alt {
+        mods += 8;
+    }
+    if modifiers.ctrl {
+        mods += 16;
+    }
+
+    match format {
+        MouseFormat::SGR => {
+            Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
+        }
+        MouseFormat::Normal(utf8) => {
+            if pressed {
+                normal_mouse_report(point, button as u8 + mods, utf8)
+            } else {
+                normal_mouse_report(point, 3 + mods, utf8)
+            }
+        }
+    }
+}
+
+fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
+    let Point { line, column } = point;
+    let max_point = if utf8 { 2015 } else { 223 };
+
+    if line >= max_point || column >= max_point {
+        return None;
+    }
+
+    let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
+
+    let mouse_pos_encode = |pos: usize| -> Vec<u8> {
+        let pos = 32 + 1 + pos;
+        let first = 0xC0 + pos / 64;
+        let second = 0x80 + (pos & 63);
+        vec![first as u8, second as u8]
+    };
+
+    if utf8 && column >= 95 {
+        msg.append(&mut mouse_pos_encode(column.0));
+    } else {
+        msg.push(32 + 1 + column.0 as u8);
+    }
+
+    if utf8 && line >= 95 {
+        msg.append(&mut mouse_pos_encode(line.0 as usize));
+    } else {
+        msg.push(32 + 1 + line.0 as u8);
+    }
+
+    Some(msg)
+}
+
+fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
+    let c = if pressed { 'M' } else { 'm' };
+
+    let msg = format!(
+        "\x1b[<{};{};{}{}",
+        button,
+        point.column + 1,
+        point.line + 1,
+        c
+    );
+
+    msg
+}
+
+#[cfg(test)]
+mod test {
+    use crate::mappings::mouse::grid_point;
+
+    #[test]
+    fn test_mouse_to_selection() {
+        let term_width = 100.;
+        let term_height = 200.;
+        let cell_width = 10.;
+        let line_height = 20.;
+        let mouse_pos_x = 100.; //Window relative
+        let mouse_pos_y = 100.; //Window relative
+        let origin_x = 10.;
+        let origin_y = 20.;
+
+        let cur_size = crate::TerminalSize::new(
+            line_height,
+            cell_width,
+            gpui::geometry::vector::vec2f(term_width, term_height),
+        );
+
+        let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
+        let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
+        let mouse_pos = mouse_pos - origin;
+        let point = grid_point(mouse_pos, cur_size, 0);
+        assert_eq!(
+            point,
+            alacritty_terminal::index::Point::new(
+                alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
+                alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
+            )
+        );
+    }
+}

crates/terminal2/src/terminal2.rs 🔗

@@ -0,0 +1,1515 @@
+pub mod mappings;
+pub use alacritty_terminal;
+pub mod terminal_settings;
+
+use alacritty_terminal::{
+    ansi::{ClearMode, Handler},
+    config::{Config, Program, PtyConfig, Scrolling},
+    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
+    event_loop::{EventLoop, Msg, Notifier},
+    grid::{Dimensions, Scroll as AlacScroll},
+    index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
+    selection::{Selection, SelectionRange, SelectionType},
+    sync::FairMutex,
+    term::{
+        cell::Cell,
+        color::Rgb,
+        search::{Match, RegexIter, RegexSearch},
+        RenderableCursor, TermMode,
+    },
+    tty::{self, setup_env},
+    Term,
+};
+use anyhow::{bail, Result};
+
+use futures::{
+    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
+    FutureExt,
+};
+
+use mappings::mouse::{
+    alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
+};
+
+use procinfo::LocalProcessInfo;
+use serde::{Deserialize, Serialize};
+use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
+use util::truncate_and_trailoff;
+
+use std::{
+    cmp::min,
+    collections::{HashMap, VecDeque},
+    fmt::Display,
+    ops::{Deref, Index, RangeInclusive},
+    os::unix::prelude::AsRawFd,
+    path::PathBuf,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use thiserror::Error;
+
+use gpui2::{
+    px, AnyWindowHandle, AppContext, ClipboardItem, EventEmitter, Keystroke, MainThread,
+    ModelContext, Modifiers, MouseButton, MouseDragEvent, MouseScrollWheel, MouseUp, Pixels, Point,
+    Task, TouchPhase,
+};
+
+use crate::mappings::{
+    colors::{get_color_at_index, to_alac_rgb},
+    keys::to_esc_str,
+};
+use lazy_static::lazy_static;
+
+///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 SCROLL_MULTIPLIER: f32 = 4.;
+const MAX_SEARCH_LINES: usize = 100;
+const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);
+const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
+const DEBUG_CELL_WIDTH: Pixels = px(5.);
+const DEBUG_LINE_HEIGHT: Pixels = px(5.);
+
+lazy_static! {
+    // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
+    // * avoid Rust-specific escaping.
+    // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
+    static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
+
+    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
+}
+
+///Upward flowing events, for changing the title and such
+#[derive(Clone, Debug)]
+pub enum Event {
+    TitleChanged,
+    BreadcrumbsChanged,
+    CloseTerminal,
+    Bell,
+    Wakeup,
+    BlinkChanged,
+    SelectionsChanged,
+    NewNavigationTarget(Option<MaybeNavigationTarget>),
+    Open(MaybeNavigationTarget),
+}
+
+/// A string inside terminal, potentially useful as a URI that can be opened.
+#[derive(Clone, Debug)]
+pub enum MaybeNavigationTarget {
+    /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
+    Url(String),
+    /// File system path, absolute or relative, existing or not.
+    /// Might have line and column number(s) attached as `file.rs:1:23`
+    PathLike(String),
+}
+
+#[derive(Clone)]
+enum InternalEvent {
+    ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
+    Resize(TerminalSize),
+    Clear,
+    // FocusNextMatch,
+    Scroll(AlacScroll),
+    ScrollToAlacPoint(AlacPoint),
+    SetSelection(Option<(Selection, AlacPoint)>),
+    UpdateSelection(Point<Pixels>),
+    // Adjusted mouse position, should open
+    FindHyperlink(Point<Pixels>, bool),
+    Copy,
+}
+
+///A translation struct for Alacritty to communicate with us from their event loop
+#[derive(Clone)]
+pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: AlacTermEvent) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+pub fn init(cx: &mut AppContext) {
+    settings2::register::<TerminalSettings>(cx);
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+pub struct TerminalSize {
+    pub cell_width: Pixels,
+    pub line_height: Pixels,
+    pub height: Pixels,
+    pub width: Pixels,
+}
+
+impl TerminalSize {
+    pub fn new(line_height: Pixels, cell_width: Pixels, size: Point<Pixels>) -> Self {
+        TerminalSize {
+            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) -> Pixels {
+        self.height
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.width
+    }
+
+    pub fn cell_width(&self) -> Pixels {
+        self.cell_width
+    }
+
+    pub fn line_height(&self) -> Pixels {
+        self.line_height
+    }
+}
+impl Default for TerminalSize {
+    fn default() -> Self {
+        TerminalSize::new(
+            DEBUG_LINE_HEIGHT,
+            DEBUG_CELL_WIDTH,
+            Point::new(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
+        )
+    }
+}
+
+impl From<TerminalSize> for WindowSize {
+    fn from(val: TerminalSize) -> Self {
+        WindowSize {
+            num_lines: val.num_lines() as u16,
+            num_cols: val.num_columns() as u16,
+            cell_width: f32::from(val.cell_width()) as u16,
+            cell_height: f32::from(val.line_height()) as u16,
+        }
+    }
+}
+
+impl Dimensions for TerminalSize {
+    /// Note: this is supposed to be for the back buffer's length,
+    /// but we exclusively use it to resize the terminal, which does not
+    /// use this method. We still have to implement it for the trait though,
+    /// hence, this comment.
+    fn total_lines(&self) -> usize {
+        self.screen_lines()
+    }
+
+    fn screen_lines(&self) -> usize {
+        self.num_lines()
+    }
+
+    fn columns(&self) -> usize {
+        self.num_columns()
+    }
+}
+
+#[derive(Error, Debug)]
+pub struct TerminalError {
+    pub directory: Option<PathBuf>,
+    pub shell: Shell,
+    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!("<non-utf8 path> {}", 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!("<none specified, using home directory> {}", dir),
+                    None => "<none specified, could not find home directory>".to_string(),
+                }
+            })
+    }
+
+    pub fn shell_to_string(&self) -> String {
+        match &self.shell {
+            Shell::System => "<system shell>".to_string(),
+            Shell::Program(p) => p.to_string(),
+            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+        }
+    }
+
+    pub fn fmt_shell(&self) -> String {
+        match &self.shell {
+            Shell::System => "<system defined shell>".to_string(),
+            Shell::Program(s) => s.to_string(),
+            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+        }
+    }
+}
+
+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<AlacTermEvent>,
+}
+
+impl TerminalBuilder {
+    pub fn new(
+        working_directory: Option<PathBuf>,
+        shell: Shell,
+        mut env: HashMap<String, String>,
+        blink_settings: Option<TerminalBlink>,
+        alternate_scroll: AlternateScroll,
+        window: AnyWindowHandle,
+    ) -> Result<TerminalBuilder> {
+        let pty_config = {
+            let alac_shell = match shell.clone() {
+                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,
+            }
+        };
+
+        //TODO: Properly set the current locale,
+        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
+        env.insert("ZED_TERM".to_string(), true.to_string());
+
+        let 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()
+        };
+
+        setup_env(&config);
+
+        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
+        //TODO: Remove with a bounded sender which can be dispatched on &self
+        let (events_tx, events_rx) = unbounded();
+        //Set up the terminal...
+        let mut term = Term::new(
+            &config,
+            &TerminalSize::default(),
+            ZedListener(events_tx.clone()),
+        );
+
+        //Start off blinking if we need to
+        if let Some(TerminalBlink::On) = blink_settings {
+            term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
+        }
+
+        //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
+        if let AlternateScroll::Off = alternate_scroll {
+            term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
+        }
+
+        let term = Arc::new(FairMutex::new(term));
+
+        //Setup the pty...
+        let pty = match tty::new(
+            &pty_config,
+            TerminalSize::default().into(),
+            window.window_id().as_u64(),
+        ) {
+            Ok(pty) => pty,
+            Err(error) => {
+                bail!(TerminalError {
+                    directory: working_directory,
+                    shell,
+                    source: error,
+                });
+            }
+        };
+
+        let fd = pty.file().as_raw_fd();
+        let shell_pid = pty.child().id();
+
+        //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,
+            events: VecDeque::with_capacity(10), //Should never get this high.
+            last_content: Default::default(),
+            last_mouse: None,
+            matches: Vec::new(),
+            last_synced: Instant::now(),
+            sync_task: None,
+            selection_head: None,
+            shell_fd: fd as u32,
+            shell_pid,
+            foreground_process_info: None,
+            breadcrumb_text: String::new(),
+            scroll_px: 0.,
+            last_mouse_position: None,
+            next_link_id: 0,
+            selection_phase: SelectionPhase::Ended,
+            cmd_pressed: false,
+            hovered_word: false,
+        };
+
+        Ok(TerminalBuilder {
+            terminal,
+            events_rx,
+        })
+    }
+
+    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
+        //Event loop
+        cx.spawn(|this, mut cx| async move {
+            use futures::StreamExt;
+
+            while let Some(event) = self.events_rx.next().await {
+                this.update(&mut cx, |this, cx| {
+                    //Process the first event immediately for lowered latency
+                    this.process_event(&event, cx);
+                })?;
+
+                'outer: loop {
+                    let mut events = vec![];
+                    let mut timer = cx.executor().timer(Duration::from_millis(4)).fuse();
+                    let mut wakeup = false;
+                    loop {
+                        futures::select_biased! {
+                            _ = timer => break,
+                            event = self.events_rx.next() => {
+                                if let Some(event) = event {
+                                    if matches!(event, AlacTermEvent::Wakeup) {
+                                        wakeup = true;
+                                    } else {
+                                        events.push(event);
+                                    }
+
+                                    if events.len() > 100 {
+                                        break;
+                                    }
+                                } else {
+                                    break;
+                                }
+                            },
+                        }
+                    }
+
+                    if events.is_empty() && wakeup == false {
+                        smol::future::yield_now().await;
+                        break 'outer;
+                    } else {
+                        this.update(&mut cx, |this, cx| {
+                            if wakeup {
+                                this.process_event(&AlacTermEvent::Wakeup, cx);
+                            }
+
+                            for event in events {
+                                this.process_event(&event, cx);
+                            }
+                        })?;
+                        smol::future::yield_now().await;
+                    }
+                }
+            }
+
+            anyhow::Ok(())
+        })
+        .detach();
+
+        self.terminal
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct IndexedCell {
+    pub point: AlacPoint,
+    pub cell: Cell,
+}
+
+impl Deref for IndexedCell {
+    type Target = Cell;
+
+    #[inline]
+    fn deref(&self) -> &Cell {
+        &self.cell
+    }
+}
+
+// TODO: Un-pub
+#[derive(Clone)]
+pub struct TerminalContent {
+    pub cells: Vec<IndexedCell>,
+    pub mode: TermMode,
+    pub display_offset: usize,
+    pub selection_text: Option<String>,
+    pub selection: Option<SelectionRange>,
+    pub cursor: RenderableCursor,
+    pub cursor_char: char,
+    pub size: TerminalSize,
+    pub last_hovered_word: Option<HoveredWord>,
+}
+
+#[derive(Clone)]
+pub struct HoveredWord {
+    pub word: String,
+    pub word_match: RangeInclusive<AlacPoint>,
+    pub id: usize,
+}
+
+impl Default for TerminalContent {
+    fn default() -> Self {
+        TerminalContent {
+            cells: Default::default(),
+            mode: Default::default(),
+            display_offset: Default::default(),
+            selection_text: Default::default(),
+            selection: Default::default(),
+            cursor: RenderableCursor {
+                shape: alacritty_terminal::ansi::CursorShape::Block,
+                point: AlacPoint::new(Line(0), Column(0)),
+            },
+            cursor_char: Default::default(),
+            size: Default::default(),
+            last_hovered_word: None,
+        }
+    }
+}
+
+#[derive(PartialEq, Eq)]
+pub enum SelectionPhase {
+    Selecting,
+    Ended,
+}
+
+pub struct Terminal {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    events: VecDeque<InternalEvent>,
+    /// This is only used for mouse mode cell change detection
+    last_mouse: Option<(AlacPoint, AlacDirection)>,
+    /// This is only used for terminal hovered word checking
+    last_mouse_position: Option<Point<Pixels>>,
+    pub matches: Vec<RangeInclusive<AlacPoint>>,
+    pub last_content: TerminalContent,
+    last_synced: Instant,
+    sync_task: Option<Task<()>>,
+    pub selection_head: Option<AlacPoint>,
+    pub breadcrumb_text: String,
+    shell_pid: u32,
+    shell_fd: u32,
+    pub foreground_process_info: Option<LocalProcessInfo>,
+    scroll_px: f32,
+    next_link_id: usize,
+    selection_phase: SelectionPhase,
+    cmd_pressed: bool,
+    hovered_word: bool,
+}
+
+impl Terminal {
+    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut MainThread<ModelContext<Self>>) {
+        match event {
+            AlacTermEvent::Title(title) => {
+                self.breadcrumb_text = title.to_string();
+                cx.emit(Event::BreadcrumbsChanged);
+            }
+            AlacTermEvent::ResetTitle => {
+                self.breadcrumb_text = String::new();
+                cx.emit(Event::BreadcrumbsChanged);
+            }
+            AlacTermEvent::ClipboardStore(_, data) => {
+                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
+            }
+            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
+                &cx.read_from_clipboard()
+                    .map(|ci| ci.text().to_string())
+                    .unwrap_or_else(|| "".to_string()),
+            )),
+            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
+            AlacTermEvent::TextAreaSizeRequest(format) => {
+                self.write_to_pty(format(self.last_content.size.into()))
+            }
+            AlacTermEvent::CursorBlinkingChange => {
+                cx.emit(Event::BlinkChanged);
+            }
+            AlacTermEvent::Bell => {
+                cx.emit(Event::Bell);
+            }
+            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
+            AlacTermEvent::MouseCursorDirty => {
+                //NOOP, Handled in render
+            }
+            AlacTermEvent::Wakeup => {
+                cx.emit(Event::Wakeup);
+
+                if self.update_process_info() {
+                    cx.emit(Event::TitleChanged);
+                }
+            }
+            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
+                self.events
+                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
+            }
+        }
+    }
+
+    /// Update the cached process info, returns whether the Zed-relevant info has changed
+    fn update_process_info(&mut self) -> bool {
+        let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
+        if pid < 0 {
+            pid = self.shell_pid as i32;
+        }
+
+        if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
+            let res = self
+                .foreground_process_info
+                .as_ref()
+                .map(|old_info| {
+                    process_info.cwd != old_info.cwd || process_info.name != old_info.name
+                })
+                .unwrap_or(true);
+
+            self.foreground_process_info = Some(process_info.clone());
+
+            res
+        } else {
+            false
+        }
+    }
+
+    ///Takes events from Alacritty and translates them to behavior on this view
+    fn process_terminal_event(
+        &mut self,
+        event: &InternalEvent,
+        term: &mut Term<ZedListener>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match event {
+            InternalEvent::ColorRequest(index, format) => {
+                let color = term.colors()[*index].unwrap_or_else(|| {
+                    let term_style = &theme::current(cx).terminal;
+                    to_alac_rgb(get_color_at_index(index, &term_style))
+                });
+                self.write_to_pty(format(color))
+            }
+            InternalEvent::Resize(mut new_size) => {
+                new_size.height = f32::max(new_size.line_height, new_size.height);
+                new_size.width = f32::max(new_size.cell_width, new_size.width);
+
+                self.last_content.size = new_size.clone();
+
+                self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
+
+                term.resize(new_size);
+            }
+            InternalEvent::Clear => {
+                // Clear back buffer
+                term.clear_screen(ClearMode::Saved);
+
+                let cursor = term.grid().cursor.point;
+
+                // Clear the lines above
+                term.grid_mut().reset_region(..cursor.line);
+
+                // Copy the current line up
+                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
+                    .iter()
+                    .cloned()
+                    .enumerate()
+                    .collect::<Vec<(usize, Cell)>>();
+
+                for (i, cell) in line {
+                    term.grid_mut()[Line(0)][Column(i)] = cell;
+                }
+
+                // Reset the cursor
+                term.grid_mut().cursor.point =
+                    AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
+                let new_cursor = term.grid().cursor.point;
+
+                // Clear the lines below the new cursor
+                if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
+                    term.grid_mut().reset_region((new_cursor.line + 1)..);
+                }
+
+                cx.emit(Event::Wakeup);
+            }
+            InternalEvent::Scroll(scroll) => {
+                term.scroll_display(*scroll);
+                self.refresh_hovered_word();
+            }
+            InternalEvent::SetSelection(selection) => {
+                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
+
+                if let Some((_, head)) = selection {
+                    self.selection_head = Some(*head);
+                }
+                cx.emit(Event::SelectionsChanged)
+            }
+            InternalEvent::UpdateSelection(position) => {
+                if let Some(mut selection) = term.selection.take() {
+                    let point = grid_point(
+                        *position,
+                        self.last_content.size,
+                        term.grid().display_offset(),
+                    );
+
+                    let side = mouse_side(*position, self.last_content.size);
+
+                    selection.update(point, side);
+                    term.selection = Some(selection);
+
+                    self.selection_head = Some(point);
+                    cx.emit(Event::SelectionsChanged)
+                }
+            }
+
+            InternalEvent::Copy => {
+                if let Some(txt) = term.selection_to_string() {
+                    cx.write_to_clipboard(ClipboardItem::new(txt))
+                }
+            }
+            InternalEvent::ScrollToAlacPoint(point) => {
+                term.scroll_to_point(*point);
+                self.refresh_hovered_word();
+            }
+            InternalEvent::FindHyperlink(position, open) => {
+                let prev_hovered_word = self.last_content.last_hovered_word.take();
+
+                let point = grid_point(
+                    *position,
+                    self.last_content.size,
+                    term.grid().display_offset(),
+                )
+                .grid_clamp(term, Boundary::Grid);
+
+                let link = term.grid().index(point).hyperlink();
+                let found_word = if link.is_some() {
+                    let mut min_index = point;
+                    loop {
+                        let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
+                        if new_min_index == min_index {
+                            break;
+                        } else if term.grid().index(new_min_index).hyperlink() != link {
+                            break;
+                        } else {
+                            min_index = new_min_index
+                        }
+                    }
+
+                    let mut max_index = point;
+                    loop {
+                        let new_max_index = max_index.add(term, Boundary::Cursor, 1);
+                        if new_max_index == max_index {
+                            break;
+                        } else if term.grid().index(new_max_index).hyperlink() != link {
+                            break;
+                        } else {
+                            max_index = new_max_index
+                        }
+                    }
+
+                    let url = link.unwrap().uri().to_owned();
+                    let url_match = min_index..=max_index;
+
+                    Some((url, true, url_match))
+                } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
+                    let maybe_url_or_path =
+                        term.bounds_to_string(*word_match.start(), *word_match.end());
+                    let original_match = word_match.clone();
+                    let (sanitized_match, sanitized_word) =
+                        if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') {
+                            (
+                                Match::new(
+                                    word_match.start().add(term, Boundary::Cursor, 1),
+                                    word_match.end().sub(term, Boundary::Cursor, 1),
+                                ),
+                                maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(),
+                            )
+                        } else {
+                            (word_match, maybe_url_or_path)
+                        };
+
+                    let is_url = match regex_match_at(term, point, &URL_REGEX) {
+                        Some(url_match) => {
+                            // `]` is a valid symbol in the `file://` URL, so the regex match will include it
+                            // consider that when ensuring that the URL match is the same as the original word
+                            if sanitized_match != original_match {
+                                url_match.start() == sanitized_match.start()
+                                    && url_match.end() == original_match.end()
+                            } else {
+                                url_match == sanitized_match
+                            }
+                        }
+                        None => false,
+                    };
+                    Some((sanitized_word, is_url, sanitized_match))
+                } else {
+                    None
+                };
+
+                match found_word {
+                    Some((maybe_url_or_path, is_url, url_match)) => {
+                        if *open {
+                            let target = if is_url {
+                                MaybeNavigationTarget::Url(maybe_url_or_path)
+                            } else {
+                                MaybeNavigationTarget::PathLike(maybe_url_or_path)
+                            };
+                            cx.emit(Event::Open(target));
+                        } else {
+                            self.update_selected_word(
+                                prev_hovered_word,
+                                url_match,
+                                maybe_url_or_path,
+                                is_url,
+                                cx,
+                            );
+                        }
+                        self.hovered_word = true;
+                    }
+                    None => {
+                        if self.hovered_word {
+                            cx.emit(Event::NewNavigationTarget(None));
+                        }
+                        self.hovered_word = false;
+                    }
+                }
+            }
+        }
+    }
+
+    fn update_selected_word(
+        &mut self,
+        prev_word: Option<HoveredWord>,
+        word_match: RangeInclusive<AlacPoint>,
+        word: String,
+        is_url: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(prev_word) = prev_word {
+            if prev_word.word == word && prev_word.word_match == word_match {
+                self.last_content.last_hovered_word = Some(HoveredWord {
+                    word,
+                    word_match,
+                    id: prev_word.id,
+                });
+                return;
+            }
+        }
+
+        self.last_content.last_hovered_word = Some(HoveredWord {
+            word: word.clone(),
+            word_match,
+            id: self.next_link_id(),
+        });
+        let navigation_target = if is_url {
+            MaybeNavigationTarget::Url(word)
+        } else {
+            MaybeNavigationTarget::PathLike(word)
+        };
+        cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
+    }
+
+    fn next_link_id(&mut self) -> usize {
+        let res = self.next_link_id;
+        self.next_link_id = self.next_link_id.wrapping_add(1);
+        res
+    }
+
+    pub fn last_content(&self) -> &TerminalContent {
+        &self.last_content
+    }
+
+    //To test:
+    //- Activate match on terminal (scrolling and selection)
+    //- Editor search snapping behavior
+
+    pub fn activate_match(&mut self, index: usize) {
+        if let Some(search_match) = self.matches.get(index).cloned() {
+            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
+
+            self.events
+                .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
+        }
+    }
+
+    pub fn select_matches(&mut self, matches: Vec<RangeInclusive<AlacPoint>>) {
+        let matches_to_select = self
+            .matches
+            .iter()
+            .filter(|self_match| matches.contains(self_match))
+            .cloned()
+            .collect::<Vec<_>>();
+        for match_to_select in matches_to_select {
+            self.set_selection(Some((
+                make_selection(&match_to_select),
+                *match_to_select.end(),
+            )));
+        }
+    }
+
+    pub fn select_all(&mut self) {
+        let term = self.term.lock();
+        let start = AlacPoint::new(term.topmost_line(), Column(0));
+        let end = AlacPoint::new(term.bottommost_line(), term.last_column());
+        drop(term);
+        self.set_selection(Some((make_selection(&(start..=end)), end)));
+    }
+
+    fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) {
+        self.events
+            .push_back(InternalEvent::SetSelection(selection));
+    }
+
+    pub fn copy(&mut self) {
+        self.events.push_back(InternalEvent::Copy);
+    }
+
+    pub fn clear(&mut self) {
+        self.events.push_back(InternalEvent::Clear)
+    }
+
+    ///Resize the terminal and the PTY.
+    pub fn set_size(&mut self, new_size: TerminalSize) {
+        self.events.push_back(InternalEvent::Resize(new_size))
+    }
+
+    ///Write the Input payload to the tty.
+    fn write_to_pty(&self, input: String) {
+        self.pty_tx.notify(input.into_bytes());
+    }
+
+    fn write_bytes_to_pty(&self, input: Vec<u8>) {
+        self.pty_tx.notify(input);
+    }
+
+    pub fn input(&mut self, input: String) {
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
+        self.write_to_pty(input);
+    }
+
+    pub fn input_bytes(&mut self, input: Vec<u8>) {
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
+        self.write_bytes_to_pty(input);
+    }
+
+    pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
+        let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
+        if let Some(esc) = esc {
+            self.input(esc);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
+        let changed = self.cmd_pressed != modifiers.cmd;
+        if !self.cmd_pressed && modifiers.cmd {
+            self.refresh_hovered_word();
+        }
+        self.cmd_pressed = modifiers.cmd;
+        changed
+    }
+
+    ///Paste text into the terminal
+    pub fn paste(&mut self, text: &str) {
+        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
+            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
+        } else {
+            text.replace("\r\n", "\r").replace('\n', "\r")
+        };
+
+        self.input(paste_text);
+    }
+
+    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
+        let term = self.term.clone();
+
+        let mut terminal = if let Some(term) = term.try_lock_unfair() {
+            term
+        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
+            term.lock_unfair() //It's been too long, force block
+        } else if let None = self.sync_task {
+            //Skip this frame
+            let delay = cx.executor().timer(Duration::from_millis(16));
+            self.sync_task = Some(cx.spawn(|weak_handle, mut cx| async move {
+                delay.await;
+                cx.update(|cx| {
+                    if let Some(handle) = weak_handle.upgrade(cx) {
+                        handle.update(cx, |terminal, cx| {
+                            terminal.sync_task.take();
+                            cx.notify();
+                        });
+                    }
+                });
+            }));
+            return;
+        } else {
+            //No lock and delayed rendering already scheduled, nothing to do
+            return;
+        };
+
+        //Note that the ordering of events matters for event processing
+        while let Some(e) = self.events.pop_front() {
+            self.process_terminal_event(&e, &mut terminal, cx)
+        }
+
+        self.last_content = Self::make_content(&terminal, &self.last_content);
+        self.last_synced = Instant::now();
+    }
+
+    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
+        let content = term.renderable_content();
+        TerminalContent {
+            cells: content
+                .display_iter
+                //TODO: Add this once there's a way to retain empty lines
+                // .filter(|ic| {
+                //     !ic.flags.contains(Flags::HIDDEN)
+                //         && !(ic.bg == Named(NamedColor::Background)
+                //             && ic.c == ' '
+                //             && !ic.flags.contains(Flags::INVERSE))
+                // })
+                .map(|ic| IndexedCell {
+                    point: ic.point,
+                    cell: ic.cell.clone(),
+                })
+                .collect::<Vec<IndexedCell>>(),
+            mode: content.mode,
+            display_offset: content.display_offset,
+            selection_text: term.selection_to_string(),
+            selection: content.selection,
+            cursor: content.cursor,
+            cursor_char: term.grid()[content.cursor.point].c,
+            size: last_content.size,
+            last_hovered_word: last_content.last_hovered_word.clone(),
+        }
+    }
+
+    pub fn focus_in(&self) {
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
+            self.write_to_pty("\x1b[I".to_string());
+        }
+    }
+
+    pub fn focus_out(&mut self) {
+        self.last_mouse_position = None;
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
+            self.write_to_pty("\x1b[O".to_string());
+        }
+    }
+
+    pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool {
+        match self.last_mouse {
+            Some((old_point, old_side)) => {
+                if old_point == point && old_side == side {
+                    false
+                } else {
+                    self.last_mouse = Some((point, side));
+                    true
+                }
+            }
+            None => {
+                self.last_mouse = Some((point, side));
+                true
+            }
+        }
+    }
+
+    pub fn mouse_mode(&self, shift: bool) -> bool {
+        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
+    }
+
+    pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Point<Pixels>) {
+        let position = e.position.sub(origin);
+        self.last_mouse_position = Some(position);
+        if self.mouse_mode(e.shift) {
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+            let side = mouse_side(position, self.last_content.size);
+
+            if self.mouse_changed(point, side) {
+                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
+                    self.pty_tx.notify(bytes);
+                }
+            }
+        } else if self.cmd_pressed {
+            self.word_from_position(Some(position));
+        }
+    }
+
+    fn word_from_position(&mut self, position: Option<Point<Pixels>>) {
+        if self.selection_phase == SelectionPhase::Selecting {
+            self.last_content.last_hovered_word = None;
+        } else if let Some(position) = position {
+            self.events
+                .push_back(InternalEvent::FindHyperlink(position, false));
+        }
+    }
+
+    pub fn mouse_drag(&mut self, e: MouseDrag, origin: Point<Pixels>) {
+        let position = e.position.sub(origin);
+        self.last_mouse_position = Some(position);
+
+        if !self.mouse_mode(e.shift) {
+            self.selection_phase = SelectionPhase::Selecting;
+            // Alacritty has the same ordering, of first updating the selection
+            // then scrolling 15ms later
+            self.events
+                .push_back(InternalEvent::UpdateSelection(position));
+
+            // Doesn't make sense to scroll the alt screen
+            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
+                let scroll_delta = match self.drag_line_delta(e) {
+                    Some(value) => value,
+                    None => return,
+                };
+
+                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
+
+                self.events
+                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
+            }
+        }
+    }
+
+    fn drag_line_delta(&mut self, e: MouseDrag) -> Option<f32> {
+        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
+        let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
+        let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
+        let scroll_delta = if e.position.y() < top {
+            (top - e.position.y()).powf(1.1)
+        } else if e.position.y() > bottom {
+            -((e.position.y() - bottom).powf(1.1))
+        } else {
+            return None; //Nothing to do
+        };
+        Some(scroll_delta)
+    }
+
+    pub fn mouse_down(&mut self, e: &MouseDown, origin: Point<Pixels>) {
+        let position = e.position.sub(origin);
+        let point = grid_point(
+            position,
+            self.last_content.size,
+            self.last_content.display_offset,
+        );
+
+        if self.mouse_mode(e.shift) {
+            if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
+                self.pty_tx.notify(bytes);
+            }
+        } else if e.button == MouseButton::Left {
+            let position = e.position.sub(origin);
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+
+            // Use .opposite so that selection is inclusive of the cell clicked.
+            let side = mouse_side(position, self.last_content.size);
+
+            let selection_type = match e.click_count {
+                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));
+
+            if let Some(sel) = selection {
+                self.events
+                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
+            }
+        }
+    }
+
+    pub fn mouse_up(&mut self, e: &MouseUp, origin: Point<Pixels>, cx: &mut ModelContext<Self>) {
+        let setting = settings2::get::<TerminalSettings>(cx);
+
+        let position = e.position.sub(origin);
+        if self.mouse_mode(e.shift) {
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+
+            if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
+                self.pty_tx.notify(bytes);
+            }
+        } else {
+            if e.button == MouseButton::Left && setting.copy_on_select {
+                self.copy();
+            }
+
+            //Hyperlinks
+            if self.selection_phase == SelectionPhase::Ended {
+                let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
+                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
+                    cx.platform().open_url(link.uri());
+                } else if self.cmd_pressed {
+                    self.events
+                        .push_back(InternalEvent::FindHyperlink(position, true));
+                }
+            }
+        }
+
+        self.selection_phase = SelectionPhase::Ended;
+        self.last_mouse = None;
+    }
+
+    ///Scroll the terminal
+    pub fn scroll_wheel(&mut self, e: MouseScrollWheel, origin: Point<Pixels>) {
+        let mouse_mode = self.mouse_mode(e.shift);
+
+        if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
+            if mouse_mode {
+                let point = grid_point(
+                    e.position.sub(origin),
+                    self.last_content.size,
+                    self.last_content.display_offset,
+                );
+
+                if let Some(scrolls) =
+                    scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
+                {
+                    for scroll in scrolls {
+                        self.pty_tx.notify(scroll);
+                    }
+                };
+            } else if self
+                .last_content
+                .mode
+                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
+                && !e.shift
+            {
+                self.pty_tx.notify(alt_scroll(scroll_lines))
+            } else {
+                if scroll_lines != 0 {
+                    let scroll = AlacScroll::Delta(scroll_lines);
+
+                    self.events.push_back(InternalEvent::Scroll(scroll));
+                }
+            }
+        }
+    }
+
+    fn refresh_hovered_word(&mut self) {
+        self.word_from_position(self.last_mouse_position);
+    }
+
+    fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
+        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
+        let line_height = self.last_content.size.line_height;
+        match e.phase {
+            /* Reset scroll state on started */
+            Some(TouchPhase::Started) => {
+                self.scroll_px = 0.;
+                None
+            }
+            /* Calculate the appropriate scroll lines */
+            Some(gpui2::TouchPhase::Moved) => {
+                let old_offset = (self.scroll_px / line_height) as i32;
+
+                self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
+
+                let new_offset = (self.scroll_px / line_height) as i32;
+
+                // Whenever we hit the edges, reset our stored scroll to 0
+                // so we can respond to changes in direction quickly
+                self.scroll_px %= self.last_content.size.height;
+
+                Some(new_offset - old_offset)
+            }
+            /* Fall back to delta / line_height */
+            None => Some(
+                ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
+            ),
+            _ => None,
+        }
+    }
+
+    pub fn find_matches(
+        &mut self,
+        searcher: RegexSearch,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
+        let term = self.term.clone();
+        cx.executor().spawn(async move {
+            let term = term.lock();
+
+            all_search_matches(&term, &searcher).collect()
+        })
+    }
+
+    pub fn title(&self) -> String {
+        self.foreground_process_info
+            .as_ref()
+            .map(|fpi| {
+                format!(
+                    "{} — {}",
+                    truncate_and_trailoff(
+                        &fpi.cwd
+                            .file_name()
+                            .map(|name| name.to_string_lossy().to_string())
+                            .unwrap_or_default(),
+                        25
+                    ),
+                    truncate_and_trailoff(
+                        &{
+                            format!(
+                                "{}{}",
+                                fpi.name,
+                                if fpi.argv.len() >= 1 {
+                                    format!(" {}", (&fpi.argv[1..]).join(" "))
+                                } else {
+                                    "".to_string()
+                                }
+                            )
+                        },
+                        25
+                    )
+                )
+            })
+            .unwrap_or_else(|| "Terminal".to_string())
+    }
+
+    pub fn can_navigate_to_selected_word(&self) -> bool {
+        self.cmd_pressed && self.hovered_word
+    }
+}
+
+impl Drop for Terminal {
+    fn drop(&mut self) {
+        self.pty_tx.0.send(Msg::Shutdown).ok();
+    }
+}
+
+impl EventEmitter for Terminal {
+    type Event = Event;
+}
+
+/// Based on alacritty/src/display/hint.rs > regex_match_at
+/// Retrieve the match, if the specified point is inside the content matching the regex.
+fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> Option<Match> {
+    visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
+}
+
+/// Copied from alacritty/src/display/hint.rs:
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let viewport_start = Line(-(term.grid().display_offset() as i32));
+    let viewport_end = viewport_start + term.bottommost_line();
+    let mut start = term.line_search_left(AlacPoint::new(viewport_start, Column(0)));
+    let mut end = term.line_search_right(AlacPoint::new(viewport_end, Column(0)));
+    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
+
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+        .skip_while(move |rm| rm.end().line < viewport_start)
+        .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
+fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
+    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
+    selection.update(*range.end(), AlacDirection::Right);
+    selection
+}
+
+fn all_search_matches<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
+    let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+}
+
+fn content_index_for_mouse(pos: AlacPoint<Pixels>, size: &TerminalSize) -> usize {
+    let col = (pos.x() / size.cell_width()).round() as usize;
+
+    let clamped_col = min(col, size.columns() - 1);
+
+    let row = (pos.y() / size.line_height()).round() as usize;
+
+    let clamped_row = min(row, size.screen_lines() - 1);
+
+    clamped_row * size.columns() + clamped_col
+}
+
+#[cfg(test)]
+mod tests {
+    use alacritty_terminal::{
+        index::{AlacColumn, Line},
+        term::cell::Cell,
+    };
+    use gpui2::geometry::vecto::vec2f;
+    use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
+
+    use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
+
+    #[test]
+    fn test_mouse_to_cell_test() {
+        let mut rng = thread_rng();
+        const ITERATIONS: usize = 10;
+        const PRECISION: usize = 1000;
+
+        for _ in 0..ITERATIONS {
+            let viewport_cells = rng.gen_range(15..20);
+            let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
+
+            let size = crate::TerminalSize {
+                cell_width: cell_size,
+                line_height: cell_size,
+                height: cell_size * (viewport_cells as f32),
+                width: cell_size * (viewport_cells as f32),
+            };
+
+            let cells = get_cells(size, &mut rng);
+            let content = convert_cells_to_content(size, &cells);
+
+            for row in 0..(viewport_cells - 1) {
+                let row = row as usize;
+                for col in 0..(viewport_cells - 1) {
+                    let col = col as usize;
+
+                    let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
+                    let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
+
+                    let mouse_pos = vec2f(
+                        col as f32 * cell_size + col_offset,
+                        row as f32 * cell_size + row_offset,
+                    );
+
+                    let content_index = content_index_for_mouse(mouse_pos, &content.size);
+                    let mouse_cell = content.cells[content_index].c;
+                    let real_cell = cells[row][col];
+
+                    assert_eq!(mouse_cell, real_cell);
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_mouse_to_cell_clamp() {
+        let mut rng = thread_rng();
+
+        let size = crate::TerminalSize {
+            cell_width: 10.,
+            line_height: 10.,
+            height: 100.,
+            width: 100.,
+        };
+
+        let cells = get_cells(size, &mut rng);
+        let content = convert_cells_to_content(size, &cells);
+
+        assert_eq!(
+            content.cells[content_index_for_mouse(vec2f(-10., -10.), &content.size)].c,
+            cells[0][0]
+        );
+        assert_eq!(
+            content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content.size)].c,
+            cells[9][9]
+        );
+    }
+
+    fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
+        let mut cells = Vec::new();
+
+        for _ in 0..((size.height() / size.line_height()) as usize) {
+            let mut row_vec = Vec::new();
+            for _ in 0..((size.width() / size.cell_width()) as usize) {
+                let cell_char = rng.sample(Alphanumeric) as char;
+                row_vec.push(cell_char)
+            }
+            cells.push(row_vec)
+        }
+
+        cells
+    }
+
+    fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
+        let mut ic = Vec::new();
+
+        for row in 0..cells.len() {
+            for col in 0..cells[row].len() {
+                let cell_char = cells[row][col];
+                ic.push(IndexedCell {
+                    point: Point::new(Line(row as i32), Column(col)),
+                    cell: Cell {
+                        c: cell_char,
+                        ..Default::default()
+                    },
+                });
+            }
+        }
+
+        TerminalContent {
+            cells: ic,
+            size,
+            ..Default::default()
+        }
+    }
+}

crates/terminal2/src/terminal_settings.rs 🔗

@@ -0,0 +1,164 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use gpui2::{fonts, AppContext};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalDockPosition {
+    Left,
+    Bottom,
+    Right,
+}
+
+#[derive(Deserialize)]
+pub struct TerminalSettings {
+    pub shell: Shell,
+    pub working_directory: WorkingDirectory,
+    font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: TerminalLineHeight,
+    pub font_features: Option<fonts::Features>,
+    pub env: HashMap<String, String>,
+    pub blinking: TerminalBlink,
+    pub alternate_scroll: AlternateScroll,
+    pub option_as_meta: bool,
+    pub copy_on_select: bool,
+    pub dock: TerminalDockPosition,
+    pub default_width: f32,
+    pub default_height: f32,
+    pub detect_venv: VenvSettings,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum VenvSettings {
+    #[default]
+    Off,
+    On {
+        activate_script: Option<ActivateScript>,
+        directories: Option<Vec<PathBuf>>,
+    },
+}
+
+pub struct VenvSettingsContent<'a> {
+    pub activate_script: ActivateScript,
+    pub directories: &'a [PathBuf],
+}
+
+impl VenvSettings {
+    pub fn as_option(&self) -> Option<VenvSettingsContent> {
+        match self {
+            VenvSettings::Off => None,
+            VenvSettings::On {
+                activate_script,
+                directories,
+            } => Some(VenvSettingsContent {
+                activate_script: activate_script.unwrap_or(ActivateScript::Default),
+                directories: directories.as_deref().unwrap_or(&[]),
+            }),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ActivateScript {
+    #[default]
+    Default,
+    Csh,
+    Fish,
+    Nushell,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalSettingsContent {
+    pub shell: Option<Shell>,
+    pub working_directory: Option<WorkingDirectory>,
+    pub font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: Option<TerminalLineHeight>,
+    pub font_features: Option<fonts::Features>,
+    pub env: Option<HashMap<String, String>>,
+    pub blinking: Option<TerminalBlink>,
+    pub alternate_scroll: Option<AlternateScroll>,
+    pub option_as_meta: Option<bool>,
+    pub copy_on_select: Option<bool>,
+    pub dock: Option<TerminalDockPosition>,
+    pub default_width: Option<f32>,
+    pub default_height: Option<f32>,
+    pub detect_venv: Option<VenvSettings>,
+}
+
+impl TerminalSettings {
+    pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
+        self.font_size
+            .map(|size| theme2::adjusted_font_size(size, cx))
+    }
+}
+
+impl settings2::Setting for TerminalSettings {
+    const KEY: Option<&'static str> = Some("terminal");
+
+    type FileContent = TerminalSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalLineHeight {
+    #[default]
+    Comfortable,
+    Standard,
+    Custom(f32),
+}
+
+impl TerminalLineHeight {
+    pub fn value(&self) -> f32 {
+        match self {
+            TerminalLineHeight::Comfortable => 1.618,
+            TerminalLineHeight::Standard => 1.3,
+            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+    Off,
+    TerminalControlled,
+    On,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+    System,
+    Program(String),
+    WithArguments { program: String, args: Vec<String> },
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AlternateScroll {
+    On,
+    Off,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WorkingDirectory {
+    CurrentProjectDirectory,
+    FirstProjectDirectory,
+    AlwaysHome,
+    Always { directory: String },
+}