Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 89 additions & 16 deletions Packages/CrowTerminal/Sources/CrowTerminal/GhosttySurfaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ public final class GhosttySurfaceView: NSView {

// MARK: - Keyboard Input

public override func performKeyEquivalent(with event: NSEvent) -> Bool {
guard event.type == .keyDown else { return false }
guard surface != nil else { return false }

// Intercept Ctrl+key and Cmd+key events so macOS doesn't steal them
// from the terminal (e.g., Ctrl+C, Ctrl+/, Ctrl+Enter).
// Cmd+C and Cmd+V are handled inside keyDown explicitly.
if event.modifierFlags.contains(.control) || event.modifierFlags.contains(.command) {
self.keyDown(with: event)
return true
}

return false
}

public override func keyDown(with event: NSEvent) {
guard let surface else { return }

Expand All @@ -196,21 +211,48 @@ public final class GhosttySurfaceView: NSView {

// Send key press to libghostty
var key = ghostty_input_key_s()
key.action = GHOSTTY_ACTION_PRESS
key.action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
key.mods = translateMods(event.modifierFlags)
key.keycode = UInt32(event.keyCode)
key.composing = false

// Only set text for printable characters (not function keys, arrows, etc.)
// macOS uses Unicode private use area F700-F7FF for function keys
let isPrintable: Bool
if let chars = event.characters, let scalar = chars.unicodeScalars.first {
isPrintable = scalar.value < 0xF700 && !event.modifierFlags.contains(.command)
} else {
isPrintable = false
// consumed_mods: control and command never contribute to text translation on macOS
key.consumed_mods = translateMods(event.modifierFlags.subtracting([.control, .command]))

// unshifted_codepoint: the base character with no modifiers applied
if event.type == .keyDown || event.type == .keyUp {
if let chars = event.characters(byApplyingModifiers: []),
let codepoint = chars.unicodeScalars.first {
key.unshifted_codepoint = codepoint.value
}
}

if isPrintable, let chars = event.characters {
chars.withCString { ptr in
// Determine the text to send with the key event.
// Control characters (< 0x20) are NOT sent as text — Ghostty handles
// control-character mapping internally. For control chars, we send the
// character with the control modifier stripped so Ghostty knows which key
// was pressed. Function keys in the PUA range (F700-F8FF) are also excluded.
let text: String? = {
guard let characters = event.characters else { return nil }
if characters.count == 1, let scalar = characters.unicodeScalars.first {
if scalar.value < 0x20 {
// Control character — return the un-control'd character
return event.characters(byApplyingModifiers: event.modifierFlags.subtracting(.control))
}
if scalar.value >= 0xF700 && scalar.value <= 0xF8FF {
// Function key in Private Use Area — no text
return nil
}
}
// Don't send text for command-modified keys
if event.modifierFlags.contains(.command) { return nil }
return characters
}()

// Only send text if the first codepoint is printable (>= 0x20)
if let text, text.count > 0,
let codepoint = text.utf8.first, codepoint >= 0x20 {
text.withCString { ptr in
key.text = ptr
let handled = ghostty_surface_key(surface, key)
if !handled {
Expand Down Expand Up @@ -259,6 +301,31 @@ public final class GhosttySurfaceView: NSView {
ghostty_surface_key(surface, key)
}

public override func flagsChanged(with event: NSEvent) {
guard let surface else { return }

let mod: UInt32
switch event.keyCode {
case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue
case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue
case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
default: return
}

let mods = translateMods(event.modifierFlags)
let action: ghostty_input_action_e = (mods.rawValue & mod != 0)
? GHOSTTY_ACTION_PRESS
: GHOSTTY_ACTION_RELEASE

var key = ghostty_input_key_s()
key.action = action
key.mods = mods
key.keycode = UInt32(event.keyCode)
ghostty_surface_key(surface, key)
}

// MARK: - Mouse Input

public override func mouseDown(with event: NSEvent) {
Expand Down Expand Up @@ -370,12 +437,18 @@ public final class GhosttySurfaceView: NSView {
// MARK: - Helpers

private func translateMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e {
var mods = ghostty_input_mods_e(rawValue: 0)
if flags.contains(.shift) { mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_SHIFT.rawValue) }
if flags.contains(.control) { mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_CTRL.rawValue) }
if flags.contains(.option) { mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_ALT.rawValue) }
if flags.contains(.command) { mods = ghostty_input_mods_e(rawValue: mods.rawValue | GHOSTTY_MODS_SUPER.rawValue) }
return mods
var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue
if flags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue }
if flags.contains(.control) { mods |= GHOSTTY_MODS_CTRL.rawValue }
if flags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue }
if flags.contains(.command) { mods |= GHOSTTY_MODS_SUPER.rawValue }
if flags.contains(.capsLock) { mods |= GHOSTTY_MODS_CAPS.rawValue }
let raw = flags.rawValue
if raw & UInt(NX_DEVICERSHIFTKEYMASK) != 0 { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue }
if raw & UInt(NX_DEVICERCTLKEYMASK) != 0 { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue }
if raw & UInt(NX_DEVICERALTKEYMASK) != 0 { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue }
if raw & UInt(NX_DEVICERCMDKEYMASK) != 0 { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue }
return ghostty_input_mods_e(rawValue: mods)
}

// MARK: - Cleanup
Expand Down
Loading