Merge branch 'main' into copy-test-update

Mikayla Maki created

Change summary

assets/keymaps/default.json                   |  17 
crates/terminal/Cargo.toml                    |   2 
crates/terminal/src/connection.rs             |  19 
crates/terminal/src/connection/keymappings.rs | 444 +++++++++++++++++++++
crates/terminal/src/terminal.rs               | 115 +----
crates/terminal/src/terminal_element.rs       |  58 +-
6 files changed, 520 insertions(+), 135 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -407,19 +407,16 @@
     {
         "context": "Terminal",
         "bindings": {
-            "ctrl-c": "terminal::Sigint",
-            "escape": "terminal::Escape",
-            "ctrl-d": "terminal::Quit",
-            "backspace": "terminal::Del",
-            "enter": "terminal::Return",
-            "left": "terminal::Left",
-            "right": "terminal::Right",
+            // Overrides for global bindings, remove at your own risk:
             "up": "terminal::Up",
             "down": "terminal::Down",
-            "tab": "terminal::Tab",
-            "cmd-v": "terminal::Paste",
+            "escape": "terminal::Escape",
+            "enter": "terminal::Enter",
+            "ctrl-c": "terminal::CtrlC",
+            // Useful terminal actions:
             "cmd-c": "terminal::Copy",
-            "ctrl-l": "terminal::Clear"
+            "cmd-v": "terminal::Paste",
+            "cmd-k": "terminal::Clear"
         }
     },
     {

crates/terminal/Cargo.toml 🔗

@@ -28,5 +28,3 @@ gpui = { path = "../gpui", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"]}
 project = { path = "../project", features = ["test-support"]}
 workspace = { path = "../workspace", features = ["test-support"] }
-
-

crates/terminal/src/connection.rs 🔗

@@ -1,3 +1,5 @@
+mod keymappings;
+
 use alacritty_terminal::{
     ansi::{ClearMode, Handler},
     config::{Config, PtyConfig},
@@ -13,13 +15,15 @@ use futures::{channel::mpsc::unbounded, StreamExt};
 use settings::Settings;
 use std::{collections::HashMap, path::PathBuf, sync::Arc};
 
-use gpui::{ClipboardItem, CursorStyle, Entity, ModelContext};
+use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
 
 use crate::{
     color_translation::{get_color_at_index, to_alac_rgb},
     ZedListener,
 };
 
+use self::keymappings::to_esc_str;
+
 const DEFAULT_TITLE: &str = "Terminal";
 
 ///Upward flowing events, for changing the title and such
@@ -182,6 +186,19 @@ impl TerminalConnection {
         self.write_to_pty("\x0c".into());
         self.term.lock().clear_screen(ClearMode::Saved);
     }
+
+    pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
+        let guard = self.term.lock();
+        let mode = guard.mode();
+        let esc = to_esc_str(keystroke, mode);
+        drop(guard);
+        if esc.is_some() {
+            self.write_to_pty(esc.unwrap());
+            true
+        } else {
+            false
+        }
+    }
 }
 
 impl Drop for TerminalConnection {

crates/terminal/src/connection/keymappings.rs 🔗

@@ -0,0 +1,444 @@
+use alacritty_terminal::term::TermMode;
+use gpui::keymap::Keystroke;
+
+/*
+Connection events still to do:
+- Reporting mouse events correctly.
+- Reporting scrolls
+- Correctly bracketing a paste
+- Storing changed colors
+- Focus change sequence
+*/
+
+#[derive(Debug)]
+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) -> Option<String> {
+    let modifiers = Modifiers::new(&keystroke);
+
+    // Manual Bindings including modifiers
+    let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
+        //Basic special keys
+        ("space", Modifiers::None) => Some(" ".to_string()),
+        ("tab", Modifiers::None) => Some("\x09".to_string()),
+        ("escape", Modifiers::None) => Some("\x1b".to_string()),
+        ("enter", Modifiers::None) => 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;
+        }
+    }
+
+    //Fallback to sending the keystroke input directly
+    //Skin colors in utf8 are implemented as a seperate, invisible character
+    //that modifies the associated emoji. Some languages may have similarly
+    //implemented modifiers, e.g. certain diacritics that can be typed as a single character.
+    //This means that we need to assume some user input can result in multi-byte,
+    //multi-char strings. This is somewhat difficult, as GPUI normalizes all
+    //keys into a string representation. Hence, the check here to filter out GPUI
+    //keys that weren't captured above.
+    if !matches_gpui_key_str(&keystroke.key) {
+        return Some(keystroke.key.clone());
+    } else {
+        None
+    }
+}
+
+///Checks if the given string matches a GPUI key string.
+///Table made from reading the source at gpui/src/platform/mac/event.rs
+fn matches_gpui_key_str(str: &str) -> bool {
+    match str {
+        "backspace" => true,
+        "up" => true,
+        "down" => true,
+        "left" => true,
+        "right" => true,
+        "pageup" => true,
+        "pagedown" => true,
+        "home" => true,
+        "end" => true,
+        "delete" => true,
+        "enter" => true,
+        "escape" => true,
+        "tab" => true,
+        "f1" => true,
+        "f2" => true,
+        "f3" => true,
+        "f4" => true,
+        "f5" => true,
+        "f6" => true,
+        "f7" => true,
+        "f8" => true,
+        "f9" => true,
+        "f10" => true,
+        "f11" => true,
+        "f12" => true,
+        "space" => true,
+        _ => false,
+    }
+}
+
+///   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 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), None);
+        assert_eq!(to_esc_str(&shift_pagedown, &none), None);
+        assert_eq!(to_esc_str(&shift_home, &none), None);
+        assert_eq!(to_esc_str(&shift_end, &none), None);
+
+        let alt_screen = TermMode::ALT_SCREEN;
+        assert_eq!(
+            to_esc_str(&shift_pageup, &alt_screen),
+            Some("\x1b[5;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_pagedown, &alt_screen),
+            Some("\x1b[6;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_home, &alt_screen),
+            Some("\x1b[1;2H".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_end, &alt_screen),
+            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), Some("\x1b[5~".to_string()));
+        assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
+    }
+
+    #[test]
+    fn test_multi_char_fallthrough() {
+        let ks = Keystroke {
+            ctrl: false,
+            alt: false,
+            shift: false,
+            cmd: false,
+
+            key: "🖖🏻".to_string(), //2 char string
+        };
+
+        assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string()));
+    }
+
+    #[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), Some("\x1b[A".to_string()));
+        assert_eq!(to_esc_str(&down, &none), Some("\x1b[B".to_string()));
+        assert_eq!(to_esc_str(&right, &none), Some("\x1b[C".to_string()));
+        assert_eq!(to_esc_str(&left, &none), Some("\x1b[D".to_string()));
+
+        assert_eq!(to_esc_str(&up, &app_cursor), Some("\x1bOA".to_string()));
+        assert_eq!(to_esc_str(&down, &app_cursor), Some("\x1bOB".to_string()));
+        assert_eq!(to_esc_str(&right, &app_cursor), Some("\x1bOC".to_string()));
+        assert_eq!(to_esc_str(&left, &app_cursor), 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
+                ),
+                to_esc_str(
+                    &Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
+                    &mode
+                ),
+                "On letter: {}/{}",
+                lower,
+                upper
+            )
+        }
+    }
+
+    #[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/terminal/src/terminal.rs 🔗

