A ui toolkit for building gpui apps
rust gpui
at main 290 lines 9.0 kB view raw
1//! Keymap module for managing keyboard shortcuts and their associated actions 2//! 3//! This module provides JSON-based keymap configuration support, allowing 4//! keybindings to be loaded from external files rather than hardcoded. 5 6use anyhow::{anyhow, Context as _, Result}; 7use serde::{Deserialize, Serialize}; 8use std::collections::HashMap; 9use std::fs; 10use std::path::Path; 11 12pub mod extensions; 13 14/// Represents a complete keymap configuration 15#[derive(Debug, Clone, Serialize, Deserialize)] 16pub struct Keymap { 17 /// Optional context where these bindings apply (e.g., "Editor", "Menu") 18 #[serde(skip_serializing_if = "Option::is_none")] 19 pub context: Option<String>, 20 21 /// The key bindings in this keymap 22 pub bindings: HashMap<String, String>, 23} 24 25impl Keymap { 26 /// Create a new keymap with the given bindings 27 pub fn new(bindings: HashMap<String, String>) -> Self { 28 Self { 29 context: None, 30 bindings, 31 } 32 } 33 34 /// Create a new keymap with context 35 pub fn with_context(context: impl Into<String>, bindings: HashMap<String, String>) -> Self { 36 Self { 37 context: Some(context.into()), 38 bindings, 39 } 40 } 41} 42 43/// A collection of keymaps, typically loaded from multiple files 44#[derive(Debug, Default)] 45pub struct KeymapCollection { 46 keymaps: Vec<Keymap>, 47} 48 49impl KeymapCollection { 50 /// Create a new empty keymap collection 51 pub fn new() -> Self { 52 Self::default() 53 } 54 55 /// Load a keymap from a JSON file 56 pub fn load_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { 57 let path = path.as_ref(); 58 let contents = fs::read_to_string(path) 59 .with_context(|| format!("Failed to read keymap file: {}", path.display()))?; 60 61 self.load_json(&contents) 62 .with_context(|| format!("Failed to parse keymap file: {}", path.display()))?; 63 64 Ok(()) 65 } 66 67 /// Load keymaps from a JSON string 68 pub fn load_json(&mut self, json: &str) -> Result<()> { 69 // Try parsing as an array first (multiple keymaps) 70 if let Ok(keymaps) = serde_json::from_str::<Vec<Keymap>>(json) { 71 self.keymaps.extend(keymaps); 72 return Ok(()); 73 } 74 75 // Try parsing as a single keymap 76 if let Ok(keymap) = serde_json::from_str::<Keymap>(json) { 77 self.keymaps.push(keymap); 78 return Ok(()); 79 } 80 81 Err(anyhow!("Invalid keymap JSON format")) 82 } 83 84 /// Load default keymaps 85 pub fn load_defaults(&mut self) -> Result<()> { 86 let default_keymap = include_str!("../default-keymap.json"); 87 self.load_json(default_keymap)?; 88 Ok(()) 89 } 90 91 /// Get all key binding specifications from this collection 92 /// 93 /// Returns a list of binding specifications that can be used to create 94 /// actual GPUI key bindings with concrete action types. 95 pub fn get_binding_specs(&self) -> Vec<BindingSpec> { 96 let mut specs = Vec::new(); 97 98 for keymap in &self.keymaps { 99 let context = keymap.context.as_deref(); 100 101 for (keystrokes, action_name) in &keymap.bindings { 102 specs.push(BindingSpec { 103 keystrokes: keystrokes.clone(), 104 action_name: action_name.clone(), 105 context: context.map(String::from), 106 }); 107 } 108 } 109 110 specs 111 } 112 113 /// Get all keymaps in this collection 114 pub fn keymaps(&self) -> &[Keymap] { 115 &self.keymaps 116 } 117 118 /// Add a keymap to this collection 119 pub fn add(&mut self, keymap: Keymap) { 120 self.keymaps.push(keymap); 121 } 122 123 /// Clear all keymaps from this collection 124 pub fn clear(&mut self) { 125 self.keymaps.clear(); 126 } 127 128 /// Find all bindings for a given action 129 pub fn find_bindings_for_action(&self, action_name: &str) -> Vec<BindingSpec> { 130 self.get_binding_specs() 131 .into_iter() 132 .filter(|spec| spec.action_name == action_name) 133 .collect() 134 } 135 136 /// Find the action for a given keystroke in a context 137 pub fn find_action(&self, keystrokes: &str, context: Option<&str>) -> Option<&str> { 138 // First try to find a binding with matching context 139 if let Some(context) = context { 140 for keymap in &self.keymaps { 141 if keymap.context.as_deref() == Some(context) { 142 if let Some(action) = keymap.bindings.get(keystrokes) { 143 return Some(action); 144 } 145 } 146 } 147 } 148 149 // Then try bindings without context (global) 150 for keymap in &self.keymaps { 151 if keymap.context.is_none() { 152 if let Some(action) = keymap.bindings.get(keystrokes) { 153 return Some(action); 154 } 155 } 156 } 157 158 None 159 } 160} 161 162/// Specification for a key binding 163#[derive(Debug, Clone)] 164pub struct BindingSpec { 165 /// The keystroke sequence (e.g., "cmd-s", "ctrl-shift-p") 166 pub keystrokes: String, 167 /// The action name to trigger 168 pub action_name: String, 169 /// Optional context where this binding applies 170 pub context: Option<String>, 171} 172 173/// Helper function to create a simple binding 174pub fn binding(key: impl Into<String>, action: impl Into<String>) -> (String, String) { 175 (key.into(), action.into()) 176} 177 178#[cfg(test)] 179mod tests { 180 use super::*; 181 182 #[test] 183 fn test_parse_simple_keymap() { 184 let json = r#"{ 185 "context": "Editor", 186 "bindings": { 187 "cmd-s": "Save", 188 "cmd-z": "Undo" 189 } 190 }"#; 191 192 let keymap: Keymap = serde_json::from_str(json).unwrap(); 193 assert_eq!(keymap.context, Some("Editor".to_string())); 194 assert_eq!(keymap.bindings.len(), 2); 195 assert_eq!(keymap.bindings.get("cmd-s"), Some(&"Save".to_string())); 196 assert_eq!(keymap.bindings.get("cmd-z"), Some(&"Undo".to_string())); 197 } 198 199 #[test] 200 fn test_parse_multiple_keymaps() { 201 let json = r#"[ 202 { 203 "bindings": { 204 "cmd-s": "Save", 205 "cmd-z": "Undo" 206 } 207 }, 208 { 209 "context": "Menu", 210 "bindings": { 211 "enter": "Select", 212 "escape": "Cancel" 213 } 214 } 215 ]"#; 216 217 let keymaps: Vec<Keymap> = serde_json::from_str(json).unwrap(); 218 assert_eq!(keymaps.len(), 2); 219 assert_eq!(keymaps[0].context, None); 220 assert_eq!(keymaps[1].context, Some("Menu".to_string())); 221 } 222 223 #[test] 224 fn test_keymap_collection() { 225 let mut collection = KeymapCollection::new(); 226 227 let json1 = r#"{ "bindings": { "cmd-s": "Save" } }"#; 228 let json2 = r#"{ "context": "Menu", "bindings": { "enter": "Select" } }"#; 229 230 collection.load_json(json1).unwrap(); 231 collection.load_json(json2).unwrap(); 232 233 assert_eq!(collection.keymaps().len(), 2); 234 assert_eq!(collection.keymaps()[0].context, None); 235 assert_eq!(collection.keymaps()[1].context, Some("Menu".to_string())); 236 237 let specs = collection.get_binding_specs(); 238 assert_eq!(specs.len(), 2); 239 assert_eq!(specs[0].keystrokes, "cmd-s"); 240 assert_eq!(specs[0].action_name, "Save"); 241 assert_eq!(specs[0].context, None); 242 } 243 244 #[test] 245 fn test_find_action() { 246 let mut collection = KeymapCollection::new(); 247 248 collection.add(Keymap::new( 249 [("cmd-s", "Save"), ("cmd-z", "Undo")] 250 .iter() 251 .map(|(k, v)| (k.to_string(), v.to_string())) 252 .collect(), 253 )); 254 255 collection.add(Keymap::with_context( 256 "Editor", 257 [("cmd-x", "Cut")] 258 .iter() 259 .map(|(k, v)| (k.to_string(), v.to_string())) 260 .collect(), 261 )); 262 263 // Global binding 264 assert_eq!(collection.find_action("cmd-s", None), Some("Save")); 265 assert_eq!( 266 collection.find_action("cmd-s", Some("Editor")), 267 Some("Save") 268 ); 269 270 // Context-specific binding 271 assert_eq!(collection.find_action("cmd-x", Some("Editor")), Some("Cut")); 272 assert_eq!(collection.find_action("cmd-x", None), None); 273 assert_eq!(collection.find_action("cmd-x", Some("Menu")), None); 274 } 275 276 #[test] 277 fn test_serialize_keymap() { 278 let mut bindings = HashMap::new(); 279 bindings.insert("cmd-s".to_string(), "Save".to_string()); 280 bindings.insert("cmd-z".to_string(), "Undo".to_string()); 281 282 let keymap = Keymap::with_context("Editor", bindings); 283 284 let json = serde_json::to_string_pretty(&keymap).unwrap(); 285 let parsed: Keymap = serde_json::from_str(&json).unwrap(); 286 287 assert_eq!(parsed.context, keymap.context); 288 assert_eq!(parsed.bindings, keymap.bindings); 289 } 290}