macOS: support decoding the surrogate pair with UnicodeHexInput (#13737)

Not yet a perfect fix for
https://github.com/ghostty-org/ghostty/discussions/13730
This commit is contained in:
Mitchell Hashimoto
2026-08-11 11:03:01 -07:00
committed by GitHub

View File

@@ -198,6 +198,8 @@ extension Ghostty {
// This is set to non-null during keyDown to accumulate insertText contents
private var keyTextAccumulator: [String]?
/// Temporary lead surrogate that's waiting for the trail
private var leadSurrogate: LeadSurrogate?
// True when we've consumed a left mouse-down only to move focus and
// should suppress the matching mouse-up from being reported.
@@ -2069,8 +2071,21 @@ extension Ghostty.SurfaceView: NSTextInputClient {
switch string {
case let v as NSAttributedString:
chars = v.string
case let v as String:
chars = v
case let v as NSString:
if let leadSurrogate = LeadSurrogate(v) {
self.leadSurrogate = leadSurrogate
chars = ""
} else if let trail = TrailSurrogate(v) {
// We ignore trail surrogate without a lead like Terminal.app.
chars = leadSurrogate?.encode(trail: trail) ?? ""
leadSurrogate = nil
} else {
chars = v as String
// Clear whenever other text got inserted.
// Ideally we should encode any adjacent lead and trail surrogate into one,
// but getting the cursor position and reading could be rather expensive to do.
leadSurrogate = nil
}
default:
return
}
@@ -2455,3 +2470,41 @@ class CachedValue<T> {
expiryTask = nil
}
}
/// Check if a UTF16 text is a single lead surrogate character
struct LeadSurrogate {
let char: UTF16Char
init?(_ text: NSString) {
guard text.length == 1 else {
return nil
}
let char = text.character(at: 0)
if UTF16.isLeadSurrogate(char) {
self.char = char
} else {
return nil
}
}
func encode(trail: TrailSurrogate) -> String {
String(decoding: [char, trail.char], as: UTF16.self)
}
}
/// Check if a UTF16 text is a single trail surrogate character
struct TrailSurrogate {
let char: UTF16Char
init?(_ text: NSString) {
guard text.length == 1 else {
return nil
}
let char = text.character(at: 0)
if UTF16.isTrailSurrogate(char) {
self.char = char
} else {
return nil
}
}
}