gpuikit_keymap/
lib.rs

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}