@@ -5,7 +5,6 @@ pub mod terminal_element;
 
 use alacritty_terminal::{
     event::{Event as AlacTermEvent, EventListener},
-    grid::Scroll,
     term::SizeInfo,
 };
 
@@ -14,7 +13,7 @@ use dirs::home_dir;
 use editor::Input;
 use futures::channel::mpsc::UnboundedSender;
 use gpui::{
-    actions, elements::*, impl_internal_actions, AppContext, ClipboardItem, Entity, ModelHandle,
+    actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle,
     MutableAppContext, View, ViewContext,
 };
 use modal::deploy_modal;
@@ -27,16 +26,6 @@ use workspace::{Item, Workspace};
 
 use crate::terminal_element::TerminalEl;
 
-//ASCII Control characters on a keyboard
-const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
-const TAB_CHAR: char = 9_u8 as char;
-const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
-const ESC_CHAR: char = 27_u8 as char; // == \x1b
-const DEL_CHAR: char = 127_u8 as char;
-const LEFT_SEQ: &str = "\x1b[D";
-const RIGHT_SEQ: &str = "\x1b[C";
-const UP_SEQ: &str = "\x1b[A";
-const DOWN_SEQ: &str = "\x1b[B";
 const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space.
 const DEBUG_TERMINAL_HEIGHT: f32 = 200.;
 const DEBUG_CELL_WIDTH: f32 = 5.;
@@ -52,44 +41,34 @@ pub struct ScrollTerminal(pub i32);
 actions!(
     terminal,
     [
-        Sigint,
-        Escape,
-        Del,
-        Return,
-        Left,
-        Right,
+        Deploy,
         Up,
         Down,
-        Tab,
+        CtrlC,
+        Escape,
+        Enter,
         Clear,
         Copy,
         Paste,
-        Deploy,
-        Quit,
-        DeployModal,
+        DeployModal
     ]
 );
-impl_internal_actions!(terminal, [ScrollTerminal]);
 
 ///Initialize and register all of our action handlers
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(Terminal::deploy);
-    cx.add_action(Terminal::send_sigint);
-    cx.add_action(Terminal::escape);
-    cx.add_action(Terminal::quit);
-    cx.add_action(Terminal::del);
-    cx.add_action(Terminal::carriage_return);
-    cx.add_action(Terminal::left);
-    cx.add_action(Terminal::right);
+    //Global binding overrrides
+    cx.add_action(Terminal::ctrl_c);
     cx.add_action(Terminal::up);
     cx.add_action(Terminal::down);
-    cx.add_action(Terminal::tab);
+    cx.add_action(Terminal::escape);
+    cx.add_action(Terminal::enter);
+    //Useful terminal actions
+    cx.add_action(Terminal::deploy);
+    cx.add_action(deploy_modal);
     cx.add_action(Terminal::copy);
     cx.add_action(Terminal::paste);
-    cx.add_action(Terminal::scroll_terminal);
     cx.add_action(Terminal::input);
     cx.add_action(Terminal::clear);
-    cx.add_action(deploy_modal);
 }
 
 ///A translation struct for Alacritty to communicate with us from their event loop
@@ -168,15 +147,6 @@ impl Terminal {
         }
     }
 
-    ///Scroll the terminal. This locks the terminal
-    fn scroll_terminal(&mut self, scroll: &ScrollTerminal, cx: &mut ViewContext<Self>) {
-        self.connection
-            .read(cx)
-            .term
-            .lock()
-            .scroll_display(Scroll::Delta(scroll.0));
-    }
-
     fn input(&mut self, Input(text): &Input, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
             //TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
@@ -200,11 +170,6 @@ impl Terminal {
         workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
     }
 
-    ///Tell Zed to close us
-    fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::CloseTerminal);
-    }
-
     ///Attempt to paste the clipboard into the terminal
     fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
         let term = self.connection.read(cx).term.lock();
@@ -224,66 +189,38 @@ impl Terminal {
         }
     }
 
-    ///Send the `up` key
+    ///Synthesize the keyboard event corresponding to 'up'
     fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(UP_SEQ.to_string());
+            connection.try_keystroke(&Keystroke::parse("up").unwrap());
         });
     }
 
-    ///Send the `down` key
+    ///Synthesize the keyboard event corresponding to 'down'
     fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(DOWN_SEQ.to_string());
+            connection.try_keystroke(&Keystroke::parse("down").unwrap());
         });
     }
 
-    ///Send the `tab` key
-    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+    ///Synthesize the keyboard event corresponding to 'ctrl-c'
+    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(TAB_CHAR.to_string());
+            connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
         });
     }
 
-    ///Send `SIGINT` (`ctrl-c`)
-    fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
-        self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(ETX_CHAR.to_string());
-        });
-    }
-
-    ///Send the `escape` key
+    ///Synthesize the keyboard event corresponding to 'escape'
     fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(ESC_CHAR.to_string());
-        });
-    }
-
-    ///Send the `delete` key. TODO: Difference between this and backspace?
-    fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
-        self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(DEL_CHAR.to_string());
-        });
-    }
-
-    ///Send a carriage return. TODO: May need to check the terminal mode.
-    fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
-        self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(CARRIAGE_RETURN_CHAR.to_string());
-        });
-    }
-
-    //Send the `left` key
-    fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
-        self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(LEFT_SEQ.to_string());
+            connection.try_keystroke(&Keystroke::parse("escape").unwrap());
         });
     }
 
