Support modifier+key combinations in send-key (ctrl+enter, shift+tab, etc.) (#1994)
* Support modifier+key combinations in send-key (ctrl+enter, shift+tab, etc.) The send-key command only supported a few hardcoded ctrl+letter combos and bare special keys. Generic modifier combinations like ctrl+enter (needed for GitHub Copilot CLI submission) returned "Unknown key". Now the parser splits on + and - separators, accumulates modifier flags (ctrl, shift, alt/opt, cmd/super), and resolves the base key via a new keycodeForNamedKey helper that maps named keys (enter, tab, escape, backspace, space, arrow keys) to virtual keycodes. Closes #1990 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix delete key mapping and filter empty parts in modifier parser - Separate "delete" (forward delete, kVK_ForwardDelete) from "backspace" (kVK_Delete) in keycodeForNamedKey. The original mapping had both pointing to kVK_Delete (which is actually Backspace on macOS). - Filter empty strings from split results to reject malformed inputs like "+shift+a" that would produce an empty modifier part. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix single named keys silently failing and remove force unwrap in send-key Single named keys like "space", "up", "delete" were rejected by guard parts.count >= 2 before reaching keycodeForNamedKey(). Now standalone named keys are handled before the modifier-combo path. Also replaced parts.last! with guard let for safe unwrapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ccd84bd578
commit
f3c797ee44
1 changed files with 52 additions and 4 deletions
|
|
@ -13284,6 +13284,23 @@ class TerminalController {
|
|||
}
|
||||
}
|
||||
|
||||
private func keycodeForNamedKey(_ name: String) -> UInt32? {
|
||||
switch name {
|
||||
case "enter", "return": return UInt32(kVK_Return)
|
||||
case "tab": return UInt32(kVK_Tab)
|
||||
case "escape", "esc": return UInt32(kVK_Escape)
|
||||
case "backspace": return UInt32(kVK_Delete)
|
||||
case "delete": return UInt32(kVK_ForwardDelete)
|
||||
case "space": return UInt32(kVK_Space)
|
||||
case "up": return UInt32(kVK_UpArrow)
|
||||
case "down": return UInt32(kVK_DownArrow)
|
||||
case "left": return UInt32(kVK_LeftArrow)
|
||||
case "right": return UInt32(kVK_RightArrow)
|
||||
case "\\": return UInt32(kVK_ANSI_Backslash)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func sendNamedKey(_ surface: ghostty_surface_t, keyName: String) -> Bool {
|
||||
switch keyName.lowercased() {
|
||||
case "ctrl-c", "ctrl+c", "sigint":
|
||||
|
|
@ -13341,12 +13358,43 @@ class TerminalController {
|
|||
sendKeyEvent(surface: surface, keycode: UInt32(kVK_PageDown))
|
||||
return true
|
||||
default:
|
||||
if keyName.lowercased().hasPrefix("ctrl-") || keyName.lowercased().hasPrefix("ctrl+") {
|
||||
let letter = keyName.dropFirst(5)
|
||||
if letter.count == 1, let char = letter.first, let keycode = keycodeForLetter(char) {
|
||||
sendKeyEvent(surface: surface, keycode: keycode, mods: GHOSTTY_MODS_CTRL)
|
||||
// Parse modifier+key combinations (e.g. "ctrl+enter", "shift+tab",
|
||||
// "ctrl+shift+a") or standalone named keys (e.g. "space", "up").
|
||||
// Separators: '+' or '-'.
|
||||
let parts = keyName.lowercased().split(separator: "+").flatMap { $0.split(separator: "-") }.map(String.init).filter { !$0.isEmpty }
|
||||
guard let baseKey = parts.last else { return false }
|
||||
|
||||
// Single named key without modifiers (e.g. "space", "up", "delete")
|
||||
if parts.count == 1 {
|
||||
if let keycode = keycodeForNamedKey(baseKey) {
|
||||
sendKeyEvent(surface: surface, keycode: keycode)
|
||||
return true
|
||||
}
|
||||
if baseKey.count == 1, let char = baseKey.first, let keycode = keycodeForLetter(char) {
|
||||
sendKeyEvent(surface: surface, keycode: keycode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var mods = GHOSTTY_MODS_NONE
|
||||
for mod in parts.dropLast() {
|
||||
switch mod {
|
||||
case "ctrl", "control": mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_CTRL.rawValue)
|
||||
case "shift": mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_SHIFT.rawValue)
|
||||
case "alt", "opt", "option": mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_ALT.rawValue)
|
||||
case "cmd", "command", "super": mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_SUPER.rawValue)
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
if let keycode = keycodeForNamedKey(baseKey) {
|
||||
sendKeyEvent(surface: surface, keycode: keycode, mods: mods)
|
||||
return true
|
||||
}
|
||||
if baseKey.count == 1, let char = baseKey.first, let keycode = keycodeForLetter(char) {
|
||||
sendKeyEvent(surface: surface, keycode: keycode, mods: mods)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue