diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index d1c8425f..748d607a 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 43E3628E37D2439B820FFC82 /* server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796713A1B3AEEBF4D0D180C7 /* server.swift */; }; 45AA5FD4A023AF751922BC22 /* BundleEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7A2DF0D1F72B80B1F04240 /* BundleEx.swift */; }; 45EA2D1C90430C432E123B51 /* keysMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0D40CBD65704BA9595C2FA /* keysMap.swift */; }; + 4A11D06CCDAD05595AB67239 /* parseKeyMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EBF5912CC4BBEC0E5E4B1C /* parseKeyMapping.swift */; }; 4CC374136F60299FB672662D /* DebugWindowsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71908440CD1ADBE13AD58E26 /* DebugWindowsCommand.swift */; }; 4E0BC093AD1FBCCA718A26EC /* parseCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ADCBFC2E29A2AD5C1B7984 /* parseCommand.swift */; }; 51AB4C0992703B2E9D0F55E2 /* MonitorDescriptionEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1D626244E63437F1CA24C3 /* MonitorDescriptionEx.swift */; }; @@ -153,6 +154,7 @@ 5F5F52E346D024960EAF5938 /* TrayMenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrayMenuModel.swift; sourceTree = ""; }; 5F7387A5BA6D187A00B00170 /* LocalPackage */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LocalPackage; path = LocalPackage; sourceTree = SOURCE_ROOT; }; 6352ADEE6625D9703CFCA99A /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; + 64EBF5912CC4BBEC0E5E4B1C /* parseKeyMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = parseKeyMapping.swift; sourceTree = ""; }; 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = parseConfig.swift; sourceTree = ""; }; 69CB1289E3FA51A35F839238 /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = ""; }; 6D9C5ED5AC77D80F1CCD103F /* JoinWithCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinWithCommandTest.swift; sourceTree = ""; }; @@ -329,6 +331,7 @@ 1C0D40CBD65704BA9595C2FA /* keysMap.swift */, 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */, CCDFB9D7321F08B5CCEF6AFB /* parseGaps.swift */, + 64EBF5912CC4BBEC0E5E4B1C /* parseKeyMapping.swift */, 0A9DFF8980BB3F90A3793BE9 /* parseOnWindowDetected.swift */, 9164C9401F7DDCACE9278DA4 /* startAtLogin.swift */, ); @@ -664,6 +667,7 @@ 4E0BC093AD1FBCCA718A26EC /* parseCommand.swift in Sources */, A0765C31043BCFB0420BF1C9 /* parseConfig.swift in Sources */, 5BA537EABFE48178D6BD2544 /* parseGaps.swift in Sources */, + 4A11D06CCDAD05595AB67239 /* parseKeyMapping.swift in Sources */, 21D0512B48E0E3C28F8CA42A /* parseOnWindowDetected.swift in Sources */, B3702BB393A9B03CCAE4C60E /* refresh.swift in Sources */, 8086A22EDCDC4C906C337D0B /* resizeWithMouse.swift in Sources */, diff --git a/LocalPackage/Sources/Common/util/commonUtil.swift b/LocalPackage/Sources/Common/util/commonUtil.swift index fce560c1..910f8fba 100644 --- a/LocalPackage/Sources/Common/util/commonUtil.swift +++ b/LocalPackage/Sources/Common/util/commonUtil.swift @@ -89,6 +89,10 @@ public extension Int { func toDouble() -> Double { Double(self) } } +public func +(lhs: [K: V], rhs: [K: V]) -> [K: V] { + lhs.merging(rhs) { _, r in r } +} + public extension String { func removePrefix(_ prefix: String) -> String { hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self diff --git a/docs/config-examples/default-config.toml b/docs/config-examples/default-config.toml index 13de6554..6f5d9289 100644 --- a/docs/config-examples/default-config.toml +++ b/docs/config-examples/default-config.toml @@ -34,6 +34,15 @@ default-root-container-layout = 'tiles' # tall monitor (anything higher than wide) gets vertical orientation default-root-container-orientation = 'auto' +# Visual indent makes it easier to understand that containers of the same orientation are nested. +# If you have 'enable-normalization-opposite-orientation-for-nested-containers' enabled then +# there is no way to observe the indent +indent-for-nested-containers-with-the-same-orientation = 30 + +# Possible values: (qwerty|dvorak) +# See https://nikitabobko.github.io/AeroSpace/guide.html#key-mapping +key-mapping.preset = 'qwerty' + # Gaps between windows (inner-*) and between monitor edges (outer-*). # Possible values: # - Constant: gaps.outer.top = 8 @@ -48,11 +57,6 @@ gaps.outer.bottom = 0 gaps.outer.top = 0 gaps.outer.right = 0 -# Visual indent makes it easier to understand that containers of the same orientation are nested. -# If you have 'enable-normalization-opposite-orientation-for-nested-containers' enabled then -# there is no way to observe the indent -indent-for-nested-containers-with-the-same-orientation = 30 - # 'main' binding mode declaration # See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes # 'main' binding mode must be always presented diff --git a/docs/goodness.adoc b/docs/goodness.adoc index 8f88c865..d73a94ce 100644 --- a/docs/goodness.adoc +++ b/docs/goodness.adoc @@ -141,6 +141,61 @@ end tell EOF''' ---- +[#disable-hide-app] +== Disable annoying and useless "hide application" + +.~/.aerospace.toml +[source,toml] +---- +[mode.main.binding] +cmd-h = [] +---- + +[#colemak-keys-remap] +== Colemak keys remap + +.~/.aerospace.toml +[source,toml] +---- +[key-mapping.key-notation-to-key-code] +q = 'q' +w = 'w' +f = 'e' +p = 'r' +g = 't' +j = 'y' +l = 'u' +u = 'i' +y = 'o' +semicolon = 'p' +leftSquareBracket = 'leftSquareBracket' +rightSquareBracket = 'rightSquareBracket' +backslash = 'backslash' + +a = 'a' +r = 's' +s = 'd' +t = 'f' +d = 'g' +h = 'h' +n = 'j' +e = 'k' +i = 'l' +o = 'semicolon' +quote = 'quote' + +z = 'z' +x = 'x' +c = 'c' +v = 'v' +b = 'b' +k = 'n' +r = 'm' +comma = 'comma' +period = 'period' +slash = 'slash' +---- + [#popular-apps-ids] == List of popular and built-in applications IDs diff --git a/docs/guide.adoc b/docs/guide.adoc index 744a168f..f92a28e6 100644 --- a/docs/guide.adoc +++ b/docs/guide.adoc @@ -81,6 +81,33 @@ aerospace workspace 1 For the list of available commands see: xref:commands.adoc[] +[#key-mapping] +=== Keyboard layouts and key mapping + +By default, key bindings in the config are perceived as `qwerty` layout. + +If you use different layout, different alphabet, or you just want to have a fancy alias for the existing key, you can use `key-mapping.key-notation-to-key-code`. + +[source,toml] +---- +# Define my fancy unicorn key notation +[key-mapping.key-notation-to-key-code] +unicorn = 'u' + +[mode.main.binding] +alt-unicorn = 'workspace unicorn' # (⁀ᗢ⁀) +---- + +* For `dvorak` users, AeroSpace offers a preconfigured preset. ++ +[source,toml] +---- +key-mapping.preset = 'dvorak' +---- + +* For `colemak` users, there is xref:goodness.adoc#colemak-keys-remap[a compiled mapping]. +`colemak` may be added as preconfigured preset similar to `dvorak` in the future, if there is enough demand + [#tree] == Tree diff --git a/src/config/Config.swift b/src/config/Config.swift index a8af681b..6595b3d4 100644 --- a/src/config/Config.swift +++ b/src/config/Config.swift @@ -17,6 +17,7 @@ struct Config: Copyable { var accordionPadding: Int = 30 var enableNormalizationOppositeOrientationForNestedContainers: Bool = true var execOnWorkspaceChange: [String] = [] + var keyMapping = KeyMapping() var gaps: Gaps = .zero var workspaceToMonitorForceAssignment: [String: [MonitorDescription]] = [:] diff --git a/src/config/HotkeyBinding.swift b/src/config/HotkeyBinding.swift index 0335f53b..f6fbd1b5 100644 --- a/src/config/HotkeyBinding.swift +++ b/src/config/HotkeyBinding.swift @@ -52,6 +52,6 @@ struct HotkeyBinding { self.modifiers = modifiers self.key = key self.commands = commands - self.binding = modifiers.toString() + "-\(key)" + self.binding = modifiers.isEmpty ? key.description : modifiers.toString() + "-\(key)" } } diff --git a/src/config/keysMap.swift b/src/config/keysMap.swift index c245c479..aa0cf623 100644 --- a/src/config/keysMap.swift +++ b/src/config/keysMap.swift @@ -1,32 +1,95 @@ import HotKey +import Common -let keysMap: [String: Key] = [ - "a": .a, - "b": .b, - "c": .c, - "d": .d, - "e": .e, - "f": .f, - "g": .g, - "h": .h, - "i": .i, - "j": .j, - "k": .k, - "l": .l, - "m": .m, - "n": .n, - "o": .o, - "p": .p, - "q": .q, - "r": .r, - "s": .s, - "t": .t, - "u": .u, - "v": .v, - "w": .w, - "x": .x, - "y": .y, - "z": .z, +private let minus = "minus" +private let equal = "equal" + +private let q = "q" +private let w = "w" +private let e = "e" +private let r = "r" +private let t = "t" +private let y = "y" +private let u = "u" +private let i = "i" +private let o = "o" +private let p = "p" +private let leftSquareBracket = "leftSquareBracket" +private let rightSquareBracket = "rightSquareBracket" +private let backslash = "backslash" + +private let a = "a" +private let s = "s" +private let d = "d" +private let f = "f" +private let g = "g" +private let h = "h" +private let j = "j" +private let k = "k" +private let l = "l" +private let semicolon = "semicolon" +private let quote = "quote" + +private let z = "z" +private let x = "x" +private let c = "c" +private let v = "v" +private let b = "b" +private let n = "n" +private let m = "m" +private let comma = "comma" +private let period = "period" +private let slash = "slash" + +func getKeysPreset(_ layout: KeyMapping.Preset) -> [String: Key] { + switch layout { + case .qwerty: + return keyNotationToKeyCode + case .dvorak: + return dvorakMap + } +} + +let keyNotationToKeyCode: [String: Key] = [ + minus: .minus, + equal: .equal, + + q: .q, + w: .w, + e: .e, + r: .r, + t: .t, + y: .y, + u: .u, + i: .i, + o: .o, + p: .p, + leftSquareBracket: .leftBracket, + rightSquareBracket: .rightBracket, + backslash: .backslash, + + a: .a, + s: .s, + d: .d, + f: .f, + g: .g, + h: .h, + j: .j, + k: .k, + l: .l, + semicolon: .semicolon, + quote: .quote, + + z: .z, + x: .x, + c: .c, + v: .v, + b: .b, + n: .n, + m: .m, + comma: .comma, + period: .period, + slash: .slash, "0": .zero, "1": .one, @@ -79,17 +142,7 @@ let keysMap: [String: Key] = [ "f19": .f19, "f20": .f20, - "minus": .minus, - "equal": .equal, - "period": .period, - "comma": .comma, - "slash": .slash, - "backslash": .backslash, - "quote": .quote, - "semicolon": .semicolon, "backtick": .grave, - "leftSquareBracket": .leftBracket, - "rightSquareBracket": .rightBracket, "space": .space, "enter": .return, "esc": .escape, @@ -102,6 +155,48 @@ let keysMap: [String: Key] = [ "right": .rightArrow, ] +private let dvorakMap: [String: Key] = keyNotationToKeyCode + [ + leftSquareBracket: .minus, + rightSquareBracket: .equal, + + quote: .q, + comma: .w, + period: .e, + p: .r, + y: .t, + f: .y, + g: .u, + c: .i, + r: .o, + l: .p, + slash: .leftBracket, + equal: .rightBracket, + backslash: .backslash, + + a: .a, + o: .s, + e: .d, + u: .f, + i: .g, + d: .h, + h: .j, + t: .k, + n: .l, + s: .semicolon, + minus: .quote, + + semicolon: .z, + q: .x, + j: .c, + k: .v, + x: .b, + b: .n, + m: .m, + w: .comma, + v: .period, + z: .slash, +] + let modifiersMap: [String: NSEvent.ModifierFlags] = [ "shift": .shift, "alt": .option, diff --git a/src/config/parseConfig.swift b/src/config/parseConfig.swift index 2b9572b8..477b711d 100644 --- a/src/config/parseConfig.swift +++ b/src/config/parseConfig.swift @@ -117,6 +117,9 @@ struct Parser: ParserProtocol { } } +private let keyMappingConfigRootKey = "key-mapping" +private let modeConfigRootKey = "mode" + private let configParser: [String: any ParserProtocol] = [ "after-login-command": Parser(\.afterLoginCommand, { parseCommandOrCommands($0).toParsedToml($1) }), "after-startup-command": Parser(\.afterStartupCommand, { parseCommandOrCommands($0).toParsedToml($1) }), @@ -134,7 +137,9 @@ private let configParser: [String: any ParserProtocol] = [ "accordion-padding": Parser(\.accordionPadding, parseInt), "exec-on-workspace-change": Parser(\.execOnWorkspaceChange, parseExecOnWorkspaceChange), - "mode": Parser(\.modes, parseModes), + keyMappingConfigRootKey: Parser(\.keyMapping, skipParsing(Config().keyMapping)), // Parsed manually + modeConfigRootKey: Parser(\.modes, skipParsing(Config().modes)), // Parsed manually + "gaps": Parser(\.gaps, parseGaps), "workspace-to-monitor-force-assignment": Parser(\.workspaceToMonitorForceAssignment, parseWorkspaceToMonitorAssignment), "on-window-detected": Parser(\.onWindowDetected, parseOnWindowDetectedArray) @@ -184,6 +189,14 @@ func parseConfig(_ rawToml: String) -> (config: Config, errors: [TomlParseError] var config = rawTable.parseTable(Config(), configParser, .root, &errors) + if let mapping = rawTable[keyMappingConfigRootKey].flatMap({ parseKeyMapping($0, .rootKey(keyMappingConfigRootKey), &errors) }) { + config.keyMapping = mapping + } + + if let modes = rawTable[modeConfigRootKey].flatMap({ parseModes($0, .rootKey(modeConfigRootKey), &errors, config.keyMapping.resolve()) }) { + config.modes = modes + } + config.preservedWorkspaceNames = config.modes.values.lazy .flatMap { (mode: Mode) -> [HotkeyBinding] in Array(mode.bindings.values) } .flatMap { (binding: HotkeyBinding) -> [String] in @@ -329,6 +342,10 @@ private func parseLayout(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace .flatMap { $0.parseLayout().orFailure(.semantic(backtrace, "Can't parse layout '\($0)'")) } } +private func skipParsing(_ value: T) -> (_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml { + { _, _ in .success(value) } +} + private func parseExecOnWorkspaceChange(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<[String]> { parseTomlArray(raw, backtrace) .flatMap { arr in @@ -398,14 +415,14 @@ func parseCaseInsensitiveRegex(_ raw: String) -> Parsed> { .map { $0.ignoresCase() } } -private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [String: Mode] { +private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> [String: Mode] { guard let rawTable = raw.table else { errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)] return [:] } var result: [String: Mode] = [:] for (key, value) in rawTable { - result[key] = parseMode(value, backtrace + .key(key), &errors) + result[key] = parseMode(value, backtrace + .key(key), &errors, mapping) } if !result.keys.contains(mainModeId) { errors += [.semantic(backtrace, "Please specify '\(mainModeId)' mode")] @@ -413,7 +430,7 @@ private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, return result } -private func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> Mode { +private func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> Mode { guard let rawTable: TOMLTable = raw.table else { errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)] return .zero @@ -424,7 +441,7 @@ private func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, let backtrace = backtrace + .key(key) switch key { case "binding": - result.bindings = parseBindings(value, backtrace, &errors) + result.bindings = parseBindings(value, backtrace, &errors, mapping) default: errors += [unknownKeyError(backtrace)] } @@ -438,7 +455,7 @@ extension Parsed where Failure == String { } } -private func parseBindings(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [String: HotkeyBinding] { +private func parseBindings(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError], _ mapping: [String: Key]) -> [String: HotkeyBinding] { guard let rawTable = raw.table else { errors += [expectedActualTypeError(expected: .table, actual: raw.type, backtrace)] return [:] @@ -446,26 +463,29 @@ private func parseBindings(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktra var result: [String: HotkeyBinding] = [:] for (binding, rawCommand): (String, TOMLValueConvertible) in rawTable { let backtrace = backtrace + .key(binding) - let binding = parseBinding(binding, backtrace) + let binding = parseBinding(binding, backtrace, mapping) .flatMap { modifiers, key -> ParsedToml in parseCommandOrCommands(rawCommand).toParsedToml(backtrace).map { HotkeyBinding(modifiers, key, $0) } } .getOrNil(appendErrorTo: &errors) if let binding { + if result.keys.contains(binding.binding) { + errors.append(.semantic(backtrace, "'\(binding.binding)' Binding redeclaration")) + } result[binding.binding] = binding } } return result } -private func parseBinding(_ raw: String, _ backtrace: TomlBacktrace) -> ParsedToml<(NSEvent.ModifierFlags, Key)> { +private func parseBinding(_ raw: String, _ backtrace: TomlBacktrace, _ mapping: [String: Key]) -> ParsedToml<(NSEvent.ModifierFlags, Key)> { let rawKeys = raw.split(separator: "-") let modifiers: ParsedToml = rawKeys.dropLast() .mapAllOrFailure { modifiersMap[String($0)].orFailure(.semantic(backtrace, "Can't parse modifiers in '\(raw)' binding")) } .map { NSEvent.ModifierFlags($0) } - let key: ParsedToml = rawKeys.last.flatMap { keysMap[String($0)] } + let key: ParsedToml = rawKeys.last.flatMap { mapping[String($0)] } .orFailure(.semantic(backtrace, "Can't parse the key in '\(raw)' binding")) return modifiers.flatMap { modifiers -> ParsedToml<(NSEvent.ModifierFlags, Key)> in key.flatMap { key -> ParsedToml<(NSEvent.ModifierFlags, Key)> in diff --git a/src/config/parseKeyMapping.swift b/src/config/parseKeyMapping.swift new file mode 100644 index 00000000..d288d3a6 --- /dev/null +++ b/src/config/parseKeyMapping.swift @@ -0,0 +1,64 @@ +import Common +import HotKey +import TOMLKit + +private let keyMappingParser: [String: any ParserProtocol] = [ + "preset": Parser(\.preset, parsePreset), + "key-notation-to-key-code": Parser(\.rawKeyNotationToKeyCode, parseKeyNotationToKeyCode), +] + +struct KeyMapping: Copyable, Equatable { + enum Preset: String, CaseIterable { + case qwerty, dvorak + } + + public init( + preset: Preset = .qwerty, + rawKeyNotationToKeyCode: [String: Key] = [:] + ) { + self.preset = preset + self.rawKeyNotationToKeyCode = rawKeyNotationToKeyCode + } + + fileprivate var preset: Preset = .qwerty + fileprivate var rawKeyNotationToKeyCode: [String: Key] = [:] + + func resolve() -> [String: Key] { + getKeysPreset(preset) + rawKeyNotationToKeyCode + } +} + +func parseKeyMapping(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> KeyMapping { + parseTable(raw, KeyMapping(), keyMappingParser, backtrace, &errors) +} + +private func parsePreset(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml { + parseString(raw, backtrace).flatMap { parseEnum($0, KeyMapping.Preset.self).toParsedToml(backtrace) } +} + +private func parseKeyNotationToKeyCode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> [String: Key] { + var result: [String: Key] = [:] + guard let table = raw.table else { + errors.append(expectedActualTypeError(expected: .table, actual: raw.type, backtrace)) + return result + } + for (key, value): (String, TOMLValueConvertible) in table { + if isValidKeyNotation(key) { + let backtrace = backtrace + .key(key) + if let value = parseString(value, backtrace).getOrNil(appendErrorTo: &errors) { + if let value = keyNotationToKeyCode[value] { + result[key] = value + } else { + errors.append(.semantic(backtrace, "'\(value)' is invalid key code")) + } + } + } else { + errors.append(.semantic(backtrace, "'\(key)' is invalid key notation")) + } + } + return result +} + +private func isValidKeyNotation(_ str: String) -> Bool { + str.rangeOfCharacter(from: .whitespacesAndNewlines) == nil && !str.contains("-") +} diff --git a/test/config/ConfigTest.swift b/test/config/ConfigTest.swift index 6fe2d8e1..b98b1546 100644 --- a/test/config/ConfigTest.swift +++ b/test/config/ConfigTest.swift @@ -1,4 +1,5 @@ import XCTest +import Nimble @testable import AeroSpace_Debug import Common @@ -325,4 +326,45 @@ final class ConfigTest: XCTestCase { "gaps.inner.vertical[1].monitor: The table is expected to have a single key", ]) } + + func testParseKeyMapping() { + let (config, errors) = parseConfig( + """ + [key-mapping.key-notation-to-key-code] + q = 'q' + unicorn = 'u' + + [mode.main.binding] + alt-unicorn = 'workspace unicorn' + """ + ) + XCTAssertEqual(errors.descriptions, []) + XCTAssertEqual(config.keyMapping, KeyMapping(preset: .qwerty, rawKeyNotationToKeyCode: [ + "q": .q, + "unicorn": .u, + ])) + let binding = HotkeyBinding(.option, .u, [WorkspaceCommand(args: WorkspaceCmdArgs(.direct(WTarget.Direct("unicorn"))))]) + XCTAssertEqual(config.modes[mainModeId]?.bindings, [binding.binding: binding]) + + let (_, errors1) = parseConfig( + """ + [key-mapping.key-notation-to-key-code] + q = 'qw' + ' f' = 'f' + """ + ) + expect(errors1.descriptions).to(equal([ + "key-mapping.key-notation-to-key-code: ' f' is invalid key notation", + "key-mapping.key-notation-to-key-code.q: 'qw' is invalid key code" + ])) + + let (dvorakConfig, dvorakErrors) = parseConfig( + """ + key-mapping.preset = 'dvorak' + """ + ) + XCTAssertEqual(dvorakErrors.descriptions, []) + XCTAssertEqual(dvorakConfig.keyMapping, KeyMapping(preset: .dvorak, rawKeyNotationToKeyCode: [:])) + expect(dvorakConfig.keyMapping.resolve()["quote"]).to(equal(.q)) + } }