-    //Send the `right` key
-    fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
+    ///Synthesize the keyboard event corresponding to 'enter'
+    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
         self.connection.update(cx, |connection, _| {
-            connection.write_to_pty(RIGHT_SEQ.to_string());
+            connection.try_keystroke(&Keystroke::parse("enter").unwrap());
         });
     }
 }
@@ -486,8 +423,8 @@ mod tests {
     ///Integration test for selections, clipboard, and terminal execution
     #[gpui::test(retries = 5)]
     async fn test_copy(cx: &mut TestAppContext) {
-        let mut cx = TerminalTestContext::new(cx);
 
+        let mut cx = TerminalTestContext::new(cx);
         let grid_content = cx
             .execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
             .await;

crates/terminal/src/terminal_element.rs 🔗

@@ -1,5 +1,5 @@
 use alacritty_terminal::{
-    grid::{Dimensions, GridIterator, Indexed},
+    grid::{Dimensions, GridIterator, Indexed, Scroll},
     index::{Column as GridCol, Line as GridLine, Point, Side},
     selection::{Selection, SelectionRange, SelectionType},
     sync::FairMutex,
@@ -9,7 +9,7 @@ use alacritty_terminal::{
     },
     Term,
 };
-use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine, Input};
+use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
 use gpui::{
     color::Color,
     elements::*,
@@ -31,9 +31,7 @@ use theme::TerminalStyle;
 use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
 use std::{fmt::Debug, ops::Sub};
 
-use crate::{
-    color_translation::convert_color, connection::TerminalConnection, ScrollTerminal, ZedListener,
-};
+use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
 
 ///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
@@ -359,25 +357,6 @@ impl Element for TerminalEl {
         _paint: &mut Self::PaintState,
         cx: &mut gpui::EventContext,
     ) -> bool {
-        //The problem:
-        //Depending on the terminal mode, we either send an escape sequence
-        //OR update our own data structures.
-        //e.g. scrolling. If we do smooth scrolling, then we need to check if
-        //we own scrolling and then if so, do our scrolling thing.
-        //Ok, so the terminal connection should have APIs for querying it semantically
-        //something like `should_handle_scroll()`. This means we need a handle to the connection.
-        //Actually, this is the only time that this app needs to talk to the outer world.
-        //TODO for scrolling rework: need a way of intercepting Home/End/PageUp etc.
-        //Sometimes going to scroll our own internal buffer, sometimes going to send ESC
-        //
-        //Same goes for key events
-        //Actually, we don't use the terminal at all in dispatch_event code, the view
-        //Handles it all. Check how the editor implements scrolling, is it view-level
-        //or element level?
-
-        //Question: Can we continue dispatching to the view, so it can talk to the connection
-        //Or should we instead add a connection into here?
-
         match event {
             Event::ScrollWheel(ScrollWheelEvent {
                 delta, position, ..
@@ -386,17 +365,30 @@ impl Element for TerminalEl {
                 .then(|| {
                     let vertical_scroll =
                         (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
-                    cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
-                })
-                .is_some(),
-            Event::KeyDown(KeyDownEvent {
-                input: Some(input), ..
-            }) => cx
-                .is_parent_view_focused()
-                .then(|| {
-                    cx.dispatch_action(Input(input.to_string()));
+
+                    if let Some(connection) = self.connection.upgrade(cx.app) {
+                        connection.update(cx.app, |connection, _| {
+                            connection
+                                .term
+                                .lock()
+                                .scroll_display(Scroll::Delta(vertical_scroll.round() as i32));
+                        })
+                    }
                 })
                 .is_some(),
+            Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
+                if !cx.is_parent_view_focused() {
+                    return false;
+                }
+
+                self.connection
+                    .upgrade(cx.app)
+                    .map(|connection| {
+                        connection
+                            .update(cx.app, |connection, _| connection.try_keystroke(keystroke))
+                    })
+                    .unwrap_or(false)
+            }
             _ => false,
         }
     }