A ui toolkit for building gpui apps
rust gpui

more tidying

+209 -959
-1
Cargo.toml
··· 22 22 [workspace.dependencies] 23 23 # workspace crates 24 24 gpuikit = { path = "crates/gpuikit", version = "0.1.0" } 25 - gpuikit-assets = { path = "crates/gpuikit-assets", version = "0.1.0" } 26 25 gpuikit-theme = { path = "crates/gpuikit-theme", version = "0.1.0" } 27 26 gpuikit-utils = { path = "crates/gpuikit-utils", version = "0.1.0" } 28 27 gpuikit-keymap = { path = "crates/gpuikit-keymap", version = "0.1.0" }
-29
crates/gpuikit-assets/Cargo.toml
··· 1 - [package] 2 - name = "gpuikit-assets" 3 - version.workspace = true 4 - edition.workspace = true 5 - authors.workspace = true 6 - license.workspace = true 7 - repository.workspace = true 8 - homepage.workspace = true 9 - description = "Asset management for GPUI applications" 10 - readme = "README.md" 11 - keywords = ["gpui", "assets", "embed", "resources", "icons"] 12 - categories = ["gui", "multimedia"] 13 - 14 - [lib] 15 - name = "gpuikit_assets" 16 - path = "src/lib.rs" 17 - 18 - [dependencies] 19 - # GPUI framework 20 - gpui = { workspace = true } 21 - 22 - # Embedding assets in binary 23 - rust-embed = { workspace = true } 24 - 25 - # Error handling 26 - anyhow = { workspace = true } 27 - 28 - [dev-dependencies] 29 - gpui = { workspace = true, features = ["test-support"] }
-126
crates/gpuikit-assets/src/lib.rs
··· 1 - //! Asset management for GPUI applications 2 - //! 3 - //! This crate provides utilities for embedding and managing assets in GPUI applications. 4 - 5 - use anyhow::{anyhow, Result}; 6 - use gpui::{AssetSource, SharedString}; 7 - use rust_embed::RustEmbed; 8 - use std::borrow::Cow; 9 - 10 - /// Trait for types that can serve as embedded asset sources 11 - pub trait EmbeddedAssets: RustEmbed { 12 - /// Get an asset by path 13 - fn get_asset(path: &str) -> Option<Cow<'static, [u8]>> { 14 - Self::get(path).map(|file| file.data) 15 - } 16 - 17 - /// Check if an asset exists 18 - fn has_asset(path: &str) -> bool { 19 - Self::get(path).is_some() 20 - } 21 - 22 - /// List all assets matching a pattern 23 - fn list_assets(pattern: Option<&str>) -> Vec<String> { 24 - let iter = Self::iter(); 25 - match pattern { 26 - Some(pat) => iter 27 - .filter(|path| path.contains(pat)) 28 - .map(|s| s.to_string()) 29 - .collect(), 30 - None => iter.map(|s| s.to_string()).collect(), 31 - } 32 - } 33 - } 34 - 35 - /// Default implementation for all RustEmbed types 36 - impl<T: RustEmbed> EmbeddedAssets for T {} 37 - 38 - /// Asset manager for loading and caching embedded assets 39 - #[derive(Debug, Clone)] 40 - pub struct AssetManager {} 41 - 42 - impl AssetManager { 43 - /// Create a new asset manager 44 - pub fn new() -> Self { 45 - Self {} 46 - } 47 - 48 - /// Load an embedded asset 49 - pub fn load_embedded<T: EmbeddedAssets>(&self, path: &str) -> Result<Vec<u8>> { 50 - T::get_asset(path) 51 - .map(|data| data.to_vec()) 52 - .ok_or_else(|| anyhow!("Asset not found: {}", path)) 53 - } 54 - 55 - /// Load an embedded text asset 56 - pub fn load_text<T: EmbeddedAssets>(&self, path: &str) -> Result<String> { 57 - let data = self.load_embedded::<T>(path)?; 58 - String::from_utf8(data).map_err(|e| anyhow!("Invalid UTF-8 in asset {}: {}", path, e)) 59 - } 60 - 61 - /// List all embedded assets 62 - pub fn list_embedded<T: EmbeddedAssets>(&self, pattern: Option<&str>) -> Vec<String> { 63 - T::list_assets(pattern) 64 - } 65 - } 66 - 67 - /// GPUI AssetSource implementation for embedded assets 68 - pub struct EmbeddedAssetSource<T: EmbeddedAssets + Send + Sync> { 69 - _phantom: std::marker::PhantomData<T>, 70 - } 71 - 72 - impl<T: EmbeddedAssets + Send + Sync> Default for EmbeddedAssetSource<T> { 73 - fn default() -> Self { 74 - Self::new() 75 - } 76 - } 77 - 78 - impl<T: EmbeddedAssets + Send + Sync> EmbeddedAssetSource<T> { 79 - /// Create a new embedded asset source 80 - pub fn new() -> Self { 81 - Self { 82 - _phantom: std::marker::PhantomData, 83 - } 84 - } 85 - } 86 - 87 - impl<T: EmbeddedAssets + Send + Sync + 'static> AssetSource for EmbeddedAssetSource<T> { 88 - fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> { 89 - Ok(T::get_asset(path)) 90 - } 91 - 92 - fn list(&self, prefix: &str) -> Result<Vec<SharedString>> { 93 - Ok(T::list_assets(Some(prefix)) 94 - .into_iter() 95 - .map(|s| s.into()) 96 - .collect()) 97 - } 98 - } 99 - 100 - /// Helper macro to define an embedded assets module 101 - #[macro_export] 102 - macro_rules! embed_assets { 103 - ($name:ident, $folder:expr) => { 104 - #[derive(::rust_embed::RustEmbed)] 105 - #[folder = $folder] 106 - pub struct $name; 107 - 108 - impl $name { 109 - /// Create a GPUI asset source for these embedded assets 110 - pub fn asset_source() -> $crate::EmbeddedAssetSource<Self> { 111 - $crate::EmbeddedAssetSource::new() 112 - } 113 - } 114 - }; 115 - } 116 - 117 - #[cfg(test)] 118 - mod tests { 119 - use super::*; 120 - 121 - #[test] 122 - fn test_asset_manager() { 123 - let _manager = AssetManager::new(); 124 - // Just verify that we can create an asset manager 125 - } 126 - }
+2 -28
crates/gpuikit-keymap/Cargo.toml
··· 6 6 license.workspace = true 7 7 repository.workspace = true 8 8 homepage.workspace = true 9 - description = "Flexible keymap and keyboard shortcut management for GPUI applications" 10 - readme = "README.md" 11 - keywords = ["gpui", "keymap", "keyboard", "shortcuts", "keybindings"] 12 - categories = ["gui", "config"] 13 9 14 10 [lib] 15 11 name = "gpuikit_keymap" 16 12 path = "src/lib.rs" 17 13 18 14 [dependencies] 19 - # GPUI framework 15 + anyhow = { workspace = true } 20 16 gpui = { workspace = true } 21 - 22 - # Serialization 17 + log = { workspace = true } 23 18 serde = { workspace = true } 24 19 serde_json = { workspace = true } 25 - 26 - # Error handling 27 - anyhow = { workspace = true } 28 - 29 - # Logging 30 - log = { workspace = true } 31 20 32 21 [dev-dependencies] 33 22 gpui = { workspace = true, features = ["test-support"] } ··· 36 25 37 26 [features] 38 27 default = [] 39 - # Enable schema generation for keymap files 40 28 schema = ["schemars"] 41 29 42 - # Optional dependencies 43 30 [dependencies.schemars] 44 31 version = "0.8" 45 32 optional = true 46 33 47 - # Package metadata for crates.io 48 34 [package.metadata.docs.rs] 49 35 all-features = true 50 36 rustdoc-args = ["--cfg", "docsrs"] 51 - 52 - [[example]] 53 - name = "basic_keymap" 54 - path = "examples/basic_keymap.rs" 55 - 56 - [[example]] 57 - name = "custom_actions" 58 - path = "examples/custom_actions.rs" 59 - 60 - [[example]] 61 - name = "load_from_json" 62 - path = "examples/load_from_json.rs"
-380
crates/gpuikit-keymap/README.md
··· 1 - # Keymap Module Documentation 2 - 3 - The `keymap` module provides JSON-based keyboard shortcut configuration for GPUI applications, allowing keybindings to be loaded from external files rather than hardcoded in the application. 4 - 5 - ## Features 6 - 7 - - **JSON Configuration**: Define keybindings in human-readable JSON files 8 - - **Multiple Contexts**: Support for context-specific keybindings (e.g., Editor, Menu, Global) 9 - - **Action Parameters**: Pass parameters to actions through keybindings 10 - - **Platform-specific**: Automatically loads platform-appropriate default keymaps 11 - - **Flexible Loading**: Load from files, strings, or create programmatically 12 - - **Type-safe**: Compile-time checking when converting to GPUI bindings 13 - 14 - ## Quick Start 15 - 16 - ### Basic Usage 17 - 18 - ```rust 19 - use gpui_editor::keymap::KeymapCollection; 20 - 21 - // Create a keymap collection 22 - let mut keymaps = KeymapCollection::new(); 23 - 24 - // Load default keymaps for the current platform 25 - keymaps.load_defaults()?; 26 - 27 - // Or load from a custom file 28 - keymaps.load_file("my-keymaps.json")?; 29 - 30 - // Get binding specifications 31 - let specs = keymaps.get_binding_specs()?; 32 - 33 - // Convert to GPUI bindings (requires mapping action names to concrete types) 34 - for spec in specs { 35 - match spec.action_name.as_str() { 36 - "editor::Save" => { 37 - cx.bind_keys([KeyBinding::new(&spec.keystrokes, SaveAction, spec.context.as_deref())]); 38 - } 39 - // ... handle other actions 40 - } 41 - } 42 - ``` 43 - 44 - ## JSON Format 45 - 46 - ### Simple Bindings 47 - 48 - ```json 49 - { 50 - "context": "Editor", 51 - "use_key_equivalents": true, 52 - "bindings": { 53 - "cmd-s": "editor::Save", 54 - "cmd-z": "editor::Undo", 55 - "cmd-shift-z": "editor::Redo" 56 - } 57 - } 58 - ``` 59 - 60 - ### Complex Bindings with Parameters 61 - 62 - ```json 63 - { 64 - "bindings": [ 65 - { 66 - "key": "cmd-z", 67 - "action": ["editor::Undo", { "count": 1 }] 68 - }, 69 - { 70 - "key": "cmd-k cmd-c", 71 - "action": "editor::CommentLine", 72 - "context": "CodeEditor" 73 - } 74 - ] 75 - } 76 - ``` 77 - 78 - ### Multiple Contexts 79 - 80 - ```json 81 - [ 82 - { 83 - "context": "Global", 84 - "bindings": { 85 - "cmd-q": "application::Quit", 86 - "cmd-n": "application::NewWindow" 87 - } 88 - }, 89 - { 90 - "context": "Editor", 91 - "bindings": { 92 - "cmd-s": "editor::Save", 93 - "cmd-z": "editor::Undo" 94 - } 95 - } 96 - ] 97 - ``` 98 - 99 - ## Keymap Structure 100 - 101 - ### Fields 102 - 103 - - **`context`** (optional): String specifying where these bindings apply 104 - - **`use_key_equivalents`**: Boolean for platform-specific key mappings 105 - - **`bindings`**: Either a map of keystrokes to actions (simple) or an array of binding entries (complex) 106 - 107 - ### Keystroke Format 108 - 109 - Keystrokes follow the pattern: `[modifiers-]key` 110 - 111 - **Modifiers:** 112 - - `cmd` / `ctrl`: Command (macOS) or Control (Windows/Linux) 113 - - `alt` / `option`: Alt/Option key 114 - - `shift`: Shift key 115 - - `fn`: Function key 116 - 117 - **Examples:** 118 - - `cmd-s`: Command+S (macOS) or Ctrl+S (Windows/Linux) 119 - - `cmd-shift-z`: Command+Shift+Z 120 - - `alt-left`: Alt+Left Arrow 121 - - `cmd-k cmd-c`: Command+K followed by Command+C (chord) 122 - 123 - ## Action Registry 124 - 125 - To convert keymap specifications to actual GPUI actions, you need an action registry: 126 - 127 - ```rust 128 - use gpui_editor::keymap::{SimpleActionRegistry, ActionRegistry}; 129 - 130 - let mut registry = SimpleActionRegistry::new(); 131 - 132 - // Register simple actions 133 - registry.register_simple("editor::Save", SaveAction); 134 - registry.register_simple("editor::Open", OpenAction); 135 - 136 - // Register actions with parameter handling 137 - registry.register("editor::Undo", |params| { 138 - if let Some(params) = params { 139 - // Handle parameters 140 - let count = params["count"].as_u64().unwrap_or(1); 141 - Box::new(UndoAction::with_count(count)) 142 - } else { 143 - Box::new(UndoAction::default()) 144 - } 145 - }); 146 - 147 - // Use the registry to convert keymaps to actions 148 - let actions = keymaps.to_actions(&registry)?; 149 - ``` 150 - 151 - ## Integration with GPUI 152 - 153 - Since GPUI's `KeyBinding::new` requires concrete action types, you need to map action names from the keymap to actual action types: 154 - 155 - ```rust 156 - // Define your actions 157 - actions!(editor, [Save, Open, Undo, Redo]); 158 - 159 - // Load keymaps 160 - let mut keymaps = KeymapCollection::new(); 161 - keymaps.load_defaults()?; 162 - 163 - // Get binding specifications 164 - let specs = keymaps.get_binding_specs()?; 165 - 166 - // Create GPUI bindings 167 - let mut bindings = Vec::new(); 168 - for spec in specs { 169 - let context = spec.context.as_deref(); 170 - 171 - match spec.action_name.as_str() { 172 - "editor::Save" => { 173 - bindings.push(KeyBinding::new(&spec.keystrokes, Save, context)); 174 - } 175 - "editor::Open" => { 176 - bindings.push(KeyBinding::new(&spec.keystrokes, Open, context)); 177 - } 178 - "editor::Undo" => { 179 - bindings.push(KeyBinding::new(&spec.keystrokes, Undo, context)); 180 - } 181 - "editor::Redo" => { 182 - bindings.push(KeyBinding::new(&spec.keystrokes, Redo, context)); 183 - } 184 - _ => { 185 - log::warn!("Unknown action: {}", spec.action_name); 186 - } 187 - } 188 - } 189 - 190 - // Register with GPUI 191 - cx.bind_keys(bindings); 192 - ``` 193 - 194 - ## Default Keymaps 195 - 196 - The module includes built-in default keymaps for different platforms: 197 - 198 - - **macOS**: Uses `cmd` modifier 199 - - **Windows/Linux**: Uses `ctrl` modifier 200 - 201 - Load defaults with: 202 - 203 - ```rust 204 - keymaps.load_defaults()?; 205 - ``` 206 - 207 - ## Programmatic Creation 208 - 209 - You can also create keymaps programmatically: 210 - 211 - ```rust 212 - use gpui_editor::keymap::{Keymap, KeyBindings, ActionValue}; 213 - use std::collections::HashMap; 214 - 215 - let mut bindings = HashMap::new(); 216 - bindings.insert( 217 - "cmd-s".to_string(), 218 - ActionValue::Simple("editor::Save".to_string()) 219 - ); 220 - bindings.insert( 221 - "cmd-z".to_string(), 222 - ActionValue::WithParams(vec![ 223 - json!("editor::Undo"), 224 - json!({ "count": 1 }) 225 - ]) 226 - ); 227 - 228 - let keymap = Keymap { 229 - context: Some("Editor".to_string()), 230 - use_key_equivalents: true, 231 - bindings: KeyBindings::Simple(bindings), 232 - }; 233 - ``` 234 - 235 - ## Helper Functions 236 - 237 - The module provides several helper functions: 238 - 239 - ```rust 240 - use gpui_editor::keymap::{binding, binding_with_context}; 241 - 242 - // Create a simple binding 243 - let b = binding("cmd-s", "editor::Save"); 244 - 245 - // Create a binding with context 246 - let b = binding_with_context("enter", "menu::Select", "Menu"); 247 - ``` 248 - 249 - ## Error Handling 250 - 251 - All loading operations return `Result<T, anyhow::Error>`: 252 - 253 - ```rust 254 - match keymaps.load_file("custom-keymap.json") { 255 - Ok(_) => println!("Loaded successfully"), 256 - Err(e) => { 257 - eprintln!("Failed to load keymap: {}", e); 258 - // Fall back to defaults 259 - keymaps.load_defaults()?; 260 - } 261 - } 262 - ``` 263 - 264 - ## Examples 265 - 266 - ### Loading from Multiple Sources 267 - 268 - ```rust 269 - let mut keymaps = KeymapCollection::new(); 270 - 271 - // Load base keymaps 272 - keymaps.load_defaults()?; 273 - 274 - // Load user customizations (if they exist) 275 - if Path::new("~/.config/myapp/keymap.json").exists() { 276 - keymaps.load_file("~/.config/myapp/keymap.json")?; 277 - } 278 - 279 - // Load project-specific keymaps 280 - if Path::new(".myapp/keymap.json").exists() { 281 - keymaps.load_file(".myapp/keymap.json")?; 282 - } 283 - ``` 284 - 285 - ### Custom Action Registry 286 - 287 - ```rust 288 - struct MyActionRegistry { 289 - actions: HashMap<String, Box<dyn Fn() -> Box<dyn Action>>>, 290 - } 291 - 292 - impl ActionRegistry for MyActionRegistry { 293 - fn get_action(&self, name: &str, params: Option<Value>) -> Option<Box<dyn Action>> { 294 - self.actions.get(name).map(|factory| factory()) 295 - } 296 - } 297 - ``` 298 - 299 - ## API Reference 300 - 301 - ### `KeymapCollection` 302 - 303 - - `new()` - Create a new empty collection 304 - - `load_file(path)` - Load keymaps from a JSON file 305 - - `load_json(json)` - Load keymaps from a JSON string 306 - - `load_defaults()` - Load platform-specific defaults 307 - - `get_binding_specs()` - Get binding specifications 308 - - `to_actions(registry)` - Convert to boxed actions using a registry 309 - - `keymaps()` - Get all keymaps in the collection 310 - - `clear()` - Remove all keymaps 311 - 312 - ### `Keymap` 313 - 314 - - `context: Option<String>` - Optional context for bindings 315 - - `use_key_equivalents: bool` - Use platform-specific keys 316 - - `bindings: KeyBindings` - The actual key bindings 317 - 318 - ### `KeyBindings` 319 - 320 - - `Simple(HashMap<String, ActionValue>)` - Simple keystroke->action map 321 - - `Complex(Vec<KeyBindingEntry>)` - List of binding entries 322 - 323 - ### `ActionValue` 324 - 325 - - `Simple(String)` - Simple action name 326 - - `WithParams(Vec<Value>)` - Action with parameters 327 - 328 - ### `BindingSpec` 329 - 330 - - `keystrokes: String` - The key combination 331 - - `action_name: String` - Name of the action 332 - - `action_params: Option<Value>` - Optional parameters 333 - - `context: Option<String>` - Optional context 334 - 335 - ## Best Practices 336 - 337 - 1. **Use Contexts**: Separate keybindings by context to avoid conflicts 338 - 2. **Platform Compatibility**: Test keymaps on all target platforms 339 - 3. **Document Actions**: Keep a list of all available actions and their parameters 340 - 4. **Provide Defaults**: Always have fallback keybindings if loading fails 341 - 5. **User Customization**: Allow users to override default keymaps 342 - 6. **Validate Early**: Check keymap validity during loading, not at runtime 343 - 344 - ## Migration from Hardcoded Bindings 345 - 346 - Before (hardcoded): 347 - ```rust 348 - cx.bind_keys([ 349 - KeyBinding::new("cmd-s", Save, None), 350 - KeyBinding::new("cmd-o", Open, None), 351 - // ... many more 352 - ]); 353 - ``` 354 - 355 - After (with keymap module): 356 - ```rust 357 - let mut keymaps = KeymapCollection::new(); 358 - keymaps.load_defaults()?; 359 - 360 - for spec in keymaps.get_binding_specs()? { 361 - // Map spec to concrete action and register 362 - } 363 - ``` 364 - 365 - ## Troubleshooting 366 - 367 - ### Common Issues 368 - 369 - 1. **"Unknown action" warnings**: Ensure all actions in the keymap are registered 370 - 2. **Platform differences**: Use `cmd` for macOS and `ctrl` for Windows/Linux 371 - 3. **Context not working**: Verify the context name matches exactly 372 - 4. **Parameters not passing**: Check JSON structure for action parameters 373 - 374 - ### Debug Output 375 - 376 - Enable logging to see keymap loading details: 377 - ```rust 378 - env::set_var("RUST_LOG", "debug"); 379 - env_logger::init(); 380 - ```
+15 -125
crates/gpuikit-keymap/default-keymap.json
··· 1 1 [ 2 2 { 3 - "context": "Editor", 4 - "use_key_equivalents": true, 5 - "bindings": { 6 - "up": "editor_demo::MoveUp", 7 - "down": "editor_demo::MoveDown", 8 - "left": "editor_demo::MoveLeft", 9 - "right": "editor_demo::MoveRight", 10 - "shift-up": "editor_demo::MoveUpWithShift", 11 - "shift-down": "editor_demo::MoveDownWithShift", 12 - "shift-left": "editor_demo::MoveLeftWithShift", 13 - "shift-right": "editor_demo::MoveRightWithShift", 14 - "backspace": "editor_demo::Backspace", 15 - "delete": "editor_demo::Delete", 16 - "enter": "editor_demo::InsertNewline", 17 - "cmd-a": "editor_demo::SelectAll", 18 - "ctrl-a": "editor_demo::SelectAll", 19 - "escape": "editor_demo::Escape", 20 - "cmd-c": "editor_demo::Copy", 21 - "ctrl-c": "editor_demo::Copy", 22 - "cmd-x": "editor_demo::Cut", 23 - "ctrl-x": "editor_demo::Cut", 24 - "cmd-v": "editor_demo::Paste", 25 - "ctrl-v": "editor_demo::Paste", 26 - "cmd-z": "editor::Undo", 27 - "ctrl-z": "editor::Undo", 28 - "cmd-shift-z": "editor::Redo", 29 - "ctrl-shift-z": "editor::Redo", 30 - "cmd-]": "editor_demo::NextTheme", 31 - "ctrl-]": "editor_demo::NextTheme", 32 - "cmd-[": "editor_demo::PreviousTheme", 33 - "ctrl-[": "editor_demo::PreviousTheme", 34 - "cmd-shift-]": "editor_demo::NextLanguage", 35 - "ctrl-shift-]": "editor_demo::NextLanguage", 36 - "cmd-shift-[": "editor_demo::PreviousLanguage", 37 - "ctrl-shift-[": "editor_demo::PreviousLanguage", 38 - "home": "editor::MoveToBeginningOfLine", 39 - "end": "editor::MoveToEndOfLine", 40 - "cmd-home": "editor::MoveToBeginning", 41 - "ctrl-home": "editor::MoveToBeginning", 42 - "cmd-end": "editor::MoveToEnd", 43 - "ctrl-end": "editor::MoveToEnd", 44 - "pageup": "editor::PageUp", 45 - "pagedown": "editor::PageDown", 46 - "cmd-up": "editor::MoveToBeginning", 47 - "ctrl-up": "editor::MoveToBeginning", 48 - "cmd-down": "editor::MoveToEnd", 49 - "ctrl-down": "editor::MoveToEnd", 50 - "cmd-left": "editor::MoveToBeginningOfLine", 51 - "ctrl-left": "editor::MoveToBeginningOfLine", 52 - "cmd-right": "editor::MoveToEndOfLine", 53 - "ctrl-right": "editor::MoveToEndOfLine", 54 - "alt-left": "editor::MoveToPreviousWordStart", 55 - "alt-right": "editor::MoveToNextWordEnd", 56 - "alt-shift-left": "editor::SelectToPreviousWordStart", 57 - "alt-shift-right": "editor::SelectToNextWordEnd", 58 - "shift-home": "editor::SelectToBeginningOfLine", 59 - "shift-end": "editor::SelectToEndOfLine", 60 - "cmd-shift-home": "editor::SelectToBeginning", 61 - "ctrl-shift-home": "editor::SelectToBeginning", 62 - "cmd-shift-end": "editor::SelectToEnd", 63 - "ctrl-shift-end": "editor::SelectToEnd", 64 - "shift-pageup": "editor::SelectPageUp", 65 - "shift-pagedown": "editor::SelectPageDown", 66 - "tab": "editor::Tab", 67 - "shift-tab": "editor::Backtab", 68 - "cmd-shift-d": "editor::DuplicateLine", 69 - "ctrl-shift-d": "editor::DuplicateLine", 70 - "alt-up": "editor::MoveLineUp", 71 - "alt-down": "editor::MoveLineDown", 72 - "cmd-/": "editor::ToggleComment", 73 - "ctrl-/": "editor::ToggleComment", 74 - "cmd-d": "editor::SelectNextOccurrence", 75 - "ctrl-d": "editor::SelectNextOccurrence", 76 - "cmd-f": "editor::Find", 77 - "ctrl-f": "editor::Find", 78 - "cmd-shift-f": "editor::FindInFiles", 79 - "ctrl-shift-f": "editor::FindInFiles", 80 - "f3": "editor::FindNext", 81 - "shift-f3": "editor::FindPrevious", 82 - "cmd-g": "editor::FindNext", 83 - "ctrl-g": "editor::FindNext", 84 - "cmd-shift-g": "editor::FindPrevious", 85 - "ctrl-shift-g": "editor::FindPrevious", 86 - "cmd-h": "editor::Replace", 87 - "ctrl-h": "editor::Replace", 88 - "cmd-shift-h": "editor::ReplaceAll", 89 - "ctrl-shift-h": "editor::ReplaceAll" 90 - } 91 - }, 92 - { 93 - "context": "Menu", 94 - "use_key_equivalents": true, 3 + "context": "Global", 95 4 "bindings": { 96 - "up": "menu::SelectPrevious", 97 - "down": "menu::SelectNext", 98 - "enter": "menu::Confirm", 99 - "escape": "menu::Cancel", 100 - "home": "menu::SelectFirst", 101 - "end": "menu::SelectLast", 102 - "pageup": "menu::SelectFirst", 103 - "pagedown": "menu::SelectLast" 5 + "cmd-q": "app::Quit", 6 + "ctrl-q": "app::Quit", 7 + "cmd-w": "window::Close", 8 + "ctrl-w": "window::Close" 104 9 } 105 10 }, 106 11 { 107 - "context": "Global", 108 - "use_key_equivalents": true, 12 + "context": "Core", 109 13 "bindings": { 110 - "cmd-q": "application::Quit", 111 - "ctrl-q": "application::Quit", 112 - "cmd-w": "workspace::CloseWindow", 113 - "ctrl-w": "workspace::CloseWindow", 114 - "cmd-n": "workspace::NewFile", 115 - "ctrl-n": "workspace::NewFile", 116 - "cmd-o": "workspace::OpenFile", 117 - "ctrl-o": "workspace::OpenFile", 118 - "cmd-s": "workspace::Save", 119 - "ctrl-s": "workspace::Save", 120 - "cmd-shift-s": "workspace::SaveAs", 121 - "ctrl-shift-s": "workspace::SaveAs", 122 - "cmd-shift-n": "workspace::NewWindow", 123 - "ctrl-shift-n": "workspace::NewWindow", 124 - "f11": "application::ToggleFullScreen", 125 - "cmd-=": "application::ZoomIn", 126 - "ctrl-=": "application::ZoomIn", 127 - "cmd-+": "application::ZoomIn", 128 - "ctrl-+": "application::ZoomIn", 129 - "cmd--": "application::ZoomOut", 130 - "ctrl--": "application::ZoomOut", 131 - "cmd-0": "application::ResetZoom", 132 - "ctrl-0": "application::ResetZoom" 14 + "up": "core::Previous", 15 + "down": "core::Next", 16 + "tab": "core:Next", 17 + "enter": "core::Confirm", 18 + "escape": "core::Cancel", 19 + "home": "core::First", 20 + "end": "core::Last", 21 + "pageup": "core::First", 22 + "pagedown": "core::Last" 133 23 } 134 24 } 135 25 ]
+124 -256
crates/gpuikit-keymap/src/lib.rs
··· 4 4 //! keybindings to be loaded from external files rather than hardcoded. 5 5 6 6 use anyhow::{anyhow, Context as _, Result}; 7 - use gpui::Action; 8 7 use serde::{Deserialize, Serialize}; 9 8 use std::collections::HashMap; 10 9 use std::fs; ··· 19 18 #[serde(skip_serializing_if = "Option::is_none")] 20 19 pub context: Option<String>, 21 20 22 - /// Whether to use platform-specific key equivalents 23 - #[serde(default)] 24 - pub use_key_equivalents: bool, 25 - 26 21 /// The key bindings in this keymap 27 - pub bindings: KeyBindings, 28 - } 29 - 30 - /// Represents the bindings section of a keymap 31 - #[derive(Debug, Clone, Serialize, Deserialize)] 32 - #[serde(untagged)] 33 - pub enum KeyBindings { 34 - /// Simple map of keystroke -> action 35 - Simple(HashMap<String, ActionValue>), 36 - /// List of individual key binding entries (for more complex configurations) 37 - Complex(Vec<KeyBindingEntry>), 38 - } 39 - 40 - /// Represents a single key binding entry 41 - #[derive(Debug, Clone, Serialize, Deserialize)] 42 - pub struct KeyBindingEntry { 43 - /// The keystroke sequence (e.g., "cmd-s", "ctrl-shift-p") 44 - pub key: String, 45 - 46 - /// The action to trigger 47 - pub action: ActionValue, 48 - 49 - /// Optional context where this binding applies 50 - #[serde(skip_serializing_if = "Option::is_none")] 51 - pub context: Option<String>, 52 - } 53 - 54 - /// Represents an action value in the keymap 55 - #[derive(Debug, Clone, Serialize, Deserialize)] 56 - #[serde(untagged)] 57 - pub enum ActionValue { 58 - /// Simple action name 59 - Simple(String), 60 - /// Action with parameters 61 - WithParams(Vec<serde_json::Value>), 22 + pub bindings: HashMap<String, String>, 62 23 } 63 24 64 - impl ActionValue { 65 - /// Get the action name from this value 66 - pub fn action_name(&self) -> Result<String> { 67 - match self { 68 - ActionValue::Simple(name) => Ok(name.clone()), 69 - ActionValue::WithParams(values) => { 70 - if let Some(first) = values.first() { 71 - if let Some(name) = first.as_str() { 72 - return Ok(name.to_string()); 73 - } 74 - } 75 - Err(anyhow!( 76 - "Invalid action format: expected action name as first element" 77 - )) 78 - } 25 + impl 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, 79 31 } 80 32 } 81 33 82 - /// Get the action parameters if any 83 - pub fn params(&self) -> Option<serde_json::Value> { 84 - match self { 85 - ActionValue::Simple(_) => None, 86 - ActionValue::WithParams(values) => { 87 - if values.len() > 1 { 88 - Some(values[1].clone()) 89 - } else { 90 - None 91 - } 92 - } 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, 93 39 } 94 40 } 95 41 } ··· 135 81 Err(anyhow!("Invalid keymap JSON format")) 136 82 } 137 83 138 - /// Load default keymaps for the current platform 84 + /// Load default keymaps 139 85 pub fn load_defaults(&mut self) -> Result<()> { 140 - let default_keymap = default_keymap_json(); 86 + let default_keymap = include_str!("../default-keymap.json"); 141 87 self.load_json(default_keymap)?; 142 88 Ok(()) 143 89 } ··· 146 92 /// 147 93 /// Returns a list of binding specifications that can be used to create 148 94 /// actual GPUI key bindings with concrete action types. 149 - pub fn get_binding_specs(&self) -> Result<Vec<BindingSpec>> { 95 + pub fn get_binding_specs(&self) -> Vec<BindingSpec> { 150 96 let mut specs = Vec::new(); 151 97 152 98 for keymap in &self.keymaps { 153 99 let context = keymap.context.as_deref(); 154 100 155 - match &keymap.bindings { 156 - KeyBindings::Simple(map) => { 157 - for (key, action_value) in map { 158 - let action_name = action_value.action_name()?; 159 - specs.push(BindingSpec { 160 - keystrokes: key.clone(), 161 - action_name, 162 - action_params: action_value.params(), 163 - context: context.map(String::from), 164 - }); 165 - } 166 - } 167 - KeyBindings::Complex(entries) => { 168 - for entry in entries { 169 - let action_name = entry.action.action_name()?; 170 - let binding_context = entry.context.as_deref().or(context); 171 - specs.push(BindingSpec { 172 - keystrokes: entry.key.clone(), 173 - action_name, 174 - action_params: entry.action.params(), 175 - context: binding_context.map(String::from), 176 - }); 177 - } 178 - } 179 - } 180 - } 181 - 182 - Ok(specs) 183 - } 184 - 185 - /// Convert this collection into boxed actions using a registry 186 - /// 187 - /// This is primarily for testing and validation purposes. 188 - pub fn to_actions( 189 - &self, 190 - action_registry: &impl ActionRegistry, 191 - ) -> Result<Vec<(String, Box<dyn Action>, Option<String>)>> { 192 - let mut actions = Vec::new(); 193 - 194 - for spec in self.get_binding_specs()? { 195 - if let Some(action) = action_registry.get_action(&spec.action_name, spec.action_params) 196 - { 197 - actions.push((spec.keystrokes, action, spec.context)); 198 - } else { 199 - log::warn!("Unknown action in keymap: {}", spec.action_name); 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 + }); 200 107 } 201 108 } 202 109 203 - Ok(actions) 110 + specs 204 111 } 205 112 206 113 /// Get all keymaps in this collection ··· 208 115 &self.keymaps 209 116 } 210 117 118 + /// Add a keymap to this collection 119 + pub fn add(&mut self, keymap: Keymap) { 120 + self.keymaps.push(keymap); 121 + } 122 + 211 123 /// Clear all keymaps from this collection 212 124 pub fn clear(&mut self) { 213 125 self.keymaps.clear(); 214 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 + .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 + } 215 160 } 216 161 217 - /// Specification for a key binding that can be used to create actual bindings 162 + /// Specification for a key binding 218 163 #[derive(Debug, Clone)] 219 164 pub struct BindingSpec { 220 165 /// The keystroke sequence (e.g., "cmd-s", "ctrl-shift-p") 221 166 pub keystrokes: String, 222 167 /// The action name to trigger 223 168 pub action_name: String, 224 - /// Optional parameters for the action 225 - pub action_params: Option<serde_json::Value>, 226 169 /// Optional context where this binding applies 227 170 pub context: Option<String>, 228 171 } 229 172 230 - /// Trait for registries that can provide Action instances from names 231 - pub trait ActionRegistry { 232 - /// Get an action by name, optionally with parameters 233 - fn get_action(&self, name: &str, params: Option<serde_json::Value>) -> Option<Box<dyn Action>>; 234 - } 235 - 236 - /// A simple action registry implementation using a HashMap 237 - pub struct SimpleActionRegistry { 238 - actions: HashMap<String, Box<dyn Fn(Option<serde_json::Value>) -> Box<dyn Action>>>, 239 - } 240 - 241 - impl SimpleActionRegistry { 242 - /// Create a new empty registry 243 - pub fn new() -> Self { 244 - Self { 245 - actions: HashMap::new(), 246 - } 247 - } 248 - 249 - /// Register an action factory 250 - pub fn register<F>(&mut self, name: impl Into<String>, factory: F) 251 - where 252 - F: Fn(Option<serde_json::Value>) -> Box<dyn Action> + 'static, 253 - { 254 - self.actions.insert(name.into(), Box::new(factory)); 255 - } 256 - 257 - /// Register a simple action (no parameters) 258 - pub fn register_simple<A: Action>(&mut self, name: impl Into<String>, action: A) 259 - where 260 - A: Clone + 'static, 261 - { 262 - let name = name.into(); 263 - self.register(name, move |_params| Box::new(action.clone())); 264 - } 265 - } 266 - 267 - impl ActionRegistry for SimpleActionRegistry { 268 - fn get_action(&self, name: &str, params: Option<serde_json::Value>) -> Option<Box<dyn Action>> { 269 - self.actions.get(name).map(|factory| factory(params)) 270 - } 271 - } 272 - 273 - /// Returns the default keymap JSON for the current platform 274 - pub fn default_keymap_json() -> &'static str { 275 - #[cfg(target_os = "macos")] 276 - { 277 - include_str!("../default-keymap.json") 278 - } 279 - 280 - #[cfg(target_os = "windows")] 281 - { 282 - include_str!("../default-keymap-windows.json") 283 - } 284 - 285 - #[cfg(target_os = "linux")] 286 - { 287 - include_str!("../default-keymap-linux.json") 288 - } 289 - 290 - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] 291 - { 292 - include_str!("../default-keymap.json") 293 - } 294 - } 295 - 296 - /// Helper function to create a KeyBinding from a simple action 297 - pub fn binding(key: impl Into<String>, action: impl Into<String>) -> KeyBindingEntry { 298 - KeyBindingEntry { 299 - key: key.into(), 300 - action: ActionValue::Simple(action.into()), 301 - context: None, 302 - } 303 - } 304 - 305 - /// Helper function to create a KeyBinding with context 306 - pub fn binding_with_context( 307 - key: impl Into<String>, 308 - action: impl Into<String>, 309 - context: impl Into<String>, 310 - ) -> KeyBindingEntry { 311 - KeyBindingEntry { 312 - key: key.into(), 313 - action: ActionValue::Simple(action.into()), 314 - context: Some(context.into()), 315 - } 173 + /// Helper function to create a simple binding 174 + pub fn binding(key: impl Into<String>, action: impl Into<String>) -> (String, String) { 175 + (key.into(), action.into()) 316 176 } 317 177 318 178 #[cfg(test)] ··· 331 191 332 192 let keymap: Keymap = serde_json::from_str(json).unwrap(); 333 193 assert_eq!(keymap.context, Some("Editor".to_string())); 334 - 335 - if let KeyBindings::Simple(map) = keymap.bindings { 336 - assert_eq!(map.len(), 2); 337 - assert!(matches!(map.get("cmd-s"), Some(ActionValue::Simple(s)) if s == "Save")); 338 - assert!(matches!(map.get("cmd-z"), Some(ActionValue::Simple(s)) if s == "Undo")); 339 - } else { 340 - panic!("Expected simple bindings"); 341 - } 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())); 342 197 } 343 198 344 199 #[test] 345 - fn test_parse_complex_keymap() { 200 + fn test_parse_multiple_keymaps() { 346 201 let json = r#"[ 347 202 { 348 - "bindings": [ 349 - { "key": "cmd-s", "action": "Save" }, 350 - { "key": "cmd-z", "action": ["Undo", { "count": 1 }] } 351 - ] 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 + } 352 214 } 353 215 ]"#; 354 216 355 217 let keymaps: Vec<Keymap> = serde_json::from_str(json).unwrap(); 356 - assert_eq!(keymaps.len(), 1); 357 - 358 - let keymap = &keymaps[0]; 359 - if let KeyBindings::Complex(entries) = &keymap.bindings { 360 - assert_eq!(entries.len(), 2); 361 - 362 - assert_eq!(entries[0].key, "cmd-s"); 363 - assert!(matches!(&entries[0].action, ActionValue::Simple(s) if s == "Save")); 364 - 365 - assert_eq!(entries[1].key, "cmd-z"); 366 - assert!(matches!(&entries[1].action, ActionValue::WithParams(_))); 367 - } else { 368 - panic!("Expected complex bindings"); 369 - } 370 - } 371 - 372 - #[test] 373 - fn test_action_value_methods() { 374 - let simple = ActionValue::Simple("Save".to_string()); 375 - assert_eq!(simple.action_name().unwrap(), "Save"); 376 - assert_eq!(simple.params(), None); 377 - 378 - let with_params = ActionValue::WithParams(vec![ 379 - serde_json::json!("Undo"), 380 - serde_json::json!({ "count": 1 }), 381 - ]); 382 - assert_eq!(with_params.action_name().unwrap(), "Undo"); 383 - assert_eq!( 384 - with_params.params(), 385 - Some(serde_json::json!({ "count": 1 })) 386 - ); 218 + assert_eq!(keymaps.len(), 2); 219 + assert_eq!(keymaps[0].context, None); 220 + assert_eq!(keymaps[1].context, Some("Menu".to_string())); 387 221 } 388 222 389 223 #[test] ··· 399 233 assert_eq!(collection.keymaps().len(), 2); 400 234 assert_eq!(collection.keymaps()[0].context, None); 401 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); 402 274 } 403 275 404 276 #[test] 405 277 fn test_serialize_keymap() { 406 278 let mut bindings = HashMap::new(); 407 - bindings.insert("cmd-s".to_string(), ActionValue::Simple("Save".to_string())); 408 - bindings.insert("cmd-z".to_string(), ActionValue::Simple("Undo".to_string())); 279 + bindings.insert("cmd-s".to_string(), "Save".to_string()); 280 + bindings.insert("cmd-z".to_string(), "Undo".to_string()); 409 281 410 - let keymap = Keymap { 411 - context: Some("Editor".to_string()), 412 - use_key_equivalents: false, 413 - bindings: KeyBindings::Simple(bindings), 414 - }; 282 + let keymap = Keymap::with_context("Editor", bindings); 415 283 416 284 let json = serde_json::to_string_pretty(&keymap).unwrap(); 417 285 let parsed: Keymap = serde_json::from_str(&json).unwrap(); 418 286 419 287 assert_eq!(parsed.context, keymap.context); 420 - assert_eq!(parsed.use_key_equivalents, keymap.use_key_equivalents); 288 + assert_eq!(parsed.bindings, keymap.bindings); 421 289 } 422 290 }
-14
crates/gpuikit-keymap/test-keymap.json
··· 1 - [ 2 - { 3 - "context": "Editor", 4 - "use_key_equivalents": true, 5 - "bindings": { 6 - "ctrl-t": "editor_demo::NextTheme", 7 - "ctrl-l": "editor_demo::NextLanguage", 8 - "alt-up": "editor_demo::MoveUp", 9 - "alt-down": "editor_demo::MoveDown", 10 - "cmd-d": "editor_demo::Delete", 11 - "ctrl-a": "editor_demo::SelectAll" 12 - } 13 - } 14 - ]
+1
crates/gpuikit/Cargo.toml
··· 29 29 serde = { workspace = true } 30 30 serde_json = { workspace = true } 31 31 log = { workspace = true } 32 + rust-embed = { workspace = true } 32 33 33 34 [dev-dependencies] 34 35 gpui = { workspace = true, features = ["test-support"] }
+1
crates/gpuikit/src/lib.rs
··· 6 6 7 7 pub mod error; 8 8 pub mod layout; 9 + pub mod resource; 9 10 10 11 pub mod style { 11 12 use crate::theme::Theme;
+66
crates/gpuikit/src/resource.rs
··· 1 + //! Resource management for gpui apps 2 + //! 3 + //! Embed icons, images, fonts, etc... 4 + 5 + use anyhow::Result; 6 + use gpui::{AssetSource, SharedString}; 7 + use rust_embed::RustEmbed; 8 + use std::borrow::Cow; 9 + 10 + /// Trait for types that can serve as embedded asset sources 11 + pub trait Resource: RustEmbed { 12 + /// Get an asset by path 13 + fn by_path(path: &str) -> Option<Cow<'static, [u8]>> { 14 + Self::get(path).map(|file| file.data) 15 + } 16 + 17 + /// Check if an asset exists 18 + fn has(path: &str) -> bool { 19 + Self::get(path).is_some() 20 + } 21 + 22 + /// List all assets 23 + fn list(prefix: Option<&str>) -> Vec<String> { 24 + match prefix { 25 + Some(prefix) => Self::iter() 26 + .filter(|path| path.starts_with(prefix)) 27 + .map(|s| s.to_string()) 28 + .collect(), 29 + None => Self::iter().map(|s| s.to_string()).collect(), 30 + } 31 + } 32 + } 33 + 34 + impl<T: RustEmbed> Resource for T {} 35 + 36 + pub struct ResourceSource<T: Resource + Send + Sync> { 37 + _phantom: std::marker::PhantomData<T>, 38 + } 39 + 40 + impl<T: Resource + Send + Sync> Default for ResourceSource<T> { 41 + fn default() -> Self { 42 + Self::new() 43 + } 44 + } 45 + 46 + impl<T: Resource + Send + Sync> ResourceSource<T> { 47 + /// Create a new embedded asset source 48 + pub fn new() -> Self { 49 + Self { 50 + _phantom: std::marker::PhantomData, 51 + } 52 + } 53 + } 54 + 55 + impl<T: Resource + Send + Sync + 'static> AssetSource for ResourceSource<T> { 56 + fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> { 57 + Ok(T::by_path(path)) 58 + } 59 + 60 + fn list(&self, prefix: &str) -> Result<Vec<SharedString>> { 61 + Ok(T::list(Some(prefix)) 62 + .into_iter() 63 + .map(|s| s.into()) 64 + .collect()) 65 + } 66 + }