Skip to content
Merged
17 changes: 17 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -71995,6 +71995,23 @@
}
}
},
"shortcut.key.space": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Space"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "スペース"
}
}
}
},
"shortcut.pressShortcut.prompt": {
"extractionState": "manual",
"localizations": {
Expand Down
41 changes: 29 additions & 12 deletions Sources/KeyboardShortcutSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,8 @@ struct ShortcutStroke: Equatable, Hashable {
switch key {
case "\t":
return String(localized: "shortcut.key.tab", defaultValue: "Tab")
case "space":
return String(localized: "shortcut.key.space", defaultValue: "Space")
case "\r":
return "↩"
case "media.brightnessDown":
Expand Down Expand Up @@ -1209,6 +1211,10 @@ struct ShortcutStroke: Equatable, Hashable {
}

var keyEquivalent: KeyEquivalent? {
if key == "space" {
return KeyEquivalent(Character(" "))
}

if Self.usesDirectKeyCodeMatching(key) {
return nil
}
Expand Down Expand Up @@ -1251,6 +1257,10 @@ struct ShortcutStroke: Equatable, Hashable {
}

var menuItemKeyEquivalent: String? {
if key == "space" {
return " "
}

if Self.usesDirectKeyCodeMatching(key) {
return nil
}
Expand Down Expand Up @@ -1476,6 +1486,7 @@ struct ShortcutStroke: Equatable, Hashable {
case 125: return "↓" // down arrow
case 126: return "↑" // up arrow
case 48: return "\t" // tab
case 49: return "space" // kVK_Space
case 36, 76: return "\r" // return, keypad enter
case 33: return "[" // kVK_ANSI_LeftBracket
case 30: return "]" // kVK_ANSI_RightBracket
Expand Down Expand Up @@ -1643,6 +1654,7 @@ struct ShortcutStroke: Equatable, Hashable {
case "media.playPause": return 16
case "media.next": return 17
case "media.previous": return 18
case "space": return 49
case "a": return 0
case "s": return 1
case "d": return 2
Expand Down Expand Up @@ -1702,7 +1714,7 @@ struct ShortcutStroke: Equatable, Hashable {
}

private static func usesDirectKeyCodeMatching(_ key: String) -> Bool {
functionKeyDisplayString(for: key) != nil || key.hasPrefix("media.")
key == "space" || functionKeyDisplayString(for: key) != nil || key.hasPrefix("media.")
}

private static func functionKeyDisplayString(for key: String) -> String? {
Expand Down Expand Up @@ -1773,7 +1785,7 @@ struct ShortcutStroke: Equatable, Hashable {
private static let supportedShortcutKeyCodes: [UInt16] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
33, 34, 35, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
33, 34, 35, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 123, 124, 125, 126,
]
}
Expand Down Expand Up @@ -1985,12 +1997,12 @@ struct StoredShortcut: Codable, Equatable, Hashable {

extension ShortcutStroke {
static func parseConfig(_ rawValue: String) -> ShortcutStroke? {
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard !rawValue.isEmpty else { return nil }

let parts = trimmed.split(separator: "+", omittingEmptySubsequences: false)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard !parts.isEmpty, let lastPart = parts.last, !lastPart.isEmpty else {
let rawParts = rawValue.split(separator: "+", omittingEmptySubsequences: false)
.map(String.init)
let parts = rawParts.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard !parts.isEmpty, let lastRawPart = rawParts.last, !lastRawPart.isEmpty else {
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
return nil
}

Expand All @@ -2014,7 +2026,7 @@ extension ShortcutStroke {
}
}

guard let key = parseConfigKeyToken(lastPart) else { return nil }
guard let key = parseConfigKeyToken(lastRawPart) else { return nil }
return ShortcutStroke(
key: key,
command: command,
Expand Down Expand Up @@ -2045,7 +2057,12 @@ extension ShortcutStroke {
}

private static func parseConfigKeyToken(_ rawValue: String) -> String? {
let lowered = rawValue.lowercased()
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return rawValue.allSatisfy { $0 == " " } ? "space" : nil
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
Comment thread
austinywang marked this conversation as resolved.
Resolved

let lowered = trimmed.lowercased()
switch lowered {
case "left", "arrowleft", "leftarrow", "←":
return "←"
Expand All @@ -2059,8 +2076,8 @@ extension ShortcutStroke {
return "\t"
case "return", "enter", "↩":
return "\r"
case "space":
return " "
case "space", "spacebar", "<space>":
return "space"
case "comma":
return ","
case "period", "dot":
Expand Down Expand Up @@ -2122,7 +2139,7 @@ extension StoredShortcut {
guard parsedStrokes.count == strokes.count, let firstStroke = parsedStrokes.first else {
return nil
}
guard !firstStroke.modifierFlags.isEmpty else { return nil }
guard !firstStroke.modifierFlags.isEmpty || firstStroke.key == "space" else { return nil }
let secondStroke = parsedStrokes.count == 2 ? parsedStrokes[1] : nil
return StoredShortcut(first: firstStroke, second: secondStroke)
}
Expand Down
66 changes: 66 additions & 0 deletions cmuxTests/WorkspaceUnitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,72 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase {
XCTAssertNil(ShortcutStroke.parseConfig("cmd+f21"))
}

func testShortcutConfigParsingRoundTripsSpaceKey() throws {
let spaceKeyCode = UInt16(0x31)
let shortcut = try XCTUnwrap(StoredShortcut.parseConfig("cmd+shift+space"))

XCTAssertEqual(shortcut.key, "space")
XCTAssertTrue(shortcut.command)
XCTAssertTrue(shortcut.shift)
XCTAssertFalse(shortcut.option)
XCTAssertFalse(shortcut.control)
XCTAssertEqual(
shortcut.firstStroke.resolvedKeyCode { keyCode, _ in
keyCode == spaceKeyCode ? " " : nil
},
spaceKeyCode
)
XCTAssertEqual(shortcut.configIdentifier, "cmd+shift+space")
XCTAssertTrue(
shortcut.matches(
keyCode: spaceKeyCode,
modifierFlags: [.command, .shift],
eventCharacter: " "
)
)

for rawShortcut in ["space", "cmd+space", "shift+space", "cmd+shift+space", "ctrl+space", "opt+space"] {
let parsedShortcut = try XCTUnwrap(StoredShortcut.parseConfig(rawShortcut))
XCTAssertEqual(parsedShortcut.key, "space")
XCTAssertEqual(parsedShortcut.firstStroke.resolvedKeyCode(), spaceKeyCode)
XCTAssertEqual(parsedShortcut.configIdentifier, rawShortcut)
}

XCTAssertEqual(StoredShortcut.parseConfig("cmd+shift+Space")?.configIdentifier, "cmd+shift+space")
XCTAssertEqual(StoredShortcut.parseConfig("cmd+shift+<space>")?.configIdentifier, "cmd+shift+space")
XCTAssertEqual(StoredShortcut.parseConfig("cmd+shift+ ")?.configIdentifier, "cmd+shift+space")
}

func testSettingsFileStoreParsesSpaceShortcutBinding() throws {
let directoryURL = try makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: directoryURL) }

let settingsFileURL = directoryURL.appendingPathComponent("settings.json", isDirectory: false)
try writeSettingsFile(
"""
{
"shortcuts": {
"bindings": {
"toggleSplitZoom": "cmd+shift+space"
}
}
}
""",
to: settingsFileURL
)

let store = KeyboardShortcutSettingsFileStore(
primaryPath: settingsFileURL.path,
fallbackPath: nil,
startWatching: false
)

XCTAssertEqual(
store.override(for: .toggleSplitZoom),
StoredShortcut(key: "space", command: true, shift: true, option: false, control: false)
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

override func setUp() {
super.setUp()
originalSettingsFileStore = KeyboardShortcutSettings.settingsFileStore
Expand Down
9 changes: 7 additions & 2 deletions web/data/cmux-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,19 +603,24 @@
"shortcutBinding": {
"oneOf": [
{
"type": "string",
"$ref": "#/$defs/shortcutStroke",
"description": "Single-stroke shortcut, for example cmd+n."
},
{
"type": "array",
"minItems": 1,
"maxItems": 2,
"items": {
"type": "string"
"$ref": "#/$defs/shortcutStroke"
},
"description": "Chorded shortcut. Example: [\"ctrl+b\", \"c\"]."
}
]
Comment thread
austinywang marked this conversation as resolved.
Resolved
},
"shortcutStroke": {
"type": "string",
"pattern": "^(?:(?:cmd|command|⌘|shift|⇧|opt|option|alt|⌥|ctrl|control|ctl|⌃)\\+)*(?: |[A-Za-z0-9]|left|arrowleft|leftarrow|←|right|arrowright|rightarrow|→|up|arrowup|uparrow|↑|down|arrowdown|downarrow|↓|tab|return|enter|↩|space|Space|spacebar|Spacebar|<space>|<Space>|comma|period|dot|slash|backslash|semicolon|quote|apostrophe|backtick|grave|minus|hyphen|plus|equals|leftbracket|openbracket|rightbracket|closebracket|f(?:[1-9]|1[0-9]|20)|F(?:[1-9]|1[0-9]|20)|volumeup|mediavolumeup|media\\.volumeup|volumedown|mediavolumedown|media\\.volumedown|brightnessup|mediabrightnessup|media\\.brightnessup|brightnessdown|mediabrightnessdown|media\\.brightnessdown|mute|mediamute|media\\.mute|playpause|mediaplaypause|media\\.playpause|nexttrack|medianext|media\\.next|media\\.nexttrack|previoustrack|mediaprevious|media\\.previous|media\\.previoustrack)$",
"description": "One keyboard shortcut stroke using modifier+key syntax. Supported key names include space, Space, <space>, and <Space>."
}
Comment thread
austinywang marked this conversation as resolved.
Resolved
}
}
Loading
Loading