diff --git a/config/src/keyassignment.rs b/config/src/keyassignment.rs index 8914e81fd..25834b42a 100644 --- a/config/src/keyassignment.rs +++ b/config/src/keyassignment.rs @@ -1,12 +1,11 @@ +use crate::de_notnan; use crate::keys::KeyNoAction; -use crate::{de_notnan, ConfigHandle}; use luahelper::impl_lua_conversion; use ordered_float::NotNan; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryFrom; use std::path::PathBuf; -use std::time::Duration; use wezterm_input_types::{KeyCode, Modifiers}; use wezterm_term::input::MouseButton; @@ -27,6 +26,7 @@ bitflags::bitflags! { const DOMAINS = 8; const KEY_ASSIGNMENTS = 16; const WORKSPACES = 32; + const COMMANDS = 64; } } @@ -57,6 +57,9 @@ impl ToString for LauncherFlags { if self.contains(Self::WORKSPACES) { s.push("WORKSPACES"); } + if self.contains(Self::COMMANDS) { + s.push("COMMANDS"); + } s.join("|") } } @@ -75,6 +78,7 @@ impl TryFrom for LauncherFlags { "DOMAINS" => flags |= Self::DOMAINS, "KEY_ASSIGNMENTS" => flags |= Self::KEY_ASSIGNMENTS, "WORKSPACES" => flags |= Self::WORKSPACES, + "COMMANDS" => flags |= Self::COMMANDS, _ => { return Err(format!("invalid LauncherFlags `{}` in `{}`", ele, s)); } @@ -373,446 +377,3 @@ pub struct KeyTables { pub struct KeyTableEntry { pub action: KeyAssignment, } - -pub struct InputMap { - pub keys: KeyTables, - pub mouse: HashMap<(MouseEventTrigger, Modifiers), KeyAssignment>, - leader: Option<(KeyCode, Modifiers, Duration)>, -} - -impl InputMap { - pub fn new(config: &ConfigHandle) -> Self { - let mut mouse = config.mouse_bindings(); - - let mut keys = config.key_bindings(); - - let leader = config.leader.as_ref().map(|leader| { - ( - leader.key.key.resolve(config.key_map_preference).clone(), - leader.key.mods, - Duration::from_millis(leader.timeout_milliseconds), - ) - }); - - fn us_layout_shift(s: &str) -> String { - match s { - "0" => ")".to_string(), - "5" => "%".to_string(), - "[" => "{".to_string(), - "]" => "}".to_string(), - "=" => "+".to_string(), - "-" => "_".to_string(), - "'" => "\"".to_string(), - s if s.len() == 1 => s.to_ascii_uppercase(), - s => s.to_string(), - } - } - let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT; - - macro_rules! k { - ($([$mod:expr, $code:literal, $action:expr]),* $(,)?) => { - $( - let mut items = vec![]; - - // Blech. Depending on the OS, a shifted key combination - // such as CTRL-SHIFT-L may present as either: - // CTRL+SHIFT + mapped lowercase l - // CTRL+SHIFT + mapped uppercase l - // CTRL + mapped uppercase l - // - // This logic synthesizes the different combinations so - // that it isn't such a headache to maintain the mapping - // and prevents missing cases. - // - // Note that the mapped form of these things assumes - // US layout for some of the special shifted/punctuation cases. - // It's not perfect. - // - // The synthesis here requires that the defaults in - // the keymap below use the lowercase form of single characters! - - let key = crate::DeferredKeyCode::try_from($code) - .unwrap() - .resolve(config.key_map_preference).clone(); - - let ukey = crate::DeferredKeyCode::try_from(us_layout_shift($code)) - .unwrap() - .resolve(config.key_map_preference).clone(); - - items.push((key.clone(), $mod)); - - if $mod == Modifiers::SUPER { - // We want each SUPER/CMD version of the keys to also have - // CTRL+SHIFT version(s) for environments where SUPER/CMD - // is reserved for the window manager. - // This bit synthesizes those. - items.push((key.clone(), ctrl_shift)); - if ukey != key { - items.push((ukey.clone(), ctrl_shift)); - items.push((ukey.clone(), Modifiers::CTRL)); - } - } else if $mod.contains(Modifiers::SHIFT) && ukey != key { - items.push((ukey.clone(), $mod)); - items.push((ukey.clone(), $mod - Modifiers::SHIFT)); - } - - log::trace!("{:?} {:?} -> {:?}", $code, $mod, items); - for key in items { - keys.default.entry(key).or_insert(KeyTableEntry { - action: $action.clone() - }); - } - - )* - }; - } - macro_rules! m { - ($([$mod:expr, $code:expr, $action:expr]),* $(,)?) => { - $( - mouse.entry(($code, $mod)).or_insert($action); - )* - }; - } - - use KeyAssignment::*; - - if !config.disable_default_key_bindings { - // Apply the default bindings; if the user has already mapped - // a given entry then that will take precedence. - k!( - // Clipboard - [ - Modifiers::SHIFT, - "Insert", - PasteFrom(ClipboardPasteSource::PrimarySelection) - ], - [ - Modifiers::CTRL, - "Insert", - CopyTo(ClipboardCopyDestination::PrimarySelection) - ], - [ - Modifiers::SUPER, - "c", - CopyTo(ClipboardCopyDestination::Clipboard) - ], - [ - Modifiers::SUPER, - "v", - PasteFrom(ClipboardPasteSource::Clipboard) - ], - [ - Modifiers::NONE, - "Copy", - CopyTo(ClipboardCopyDestination::Clipboard) - ], - [ - Modifiers::NONE, - "Paste", - PasteFrom(ClipboardPasteSource::Clipboard) - ], - // Window management - [Modifiers::ALT, "Return", ToggleFullScreen], - [Modifiers::SUPER, "m", Hide], - [Modifiers::SUPER, "n", SpawnWindow], - [ - Modifiers::SUPER, - "k", - ClearScrollback(ScrollbackEraseMode::ScrollbackOnly) - ], - [ - Modifiers::SUPER, - "f", - Search(Pattern::CaseSensitiveString("".into())) - ], - [ctrl_shift, "l", ShowDebugOverlay], - [ctrl_shift, "Space", QuickSelect], - // Font size manipulation - [Modifiers::SUPER, "-", DecreaseFontSize], - [Modifiers::SUPER, "0", ResetFontSize], - [Modifiers::SUPER, "=", IncreaseFontSize], - // Font size, CTRL variant. - [Modifiers::CTRL, "-", DecreaseFontSize], - [Modifiers::CTRL, "0", ResetFontSize], - [Modifiers::CTRL, "=", IncreaseFontSize], - // Tab navigation and management - [ - Modifiers::SUPER, - "t", - SpawnTab(SpawnTabDomain::CurrentPaneDomain) - ], - [Modifiers::SUPER, "1", ActivateTab(0)], - [Modifiers::SUPER, "2", ActivateTab(1)], - [Modifiers::SUPER, "3", ActivateTab(2)], - [Modifiers::SUPER, "4", ActivateTab(3)], - [Modifiers::SUPER, "5", ActivateTab(4)], - [Modifiers::SUPER, "6", ActivateTab(5)], - [Modifiers::SUPER, "7", ActivateTab(6)], - [Modifiers::SUPER, "8", ActivateTab(7)], - [Modifiers::SUPER, "9", ActivateTab(-1)], - [Modifiers::SUPER, "w", CloseCurrentTab { confirm: true }], - [ - Modifiers::SUPER | Modifiers::SHIFT, - "[", - ActivateTabRelative(-1) - ], - [ctrl_shift, "Tab", ActivateTabRelative(-1)], - [Modifiers::CTRL, "PageUp", ActivateTabRelative(-1)], - [ - Modifiers::SUPER | Modifiers::SHIFT, - "]", - ActivateTabRelative(1) - ], - [Modifiers::CTRL, "Tab", ActivateTabRelative(1)], - [Modifiers::CTRL, "PageDown", ActivateTabRelative(1)], - [Modifiers::SUPER, "r", ReloadConfiguration], - [ctrl_shift, "PageUp", MoveTabRelative(-1)], - [ctrl_shift, "PageDown", MoveTabRelative(1)], - [ - Modifiers::SHIFT, - "PageUp", - ScrollByPage(NotNan::new(-1.0).unwrap()) - ], - [ - Modifiers::SHIFT, - "PageDown", - ScrollByPage(NotNan::new(1.0).unwrap()) - ], - [ctrl_shift, "x", ActivateCopyMode], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "'", - SplitVertical(SpawnCommand { - domain: SpawnTabDomain::CurrentPaneDomain, - ..Default::default() - }) - ], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "5", - SplitHorizontal(SpawnCommand { - domain: SpawnTabDomain::CurrentPaneDomain, - ..Default::default() - }) - ], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "LeftArrow", - AdjustPaneSize(PaneDirection::Left, 1) - ], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "RightArrow", - AdjustPaneSize(PaneDirection::Right, 1) - ], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "UpArrow", - AdjustPaneSize(PaneDirection::Up, 1) - ], - [ - Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT, - "DownArrow", - AdjustPaneSize(PaneDirection::Down, 1) - ], - [ - ctrl_shift, - "LeftArrow", - ActivatePaneDirection(PaneDirection::Left) - ], - [ - ctrl_shift, - "RightArrow", - ActivatePaneDirection(PaneDirection::Right) - ], - [ - ctrl_shift, - "UpArrow", - ActivatePaneDirection(PaneDirection::Up) - ], - [ - ctrl_shift, - "DownArrow", - ActivatePaneDirection(PaneDirection::Down) - ], - [ctrl_shift, "z", TogglePaneZoomState], - ); - - #[cfg(target_os = "macos")] - k!( - [Modifiers::SUPER, "h", HideApplication], - [Modifiers::SUPER, "q", QuitApplication], - ); - } - - if !config.disable_default_mouse_bindings { - m!( - [ - Modifiers::NONE, - MouseEventTrigger::Down { - streak: 3, - button: MouseButton::Left - }, - SelectTextAtMouseCursor(SelectionMode::Line) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Down { - streak: 2, - button: MouseButton::Left - }, - SelectTextAtMouseCursor(SelectionMode::Word) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Down { - streak: 1, - button: MouseButton::Left - }, - SelectTextAtMouseCursor(SelectionMode::Cell) - ], - [ - Modifiers::SHIFT, - MouseEventTrigger::Down { - streak: 1, - button: MouseButton::Left - }, - ExtendSelectionToMouseCursor(None) - ], - [ - Modifiers::SHIFT, - MouseEventTrigger::Up { - streak: 1, - button: MouseButton::Left - }, - CompleteSelectionOrOpenLinkAtMouseCursor( - ClipboardCopyDestination::PrimarySelection - ) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Up { - streak: 1, - button: MouseButton::Left - }, - CompleteSelectionOrOpenLinkAtMouseCursor( - ClipboardCopyDestination::PrimarySelection - ) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Up { - streak: 2, - button: MouseButton::Left - }, - CompleteSelection(ClipboardCopyDestination::PrimarySelection) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Up { - streak: 3, - button: MouseButton::Left - }, - CompleteSelection(ClipboardCopyDestination::PrimarySelection) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Drag { - streak: 1, - button: MouseButton::Left - }, - ExtendSelectionToMouseCursor(Some(SelectionMode::Cell)) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Drag { - streak: 2, - button: MouseButton::Left - }, - ExtendSelectionToMouseCursor(Some(SelectionMode::Word)) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Drag { - streak: 3, - button: MouseButton::Left - }, - ExtendSelectionToMouseCursor(Some(SelectionMode::Line)) - ], - [ - Modifiers::NONE, - MouseEventTrigger::Down { - streak: 1, - button: MouseButton::Middle - }, - PasteFrom(ClipboardPasteSource::PrimarySelection) - ], - [ - Modifiers::SUPER, - MouseEventTrigger::Drag { - streak: 1, - button: MouseButton::Left, - }, - StartWindowDrag - ], - [ - ctrl_shift, - MouseEventTrigger::Drag { - streak: 1, - button: MouseButton::Left, - }, - StartWindowDrag - ], - ); - } - - keys.default - .retain(|_, v| v.action != KeyAssignment::DisableDefaultAssignment); - mouse.retain(|_, v| *v != KeyAssignment::DisableDefaultAssignment); - - Self { - keys, - leader, - mouse, - } - } - - pub fn is_leader(&self, key: &KeyCode, mods: Modifiers) -> Option { - if let Some((leader_key, leader_mods, timeout)) = self.leader.as_ref() { - if *leader_key == *key && *leader_mods == mods { - return Some(timeout.clone()); - } - } - None - } - - fn remove_positional_alt(mods: Modifiers) -> Modifiers { - mods - (Modifiers::LEFT_ALT | Modifiers::RIGHT_ALT) - } - - pub fn has_table(&self, name: &str) -> bool { - self.keys.by_name.contains_key(name) - } - - pub fn lookup_key( - &self, - key: &KeyCode, - mods: Modifiers, - table_name: Option<&str>, - ) -> Option { - let table = match table_name { - Some(name) => self.keys.by_name.get(name)?, - None => &self.keys.default, - }; - - table - .get(&key.normalize_shift(Self::remove_positional_alt(mods))) - .cloned() - } - - pub fn lookup_mouse(&self, event: MouseEventTrigger, mods: Modifiers) -> Option { - self.mouse - .get(&(event, Self::remove_positional_alt(mods))) - .cloned() - } -} diff --git a/wezterm-gui/src/commands.rs b/wezterm-gui/src/commands.rs new file mode 100644 index 000000000..60259a522 --- /dev/null +++ b/wezterm-gui/src/commands.rs @@ -0,0 +1,652 @@ +use config::keyassignment::*; +use config::{ConfigHandle, DeferredKeyCode}; +use ordered_float::NotNan; +use std::borrow::Cow; +use std::convert::TryFrom; +use window::{KeyCode, Modifiers}; +use KeyAssignment::*; + +type ExpandFn = fn(&mut Expander); + +/// Describes an argument/parameter/context that is required +/// in order for the command to have meaning. +/// The intent is for this to be used when filtering the items +/// that should be shown in eg: a context menu. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ArgType { + /// Operates on the active pane + ActivePane, + /// Operates on the active tab + ActiveTab, + /// Operates on the active window + ActiveWindow, +} + +/// A helper function used to synthesize key binding permutations. +/// If the input is a character on a US ANSI keyboard layout, returns +/// the the typical character that is produced when holding down +/// the shift key and pressing the original key. +/// This doesn't produce an exhaustive list because there are only +/// a handful of default assignments in the command DEFS below. +fn us_layout_shift(s: &str) -> String { + match s { + "0" => ")".to_string(), + "5" => "%".to_string(), + "[" => "{".to_string(), + "]" => "}".to_string(), + "=" => "+".to_string(), + "-" => "_".to_string(), + "'" => "\"".to_string(), + s if s.len() == 1 => s.to_ascii_uppercase(), + s => s.to_string(), + } +} + +/// `CommandDef` defines a command in the UI. +pub struct CommandDef { + /// Brief description + pub brief: &'static str, + /// A longer, more detailed, description + pub doc: &'static str, + /// A function that can produce 0 or more ExpandedCommand's. + /// The intent is that we can use this to dynamically populate + /// a list of commands for a given context. + pub exp: ExpandFn, + /// The key assignments associated with this command. + pub keys: &'static [(Modifiers, &'static str)], + /// The argument types/context in which this command is valid. + pub args: &'static [ArgType], +} + +impl std::fmt::Debug for CommandDef { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct("CommandDef") + .field("brief", &self.brief) + .field("doc", &self.doc) + .field("keys", &self.keys) + .field("args", &self.args) + .finish() + } +} + +impl CommandDef { + /// Blech. Depending on the OS, a shifted key combination + /// such as CTRL-SHIFT-L may present as either: + /// CTRL+SHIFT + mapped lowercase l + /// CTRL+SHIFT + mapped uppercase l + /// CTRL + mapped uppercase l + /// + /// This logic synthesizes the different combinations so + /// that it isn't such a headache to maintain the mapping + /// and prevents missing cases. + /// + /// Note that the mapped form of these things assumes + /// US layout for some of the special shifted/punctuation cases. + /// It's not perfect. + /// + /// The synthesis here requires that the defaults in + /// the keymap below use the lowercase form of single characters! + fn permute_keys(&self, config: &ConfigHandle) -> Vec<(Modifiers, KeyCode)> { + let mut keys = vec![]; + + for &(mods, label) in self.keys { + let key = DeferredKeyCode::try_from(label) + .unwrap() + .resolve(config.key_map_preference) + .clone(); + + let ukey = DeferredKeyCode::try_from(us_layout_shift(label)) + .unwrap() + .resolve(config.key_map_preference) + .clone(); + + keys.push((mods, key.clone())); + + if mods == Modifiers::SUPER { + // We want each SUPER/CMD version of the keys to also have + // CTRL+SHIFT version(s) for environments where SUPER/CMD + // is reserved for the window manager. + // This bit synthesizes those. + keys.push((Modifiers::CTRL | Modifiers::SHIFT, key.clone())); + if ukey != key { + keys.push((Modifiers::CTRL | Modifiers::SHIFT, ukey.clone())); + keys.push((Modifiers::CTRL, ukey.clone())); + } + } else if mods.contains(Modifiers::SHIFT) && ukey != key { + keys.push((mods, ukey.clone())); + keys.push((mods - Modifiers::SHIFT, ukey.clone())); + } + } + + keys + } + + /// Produces the list of default key assignments and actions. + /// Used by the InputMap. + pub fn default_key_assignments( + config: &ConfigHandle, + ) -> Vec<(Modifiers, KeyCode, KeyAssignment)> { + let mut result = vec![]; + for cmd in Self::expanded_commands(config) { + for (mods, code) in cmd.keys { + result.push((mods, code.clone(), cmd.action.clone())); + } + } + result + } + + /// Produces the complete set of expanded commands. + pub fn expanded_commands(config: &ConfigHandle) -> Vec { + let mut result = vec![]; + for def in DEFS { + let expander = Expander::new(def, config); + result.append(&mut expander.expand()); + } + result + } +} + +#[derive(Debug)] +pub struct ExpandedCommand { + pub brief: Cow<'static, str>, + pub doc: Cow<'static, str>, + pub action: KeyAssignment, + pub keys: Vec<(Modifiers, KeyCode)>, +} + +#[derive(Debug)] +pub struct Expander { + template: &'static CommandDef, + commands: Vec, + config: ConfigHandle, +} + +impl Expander { + pub fn push(&mut self, action: KeyAssignment) { + let expanded = ExpandedCommand { + brief: self.template.brief.into(), + doc: self.template.doc.into(), + keys: self.template.permute_keys(&self.config), + action, + }; + self.commands.push(expanded); + } + + pub fn new(template: &'static CommandDef, config: &ConfigHandle) -> Self { + Self { + template, + commands: vec![], + config: config.clone(), + } + } + + pub fn expand(mut self) -> Vec { + (self.template.exp)(&mut self); + self.commands + } +} + +static DEFS: &[CommandDef] = &[ + CommandDef { + brief: "Paste primary selection", + doc: "Pastes text from the primary selection", + exp: |exp| exp.push(PasteFrom(ClipboardPasteSource::PrimarySelection)), + keys: &[(Modifiers::SHIFT, "Insert")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Copy to primary selection", + doc: "Copies text from the primary selection", + exp: |exp| { + exp.push(CopyTo(ClipboardCopyDestination::PrimarySelection)); + }, + keys: &[(Modifiers::CTRL, "Insert")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Copy to clipboard", + doc: "Copies text to the clipboard", + exp: |exp| exp.push(CopyTo(ClipboardCopyDestination::Clipboard)), + keys: &[(Modifiers::SUPER, "c"), (Modifiers::NONE, "Copy")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Paste from clipboard", + doc: "Pastes text from the clipboard", + exp: |exp| exp.push(PasteFrom(ClipboardPasteSource::Clipboard)), + keys: &[(Modifiers::SUPER, "v"), (Modifiers::NONE, "Paste")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Toggle full screen mode", + doc: "Switch between normal and full screen mode", + exp: |exp| { + exp.push(ToggleFullScreen); + }, + keys: &[(Modifiers::ALT, "Return")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Hide/Minimize Window", + doc: "Hides/Mimimizes the current window", + exp: |exp| { + exp.push(Hide); + }, + keys: &[(Modifiers::SUPER, "m")], + args: &[ArgType::ActiveWindow], + }, + #[cfg(target_os = "macos")] + CommandDef { + brief: "Hide Application (macOS only)", + doc: "Hides all of the windows of the application. \ + This is macOS specific.", + exp: |exp| { + exp.push(HideApplication); + }, + keys: &[(Modifiers::SUPER, "h")], + args: &[], + }, + #[cfg(target_os = "macos")] + CommandDef { + brief: "Quit WezTerm (macOS only)", + doc: "Quits WezTerm", + exp: |exp| { + exp.push(HideApplication); + }, + keys: &[(Modifiers::SUPER, "h")], + args: &[], + }, + CommandDef { + brief: "New Window", + doc: "Launches the default program into a new window", + exp: |exp| { + exp.push(SpawnWindow); + }, + keys: &[(Modifiers::SUPER, "n")], + args: &[], + }, + CommandDef { + brief: "Clear scrollback", + doc: "Clears any text that has scrolled out of the \ + viewport of the current pane", + exp: |exp| { + exp.push(ClearScrollback(ScrollbackEraseMode::ScrollbackOnly)); + }, + keys: &[(Modifiers::SUPER, "k")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Search pane output", + doc: "Enters the search mode UI for the current pane", + exp: |exp| { + exp.push(Search(Pattern::CaseSensitiveString("".into()))); + }, + keys: &[(Modifiers::SUPER, "f")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Show debug overlay", + doc: "Activates the debug overlay and Lua REPL", + exp: |exp| { + exp.push(ShowDebugOverlay); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "l")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Enter QuickSelect mode", + doc: "Activates the quick selection UI for the current pane", + exp: |exp| { + exp.push(QuickSelect); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "Space")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Decrease font size", + doc: "Scales the font size smaller by 10%", + exp: |exp| { + exp.push(DecreaseFontSize); + }, + keys: &[(Modifiers::SUPER, "-"), (Modifiers::CTRL, "-")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Reset font size", + doc: "Restores the font size to match your configuration file", + exp: |exp| { + exp.push(ResetFontSize); + }, + keys: &[(Modifiers::SUPER, "0"), (Modifiers::CTRL, "0")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Increase font size", + doc: "Scales the font size larger by 10%", + exp: |exp| { + exp.push(IncreaseFontSize); + }, + keys: &[(Modifiers::SUPER, "="), (Modifiers::CTRL, "=")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "New Tab", + doc: "Create a new tab in the same domain as the current pane", + exp: |exp| { + exp.push(SpawnTab(SpawnTabDomain::CurrentPaneDomain)); + }, + keys: &[(Modifiers::SUPER, "t")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 1st Tab", + doc: "Activates the left-most tab", + + exp: |exp| { + exp.push(ActivateTab(0)); + }, + keys: &[(Modifiers::SUPER, "1")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 2nd Tab", + doc: "Activates the 2nd tab from the left", + exp: |exp| { + exp.push(ActivateTab(1)); + }, + keys: &[(Modifiers::SUPER, "2")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 3rd Tab", + doc: "Activates the 3rd tab from the left", + exp: |exp| { + exp.push(ActivateTab(2)); + }, + keys: &[(Modifiers::SUPER, "3")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 4th Tab", + doc: "Activates the 4th tab from the left", + exp: |exp| { + exp.push(ActivateTab(3)); + }, + keys: &[(Modifiers::SUPER, "4")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 5th Tab", + doc: "Activates the 5th tab from the left", + exp: |exp| { + exp.push(ActivateTab(4)); + }, + keys: &[(Modifiers::SUPER, "5")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 6th Tab", + doc: "Activates the 6th tab from the left", + exp: |exp| { + exp.push(ActivateTab(5)); + }, + keys: &[(Modifiers::SUPER, "6")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 7th Tab", + doc: "Activates the 7th tab from the left", + exp: |exp| { + exp.push(ActivateTab(6)); + }, + keys: &[(Modifiers::SUPER, "7")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate 8th Tab", + doc: "Activates the 8th tab from the left", + exp: |exp| { + exp.push(ActivateTab(7)); + }, + keys: &[(Modifiers::SUPER, "8")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate right-most tab", + doc: "Activates the tab on the far right", + exp: |exp| { + exp.push(ActivateTab(-1)); + }, + keys: &[(Modifiers::SUPER, "9")], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Close current tab", + doc: "Closes the current tab, terminating all the \ + processes that are running in its panes.", + exp: |exp| { + exp.push(CloseCurrentTab { confirm: true }); + }, + keys: &[(Modifiers::SUPER, "w")], + args: &[ArgType::ActiveTab], + }, + CommandDef { + brief: "Activate the tab to the left", + doc: "Activates the tab to the left. If this is the left-most \ + tab then cycles around and activates the right-most tab", + exp: |exp| { + exp.push(ActivateTabRelative(-1)); + }, + keys: &[ + (Modifiers::SUPER.union(Modifiers::SHIFT), "["), + (Modifiers::CTRL.union(Modifiers::SHIFT), "Tab"), + (Modifiers::CTRL, "PageUp"), + ], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Activate the tab to the right", + doc: "Activates the tab to the right. If this is the right-most \ + tab then cycles around and activates the left-most tab", + exp: |exp| { + exp.push(ActivateTabRelative(1)); + }, + keys: &[ + (Modifiers::SUPER.union(Modifiers::SHIFT), "]"), + (Modifiers::CTRL, "Tab"), + (Modifiers::CTRL, "PageDown"), + ], + args: &[ArgType::ActiveWindow], + }, + CommandDef { + brief: "Reload configuration", + doc: "Reloads the configuration file", + exp: |exp| { + exp.push(ReloadConfiguration); + }, + keys: &[(Modifiers::SUPER, "r")], + args: &[], + }, + CommandDef { + brief: "Move tab one place to the left", + doc: "Rearranges the tabs so that the current tab moves \ + one place to the left", + exp: |exp| { + exp.push(MoveTabRelative(-1)); + }, + keys: &[(Modifiers::SUPER.union(Modifiers::SHIFT), "PageUp")], + args: &[ArgType::ActiveTab], + }, + CommandDef { + brief: "Move tab one place to the right", + doc: "Rearranges the tabs so that the current tab moves \ + one place to the right", + exp: |exp| { + exp.push(MoveTabRelative(1)); + }, + keys: &[(Modifiers::SUPER.union(Modifiers::SHIFT), "PageDown")], + args: &[ArgType::ActiveTab], + }, + CommandDef { + brief: "Scroll Up One Page", + doc: "Scrolls the viewport up by 1 page", + exp: |exp| exp.push(ScrollByPage(NotNan::new(-1.0).unwrap())), + keys: &[(Modifiers::SHIFT, "PageUp")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Scroll Down One Page", + doc: "Scrolls the viewport down by 1 page", + + exp: |exp| exp.push(ScrollByPage(NotNan::new(1.0).unwrap())), + keys: &[(Modifiers::SHIFT, "PageUp")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Activate Copy Mode", + doc: "Enter mouse-less copy mode to select text using only \ + the keyboard", + exp: |exp| { + exp.push(ActivateCopyMode); + }, + keys: &[(Modifiers::SUPER.union(Modifiers::SHIFT), "x")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Split Vertically (Top/Bottom)", + doc: "Split the current pane vertically into two panes, by spawning \ + the default program into the bottom half", + exp: |exp| { + exp.push(SplitVertical(SpawnCommand { + domain: SpawnTabDomain::CurrentPaneDomain, + ..Default::default() + })); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "'", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Split Horizontally (Left/Right)", + doc: "Split the current pane horizontally into two panes, by spawning \ + the default program into the right hand side", + exp: |exp| { + exp.push(SplitHorizontal(SpawnCommand { + domain: SpawnTabDomain::CurrentPaneDomain, + ..Default::default() + })); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "5", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Adjust Pane Size to the Left", + doc: "Adjusts the closest split divider to the left", + exp: |exp| { + exp.push(AdjustPaneSize(PaneDirection::Left, 1)); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "LeftArrow", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Adjust Pane Size to the Right", + doc: "Adjusts the closest split divider to the right", + exp: |exp| { + exp.push(AdjustPaneSize(PaneDirection::Right, 1)); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "RightArrow", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Adjust Pane Size Upwards", + doc: "Adjusts the closest split divider towards the top", + exp: |exp| { + exp.push(AdjustPaneSize(PaneDirection::Up, 1)); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "UpArrow", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Adjust Pane Size Downwards", + doc: "Adjusts the closest split divider towards the bottom", + exp: |exp| { + exp.push(AdjustPaneSize(PaneDirection::Down, 1)); + }, + keys: &[( + Modifiers::CTRL + .union(Modifiers::ALT) + .union(Modifiers::SHIFT), + "DownArrow", + )], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Activate Pane Left", + doc: "Activates the pane to the left of the current pane", + exp: |exp| { + exp.push(ActivatePaneDirection(PaneDirection::Left)); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "LeftArrow")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Activate Pane Right", + doc: "Activates the pane to the right of the current pane", + exp: |exp| { + exp.push(ActivatePaneDirection(PaneDirection::Right)); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "RightArrow")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Activate Pane Up", + doc: "Activates the pane to the top of the current pane", + exp: |exp| { + exp.push(ActivatePaneDirection(PaneDirection::Up)); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "UpArrow")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Activate Pane Down", + doc: "Activates the pane to the bottom of the current pane", + exp: |exp| { + exp.push(ActivatePaneDirection(PaneDirection::Down)); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "DownArrow")], + args: &[ArgType::ActivePane], + }, + CommandDef { + brief: "Toggle Pane Zoom", + doc: "Toggles the zoom state for the current pane", + exp: |exp| { + exp.push(TogglePaneZoomState); + }, + keys: &[(Modifiers::CTRL.union(Modifiers::SHIFT), "z")], + args: &[ArgType::ActivePane], + }, +]; diff --git a/wezterm-gui/src/inputmap.rs b/wezterm-gui/src/inputmap.rs new file mode 100644 index 000000000..41d53bc97 --- /dev/null +++ b/wezterm-gui/src/inputmap.rs @@ -0,0 +1,222 @@ +use crate::commands::CommandDef; +use config::keyassignment::{ + ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, KeyTableEntry, KeyTables, + MouseEventTrigger, SelectionMode, +}; +use config::ConfigHandle; +use std::collections::HashMap; +use std::time::Duration; +use wezterm_term::input::MouseButton; +use window::{KeyCode, Modifiers}; + +pub struct InputMap { + pub keys: KeyTables, + pub mouse: HashMap<(MouseEventTrigger, Modifiers), KeyAssignment>, + leader: Option<(KeyCode, Modifiers, Duration)>, +} + +impl InputMap { + pub fn new(config: &ConfigHandle) -> Self { + let mut mouse = config.mouse_bindings(); + + let mut keys = config.key_bindings(); + + let leader = config.leader.as_ref().map(|leader| { + ( + leader.key.key.resolve(config.key_map_preference).clone(), + leader.key.mods, + Duration::from_millis(leader.timeout_milliseconds), + ) + }); + + let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT; + + macro_rules! m { + ($([$mod:expr, $code:expr, $action:expr]),* $(,)?) => { + $( + mouse.entry(($code, $mod)).or_insert($action); + )* + }; + } + + use KeyAssignment::*; + + if !config.disable_default_key_bindings { + for (mods, code, action) in CommandDef::default_key_assignments(config) { + keys.default + .entry((code, mods)) + .or_insert(KeyTableEntry { action }); + } + } + + if !config.disable_default_mouse_bindings { + m!( + [ + Modifiers::NONE, + MouseEventTrigger::Down { + streak: 3, + button: MouseButton::Left + }, + SelectTextAtMouseCursor(SelectionMode::Line) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Down { + streak: 2, + button: MouseButton::Left + }, + SelectTextAtMouseCursor(SelectionMode::Word) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Down { + streak: 1, + button: MouseButton::Left + }, + SelectTextAtMouseCursor(SelectionMode::Cell) + ], + [ + Modifiers::SHIFT, + MouseEventTrigger::Down { + streak: 1, + button: MouseButton::Left + }, + ExtendSelectionToMouseCursor(None) + ], + [ + Modifiers::SHIFT, + MouseEventTrigger::Up { + streak: 1, + button: MouseButton::Left + }, + CompleteSelectionOrOpenLinkAtMouseCursor( + ClipboardCopyDestination::PrimarySelection + ) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Up { + streak: 1, + button: MouseButton::Left + }, + CompleteSelectionOrOpenLinkAtMouseCursor( + ClipboardCopyDestination::PrimarySelection + ) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Up { + streak: 2, + button: MouseButton::Left + }, + CompleteSelection(ClipboardCopyDestination::PrimarySelection) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Up { + streak: 3, + button: MouseButton::Left + }, + CompleteSelection(ClipboardCopyDestination::PrimarySelection) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Drag { + streak: 1, + button: MouseButton::Left + }, + ExtendSelectionToMouseCursor(Some(SelectionMode::Cell)) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Drag { + streak: 2, + button: MouseButton::Left + }, + ExtendSelectionToMouseCursor(Some(SelectionMode::Word)) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Drag { + streak: 3, + button: MouseButton::Left + }, + ExtendSelectionToMouseCursor(Some(SelectionMode::Line)) + ], + [ + Modifiers::NONE, + MouseEventTrigger::Down { + streak: 1, + button: MouseButton::Middle + }, + PasteFrom(ClipboardPasteSource::PrimarySelection) + ], + [ + Modifiers::SUPER, + MouseEventTrigger::Drag { + streak: 1, + button: MouseButton::Left, + }, + StartWindowDrag + ], + [ + ctrl_shift, + MouseEventTrigger::Drag { + streak: 1, + button: MouseButton::Left, + }, + StartWindowDrag + ], + ); + } + + keys.default + .retain(|_, v| v.action != KeyAssignment::DisableDefaultAssignment); + mouse.retain(|_, v| *v != KeyAssignment::DisableDefaultAssignment); + + Self { + keys, + leader, + mouse, + } + } + + pub fn is_leader(&self, key: &KeyCode, mods: Modifiers) -> Option { + if let Some((leader_key, leader_mods, timeout)) = self.leader.as_ref() { + if *leader_key == *key && *leader_mods == mods { + return Some(timeout.clone()); + } + } + None + } + + fn remove_positional_alt(mods: Modifiers) -> Modifiers { + mods - (Modifiers::LEFT_ALT | Modifiers::RIGHT_ALT) + } + + pub fn has_table(&self, name: &str) -> bool { + self.keys.by_name.contains_key(name) + } + + pub fn lookup_key( + &self, + key: &KeyCode, + mods: Modifiers, + table_name: Option<&str>, + ) -> Option { + let table = match table_name { + Some(name) => self.keys.by_name.get(name)?, + None => &self.keys.default, + }; + + table + .get(&key.normalize_shift(Self::remove_positional_alt(mods))) + .cloned() + } + + pub fn lookup_mouse(&self, event: MouseEventTrigger, mods: Modifiers) -> Option { + self.mouse + .get(&(event, Self::remove_positional_alt(mods))) + .cloned() + } +} diff --git a/wezterm-gui/src/main.rs b/wezterm-gui/src/main.rs index 213d09f44..833cdf512 100644 --- a/wezterm-gui/src/main.rs +++ b/wezterm-gui/src/main.rs @@ -26,10 +26,12 @@ use wezterm_toast_notification::*; mod cache; mod colorease; +mod commands; mod customglyph; mod download; mod frontend; mod glyphcache; +mod inputmap; mod markdown; mod overlay; mod quad; diff --git a/wezterm-gui/src/overlay/launcher.rs b/wezterm-gui/src/overlay/launcher.rs index 807510b23..152acc37c 100644 --- a/wezterm-gui/src/overlay/launcher.rs +++ b/wezterm-gui/src/overlay/launcher.rs @@ -5,10 +5,11 @@ //! be rendered as a popup/context menu if the system supports it; at the //! time of writing our window layer doesn't provide an API for context //! menus. +use crate::inputmap::InputMap; use crate::termwindow::TermWindowNotif; use anyhow::anyhow; use config::configuration; -use config::keyassignment::{InputMap, KeyAssignment, SpawnCommand, SpawnTabDomain}; +use config::keyassignment::{KeyAssignment, SpawnCommand, SpawnTabDomain}; use config::lua::truncate_right; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; @@ -332,6 +333,16 @@ impl LauncherState { key_entries.sort_by(|a, b| a.label.cmp(&b.label)); self.entries.append(&mut key_entries); } + + if args.flags.contains(LauncherFlags::COMMANDS) { + let commands = crate::commands::CommandDef::expanded_commands(&config); + for cmd in commands { + self.entries.push(Entry { + label: format!("{}. {}", cmd.brief, cmd.doc), + kind: EntryKind::KeyAssignment(cmd.action), + }); + } + } } fn render(&mut self, term: &mut TermWizTerminal) -> termwiz::Result<()> { diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index 7fa85d8f0..89d296364 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -5,6 +5,7 @@ use crate::cache::LruCache; use crate::colorease::ColorEase; use crate::frontend::front_end; use crate::glium::texture::SrgbTexture2d; +use crate::inputmap::InputMap; use crate::overlay::{ confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program, launcher, start_overlay, start_overlay_pane, CopyOverlay, LauncherArgs, LauncherFlags, @@ -21,7 +22,7 @@ use ::wezterm_term::input::{ClickPosition, MouseButton as TMB}; use ::window::*; use anyhow::{anyhow, ensure, Context}; use config::keyassignment::{ - ClipboardCopyDestination, ClipboardPasteSource, InputMap, KeyAssignment, QuickSelectArguments, + ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, QuickSelectArguments, SpawnCommand, }; use config::{