native macOS codings agent orchestrator
5
fork

Configure Feed

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

Align CLI key tokens with ANSI event support

onevpaw 4edb3d55 04cdf259

+114 -33
+4 -1
ProwlCLI/Commands/KeyCommand.swift
··· 7 7 struct KeyCommand: ParsableCommand { 8 8 static let configuration = CommandConfiguration( 9 9 commandName: "key", 10 - abstract: "Send a user key event to a terminal pane.", 10 + abstract: "Send a keyboard shortcut or special key 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 14 14 the second is the key token. 15 + 16 + Printable punctuation currently follows ANSI-style key token semantics. 17 + For shifted symbols, prefer modifier combos such as shift-1 or shift-left-bracket. 15 18 """ 16 19 ) 17 20
+11
ProwlCLITests/ProwlCLIIntegrationTests.swift
··· 975 975 ("ctrl-z", "ctrl-z"), 976 976 ("deleteforward", "delete-forward"), 977 977 ("f12", "f12"), 978 + ("[", "left-bracket"), 978 979 ] 979 980 980 981 for (raw, normalized) in tokenCases { ··· 1000 1001 XCTFail("Expected key command envelope for token '\(raw)'") 1001 1002 } 1002 1003 } 1004 + } 1005 + 1006 + func testKeyCommandRejectsUnsupportedShiftedSymbolLiteral() throws { 1007 + let result = try runProwl(args: ["key", "!", "--json"]) 1008 + XCTAssertNotEqual(result.exitCode, 0) 1009 + let payload = try jsonObject(from: result.stdout) 1010 + XCTAssertEqual(payload["ok"] as? Bool, false) 1011 + XCTAssertEqual(payload["command"] as? String, "key") 1012 + let error = try XCTUnwrap(payload["error"] as? [String: Any]) 1013 + XCTAssertEqual(error["code"] as? String, CLIErrorCode.unsupportedKey) 1003 1014 } 1004 1015 1005 1016 // MARK: - Read command tests
+11 -1
doc-onevcat/contracts/cli/key.md
··· 13 13 - normalize aliases (`return` -> `enter`, `escape` -> `esc`, `pgup` -> `pageup`, ...) 14 14 - accept modifier prefixes and combinations such as `cmd-c`, `shift-tab`, `opt-enter`, `cmd-shift-k` 15 15 - accept additional named keys such as `delete-forward`, `insert`, and `f1`...`f12` 16 + - keep CLI token acceptance aligned with the runtime's ANSI-style NSEvent materialization 16 17 - add `--repeat <n>` as first-class input instead of forcing shell loops 17 18 - split `requested` vs `normalized` in output so callers can debug token normalization 18 19 - add `delivery` counters for machine verification (`attempted` / `delivered`) ··· 21 22 ## Contract goals 22 23 23 24 - `key` should be deterministic for agent loops and TUI automation. 25 + - v1 follows ANSI-style key token semantics for shortcuts, special keys, and supported punctuation. 26 + - full layout-aware fidelity across keyboard layouts is out of scope for v1. 24 27 - output must clearly answer: 25 28 - what was requested 26 29 - what token was actually accepted ··· 66 69 67 70 - navigation keys such as `tab`, `up`, `down`, `left`, `right`, `pageup`, `pagedown`, `home`, `end` 68 71 - editing keys such as `enter`, `backspace`, `delete-forward`, `insert` 69 - - printable keys such as letters, digits, and common punctuation 72 + - printable keys such as letters, digits, and supported ANSI punctuation tokens 70 73 - control combinations such as `ctrl-c`, `ctrl-d`, `ctrl-l`, `ctrl-z` 71 74 - shortcut combinations such as `cmd-c`, `cmd-shift-k`, `shift-tab`, `opt-enter` 72 75 - function keys `f1`...`f12` ··· 83 86 - `pgdn` -> `pagedown` 84 87 - `forward-delete` / `deleteforward` -> `delete-forward` 85 88 - `ins` -> `insert` 89 + - punctuation aliases such as `[` -> `left-bracket`, `]` -> `right-bracket`, `,` -> `comma`, `'` -> `quote` 86 90 - `command-*` -> `cmd-*` 87 91 - `alt-*` / `option-*` -> `opt-*` 88 92 - `ctrl+*` -> `ctrl-*` 93 + 94 + ### ANSI punctuation note 95 + 96 + - prefer canonical ANSI-style punctuation tokens such as `minus`, `equal`, `comma`, `period`, `slash`, `backslash`, `quote`, `left-bracket` 97 + - express shifted symbols via modifier combos such as `shift-1`, `shift-quote`, `shift-left-bracket` 98 + - raw shifted symbol literals such as `!`, `@`, `{`, `}` are intentionally unsupported in v1 89 99 90 100 ### `--repeat` 91 101
+55 -10
supacode/CLIService/Shared/KeyCommandPayload.swift
··· 219 219 "apostrophe": KeyBaseDescriptor(canonical: "quote", category: .editing), 220 220 "grave": KeyBaseDescriptor(canonical: "grave", category: .editing), 221 221 "backtick": KeyBaseDescriptor(canonical: "grave", category: .editing), 222 - "left-bracket": KeyBaseDescriptor(canonical: "[", category: .editing), 223 - "leftbracket": KeyBaseDescriptor(canonical: "[", category: .editing), 224 - "lbracket": KeyBaseDescriptor(canonical: "[", category: .editing), 225 - "right-bracket": KeyBaseDescriptor(canonical: "]", category: .editing), 226 - "rightbracket": KeyBaseDescriptor(canonical: "]", category: .editing), 227 - "rbracket": KeyBaseDescriptor(canonical: "]", category: .editing), 222 + "left-bracket": KeyBaseDescriptor(canonical: "left-bracket", category: .editing), 223 + "leftbracket": KeyBaseDescriptor(canonical: "left-bracket", category: .editing), 224 + "lbracket": KeyBaseDescriptor(canonical: "left-bracket", category: .editing), 225 + "right-bracket": KeyBaseDescriptor(canonical: "right-bracket", category: .editing), 226 + "rightbracket": KeyBaseDescriptor(canonical: "right-bracket", category: .editing), 227 + "rbracket": KeyBaseDescriptor(canonical: "right-bracket", category: .editing), 228 + ] 229 + 230 + private static let singleCharacterBaseDescriptors: [String: KeyBaseDescriptor] = [ 231 + "a": KeyBaseDescriptor(canonical: "a", category: .editing), 232 + "b": KeyBaseDescriptor(canonical: "b", category: .editing), 233 + "c": KeyBaseDescriptor(canonical: "c", category: .editing), 234 + "d": KeyBaseDescriptor(canonical: "d", category: .editing), 235 + "e": KeyBaseDescriptor(canonical: "e", category: .editing), 236 + "f": KeyBaseDescriptor(canonical: "f", category: .editing), 237 + "g": KeyBaseDescriptor(canonical: "g", category: .editing), 238 + "h": KeyBaseDescriptor(canonical: "h", category: .editing), 239 + "i": KeyBaseDescriptor(canonical: "i", category: .editing), 240 + "j": KeyBaseDescriptor(canonical: "j", category: .editing), 241 + "k": KeyBaseDescriptor(canonical: "k", category: .editing), 242 + "l": KeyBaseDescriptor(canonical: "l", category: .editing), 243 + "m": KeyBaseDescriptor(canonical: "m", category: .editing), 244 + "n": KeyBaseDescriptor(canonical: "n", category: .editing), 245 + "o": KeyBaseDescriptor(canonical: "o", category: .editing), 246 + "p": KeyBaseDescriptor(canonical: "p", category: .editing), 247 + "q": KeyBaseDescriptor(canonical: "q", category: .editing), 248 + "r": KeyBaseDescriptor(canonical: "r", category: .editing), 249 + "s": KeyBaseDescriptor(canonical: "s", category: .editing), 250 + "t": KeyBaseDescriptor(canonical: "t", category: .editing), 251 + "u": KeyBaseDescriptor(canonical: "u", category: .editing), 252 + "v": KeyBaseDescriptor(canonical: "v", category: .editing), 253 + "w": KeyBaseDescriptor(canonical: "w", category: .editing), 254 + "x": KeyBaseDescriptor(canonical: "x", category: .editing), 255 + "y": KeyBaseDescriptor(canonical: "y", category: .editing), 256 + "z": KeyBaseDescriptor(canonical: "z", category: .editing), 257 + "0": KeyBaseDescriptor(canonical: "0", category: .editing), 258 + "1": KeyBaseDescriptor(canonical: "1", category: .editing), 259 + "2": KeyBaseDescriptor(canonical: "2", category: .editing), 260 + "3": KeyBaseDescriptor(canonical: "3", category: .editing), 261 + "4": KeyBaseDescriptor(canonical: "4", category: .editing), 262 + "5": KeyBaseDescriptor(canonical: "5", category: .editing), 263 + "6": KeyBaseDescriptor(canonical: "6", category: .editing), 264 + "7": KeyBaseDescriptor(canonical: "7", category: .editing), 265 + "8": KeyBaseDescriptor(canonical: "8", category: .editing), 266 + "9": KeyBaseDescriptor(canonical: "9", category: .editing), 267 + ",": KeyBaseDescriptor(canonical: "comma", category: .editing), 268 + ".": KeyBaseDescriptor(canonical: "period", category: .editing), 269 + "/": KeyBaseDescriptor(canonical: "slash", category: .editing), 270 + "\\": KeyBaseDescriptor(canonical: "backslash", category: .editing), 271 + ";": KeyBaseDescriptor(canonical: "semicolon", category: .editing), 272 + "'": KeyBaseDescriptor(canonical: "quote", category: .editing), 273 + "`": KeyBaseDescriptor(canonical: "grave", category: .editing), 274 + "[": KeyBaseDescriptor(canonical: "left-bracket", category: .editing), 275 + "]": KeyBaseDescriptor(canonical: "right-bracket", category: .editing), 228 276 ] 229 277 230 278 public static func normalize(_ raw: String) -> String? { ··· 290 338 291 339 private static func baseDescriptor(for raw: String) -> KeyBaseDescriptor? { 292 340 if let base = namedBaseDescriptors[raw] { return base } 293 - 294 - if raw.count == 1, let scalar = raw.unicodeScalars.first, scalar.isASCII, !scalar.properties.isWhitespace { 295 - return KeyBaseDescriptor(canonical: raw, category: .editing) 296 - } 341 + if let base = singleCharacterBaseDescriptors[raw] { return base } 297 342 298 343 if raw.count >= 2, 299 344 raw.first == "f",
+10 -21
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 2042 2042 "space": UInt16(kVK_Space), 2043 2043 "minus": UInt16(kVK_ANSI_Minus), 2044 2044 "equal": UInt16(kVK_ANSI_Equal), 2045 - ",": UInt16(kVK_ANSI_Comma), 2046 2045 "comma": UInt16(kVK_ANSI_Comma), 2047 - ".": UInt16(kVK_ANSI_Period), 2048 2046 "period": UInt16(kVK_ANSI_Period), 2049 - "/": UInt16(kVK_ANSI_Slash), 2050 2047 "slash": UInt16(kVK_ANSI_Slash), 2051 - "\\": UInt16(kVK_ANSI_Backslash), 2052 2048 "backslash": UInt16(kVK_ANSI_Backslash), 2053 - ";": UInt16(kVK_ANSI_Semicolon), 2054 2049 "semicolon": UInt16(kVK_ANSI_Semicolon), 2055 - "'": UInt16(kVK_ANSI_Quote), 2056 2050 "quote": UInt16(kVK_ANSI_Quote), 2057 - "`": UInt16(kVK_ANSI_Grave), 2058 2051 "grave": UInt16(kVK_ANSI_Grave), 2059 - "[": UInt16(kVK_ANSI_LeftBracket), 2060 - "]": UInt16(kVK_ANSI_RightBracket), 2052 + "left-bracket": UInt16(kVK_ANSI_LeftBracket), 2053 + "right-bracket": UInt16(kVK_ANSI_RightBracket), 2061 2054 ] 2062 2055 2063 2056 private static let shiftedCharacterMap: [String: String] = [ ··· 2073 2066 "0": ")", 2074 2067 "minus": "_", 2075 2068 "equal": "+", 2076 - ",": "<", 2077 2069 "comma": "<", 2078 - ".": ">", 2079 2070 "period": ">", 2080 - "/": "?", 2081 2071 "slash": "?", 2082 - "\\": "|", 2083 2072 "backslash": "|", 2084 - ";": ":", 2085 2073 "semicolon": ":", 2086 - "'": "\"", 2087 2074 "quote": "\"", 2088 - "`": "~", 2089 - "[": "{", 2090 - "]": "}", 2075 + "grave": "~", 2076 + "left-bracket": "{", 2077 + "right-bracket": "}", 2091 2078 ] 2092 2079 2093 2080 private static func eventModifiers(from modifiers: [KeyModifier]) -> NSEvent.ModifierFlags { ··· 2137 2124 case "semicolon": return ";" 2138 2125 case "quote": return "'" 2139 2126 case "grave": return "`" 2127 + case "left-bracket": return "[" 2128 + case "right-bracket": return "]" 2140 2129 default: 2141 2130 guard token.count == 1 else { return nil } 2142 2131 return token.first ··· 2161 2150 } 2162 2151 2163 2152 switch token { 2164 - case "[": return String(UnicodeScalar(27)!) 2165 - case "\\", "backslash": return String(UnicodeScalar(28)!) 2166 - case "]": return String(UnicodeScalar(29)!) 2153 + case "left-bracket": return String(UnicodeScalar(27)!) 2154 + case "backslash": return String(UnicodeScalar(28)!) 2155 + case "right-bracket": return String(UnicodeScalar(29)!) 2167 2156 case "6": return String(UnicodeScalar(30)!) 2168 2157 case "minus": return String(UnicodeScalar(31)!) 2169 2158 default: return nil
+23
supacodeTests/CLIKeyTokenExpansionTests.swift
··· 12 12 #expect(KeyTokens.normalize("ctrl-z") == "ctrl-z") 13 13 } 14 14 15 + @Test func normalizesPrintableAliasesToCanonicalAnsiTokens() { 16 + #expect(KeyTokens.normalize("[") == "left-bracket") 17 + #expect(KeyTokens.normalize("]") == "right-bracket") 18 + #expect(KeyTokens.normalize(",") == "comma") 19 + #expect(KeyTokens.normalize("'") == "quote") 20 + } 21 + 15 22 @Test func normalizesAdditionalNamedKeys() { 16 23 #expect(KeyTokens.normalize("deleteforward") == "delete-forward") 17 24 #expect(KeyTokens.normalize("forward-delete") == "delete-forward") ··· 19 26 #expect(KeyTokens.normalize("f12") == "f12") 20 27 } 21 28 29 + @Test func rejectsUnsupportedShiftedSymbolLiterals() { 30 + #expect(KeyTokens.normalize("!") == nil) 31 + #expect(KeyTokens.normalize("@") == nil) 32 + #expect(CLIKeySpec.from(token: "!") == nil) 33 + #expect(CLIKeySpec.from(token: "@") == nil) 34 + } 35 + 22 36 @Test func expandedCategoriesAreReported() { 23 37 #expect(KeyTokens.category(for: "cmd-c") == .shortcut) 24 38 #expect(KeyTokens.category(for: "ctrl-z") == .control) ··· 41 55 #expect(spec.modifiers == [.command, .shift]) 42 56 #expect(spec.characters == "K") 43 57 #expect(spec.charactersIgnoringModifiers == "k") 58 + } 59 + 60 + @Test func cliKeySpecBuildsShiftedAnsiPunctuationEvent() throws { 61 + let spec = try #require(CLIKeySpec.from(token: "shift-left-bracket")) 62 + 63 + #expect(spec.keyCode == UInt16(kVK_ANSI_LeftBracket)) 64 + #expect(spec.modifiers == [.shift]) 65 + #expect(spec.characters == "{") 66 + #expect(spec.charactersIgnoringModifiers == "[") 44 67 } 45 68 46 69 @Test func cliKeySpecBuildsFunctionAndForwardDeleteEvents() throws {