Basic keybindings infra done

Mikayla Maki created

Change summary

assets/keymaps/default.json              |  10 
crates/terminal/Cargo.toml               |   4 
crates/terminal/src/connection.rs        |  19 +
crates/terminal/src/connection/events.rs | 260 ++++++++++++++++++++++++++
crates/terminal/src/terminal_element.rs  |  22 +
5 files changed, 299 insertions(+), 16 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -409,7 +409,6 @@
         "bindings": {
             "ctrl-c": "terminal::Sigint",
             "escape": "terminal::Escape",
-            "shift-escape": "terminal::DeployModal",
             "ctrl-d": "terminal::Quit",
             "backspace": "terminal::Del",
             "enter": "terminal::Return",
@@ -419,8 +418,13 @@
             "down": "terminal::Down",
             "tab": "terminal::Tab",
             "cmd-v": "terminal::Paste",
-            "cmd-c": "terminal::Copy",
-            "ctrl-l": "terminal::Clear"
+            "cmd-c": "terminal::Copy"
+        }
+    },
+    {
+        "context": "ModalTerminal",
+        "bindings": {
+            "shift-escape": "terminal::DeployModal"
         }
     }
 ]

crates/terminal/Cargo.toml 🔗

@@ -27,6 +27,4 @@ dirs = "4.0.0"
 gpui = { path = "../gpui", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"]}
 project = { path = "../project", features = ["test-support"]}
-workspace = { path = "../workspace", features = ["test-support"] }
-
-
+workspace = { path = "../workspace", features = ["test-support"] }

crates/terminal/src/connection.rs 🔗

@@ -1,3 +1,5 @@
+mod events;
+
 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::{ClipboardItem, CursorStyle, Entity, KeyDownEvent, ModelContext};
 
 use crate::{
     color_translation::{get_color_at_index, to_alac_rgb},
     ZedListener,
 };
 
+use self::events::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, key_down: &KeyDownEvent) -> bool {
+        let guard = self.term.lock();
+        let mode = guard.mode();
+        let esc = to_esc_str(key_down, mode);
+        drop(guard);
+        if esc.is_some() {
+            self.write_to_pty(esc.unwrap());
+            true
+        } else {
+            false
+        }
+    }
 }
 
 impl Drop for TerminalConnection {

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

@@ -0,0 +1,260 @@
+use alacritty_terminal::term::TermMode;
+use gpui::{keymap::Keystroke, KeyDownEvent};
+
+pub enum ModifierCombinations {
+    None,
+    Alt,
+    Ctrl,
+    Shift,
+    Other,
+}
+
+impl ModifierCombinations {
+    fn new(ks: &Keystroke) -> Self {
+        match (ks.alt, ks.ctrl, ks.shift, ks.cmd) {
+            (false, false, false, false) => ModifierCombinations::None,
+            (true, false, false, false) => ModifierCombinations::Alt,
+            (false, true, false, false) => ModifierCombinations::Ctrl,
+            (false, false, true, false) => ModifierCombinations::Shift,
+            _ => ModifierCombinations::Other,
+        }
+    }
+}
+
+pub fn to_esc_str(event: &KeyDownEvent, mode: &TermMode) -> Option<String> {
+    let modifiers = ModifierCombinations::new(&event.keystroke);
+
+    // Manual Bindings including modifiers
+    let manual_esc_str = match (event.keystroke.key.as_ref(), modifiers) {
+        ("l", ModifierCombinations::Ctrl) => Some("\x0c".to_string()),
+        ("tab", ModifierCombinations::Shift) => Some("\x1b[Z".to_string()),
+        ("backspace", ModifierCombinations::Alt) => Some("\x1b\x7f".to_string()),
+        ("backspace", ModifierCombinations::Shift) => Some("\x7f".to_string()),
+        ("home", ModifierCombinations::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2H".to_string())
+        }
+        ("end", ModifierCombinations::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2F".to_string())
+        }
+        ("pageup", ModifierCombinations::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[5;2~".to_string())
+        }
+        ("pagedown", ModifierCombinations::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[6;2~".to_string())
+        }
+        ("home", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOH".to_string())
+        }
+        ("home", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[H".to_string())
+        }
+        ("end", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOF".to_string())
+        }
+        ("end", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[F".to_string())
+        }
+        ("up", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOA".to_string())
+        }
+        ("up", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[A".to_string())
+        }
+        ("down", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOB".to_string())
+        }
+        ("down", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[B".to_string())
+        }
+        ("right", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOC".to_string())
+        }
+        ("right", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[C".to_string())
+        }
+        ("left", ModifierCombinations::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOD".to_string())
+        }
+        ("left", ModifierCombinations::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[D".to_string())
+        }
+        ("back", ModifierCombinations::None) => Some("\x7f".to_string()),
+        ("insert", ModifierCombinations::None) => Some("\x1b[2~".to_string()),
+        ("delete", ModifierCombinations::None) => Some("\x1b[3~".to_string()),
+        ("pageup", ModifierCombinations::None) => Some("\x1b[5~".to_string()),
+        ("pagedown", ModifierCombinations::None) => Some("\x1b[6~".to_string()),
+        ("f1", ModifierCombinations::None) => Some("\x1bOP".to_string()),
+        ("f2", ModifierCombinations::None) => Some("\x1bOQ".to_string()),
+        ("f3", ModifierCombinations::None) => Some("\x1bOR".to_string()),
+        ("f4", ModifierCombinations::None) => Some("\x1bOS".to_string()),
+        ("f5", ModifierCombinations::None) => Some("\x1b[15~".to_string()),
+        ("f6", ModifierCombinations::None) => Some("\x1b[17~".to_string()),
+        ("f7", ModifierCombinations::None) => Some("\x1b[18~".to_string()),
+        ("f8", ModifierCombinations::None) => Some("\x1b[19~".to_string()),
+        ("f9", ModifierCombinations::None) => Some("\x1b[20~".to_string()),
+        ("f10", ModifierCombinations::None) => Some("\x1b[21~".to_string()),
+        ("f11", ModifierCombinations::None) => Some("\x1b[23~".to_string()),
+        ("f12", ModifierCombinations::None) => Some("\x1b[24~".to_string()),
+        ("f13", ModifierCombinations::None) => Some("\x1b[25~".to_string()),
+        ("f14", ModifierCombinations::None) => Some("\x1b[26~".to_string()),
+        ("f15", ModifierCombinations::None) => Some("\x1b[28~".to_string()),
+        ("f16", ModifierCombinations::None) => Some("\x1b[29~".to_string()),
+        ("f17", ModifierCombinations::None) => Some("\x1b[31~".to_string()),
+        ("f18", ModifierCombinations::None) => Some("\x1b[32~".to_string()),
+        ("f19", ModifierCombinations::None) => Some("\x1b[33~".to_string()),
+        ("f20", ModifierCombinations::None) => Some("\x1b[34~".to_string()),
+        // NumpadEnter, Action::Esc("\n".into());
+        _ => None,
+    };
+    if manual_esc_str.is_some() {
+        return manual_esc_str;
+    }
+
+    // Automated bindings applying modifiers
+    let modifier_code = modifier_code(&event.keystroke);
+    let modified_esc_str = match event.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 keystroke input sent directly
+    return event.input.clone();
+}
+
+/*
+So, to match  alacritty keyboard handling, we need to check APP_CURSOR, and ALT_SCREEN
+
+And we need to convert the strings that GPUI returns to keys
+
+And we need a way of easily declaring and matching a modifier pattern on those keys
+
+And we need to block writing the input to the pty if any of these match
+
+And I need to figure out how to express this in a cross platform way
+
+And a way of optionally interfacing this with actions for rebinding in defaults.json
+
+Design notes:
+I would like terminal mode checking to be concealed behind the TerminalConnection in as many ways as possible.
+Alacritty has a lot of stuff intermixed for it's input handling. TerminalConnection should be in charge
+of anything that needs to conform to a standard that isn't handled by Term, e.g.:
+- Reporting mouse events correctly.
+- Reporting scrolls -> Depends on MOUSE_MODE, ALT_SCREEN, and ALTERNATE_SCROLL, etc.
+- Correctly bracketing a paste
+- Storing changed colors
+- Focus change sequence
+
+Scrolling might be handled internally or externally, need a way to ask. Everything else should probably happen internally.
+
+Standards/OS compliance is in connection.rs.
+This takes GPUI events and translates them to the correct terminal stuff
+This means that standards compliance outside of connection should be kept to a minimum. Yes, this feels good.
+Connection needs to be split up then, into a bunch of event handlers
+
+Punting on these by pushing them up to a scrolling element
+(either on dispatch_event directly or a seperate scrollbar)
+        Home,     ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToTop;
+        End,      ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToBottom;
+        PageUp,   ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageUp;
+        PageDown, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageDown;
+
+
+
+NOTE, THE FOLLOWING HAS 2 BINDINGS:
+K, ModifiersState::LOGO, Action::Esc("\x0c".into());
+K, ModifiersState::LOGO, Action::ClearHistory; => ctx.terminal_mut().clear_screen(ClearMode::Saved),
+
+*/
+
+///   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_match_alacritty_keybindings() {
+        // let bindings = alacritty::config::bindings::default_key_bindings();
+        //TODO
+    }
+
+    #[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_element.rs 🔗

@@ -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::*,
@@ -389,14 +389,18 @@ impl Element for TerminalEl {
                     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()));
-                })
-                .is_some(),
+            Event::KeyDown(e @ KeyDownEvent { .. }) => {
+                if !cx.is_parent_view_focused() {
+                    return false;
+                }
+
+                self.connection
+                    .upgrade(cx.app)
+                    .map(|connection| {
+                        connection.update(cx.app, |connection, _| connection.try_keystroke(e))
+                    })
+                    .unwrap_or(false)
+            }
             _ => false,
         }
     }