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