diff --git a/README.md b/README.md index f08bca7..235bbaa 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ * âŒ¨ī¸ **Key Patterns**: Supports single keys (`a`), combinations (`ctrl-b`), and multi-key sequences (`ctrl-b n`). * 🧠 **Key Groups**: Use built-in pattern matching for common key groups (`@upper`, `@lower`, `@alpha`, `@alnum`, and `@any`). * 📸 **Key Group Capturing**: Capture specific keypress data (like the actual `char` from `@any` or `@digit`) directly into your action enum variants at runtime. +* đŸˇī¸ **Custom Symbols & Help**: Define custom display symbols (e.g., `^B`) and help text for key bindings. * đŸ§Ŧ **Compile-Time Safety**: The `keymap_derive` macro validates key syntax at compile time, preventing runtime errors. * 🌐 **Backend-Agnostic**: Works with multiple backends, including `crossterm`, `termion`, and `wasm`. * đŸĒļ **Lightweight & Extensible**: Designed to be minimal and easy to extend with new backends or features. @@ -95,8 +96,8 @@ pub enum Action { #[key("right", "l")] Right, - /// Jump. - #[key("space")] + /// Jump with custom key, display symbol and help text. + #[key("space", symbol = "âŖ", help = "jump over obstacles")] Jump, /// Key Group Capturing action (e.g. tracking which character was pressed). @@ -119,7 +120,8 @@ let config = Action::keymap_config(); match config.get(&key) { Some(action) => match action { Action::Quit => break, - Action::Jump => println!("Jump!"), + Action::Jump => println!("Jump! Symbol: {:?}, Help: {:?}", + action.keymap_item().symbol, action.keymap_item().help), _ => println!("Action: {action:?} - {}", action.keymap_item().description), } _ => {} @@ -241,6 +243,8 @@ assert_eq!( | **Key Combinations** | Keys pressed simultaneously with modifiers (Ctrl, Alt, Shift). | `ctrl-c`, `alt-f4`, `ctrl-alt-shift-f1` | | **Key Sequences** | Multiple keys pressed in order. | `g g` (press `g` twice), `ctrl-b n` (Ctrl+B, then N), `ctrl-b c` (tmux-style new window) | | **Key Groups** | Predefined patterns matching sets of keys. | `@upper` (A-Z), `@alpha` (A-Z, a-z), `@any` (any key) | +| **Custom Symbols** | Custom display symbols for key bindings (e.g., for UI display). | `symbol = "^B"` | +| **Help Text** | Short help descriptions for key bindings. | `help = "jump"` | **Examples in Configuration:** ```toml diff --git a/examples/wasm/game.js b/examples/wasm/game.js index 616b5fa..0e1222d 100644 --- a/examples/wasm/game.js +++ b/examples/wasm/game.js @@ -470,7 +470,8 @@ class Game { this.gameSpeed = CONFIG.GAME.INITIAL_SPEED; this.gameOver = false; this.paused = false; - this.key = ""; + this.symbol = ""; + this.help = ""; // Delta time approach instead of frame limiting this.lastTime = 0; @@ -501,7 +502,8 @@ class Game { this.gameSpeed = CONFIG.GAME.INITIAL_SPEED; this.gameOver = false; this.paused = false; - this.key = ""; + this.symbol = ""; + this.help = ""; this._hideGameOverUI(); this.accumulator = 0; // Ensure game logic runs on first frame after reset @@ -512,11 +514,9 @@ class Game { }); } - setKey(key, desc) { - this.key = [key, desc] - .filter(Boolean) - .map((s) => s.toLowerCase()) - .join(" - "); + setKey(symbol = "", help = "") { + this.symbol = symbol; + this.help = help; } togglePause() { @@ -551,14 +551,13 @@ class Game { } _drawKey() { - let fontSize = 10; - ctx.fillStyle = "#ccc"; - ctx.font = `${fontSize}px "${CONFIG.GAME.FONT_FACE}"`; ctx.textAlign = "center"; + ctx.fillStyle = "#ccc"; + ctx.font = `10px "${CONFIG.GAME.FONT_FACE}"`; ctx.fillText( - this.key, + [this.symbol, this.help].filter(Boolean).join(" "), canvas.width / 2, - GROUND_Y + CONFIG.GROUND_HEIGHT - (CONFIG.GROUND_HEIGHT - fontSize) / 2, + GROUND_Y + CONFIG.GROUND_HEIGHT - (CONFIG.GROUND_HEIGHT - 10) / 2, ); } @@ -693,17 +692,30 @@ export function pauseGame() { } } -export function setKey(key, description) { - game.setKey(key, description); +export function setKey(key, description, symbol, help) { + game.setKey(key, description, symbol, help); } -export function setSkin(c) { - // Handle char code or string character - const char = typeof c === 'number' ? String.fromCharCode(c) : c; - const digit = parseInt(char); - if (isNaN(digit)) return; +export function renderKeybindings(info) { + const data = JSON.parse(info); + const container = document.getElementById("keybindings-info"); + if (!container) return; + + container.innerHTML = data + .map((entry) => { + const keys = entry.keys.join(", "); + const sym = entry.symbol || keys || ""; + const help = entry.help || ""; + return `
+ ${sym} + ${help} + ${keys} +
`; + }) + .join(""); +} - // Change rainbow trail colors based on digit +export function setSkin(digit) { const baseHue = (digit * 36) % 360; game.rainbowTrail.colors = Array.from({ length: 6 }, (_, i) => { return `hsl(${(baseHue + i * 20) % 360}, 100%, 50%)`; diff --git a/examples/wasm/index.css b/examples/wasm/index.css index f2438a3..4f30b79 100644 --- a/examples/wasm/index.css +++ b/examples/wasm/index.css @@ -128,6 +128,39 @@ canvas { #share-button:hover { background-color: #282828; } +#keybindings-info { + display: flex; + flex-wrap: wrap; + gap: 2px 16px; + padding: 8px 12px; + background: #1a1a2e; + border-top: 2px solid #333; + font-family: 'Silkscreen', cursive; + font-size: 12px; +} +.keybinding-row { + display: flex; + align-items: center; + gap: 6px; + width: calc(50% - 8px); + padding: 2px 0; +} +.keybinding-symbol { + color: #ffd93d; + font-weight: 700; + min-width: 24px; + text-align: center; +} +.keybinding-help { + color: #ccc; + flex-shrink: 0; + font-size: 12px; +} +.keybinding-keys { + color: #6b7280; + font-size: 10px; + margin-left: auto; +} .rainbow { font-family: 'Press Start 2P', cursive; animation: rainbowColors 4s linear infinite; diff --git a/examples/wasm/index.html b/examples/wasm/index.html index 0ee3047..d706e0e 100644 --- a/examples/wasm/index.html +++ b/examples/wasm/index.html @@ -33,39 +33,44 @@

DEMO: Nyan Jump! (WASM Edition)

GAME OVER!
+

EXAMPLE: Derive Macro

Define an enum and automatically derive key bindings using the #[derive(KeyMap)] macro.

-
#[derive(keymap::KeyMap, Hash, PartialEq, Eq, Clone)]
+    
#[derive(Debug, Clone, keymap::KeyMap, Hash, PartialEq, Eq)]
 pub enum Action {
   /// Jump over obstacles
-  #[key("space")]
+  #[key("space", symbol = "↑", help = "jump")] // symbol gets overridden by toml config
   Jump,
 
   /// Move leftward
-  #[key("left")]
+  #[key("left", help = "move left")]
   Left,
 
   /// Move rightward
-  #[key("right")]
+  #[key("right", help = "move right")]
   Right,
 
-  /// Select a skin (Key Group Capturing!)
-  #[key("@digit")]
-  SelectSkin(char),
+  /// Pause
+  #[key("p", help = "pause")]
+  Pause,
 
   /// Restart
-  #[key("q", "esc")]
+  #[key("q", "esc", help = "quit")]
   Quit,
+
+  /// Select a skin
+  #[key("@digit", symbol = "0-9", help = "select skin")]
+  SelectSkin(u8),
 }

EXAMPLE: Key Override

Customize or extend key bindings through configuration files without changing the source code.

# Now both 'space' and the 'up' arrow can be used to jump
-Jump = { keys = ["space", "up"], description = "Jump Jump!" }
+Jump = { keys = ["space", "up"], symbol = "âŖ", help = "jump", description = "Jump Jump!" }
 
diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index 484c2a9..2aa7b19 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -12,45 +12,46 @@ extern "C" { fn isGameOver() -> bool; fn resetGame(); fn pauseGame(); - fn setKey(key: String, desc: String); - fn setSkin(c: char); + fn setKey(key: String, desc: String, symbol: String, help: String); + fn setSkin(digit: u8); + fn renderKeybindings(info: String); } #[derive(Debug, Clone, keymap::KeyMap, Hash, PartialEq, Eq)] pub enum Action { /// Jump over obstacles - #[key("space")] + #[key("space", symbol = "↑", help = "jump")] // symbol gets overridden by toml config Jump, /// Move leftward - #[key("left")] + #[key("left", help = "move left")] Left, /// Move rightward - #[key("right")] + #[key("right", help = "move right")] Right, /// Pause - #[key("p")] + #[key("p", help = "pause")] Pause, /// Restart - #[key("q", "esc")] + #[key("q", "esc", help = "quit")] Quit, /// Select a skin - #[key("@digit")] - SelectSkin(char), + #[key("@digit", symbol = "0-9", help = "select skin")] + SelectSkin(u8), } /// Overrides the default keymap #[allow(unused)] pub const DERIVED_CONFIG: &str = r#" -Jump = { keys = ["space", "up"], description = "Jump Jump!" } -Quit = { keys = ["q", "esc"], description = "Quit!" } -Left = { keys = ["left", "alt-l"], description = "Move Left" } -Right = { keys = ["right", "alt-r"], description = "Move Right" } -SelectSkin = { keys = ["@digit"], description = "Select a skin" } +Jump = { keys = ["space", "up"], symbol = "âŖ", help = "jump", description = "Jump Jump!" } +Quit = { keys = ["q", "esc"], symbol = "↩", help = "quit", description = "Quit!" } +Left = { keys = ["left", "alt-l"], symbol = "←", help = "move left", description = "Move Left" } +Right = { keys = ["right", "alt-r"], symbol = "→", help = "move right", description = "Move Right" } +SelectSkin = { keys = ["@digit"], symbol = "0-9", help = "select skin", description = "Select a skin" } "#; #[allow(unused)] @@ -58,6 +59,22 @@ pub fn derived_config() -> DerivedConfig { toml::from_str(DERIVED_CONFIG).unwrap() } +fn json_escape(s: &str) -> String { + let mut buf = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => buf.push_str("\\\""), + '\\' => buf.push_str("\\\\"), + '\n' => buf.push_str("\\n"), + '\r' => buf.push_str("\\r"), + '\t' => buf.push_str("\\t"), + c if c.is_control() => buf.push_str(&format!("\\u{:04x}", c as u32)), + c => buf.push(c), + } + } + buf +} + #[wasm_bindgen] pub fn get_keymap_as_json() -> String { let keymap = derived_config(); @@ -65,13 +82,14 @@ pub fn get_keymap_as_json() -> String { .items .iter() .map(|(action, entry)| { - let keys: Vec = entry.keys.iter().map(|k| format!("\"{}\"", k)).collect(); - let description = entry.description.clone(); + let keys: Vec = entry.keys.iter().map(|k| format!("\"{}\"", json_escape(k))).collect(); + let description = json_escape(&entry.description); + let symbol = json_escape(entry.symbol.as_deref().unwrap_or_default()); + let help = json_escape(entry.help.as_deref().unwrap_or_default()); + let action_str = json_escape(&format!("{:?}", action)); format!( - "{{ \"action\": \"{:?}\", \"keys\": [{}], \"description\": \"{}\" }}", - action, + "{{ \"action\": \"{action_str}\", \"keys\": [{}], \"description\": \"{description}\", \"symbol\": \"{symbol}\", \"help\": \"{help}\" }}", keys.join(","), - description ) }) .collect(); @@ -99,6 +117,7 @@ pub fn main() { on_keydown.forget(); on_keyup.forget(); + renderKeybindings(get_keymap_as_json()); resetGame(); } @@ -110,11 +129,19 @@ pub fn handle_key_event(event: &KeyboardEvent, is_keydown: bool) { if is_keydown { let key = event.to_keymap().unwrap(); let mut desc = String::new(); + let mut symbol = String::new(); + let mut help = String::new(); if let Some((_, item)) = config.get_item(event) { desc = item.description.clone(); + if item.keys.iter().any(|k| k.starts_with('@')) { + symbol = key.to_string(); + } else { + symbol = item.symbol.clone().unwrap_or_default(); + } + help = item.help.clone().unwrap_or_default(); }; - setKey(key.to_string(), desc); + setKey(key.to_string(), desc, symbol, help); } // Use .get_bound() to support Key Group Capturing for SelectSkin @@ -128,7 +155,6 @@ pub fn handle_key_event(event: &KeyboardEvent, is_keydown: bool) { Action::SelectSkin(c) => { if is_keydown { setSkin(c); - setKey(format!("Skin selected: {c}"), "Key Group Capturing!".to_string()); } } _ if !is_game_over => match action { diff --git a/keymap_derive/src/item.rs b/keymap_derive/src/item.rs index 590ed9d..27eeebd 100644 --- a/keymap_derive/src/item.rs +++ b/keymap_derive/src/item.rs @@ -1,5 +1,5 @@ use keymap_parser::{parse_seq, Node}; -use syn::{punctuated::Punctuated, token::Comma, Attribute, LitStr, Token, Variant}; +use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Token, Variant}; /// An attribute path name #[key(...)] const KEY_IDENT: &str = "key"; @@ -14,18 +14,119 @@ pub(crate) struct Item<'a> { pub nodes: Vec>, pub ignore: bool, pub description: String, + pub symbol: Option, + pub help: Option, +} + +/// Helper struct representing the arguments parsed from a `#[key(...)]` attribute. +/// +/// It supports a hybrid syntax: +/// 1. Positional string literals (e.g. `"ctrl-b"`, `"space"`), which represent the keys to bind. +/// 2. The `ignore` boolean flag (e.g. `#[key(ignore)]`). +/// 3. Named name-value fields: +/// - `symbol = "..."` (e.g. `symbol = "^B"`) defining a custom quick visual symbol for display. +/// - `help = "..."` (e.g. `help = "jump"`) defining a short help text description for the binding. +/// +/// Example: +/// +/// #[key("ctrl-b", symbol = "^B", help = "jump")] +/// | |______| |__________| |___________| +/// path keys symbol help +struct KeyAttrArgs { + keys: Vec, + ignore: bool, + symbol: Option, + help: Option, +} + +impl syn::parse::Parse for KeyAttrArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut keys = Vec::new(); + let mut ignore = false; + let mut symbol = None; + let mut help = None; + + while !input.is_empty() { + if input.peek(syn::LitStr) { + // Parse positional key bindings like "ctrl-b" + let lit: syn::LitStr = input.parse()?; + keys.push(lit.value()); + } else if input.peek(syn::Ident) { + let ident: syn::Ident = input.parse()?; + if ident == "ignore" { + // Parse the single 'ignore' flag + ignore = true; + } else if ident == "symbol" { + // Parse 'symbol = "..."' + let _: Token![=] = input.parse()?; + let lit: syn::LitStr = input.parse()?; + symbol = Some(lit.value()); + } else if ident == "help" { + // Parse 'help = "..."' + let _: Token![=] = input.parse()?; + let lit: syn::LitStr = input.parse()?; + help = Some(lit.value()); + } else { + return Err(syn::Error::new( + ident.span(), + format!("Unknown key attribute argument: {}", ident), + )); + } + } else { + return Err(syn::Error::new( + input.span(), + "Expected string literal or identifier", + )); + } + + // Consume optional comma separator if there are remaining arguments + if !input.is_empty() { + let _: Token![,] = input.parse()?; + } + } + + Ok(KeyAttrArgs { + keys, + ignore, + symbol, + help, + }) + } } pub(crate) fn parse_items( variants: &Punctuated, ) -> Result>, syn::Error> { - // NOTE: All variants are parsed, even those without the #[key(...)] attribute. - // This allows the deserializer to override keys and descriptions for variants that don't define them explicitly. variants .iter() .map(|variant| { - let ignore = parse_ignore(variant); - let (keys, nodes) = parse_keys(variant, ignore)?; + let mut keys = Vec::new(); + let mut nodes = Vec::new(); + let mut ignore = false; + let mut symbol = None; + let mut help = None; + + for attr in &variant.attrs { + if attr.path().is_ident(KEY_IDENT) { + let args: KeyAttrArgs = attr.parse_args()?; + if args.ignore { + ignore = true; + } + for key in args.keys { + let seq = parse_seq(&key).map_err(|e| { + syn::Error::new(attr.span(), format!("Invalid key \"{key}\": {e}")) + })?; + keys.push(key); + nodes.push(seq); + } + if args.symbol.is_some() { + symbol = args.symbol; + } + if args.help.is_some() { + help = args.help; + } + } + } Ok(Item { variant, @@ -33,25 +134,13 @@ pub(crate) fn parse_items( description: parse_doc(variant), keys, nodes, + symbol, + help, }) }) .collect() } -fn parse_ignore(variant: &Variant) -> bool { - variant.attrs.iter().any(|attr| { - let mut ignore = false; - if attr.path().is_ident(KEY_IDENT) { - let _ = attr.parse_nested_meta(|meta| { - ignore = meta.path.is_ident("ignore"); - Ok(()) - }); - } - - ignore - }) -} - fn parse_doc(variant: &Variant) -> String { variant .attrs @@ -70,39 +159,3 @@ fn parse_doc(variant: &Variant) -> String { .collect::>() .join("\n") } - -/// Parse attribute arguments. -/// -/// Example: -/// -/// #[key("a", "g g")] -/// | |________| -/// path (args) -fn parse_args(attr: &Attribute) -> syn::Result> { - attr.parse_args_with(Punctuated::::parse_separated_nonempty) -} - -fn parse_keys(variant: &Variant, ignore: bool) -> syn::Result<(Vec, Vec>)> { - let mut keys = Vec::new(); - let mut nodes = Vec::new(); - - for attr in &variant.attrs { - if !attr.path().is_ident(KEY_IDENT) || ignore { - continue; - } - - // Collect arguments - // - // e.g. [["a"], ["g g"]] - for arg in parse_args(attr)? { - let val = arg.value(); - let seq = parse_seq(&val) - .map_err(|e| syn::Error::new(arg.span(), format!("Invalid key \"{val}\": {e}")))?; - - keys.push(val); - nodes.push(seq); - } - } - - Ok((keys, nodes)) -} diff --git a/keymap_derive/src/lib.rs b/keymap_derive/src/lib.rs index 913508f..594e218 100644 --- a/keymap_derive/src/lib.rs +++ b/keymap_derive/src/lib.rs @@ -187,6 +187,15 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre } }; + let symbol_opt = match &item.symbol { + Some(sym) => quote! { .with_symbol(Some(#sym)) }, + None => quote! {}, + }; + let help_opt = match &item.help { + Some(h) => quote! { .with_help(Some(#h)) }, + None => quote! {}, + }; + match_arms_deserialize.push(quote! { #variant_name_str => Ok(#variant_expr_default), }); @@ -194,7 +203,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre #variant_pat => ::keymap::Item::new( vec![#(#keys),*], #doc.to_string() - ), + ) #symbol_opt #help_opt, }); entries.push(quote! { @@ -203,7 +212,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre ::keymap::Item::new( vec![#(#keys),*], #doc.to_string() - ) + ) #symbol_opt #help_opt ), }); } diff --git a/keymap_derive/tests/derive.rs b/keymap_derive/tests/derive.rs index aad5445..379b6a3 100644 --- a/keymap_derive/tests/derive.rs +++ b/keymap_derive/tests/derive.rs @@ -44,6 +44,20 @@ enum IgnoreTest { IgnoredWithData(NoDefault), } +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum CustomSymbolTest { + /// Active item with custom symbol and help + #[key("ctrl-b", symbol = "^B", help = "jump over obstacles")] + Active, + /// Active item with help but no symbol (falls back to ctrl-b) + #[key("ctrl-b", help = "do jump")] + FallbackSymbol, + /// Active item with no symbol or help (falls back to ctrl-b) + #[key("ctrl-b")] + NoSymbolOrHelp, +} + #[cfg(test)] mod tests { use keymap_dev::{Error, Item, KeyMap, KeyMapConfig, ToKeyMap}; @@ -213,4 +227,24 @@ mod tests { .iter() .any(|(v, _)| matches!(v, IgnoreTest::IgnoredWithData(_)))); } + + #[test] + fn test_custom_symbol_and_help() { + let config = CustomSymbolTest::keymap_config(); + + // 1. symbol and help both specified + let (_, item1) = &config.items[0]; + assert_eq!(item1.symbol.as_deref(), Some("^B")); + assert_eq!(item1.help.as_deref(), Some("jump over obstacles")); + + // 2. help specified, symbol omitted (should fallback to "ctrl-b") + let (_, item2) = &config.items[1]; + assert_eq!(item2.symbol.as_deref(), Some("ctrl-b")); + assert_eq!(item2.help.as_deref(), Some("do jump")); + + // 3. both symbol and help omitted (should fallback to "ctrl-b" and None) + let (_, item3) = &config.items[2]; + assert_eq!(item3.symbol.as_deref(), Some("ctrl-b")); + assert_eq!(item3.help.as_deref(), None); + } } diff --git a/src/config.rs b/src/config.rs index 0dce367..5ae0da5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -240,12 +240,10 @@ impl Deref for DerivedConfig { /// # Example /// /// ```ignore -/// let item = Item { -/// keys: vec!["a".into(), "b c".into()], -/// description: "Some command".into(), -/// }; +/// let item = Item::new(vec!["a".into(), "b c".into()], "Some command".into()); /// ``` #[derive(Debug, Deserialize, PartialEq)] +#[serde(from = "ItemRaw")] pub struct Item { /// A collection of key expressions. Each expression will be run through /// `keymap_parser::parse_seq`, so special notations like `@digit` or @@ -253,8 +251,36 @@ pub struct Item { pub keys: Vec, /// A short description for display or documentation purposes. - #[serde(default)] pub description: String, + + /// A short key symbol or shortcut representation for the user interface (e.g. `^B`). + pub symbol: Option, + + /// A short help description of the binding (e.g. `jump`). + pub help: Option, +} + +/// Raw deserialization target — Serde deserializes into this first, +/// then [`From`] converts it into [`Item`] with fallback logic. +#[derive(Deserialize)] +struct ItemRaw { + keys: Vec, + #[serde(default)] + description: String, + symbol: Option, + help: Option, +} + +impl From for Item { + fn from(raw: ItemRaw) -> Self { + let symbol = raw.symbol.or_else(|| raw.keys.first().cloned()); + Self { + keys: raw.keys, + description: raw.description, + symbol, + help: raw.help, + } + } } impl Config { @@ -439,7 +465,7 @@ impl Config { } impl Item { - /// Create a new `Item` with the given list of key expressions and a + /// Creates a new `Item` with the given list of key expressions and a /// description. /// /// # Parameters @@ -454,7 +480,25 @@ impl Item { /// let item = Item::new(vec!["c".into(), "x y".into()], "Some command".into()); /// ``` pub fn new(keys: Vec, description: String) -> Self { - Self { keys, description } + let symbol = keys.first().cloned(); + Self { + keys, + description, + symbol, + help: None, + } + } + + /// Sets a custom key symbol for display. + pub fn with_symbol>(mut self, symbol: Option) -> Self { + self.symbol = symbol.map(Into::into); + self + } + + /// Sets a custom help text. + pub fn with_help>(mut self, help: Option) -> Self { + self.help = help.map(Into::into); + self } } @@ -576,6 +620,12 @@ where if item.description.is_empty() { item.description = config.items[pos].1.description.clone(); } + if item.help.is_none() { + item.help = config.items[pos].1.help.clone(); + } + if item.symbol.is_none() && item.keys == config.items[pos].1.keys { + item.symbol = config.items[pos].1.symbol.clone(); + } config.items[pos].1 = item; } else { // Append a new entry