Detailed changes
@@ -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"
@@ -78,6 +78,7 @@ members = [
"crates/storybook2",
"crates/sum_tree",
"crates/terminal",
+ "crates/terminal2",
"crates/text",
"crates/theme",
"crates/theme2",
@@ -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)
}
@@ -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)
}
@@ -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 {
@@ -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 {
@@ -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
@@ -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);
+ }
+ }
+}
@@ -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())
+ );
+ }
+}
@@ -0,0 +1,3 @@
+pub mod colors;
+pub mod keys;
+pub mod mouse;
@@ -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),
+ )
+ );
+ }
+}
@@ -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()
+ }
+ }
+}
@@ -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 },
+}