native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Expand CLI key token support

onevpaw a8f402ce 3b590137

+527 -135
+2 -2
ProwlCLI/Commands/KeyCommand.swift
··· 7 7 struct KeyCommand: ParsableCommand { 8 8 static let configuration = CommandConfiguration( 9 9 commandName: "key", 10 - abstract: "Send a key event to a terminal pane.", 10 + abstract: "Send a user key event to a terminal pane.", 11 11 discussion: """ 12 12 With one positional argument, the key is sent to the current pane. 13 13 With two positional arguments, the first is the target (auto-resolved) and ··· 61 61 guard let normalized = KeyTokens.normalize(rawToken) else { 62 62 throw ExitError( 63 63 code: CLIErrorCode.unsupportedKey, 64 - message: "The key token '\(rawToken.lowercased())' is not supported in v1." 64 + message: "The key token '\(rawToken.lowercased())' is not supported." 65 65 ) 66 66 } 67 67
+36 -1
ProwlCLITests/ProwlCLIIntegrationTests.swift
··· 808 808 } 809 809 810 810 func testKeyCommandRejectUnsupportedKey() throws { 811 - let result = try runProwl(args: ["key", "ctrl-z", "--json"]) 811 + let result = try runProwl(args: ["key", "hyper-k", "--json"]) 812 812 XCTAssertNotEqual(result.exitCode, 0) 813 813 let payload = try jsonObject(from: result.stdout) 814 814 XCTAssertEqual(payload["ok"] as? Bool, false) ··· 963 963 XCTAssertEqual(input.rawToken, alias, "rawToken should preserve '\(alias)'") 964 964 } else { 965 965 XCTFail("Expected key command envelope for alias '\(alias)'") 966 + } 967 + } 968 + } 969 + 970 + func testKeyCommandExpandedTokensAccepted() throws { 971 + let tokenCases: [(raw: String, normalized: String)] = [ 972 + ("cmd-c", "cmd-c"), 973 + ("command-shift-k", "cmd-shift-k"), 974 + ("alt-enter", "opt-enter"), 975 + ("ctrl-z", "ctrl-z"), 976 + ("deleteforward", "delete-forward"), 977 + ("f12", "f12"), 978 + ] 979 + 980 + for (raw, normalized) in tokenCases { 981 + let socketPath = temporarySocketPath(suffix: "key-expanded-\(normalized.replacingOccurrences(of: "-", with: "_"))") 982 + let response = CommandResponse( 983 + ok: true, 984 + command: "key", 985 + schemaVersion: "prowl.cli.key.v1" 986 + ) 987 + 988 + let (requestData, result) = try runWithMockServer( 989 + socketPath: socketPath, 990 + response: response, 991 + args: ["key", raw, "--json"] 992 + ) 993 + 994 + XCTAssertEqual(result.exitCode, 0, "Token '\(raw)' should be accepted") 995 + let envelope = try JSONDecoder().decode(CommandEnvelope.self, from: requestData) 996 + if case .key(let input) = envelope.command { 997 + XCTAssertEqual(input.token, normalized) 998 + XCTAssertEqual(input.rawToken, raw) 999 + } else { 1000 + XCTFail("Expected key command envelope for token '\(raw)'") 966 1001 } 967 1002 } 968 1003 }
+20 -23
doc-onevcat/contracts/cli/key.md
··· 8 8 9 9 ## What changed in this revision 10 10 11 - Compared with the initial draft, this version makes `key` safer and more script-friendly: 11 + Compared with the initial draft, this version makes `key` broader and more script-friendly: 12 12 13 - - define a strict input grammar with aliases (`return` -> `enter`, `escape` -> `esc`, `pgup` -> `pageup`, ...) 13 + - normalize aliases (`return` -> `enter`, `escape` -> `esc`, `pgup` -> `pageup`, ...) 14 + - accept modifier prefixes and combinations such as `cmd-c`, `shift-tab`, `opt-enter`, `cmd-shift-k` 15 + - accept additional named keys such as `delete-forward`, `insert`, and `f1`...`f12` 14 16 - add `--repeat <n>` as first-class input instead of forcing shell loops 15 17 - split `requested` vs `normalized` in output so callers can debug token normalization 16 18 - add `delivery` counters for machine verification (`attempted` / `delivered`) ··· 60 62 61 63 ### Canonical tokens 62 64 63 - - `enter` 64 - - `esc` 65 - - `tab` 66 - - `backspace` 67 - - `up` 68 - - `down` 69 - - `left` 70 - - `right` 71 - - `pageup` 72 - - `pagedown` 73 - - `home` 74 - - `end` 75 - - `ctrl-c` 76 - - `ctrl-d` 77 - - `ctrl-l` 65 + Canonical normalized tokens now include: 66 + 67 + - navigation keys such as `tab`, `up`, `down`, `left`, `right`, `pageup`, `pagedown`, `home`, `end` 68 + - editing keys such as `enter`, `backspace`, `delete-forward`, `insert` 69 + - printable keys such as letters, digits, and common punctuation 70 + - control combinations such as `ctrl-c`, `ctrl-d`, `ctrl-l`, `ctrl-z` 71 + - shortcut combinations such as `cmd-c`, `cmd-shift-k`, `shift-tab`, `opt-enter` 72 + - function keys `f1`...`f12` 78 73 79 74 ### Accepted aliases (normalized to canonical) 80 75 ··· 86 81 - `arrow-right` -> `right` 87 82 - `pgup` -> `pageup` 88 83 - `pgdn` -> `pagedown` 89 - - `ctrl+c` -> `ctrl-c` 90 - - `ctrl+d` -> `ctrl-d` 91 - - `ctrl+l` -> `ctrl-l` 84 + - `forward-delete` / `deleteforward` -> `delete-forward` 85 + - `ins` -> `insert` 86 + - `command-*` -> `cmd-*` 87 + - `alt-*` / `option-*` -> `opt-*` 88 + - `ctrl+*` -> `ctrl-*` 92 89 93 90 ### `--repeat` 94 91 ··· 162 159 - `normalized`: string 163 160 - canonical token used by runtime 164 161 - must be one of the canonical tokens listed above 165 - - `category`: `"navigation"` | `"editing"` | `"control"` 162 + - `category`: `"navigation"` | `"editing"` | `"control"` | `"shortcut"` | `"function"` 166 163 167 164 ### `delivery` 168 165 ··· 216 213 "schema_version": "prowl.cli.key.v1", 217 214 "error": { 218 215 "code": "UNSUPPORTED_KEY", 219 - "message": "The key token 'ctrl-z' is not supported in v1", 216 + "message": "The key token 'hyper-k' is not supported.", 220 217 "details": { 221 - "token": "ctrl-z" 218 + "token": "hyper-k" 222 219 } 223 220 } 224 221 }
+1 -1
supacode/CLIService/KeyCommandHandler.swift
··· 70 70 guard let category = KeyTokens.category(for: input.token) else { 71 71 return errorResponse( 72 72 code: CLIErrorCode.unsupportedKey, 73 - message: "The key token '\(input.token)' is not supported in v1." 73 + message: "The key token '\(input.token)' is not supported." 74 74 ) 75 75 } 76 76
+166 -59
supacode/CLIService/Shared/KeyCommandPayload.swift
··· 46 46 case navigation 47 47 case editing 48 48 case control 49 + case shortcut 50 + case function 49 51 } 50 52 51 53 public struct KeyDelivery: Codable, Sendable { ··· 124 126 125 127 // MARK: - Token Normalization 126 128 129 + public enum KeyModifier: String, Codable, Sendable, CaseIterable { 130 + case cmd 131 + case shift 132 + case opt 133 + case ctrl 134 + 135 + static let canonicalOrder: [KeyModifier] = [.cmd, .shift, .opt, .ctrl] 136 + 137 + static func resolve(_ raw: String) -> KeyModifier? { 138 + switch raw { 139 + case "cmd", "command", "super": 140 + return .cmd 141 + case "shift": 142 + return .shift 143 + case "opt", "option", "alt": 144 + return .opt 145 + case "ctrl", "control": 146 + return .ctrl 147 + default: 148 + return nil 149 + } 150 + } 151 + } 152 + 153 + public struct KeyTokenDescriptor: Equatable, Sendable { 154 + public let normalized: String 155 + public let baseToken: String 156 + public let modifiers: [KeyModifier] 157 + public let category: KeyCategory 158 + 159 + public init(normalized: String, baseToken: String, modifiers: [KeyModifier], category: KeyCategory) { 160 + self.normalized = normalized 161 + self.baseToken = baseToken 162 + self.modifiers = modifiers 163 + self.category = category 164 + } 165 + } 166 + 167 + private struct KeyBaseDescriptor { 168 + let canonical: String 169 + let category: KeyCategory 170 + } 171 + 127 172 /// Shared token definitions for the `key` command. 128 173 /// Used by CLI for validation and by app for response building. 129 174 public enum KeyTokens { 130 - /// Alias map: accepted aliases → canonical token. 131 - public static let aliases: [String: String] = [ 132 - "return": "enter", 133 - "escape": "esc", 134 - "arrow-up": "up", 135 - "arrow-down": "down", 136 - "arrow-left": "left", 137 - "arrow-right": "right", 138 - "pgup": "pageup", 139 - "pgdn": "pagedown", 140 - "ctrl+c": "ctrl-c", 141 - "ctrl+d": "ctrl-d", 142 - "ctrl+l": "ctrl-l", 143 - ] 175 + public static func normalize(_ raw: String) -> String? { 176 + descriptor(for: raw)?.normalized 177 + } 178 + 179 + public static func category(for canonical: String) -> KeyCategory? { 180 + descriptor(for: canonical)?.category 181 + } 182 + 183 + public static func descriptor(for raw: String) -> KeyTokenDescriptor? { 184 + let normalizedInput = raw 185 + .trimmingCharacters(in: .whitespacesAndNewlines) 186 + .lowercased() 187 + .replacingOccurrences(of: "+", with: "-") 144 188 145 - /// All canonical tokens recognized in v1. 146 - public static let canonical: Set<String> = [ 147 - "enter", 148 - "esc", 149 - "tab", 150 - "backspace", 151 - "up", 152 - "down", 153 - "left", 154 - "right", 155 - "pageup", 156 - "pagedown", 157 - "home", 158 - "end", 159 - "ctrl-c", 160 - "ctrl-d", 161 - "ctrl-l", 162 - ] 189 + guard !normalizedInput.isEmpty else { return nil } 190 + 191 + let parts = normalizedInput 192 + .split(separator: "-", omittingEmptySubsequences: false) 193 + .map(String.init) 194 + guard !parts.isEmpty, !parts.contains(where: \.isEmpty) else { return nil } 195 + 196 + for baseLength in [2, 1] where parts.count >= baseLength { 197 + let baseRaw = parts.suffix(baseLength).joined(separator: "-") 198 + guard let base = baseDescriptor(for: baseRaw) else { continue } 199 + 200 + let modifierParts = parts.dropLast(baseLength) 201 + var modifierSet = Set<KeyModifier>() 202 + for modifierRaw in modifierParts { 203 + guard let modifier = KeyModifier.resolve(modifierRaw), modifierSet.insert(modifier).inserted else { 204 + return nil 205 + } 206 + } 207 + 208 + let modifiers = KeyModifier.canonicalOrder.filter { modifierSet.contains($0) } 209 + let normalized = (modifiers.map(\.rawValue) + [base.canonical]).joined(separator: "-") 210 + let category = category(for: base, modifiers: modifiers) 211 + return KeyTokenDescriptor( 212 + normalized: normalized, 213 + baseToken: base.canonical, 214 + modifiers: modifiers, 215 + category: category 216 + ) 217 + } 163 218 164 - /// Category for each canonical token. 165 - public static let categories: [String: KeyCategory] = [ 166 - "up": .navigation, 167 - "down": .navigation, 168 - "left": .navigation, 169 - "right": .navigation, 170 - "pageup": .navigation, 171 - "pagedown": .navigation, 172 - "home": .navigation, 173 - "end": .navigation, 174 - "tab": .navigation, 175 - "enter": .editing, 176 - "backspace": .editing, 177 - "esc": .control, 178 - "ctrl-c": .control, 179 - "ctrl-d": .control, 180 - "ctrl-l": .control, 181 - ] 219 + return nil 220 + } 182 221 183 - /// Normalize a user-provided token string to its canonical form. 184 - /// Returns `nil` if the token is not recognized. 185 - public static func normalize(_ raw: String) -> String? { 186 - let lowered = raw.lowercased() 187 - let resolved = aliases[lowered] ?? lowered 188 - return canonical.contains(resolved) ? resolved : nil 222 + private static func category(for base: KeyBaseDescriptor, modifiers: [KeyModifier]) -> KeyCategory { 223 + guard !modifiers.isEmpty else { return base.category } 224 + if modifiers == [.ctrl] { 225 + return .control 226 + } 227 + return .shortcut 189 228 } 190 229 191 - /// Get the category for a canonical token. 192 - public static func category(for canonical: String) -> KeyCategory? { 193 - categories[canonical] 230 + private static func baseDescriptor(for raw: String) -> KeyBaseDescriptor? { 231 + switch raw { 232 + case "enter", "return": 233 + return KeyBaseDescriptor(canonical: "enter", category: .editing) 234 + case "esc", "escape": 235 + return KeyBaseDescriptor(canonical: "esc", category: .control) 236 + case "tab": 237 + return KeyBaseDescriptor(canonical: "tab", category: .navigation) 238 + case "backspace", "delete", "del": 239 + return KeyBaseDescriptor(canonical: "backspace", category: .editing) 240 + case "delete-forward", "forward-delete", "deleteforward", "forwarddelete": 241 + return KeyBaseDescriptor(canonical: "delete-forward", category: .editing) 242 + case "insert", "ins": 243 + return KeyBaseDescriptor(canonical: "insert", category: .editing) 244 + case "up", "arrow-up": 245 + return KeyBaseDescriptor(canonical: "up", category: .navigation) 246 + case "down", "arrow-down": 247 + return KeyBaseDescriptor(canonical: "down", category: .navigation) 248 + case "left", "arrow-left": 249 + return KeyBaseDescriptor(canonical: "left", category: .navigation) 250 + case "right", "arrow-right": 251 + return KeyBaseDescriptor(canonical: "right", category: .navigation) 252 + case "pageup", "page-up", "pgup": 253 + return KeyBaseDescriptor(canonical: "pageup", category: .navigation) 254 + case "pagedown", "page-down", "pgdn": 255 + return KeyBaseDescriptor(canonical: "pagedown", category: .navigation) 256 + case "home": 257 + return KeyBaseDescriptor(canonical: "home", category: .navigation) 258 + case "end": 259 + return KeyBaseDescriptor(canonical: "end", category: .navigation) 260 + case "space": 261 + return KeyBaseDescriptor(canonical: "space", category: .editing) 262 + case "minus", "hyphen", "dash": 263 + return KeyBaseDescriptor(canonical: "minus", category: .editing) 264 + case "equal", "equals": 265 + return KeyBaseDescriptor(canonical: "equal", category: .editing) 266 + case "comma": 267 + return KeyBaseDescriptor(canonical: "comma", category: .editing) 268 + case "period", "dot": 269 + return KeyBaseDescriptor(canonical: "period", category: .editing) 270 + case "slash": 271 + return KeyBaseDescriptor(canonical: "slash", category: .editing) 272 + case "backslash": 273 + return KeyBaseDescriptor(canonical: "backslash", category: .editing) 274 + case "semicolon": 275 + return KeyBaseDescriptor(canonical: "semicolon", category: .editing) 276 + case "quote", "apostrophe": 277 + return KeyBaseDescriptor(canonical: "quote", category: .editing) 278 + case "grave", "backtick": 279 + return KeyBaseDescriptor(canonical: "grave", category: .editing) 280 + case "left-bracket", "leftbracket", "lbracket": 281 + return KeyBaseDescriptor(canonical: "[", category: .editing) 282 + case "right-bracket", "rightbracket", "rbracket": 283 + return KeyBaseDescriptor(canonical: "]", category: .editing) 284 + default: 285 + break 286 + } 287 + 288 + if raw.count == 1, let scalar = raw.unicodeScalars.first, scalar.isASCII, !scalar.properties.isWhitespace { 289 + return KeyBaseDescriptor(canonical: raw, category: .editing) 290 + } 291 + 292 + if raw.count >= 2, 293 + raw.first == "f", 294 + let number = Int(raw.dropFirst()), 295 + (1...12).contains(number) 296 + { 297 + return KeyBaseDescriptor(canonical: raw, category: .function) 298 + } 299 + 300 + return nil 194 301 } 195 302 }
+244 -48
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 1878 1878 let charactersIgnoringModifiers: String 1879 1879 let modifiers: NSEvent.ModifierFlags 1880 1880 1881 - // swiftlint:disable:next cyclomatic_complexity 1882 1881 static func from(token: String) -> CLIKeySpec? { 1882 + guard let descriptor = KeyTokens.descriptor(for: token), 1883 + let base = baseSpec(for: descriptor.baseToken) 1884 + else { 1885 + return nil 1886 + } 1887 + 1888 + var modifierFlags = eventModifiers(from: descriptor.modifiers) 1889 + if base.usesFunctionModifier { 1890 + modifierFlags.insert(.function) 1891 + } 1892 + 1893 + let charactersIgnoringModifiers = base.charactersIgnoringModifiers 1894 + let characters: String 1895 + if descriptor.modifiers == [.ctrl], let controlCharacter = controlCharacter(for: descriptor.baseToken) { 1896 + characters = controlCharacter 1897 + } else if descriptor.modifiers.contains(.shift) { 1898 + characters = shiftedCharacters(for: descriptor.baseToken) ?? charactersIgnoringModifiers 1899 + } else { 1900 + characters = charactersIgnoringModifiers 1901 + } 1902 + 1903 + return CLIKeySpec( 1904 + keyCode: base.keyCode, 1905 + characters: characters, 1906 + charactersIgnoringModifiers: charactersIgnoringModifiers, 1907 + modifiers: modifierFlags 1908 + ) 1909 + } 1910 + 1911 + private struct BaseSpec { 1912 + let keyCode: UInt16 1913 + let charactersIgnoringModifiers: String 1914 + let usesFunctionModifier: Bool 1915 + } 1916 + 1917 + private static let namedBaseSpecs: [String: BaseSpec] = [ 1918 + "enter": BaseSpec(keyCode: UInt16(kVK_Return), charactersIgnoringModifiers: "\r", usesFunctionModifier: false), 1919 + "esc": BaseSpec(keyCode: UInt16(kVK_Escape), charactersIgnoringModifiers: "\u{1B}", usesFunctionModifier: false), 1920 + "tab": BaseSpec(keyCode: UInt16(kVK_Tab), charactersIgnoringModifiers: "\t", usesFunctionModifier: false), 1921 + "backspace": BaseSpec(keyCode: UInt16(kVK_Delete), charactersIgnoringModifiers: "\u{7F}", usesFunctionModifier: false), 1922 + "delete-forward": BaseSpec( 1923 + keyCode: UInt16(kVK_ForwardDelete), 1924 + charactersIgnoringModifiers: String(UnicodeScalar(NSDeleteFunctionKey)!), 1925 + usesFunctionModifier: true 1926 + ), 1927 + "insert": BaseSpec( 1928 + keyCode: UInt16(kVK_Help), 1929 + charactersIgnoringModifiers: String(UnicodeScalar(NSInsertFunctionKey)!), 1930 + usesFunctionModifier: true 1931 + ), 1932 + "up": BaseSpec( 1933 + keyCode: UInt16(kVK_UpArrow), 1934 + charactersIgnoringModifiers: String(UnicodeScalar(NSUpArrowFunctionKey)!), 1935 + usesFunctionModifier: true 1936 + ), 1937 + "down": BaseSpec( 1938 + keyCode: UInt16(kVK_DownArrow), 1939 + charactersIgnoringModifiers: String(UnicodeScalar(NSDownArrowFunctionKey)!), 1940 + usesFunctionModifier: true 1941 + ), 1942 + "left": BaseSpec( 1943 + keyCode: UInt16(kVK_LeftArrow), 1944 + charactersIgnoringModifiers: String(UnicodeScalar(NSLeftArrowFunctionKey)!), 1945 + usesFunctionModifier: true 1946 + ), 1947 + "right": BaseSpec( 1948 + keyCode: UInt16(kVK_RightArrow), 1949 + charactersIgnoringModifiers: String(UnicodeScalar(NSRightArrowFunctionKey)!), 1950 + usesFunctionModifier: true 1951 + ), 1952 + "pageup": BaseSpec( 1953 + keyCode: UInt16(kVK_PageUp), 1954 + charactersIgnoringModifiers: String(UnicodeScalar(NSPageUpFunctionKey)!), 1955 + usesFunctionModifier: true 1956 + ), 1957 + "pagedown": BaseSpec( 1958 + keyCode: UInt16(kVK_PageDown), 1959 + charactersIgnoringModifiers: String(UnicodeScalar(NSPageDownFunctionKey)!), 1960 + usesFunctionModifier: true 1961 + ), 1962 + "home": BaseSpec( 1963 + keyCode: UInt16(kVK_Home), 1964 + charactersIgnoringModifiers: String(UnicodeScalar(NSHomeFunctionKey)!), 1965 + usesFunctionModifier: true 1966 + ), 1967 + "end": BaseSpec( 1968 + keyCode: UInt16(kVK_End), 1969 + charactersIgnoringModifiers: String(UnicodeScalar(NSEndFunctionKey)!), 1970 + usesFunctionModifier: true 1971 + ), 1972 + ] 1973 + 1974 + private static let functionKeyMap: [String: (UInt16, Int)] = [ 1975 + "f1": (UInt16(kVK_F1), NSF1FunctionKey), 1976 + "f2": (UInt16(kVK_F2), NSF2FunctionKey), 1977 + "f3": (UInt16(kVK_F3), NSF3FunctionKey), 1978 + "f4": (UInt16(kVK_F4), NSF4FunctionKey), 1979 + "f5": (UInt16(kVK_F5), NSF5FunctionKey), 1980 + "f6": (UInt16(kVK_F6), NSF6FunctionKey), 1981 + "f7": (UInt16(kVK_F7), NSF7FunctionKey), 1982 + "f8": (UInt16(kVK_F8), NSF8FunctionKey), 1983 + "f9": (UInt16(kVK_F9), NSF9FunctionKey), 1984 + "f10": (UInt16(kVK_F10), NSF10FunctionKey), 1985 + "f11": (UInt16(kVK_F11), NSF11FunctionKey), 1986 + "f12": (UInt16(kVK_F12), NSF12FunctionKey), 1987 + ] 1988 + 1989 + private static func eventModifiers(from modifiers: [KeyModifier]) -> NSEvent.ModifierFlags { 1990 + modifiers.reduce(into: NSEvent.ModifierFlags()) { result, modifier in 1991 + switch modifier { 1992 + case .cmd: result.insert(.command) 1993 + case .shift: result.insert(.shift) 1994 + case .opt: result.insert(.option) 1995 + case .ctrl: result.insert(.control) 1996 + } 1997 + } 1998 + } 1999 + 2000 + private static func baseSpec(for token: String) -> BaseSpec? { 2001 + if let base = namedBaseSpecs[token] { return base } 2002 + if let (keyCode, scalar) = functionKeyMap[token] { 2003 + return BaseSpec(keyCode: keyCode, charactersIgnoringModifiers: String(UnicodeScalar(scalar)!), usesFunctionModifier: true) 2004 + } 2005 + return printableBaseSpec(for: token) 2006 + } 2007 + 2008 + private static func printableBaseSpec(for token: String) -> BaseSpec? { 2009 + guard let character = printableCharacter(for: token), let keyCode = printableKeyCode(for: token) else { return nil } 2010 + return BaseSpec(keyCode: keyCode, charactersIgnoringModifiers: String(character), usesFunctionModifier: false) 2011 + } 2012 + 2013 + private static func printableCharacter(for token: String) -> Character? { 1883 2014 switch token { 1884 - case "enter": 1885 - return CLIKeySpec(keyCode: 36, characters: "\r", charactersIgnoringModifiers: "\r", modifiers: []) 1886 - case "esc": 1887 - return CLIKeySpec(keyCode: 53, characters: "\u{1B}", charactersIgnoringModifiers: "\u{1B}", modifiers: []) 1888 - case "tab": 1889 - return CLIKeySpec(keyCode: 48, characters: "\t", charactersIgnoringModifiers: "\t", modifiers: []) 1890 - case "backspace": 1891 - return CLIKeySpec(keyCode: 51, characters: "\u{7F}", charactersIgnoringModifiers: "\u{7F}", modifiers: []) 1892 - case "up": 1893 - return CLIKeySpec( 1894 - keyCode: 126, characters: "\u{F700}", charactersIgnoringModifiers: "\u{F700}", modifiers: [.function] 1895 - ) 1896 - case "down": 1897 - return CLIKeySpec( 1898 - keyCode: 125, characters: "\u{F701}", charactersIgnoringModifiers: "\u{F701}", modifiers: [.function] 1899 - ) 1900 - case "left": 1901 - return CLIKeySpec( 1902 - keyCode: 123, characters: "\u{F702}", charactersIgnoringModifiers: "\u{F702}", modifiers: [.function] 1903 - ) 1904 - case "right": 1905 - return CLIKeySpec( 1906 - keyCode: 124, characters: "\u{F703}", charactersIgnoringModifiers: "\u{F703}", modifiers: [.function] 1907 - ) 1908 - case "pageup": 1909 - return CLIKeySpec( 1910 - keyCode: 116, characters: "\u{F72C}", charactersIgnoringModifiers: "\u{F72C}", modifiers: [.function] 1911 - ) 1912 - case "pagedown": 1913 - return CLIKeySpec( 1914 - keyCode: 121, characters: "\u{F72D}", charactersIgnoringModifiers: "\u{F72D}", modifiers: [.function] 1915 - ) 1916 - case "home": 1917 - return CLIKeySpec( 1918 - keyCode: 115, characters: "\u{F729}", charactersIgnoringModifiers: "\u{F729}", modifiers: [.function] 1919 - ) 1920 - case "end": 1921 - return CLIKeySpec( 1922 - keyCode: 119, characters: "\u{F72B}", charactersIgnoringModifiers: "\u{F72B}", modifiers: [.function] 1923 - ) 1924 - case "ctrl-c": 1925 - return CLIKeySpec(keyCode: 8, characters: "\u{3}", charactersIgnoringModifiers: "c", modifiers: [.control]) 1926 - case "ctrl-d": 1927 - return CLIKeySpec(keyCode: 2, characters: "\u{4}", charactersIgnoringModifiers: "d", modifiers: [.control]) 1928 - case "ctrl-l": 1929 - return CLIKeySpec(keyCode: 37, characters: "\u{C}", charactersIgnoringModifiers: "l", modifiers: [.control]) 2015 + case "space": return " " 2016 + case "minus": return "-" 2017 + case "equal": return "=" 2018 + case "comma": return "," 2019 + case "period": return "." 2020 + case "slash": return "/" 2021 + case "backslash": return "\\" 2022 + case "semicolon": return ";" 2023 + case "quote": return "'" 2024 + case "grave": return "`" 1930 2025 default: 1931 - return nil 2026 + guard token.count == 1 else { return nil } 2027 + return token.first 2028 + } 2029 + } 2030 + 2031 + private static func printableKeyCode(for token: String) -> UInt16? { 2032 + switch token { 2033 + case "a": return UInt16(kVK_ANSI_A) 2034 + case "b": return UInt16(kVK_ANSI_B) 2035 + case "c": return UInt16(kVK_ANSI_C) 2036 + case "d": return UInt16(kVK_ANSI_D) 2037 + case "e": return UInt16(kVK_ANSI_E) 2038 + case "f": return UInt16(kVK_ANSI_F) 2039 + case "g": return UInt16(kVK_ANSI_G) 2040 + case "h": return UInt16(kVK_ANSI_H) 2041 + case "i": return UInt16(kVK_ANSI_I) 2042 + case "j": return UInt16(kVK_ANSI_J) 2043 + case "k": return UInt16(kVK_ANSI_K) 2044 + case "l": return UInt16(kVK_ANSI_L) 2045 + case "m": return UInt16(kVK_ANSI_M) 2046 + case "n": return UInt16(kVK_ANSI_N) 2047 + case "o": return UInt16(kVK_ANSI_O) 2048 + case "p": return UInt16(kVK_ANSI_P) 2049 + case "q": return UInt16(kVK_ANSI_Q) 2050 + case "r": return UInt16(kVK_ANSI_R) 2051 + case "s": return UInt16(kVK_ANSI_S) 2052 + case "t": return UInt16(kVK_ANSI_T) 2053 + case "u": return UInt16(kVK_ANSI_U) 2054 + case "v": return UInt16(kVK_ANSI_V) 2055 + case "w": return UInt16(kVK_ANSI_W) 2056 + case "x": return UInt16(kVK_ANSI_X) 2057 + case "y": return UInt16(kVK_ANSI_Y) 2058 + case "z": return UInt16(kVK_ANSI_Z) 2059 + case "0": return UInt16(kVK_ANSI_0) 2060 + case "1": return UInt16(kVK_ANSI_1) 2061 + case "2": return UInt16(kVK_ANSI_2) 2062 + case "3": return UInt16(kVK_ANSI_3) 2063 + case "4": return UInt16(kVK_ANSI_4) 2064 + case "5": return UInt16(kVK_ANSI_5) 2065 + case "6": return UInt16(kVK_ANSI_6) 2066 + case "7": return UInt16(kVK_ANSI_7) 2067 + case "8": return UInt16(kVK_ANSI_8) 2068 + case "9": return UInt16(kVK_ANSI_9) 2069 + case "space": return UInt16(kVK_Space) 2070 + case "minus": return UInt16(kVK_ANSI_Minus) 2071 + case "equal": return UInt16(kVK_ANSI_Equal) 2072 + case ",", "comma": return UInt16(kVK_ANSI_Comma) 2073 + case ".", "period": return UInt16(kVK_ANSI_Period) 2074 + case "/", "slash": return UInt16(kVK_ANSI_Slash) 2075 + case "\\", "backslash": return UInt16(kVK_ANSI_Backslash) 2076 + case ";", "semicolon": return UInt16(kVK_ANSI_Semicolon) 2077 + case "'", "quote": return UInt16(kVK_ANSI_Quote) 2078 + case "`", "grave": return UInt16(kVK_ANSI_Grave) 2079 + case "[": return UInt16(kVK_ANSI_LeftBracket) 2080 + case "]": return UInt16(kVK_ANSI_RightBracket) 2081 + default: return nil 2082 + } 2083 + } 2084 + 2085 + private static func shiftedCharacters(for token: String) -> String? { 2086 + if token.count == 1, let character = token.first, character.isLetter { 2087 + return String(character).uppercased() 2088 + } 2089 + 2090 + switch token { 2091 + case "1": return "!" 2092 + case "2": return "@" 2093 + case "3": return "#" 2094 + case "4": return "$" 2095 + case "5": return "%" 2096 + case "6": return "^" 2097 + case "7": return "&" 2098 + case "8": return "*" 2099 + case "9": return "(" 2100 + case "0": return ")" 2101 + case "minus": return "_" 2102 + case "equal": return "+" 2103 + case ",", "comma": return "<" 2104 + case ".", "period": return ">" 2105 + case "/", "slash": return "?" 2106 + case "\\", "backslash": return "|" 2107 + case ";", "semicolon": return ":" 2108 + case "'", "quote": return "\"" 2109 + case "`", "grave": return "~" 2110 + case "[": return "{" 2111 + case "]": return "}" 2112 + default: return nil 2113 + } 2114 + } 2115 + 2116 + private static func controlCharacter(for token: String) -> String? { 2117 + if token.count == 1, let scalar = token.unicodeScalars.first, scalar.properties.isAlphabetic { 2118 + return String(UnicodeScalar(scalar.value & 0x1F)!) 2119 + } 2120 + 2121 + switch token { 2122 + case "[": return String(UnicodeScalar(27)!) 2123 + case "\\", "backslash": return String(UnicodeScalar(28)!) 2124 + case "]": return String(UnicodeScalar(29)!) 2125 + case "6": return String(UnicodeScalar(30)!) 2126 + case "minus": return String(UnicodeScalar(31)!) 2127 + default: return nil 1932 2128 } 1933 2129 } 1934 2130 }
+1 -1
supacodeTests/CLIKeyCommandHandlerTests.swift
··· 238 238 @Test func unsupportedKeyReturnsError() async throws { 239 239 let handler = Self.makeHandler() 240 240 let response = await handler.handle( 241 - envelope: Self.makeEnvelope(token: "ctrl-z") 241 + envelope: Self.makeEnvelope(token: "hyper-k") 242 242 ) 243 243 244 244 #expect(response.ok == false)
+57
supacodeTests/CLIKeyTokenExpansionTests.swift
··· 1 + import AppKit 2 + import Carbon 3 + import Testing 4 + 5 + @testable import supacode 6 + 7 + struct CLIKeyTokenExpansionTests { 8 + @Test func normalizesExpandedModifierCombos() { 9 + #expect(KeyTokens.normalize("cmd-c") == "cmd-c") 10 + #expect(KeyTokens.normalize("command-shift-k") == "cmd-shift-k") 11 + #expect(KeyTokens.normalize("alt-enter") == "opt-enter") 12 + #expect(KeyTokens.normalize("ctrl-z") == "ctrl-z") 13 + } 14 + 15 + @Test func normalizesAdditionalNamedKeys() { 16 + #expect(KeyTokens.normalize("deleteforward") == "delete-forward") 17 + #expect(KeyTokens.normalize("forward-delete") == "delete-forward") 18 + #expect(KeyTokens.normalize("ins") == "insert") 19 + #expect(KeyTokens.normalize("f12") == "f12") 20 + } 21 + 22 + @Test func expandedCategoriesAreReported() { 23 + #expect(KeyTokens.category(for: "cmd-c") == .shortcut) 24 + #expect(KeyTokens.category(for: "ctrl-z") == .control) 25 + #expect(KeyTokens.category(for: "f12") == .function) 26 + } 27 + 28 + @Test func cliKeySpecBuildsCommandShortcutEvent() throws { 29 + let spec = try #require(CLIKeySpec.from(token: "cmd-c")) 30 + 31 + #expect(spec.keyCode == UInt16(kVK_ANSI_C)) 32 + #expect(spec.modifiers == [.command]) 33 + #expect(spec.characters == "c") 34 + #expect(spec.charactersIgnoringModifiers == "c") 35 + } 36 + 37 + @Test func cliKeySpecBuildsShiftedShortcutEvent() throws { 38 + let spec = try #require(CLIKeySpec.from(token: "cmd-shift-k")) 39 + 40 + #expect(spec.keyCode == UInt16(kVK_ANSI_K)) 41 + #expect(spec.modifiers == [.command, .shift]) 42 + #expect(spec.characters == "K") 43 + #expect(spec.charactersIgnoringModifiers == "k") 44 + } 45 + 46 + @Test func cliKeySpecBuildsFunctionAndForwardDeleteEvents() throws { 47 + let f12 = try #require(CLIKeySpec.from(token: "f12")) 48 + #expect(f12.keyCode == UInt16(kVK_F12)) 49 + #expect(f12.modifiers == [.function]) 50 + #expect(f12.characters == String(UnicodeScalar(NSF12FunctionKey)!)) 51 + 52 + let deleteForward = try #require(CLIKeySpec.from(token: "delete-forward")) 53 + #expect(deleteForward.keyCode == UInt16(kVK_ForwardDelete)) 54 + #expect(deleteForward.modifiers == [.function]) 55 + #expect(deleteForward.characters == String(UnicodeScalar(NSDeleteFunctionKey)!)) 56 + } 57 + }