keymap: ForeignSet is non_exhaustive and built with ForeignSet::new, with_name and with_truncated

This commit is contained in:
aiongg committed 2026-10-23 07:19:55 +00:00
1 parent 78639905f7
commit 0ccb6a9304
8 files changed
+94 -116

No files matched your search

+11 -9
View File
@@ -206,7 +206,9 @@ assert_eq!(sets.active_overrides(&builtins), sets.get("Mine", &builtins).unwrap(
The app's reader turns a file (an InDesign Show Set, an Illustrator `.kys`) into an
[`import::ForeignSet`](src/import.rs): entries with a menu, a path and keys read by
`ForeignKey::read`. `import::plan` matches each entry to one command and changes nothing;
`ForeignKey::read`. The reader builds it with `ForeignSet::new(entries)`, `with_name` and
`with_truncated`; the struct is `#[non_exhaustive]`, so struct literals outside the crate do not
compile. `import::plan` matches each entry to one command and changes nothing;
`ImportPlan::apply` applies the plan and returns an `ImportReport` (serde, camelCase). Report
rows carry the entry's `item` (its path) and `line`.
@@ -220,14 +222,11 @@ let save = Bindable::new("file.save", "Save", Scope::app())
let mut keymap = Keymap::new(vec![save], Scopes::new(), Platform::Windows);
let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows);
let set = ForeignSet {
name: Some("My Set".into()),
entries: vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13),
])
.with_name("My Set");
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() };
let planned = plan(&set, &keymap, &options);
// A review UI can show planned.commands and planned.report here.
@@ -328,6 +327,9 @@ Adopt it in three steps:
for the app's own modal or preferences page) every frame.
3. Act on the `Outcome` it returns, and drop the state when `is_open()` turns false.
Apps that import other applications' sets read each file into a `ForeignSet` built with
`ForeignSet::new` ([Importing a shortcut set](#importing-a-shortcut-set)).
```rust,ignore
use craft_keymap::ui::{DialogState, I18nText, Options, Outcome, ShortcutsDialog};
+21 -1
View File
@@ -29,8 +29,9 @@ pub const MAX_PATH_SEGMENTS: usize = 32;
/// in [`ImportReport::truncated`]. Entries without keys get no suggestion and do not count.
pub const MAX_SUGGESTIONS: usize = 200;
/// A shortcut set as an app's reader found it.
/// A shortcut set as an app's reader found it. Build it with [`ForeignSet::new`].
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct ForeignSet {
pub name: Option<String>,
pub entries: Vec<ForeignEntry>,
@@ -62,6 +63,25 @@ pub struct ForeignEntry {
pub panel_menu: bool,
}
impl ForeignSet {
/// A set without a name.
pub fn new(entries: Vec<ForeignEntry>) -> ForeignSet {
ForeignSet { entries, ..ForeignSet::default() }
}
/// The set's name as the source gives it.
pub fn with_name(mut self, name: impl Into<String>) -> ForeignSet {
self.name = Some(name.into());
self
}
/// Entries the reader dropped at its own limits ([`ForeignSet::truncated`]).
pub fn with_truncated(mut self, truncated: usize) -> ForeignSet {
self.truncated = truncated;
self
}
}
impl ForeignEntry {
/// An entry without keys.
pub fn new<S: Into<String>>(menu: Option<&str>, path: impl IntoIterator<Item = S>) -> ForeignEntry {
+5 -8
View File
@@ -118,14 +118,11 @@
//! let mut keymap = Keymap::new(vec![save], Scopes::new(), Platform::Windows);
//!
//! let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows);
//! let set = ForeignSet {
//! name: Some("My Set".into()),
//! entries: vec![
//! ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12),
//! ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13),
//! ],
//! truncated: 0,
//! };
//! let set = ForeignSet::new(vec![
//! ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12),
//! ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13),
//! ])
//! .with_name("My Set");
//! let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() };
//! let planned = plan(&set, &keymap, &options);
//! assert_eq!(planned.commands.len(), 1);
+6 -13
View File
@@ -1469,11 +1469,7 @@ mod tests {
assert!(matches!(st.finish(), Some(Outcome::Import { .. })));
assert!(fx.km.overrides().is_empty() && !st.can_undo());
let read = |text: &str| ForeignKey::read(Some("Default"), text, crate::Flavor::Windows);
let set = ForeignSet {
name: Some("Theirs".into()),
entries: vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S"))],
truncated: 0,
};
let set = ForeignSet::new(vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S"))]).with_name("Theirs");
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() };
st.review_import(&mut fx.env(), set, options);
st.import_reviewed(&mut fx.env());
@@ -1548,14 +1544,11 @@ mod tests {
let mut fx = Fx::new(Options::default());
let mut st = open(&mut fx);
let read = |text: &str| ForeignKey::read(Some("Default"), text, crate::Flavor::Windows);
let set = ForeignSet {
name: Some("Theirs".into()),
entries: vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
])
.with_name("Theirs");
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() };
st.review_import(&mut fx.env(), set, options);
let r = st.review.as_ref();
+34 -58
View File
@@ -110,11 +110,11 @@ fn windows_set() -> ForeignSet {
entry(None, &["Somewhere", "Zoom In"], &[(d, "Ctrl+F11")], Flavor::Windows),
win("Scripts", &["Frobnicate"], &[(d, "Ctrl+Alt+R")]),
];
ForeignSet { name: Some("My Set".into()), entries, truncated: 0 }
ForeignSet::new(entries).with_name("My Set")
}
fn numpad_set() -> ForeignSet {
ForeignSet { name: None, entries: vec![win("Window", &["Layers"], &[("Default", "Num 7")]).at_line(2)], truncated: 0 }
ForeignSet::new(vec![win("Window", &["Layers"], &[("Default", "Num 7")]).at_line(2)])
}
fn windows_options() -> ImportOptions {
@@ -254,16 +254,13 @@ fn the_set_wins_and_the_report_says_how() {
fn a_mac_set_reads_its_modifiers_and_scopes_text_keys() {
let mut km = keymap(Platform::Mac);
let mac = |menu: &str, path: &[&str], keys: &[(&str, &str)]| entry(Some(menu), path, keys, Flavor::Mac);
let set = ForeignSet {
name: Some("Mac".into()),
entries: vec![
mac("File", &["Save"], &[("Default", "⇧⌘S")]),
mac("Edit", &["Paste"], &[("Default", "Cmd+Ctrl+V")]),
mac("Type", &["Style", "Italic"], &[("Text", "Shift+Cmd+I")]),
mac("Type", &["Style", "Bold"], &[("Default", "Cmd+B"), ("Tables", "Cmd+Opt+B")]),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
mac("File", &["Save"], &[("Default", "⇧⌘S")]),
mac("Edit", &["Paste"], &[("Default", "Cmd+Ctrl+V")]),
mac("Type", &["Style", "Italic"], &[("Text", "Shift+Cmd+I")]),
mac("Type", &["Style", "Bold"], &[("Default", "Cmd+B"), ("Tables", "Cmd+Opt+B")]),
])
.with_name("Mac");
let text = Scope::new("Text").unwrap();
let options = ImportOptions {
contexts: vec![("Default".into(), ContextRule::Home), ("Text".into(), ContextRule::Scope(text))],
@@ -283,8 +280,7 @@ fn a_mac_set_reads_its_modifiers_and_scopes_text_keys() {
#[test]
fn start_and_none_defined_options() {
let set =
ForeignSet { name: None, entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q")]), win("Edit", &["Duplicate"], &[])], truncated: 0 };
let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q")]), win("Edit", &["Duplicate"], &[])]);
let user = || {
let mut o = Overrides::new();
o.insert("file.save", vec!["Cmd+Alt+S".into()]);
@@ -316,15 +312,11 @@ fn a_failed_change_keeps_its_shortcut_when_a_later_change_frees_room() {
// Save's change finds the overrides full. Close's change then frees an entry, and Zoom In
// takes Cmd+S. Removing Cmd+S from Save would fit now, but Save's import failed, so Save
// keeps it and Zoom In stays as it was.
let set = ForeignSet {
name: None,
entries: vec![
win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]),
win("File", &["Close"], &[("Default", "Ctrl+W")]),
win("View", &["Zoom In"], &[("Default", "Ctrl+S")]),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]),
win("File", &["Close"], &[("Default", "Ctrl+W")]),
win("View", &["Zoom In"], &[("Default", "Ctrl+S")]),
]);
let mut o = Overrides::new();
o.insert("file.close", vec!["Cmd+Q".into()]);
o.insert("view.zoomIn", vec!["Cmd+Alt+Z".into()]);
@@ -346,15 +338,11 @@ fn a_failed_change_keeps_its_shortcut_when_a_later_change_frees_room() {
#[test]
fn changes_that_find_the_overrides_full_are_reported() {
let set = ForeignSet {
name: None,
entries: vec![
win("File", &["Close"], &[("Default", "Ctrl+J")]),
win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]),
win("Edit", &["Duplicate"], &[]),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
win("File", &["Close"], &[("Default", "Ctrl+J")]),
win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]),
win("Edit", &["Duplicate"], &[]),
]);
let full = || {
let mut o = Overrides::new();
o.insert("file.close", vec!["Cmd+Q".into()]);
@@ -406,11 +394,7 @@ fn changes_that_find_the_overrides_full_are_reported() {
has(&km, "edit.duplicate", &[]);
// A later entry whose change finds the overrides full leaves the chord with the earlier one.
let set = ForeignSet {
name: None,
entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Alt+J")]), win("File", &["Save"], &[("Default", "Ctrl+Alt+J")])],
truncated: 0,
};
let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Alt+J")]), win("File", &["Save"], &[("Default", "Ctrl+Alt+J")])]);
let mut km = keymap(Platform::Windows).with_overrides(full());
let r = plan(&set, &km, &options(Start::Current)).apply(&mut km);
has(&km, "file.close", &["Cmd+Alt+J"]);
@@ -422,11 +406,7 @@ fn changes_that_find_the_overrides_full_are_reported() {
#[test]
fn keys_in_unknown_scopes_are_skipped_and_the_rest_apply() {
let set = ForeignSet {
name: None,
entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q"), ("Story", "Ctrl+K"), ("Text", "Ctrl+L")])],
truncated: 0,
};
let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q"), ("Story", "Ctrl+K"), ("Text", "Ctrl+L")])]);
let options = ImportOptions {
contexts: vec![
("Default".into(), ContextRule::Home),
@@ -450,7 +430,7 @@ fn keys_in_unknown_scopes_are_skipped_and_the_rest_apply() {
#[test]
fn suggestions_are_reported_never_applied() {
let mut km = keymap(Platform::Windows);
let set = ForeignSet { name: None, entries: vec![win("Edit", &["Chek Spelling Now"], &[("Default", "Ctrl+K")])], truncated: 0 };
let set = ForeignSet::new(vec![win("Edit", &["Chek Spelling Now"], &[("Default", "Ctrl+K")])]);
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() };
let p = plan(&set, &km, &options);
assert!(p.commands.is_empty());
@@ -465,7 +445,7 @@ fn suggestions_are_reported_never_applied() {
#[test]
fn entries_the_reader_dropped_count_as_truncated() {
let set = ForeignSet { name: None, entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q")])], truncated: 7 };
let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q")])]).with_truncated(7);
assert_eq!(plan(&set, &keymap(Platform::Windows), &default_context()).report.truncated, 7);
}
@@ -486,7 +466,7 @@ fn hostile_sets_are_capped_and_counted() {
// Entries without keys fill the set to the cap and 500 past it.
let keyless = MAX_IMPORT_ENTRIES - entries.len();
entries.extend(std::iter::repeat_n(ForeignEntry::new(None, Vec::<String>::new()), keyless + 500));
let set = ForeignSet { name: Some(long.clone()), entries, truncated: 0 };
let set = ForeignSet::new(entries).with_name(long.clone());
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() };
let r = plan(&set, &km, &options).apply(&mut km);
assert_eq!(r.suggestions.len(), MAX_SUGGESTIONS);
@@ -529,7 +509,7 @@ fn hostile_sets_finish_in_time() {
}
entries.insert(0, big);
entries.insert(1, win("Edit", &["Paste"], &[("Default", &long), ("Default", "Ctrl+\u{0}"), ("Default", "Win+E")]));
let set = ForeignSet { name: Some(long.clone()), entries, truncated: 0 };
let set = ForeignSet::new(entries).with_name(long.clone());
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() };
let started = std::time::Instant::now();
let r = plan(&set, &km, &options).apply(&mut km);
@@ -553,17 +533,13 @@ fn hostile_sets_finish_in_time() {
fn rows_carry_source_lines_and_approximated_keys() {
let mut km = keymap(Platform::Windows);
let d = "Default";
let set = ForeignSet {
name: None,
entries: vec![
win("Window", &["Layers"], &[(d, "Ctrl+Num 5"), (d, "Num 7")]).at_line(3),
win("Window", &["Layers"], &[(d, "Shift+F7")]).at_line(4),
win("Edit", &["Paste"], &[(d, "Ctrl+Num 5")]).at_line(5),
win("Edit", &["Chek Spelling Now"], &[(d, "Ctrl+K")]).at_line(8),
win("File", &["Save"], &[(d, "Ctrl+Hyperspace")]).at_line(9),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
win("Window", &["Layers"], &[(d, "Ctrl+Num 5"), (d, "Num 7")]).at_line(3),
win("Window", &["Layers"], &[(d, "Shift+F7")]).at_line(4),
win("Edit", &["Paste"], &[(d, "Ctrl+Num 5")]).at_line(5),
win("Edit", &["Chek Spelling Now"], &[(d, "Ctrl+K")]).at_line(8),
win("File", &["Save"], &[(d, "Ctrl+Hyperspace")]).at_line(9),
]);
let options = ImportOptions { contexts: vec![(d.into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() };
let r = plan(&set, &km, &options).apply(&mut km);
// A keypad key binds the main key and is reported. Paste, a later entry, takes Cmd+5.
@@ -639,7 +615,7 @@ fn report_json_uses_camel_case_names() {
fn import_into(catalog: Vec<Bindable>, entries: Vec<ForeignEntry>, options: &ImportOptions) -> (Keymap, ImportReport) {
let mut km = Keymap::new(catalog, Scopes::new(), Platform::Windows);
let report = plan(&ForeignSet { name: None, entries, truncated: 0 }, &km, options).apply(&mut km);
let report = plan(&ForeignSet::new(entries), &km, options).apply(&mut km);
(km, report)
}
+1 -5
View File
@@ -162,11 +162,7 @@ fn duplicate_copies_built_in_and_user_sets() {
fn a_foreign_set_imports_as_a_new_set_and_leaves_the_keymap_alone() {
let km = keymap();
let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Mac);
let set = ForeignSet {
name: Some("Other App".into()),
entries: vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Cmd+Shift+S"))],
truncated: 0,
};
let set = ForeignSet::new(vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Cmd+Shift+S"))]).with_name("Other App");
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], start: Start::Defaults, ..ImportOptions::default() };
let planned = plan(&set, &km, &options);
let builtins = [("Craft", Overrides::new())];
+8 -11
View File
@@ -724,17 +724,14 @@ fn set_export_import_round_trip() {
fn foreign_set() -> ForeignSet {
let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows);
ForeignSet {
name: Some("Theirs".into()),
entries: vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5),
ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6),
ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7),
],
truncated: 0,
}
ForeignSet::new(vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5),
ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6),
ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7),
])
.with_name("Theirs")
}
fn review(h: &mut Harness<'_, Fx>) {
+8 -11
View File
@@ -117,17 +117,14 @@ fn fonts() -> egui::FontDefinitions {
fn foreign_review(h: &mut Harness<'_, Fx>) {
let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows);
let set = ForeignSet {
name: Some("Theirs".into()),
entries: vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5),
ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6),
ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7),
],
truncated: 0,
};
let set = ForeignSet::new(vec![
ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3),
ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4),
ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5),
ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6),
ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7),
])
.with_name("Theirs");
let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() };
let (d, state) = h.state_mut().dialog();
d.review_import(state, set, options);