Skip to content

Commit ec4030b

Browse files
committed
Improved management of multi-char keyboards
This is an alterantive to the proposal in migueldeicaza#418 which was a follow up to migueldeicaza#409 Rather than special casing the keyboards that way, this improves the existing UITextInput handling and it improves the chinese, japanese and vietnamese output - but still fails with Korean. I could not figure out why iOS refuses to let me participate in the composition of text, even if the system seems to do just fine on its own (like native UITextFields). In particular this problem as documented by Nicolas: > Korean input: ㅇ followed by ㅜ produces 우 (as expected), but ㅇ ㅜ ㅇ produces 우ㅇ instead of 웅. So for Korean, I add a dreaded special case.
1 parent 7b49efe commit ec4030b

3 files changed

Lines changed: 167 additions & 43 deletions

File tree

Sources/SwiftTerm/Apple/AppleTerminalView.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,6 @@ extension TerminalView {
728728
let runFont = runAttributes[.font] as! TTFont
729729
let startColumn = segment.column + (processedGlyphs * segment.columnWidth)
730730
let endColumn = startColumn + (runGlyphsCount * segment.columnWidth)
731-
if row == 0 {
732-
print(run)
733-
}
734731
var backgroundColor: TTColor?
735732
if runAttributes.keys.contains(.selectionBackgroundColor) {
736733
backgroundColor = runAttributes[.selectionBackgroundColor] as? TTColor
@@ -1105,7 +1102,6 @@ extension TerminalView {
11051102
let oldPosition = terminal.buffer.yDisp
11061103

11071104
let maxScrollback = terminal.buffer.lines.count - terminal.rows
1108-
print ("maxScrollBack: \(maxScrollback)")
11091105
var newScrollPosition = Int (Double (maxScrollback) * toPosition)
11101106

11111107
if newScrollPosition < 0 {
@@ -1114,8 +1110,7 @@ extension TerminalView {
11141110
if newScrollPosition > maxScrollback {
11151111
newScrollPosition = maxScrollback
11161112
}
1117-
print ("newScrollpsitin: \(newScrollPosition)")
1118-
1113+
11191114
if newScrollPosition != oldPosition {
11201115
scrollTo(row: newScrollPosition)
11211116
}
@@ -1220,6 +1215,13 @@ extension TerminalView {
12201215
public func send(data: ArraySlice<UInt8>)
12211216
{
12221217
ensureCaretIsVisible ()
1218+
#if os(iOS) || os(visionOS)
1219+
if TerminalView.textInputDebugEnabled {
1220+
let previewBytes = data.prefix(32).map { String(format: "%02X", $0) }.joined(separator: " ")
1221+
print("UITextInput[\(TerminalView.textInputLogCounter + 1)]: send bytes=\(data.count) [\(previewBytes)]")
1222+
TerminalView.textInputLogCounter += 1
1223+
}
1224+
#endif
12231225
terminalDelegate?.send (source: self, data: data)
12241226
}
12251227

@@ -1228,6 +1230,12 @@ extension TerminalView {
12281230
* - Parameter txt: the string to send to the client
12291231
*/
12301232
public func send (txt: String) {
1233+
#if os(iOS) || os(visionOS)
1234+
if TerminalView.textInputDebugEnabled {
1235+
print("UITextInput[\(TerminalView.textInputLogCounter + 1)]: send txt=\(txt.debugDescription)")
1236+
TerminalView.textInputLogCounter += 1
1237+
}
1238+
#endif
12311239
let array = [UInt8] (txt.utf8)
12321240
send (data: array[...])
12331241
}

Sources/SwiftTerm/iOS/iOSTerminalView.swift

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ internal var log: Logger = Logger(subsystem: "org.tirania.SwiftTerm", category:
4242
* defaults, otherwise, this uses its own set of defaults colors.
4343
*/
4444
open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollViewDelegate, TerminalDelegate {
45+
public static var textInputDebugEnabled: Bool = false
46+
internal static var textInputLogCounter: Int = 0
47+
4548
struct FontSet {
4649
public let normal: UIFont
4750
let bold: UIFont
@@ -1084,14 +1087,20 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
10841087
}
10851088

10861089
public var hasText: Bool {
1087-
return true
1090+
return !textInputStorage.isEmpty
10881091
}
10891092

10901093
/*
1091-
Soft keyboard input. Hardware keyboard input is handled in pressesBegan.
1094+
Soft keyboard input. Hardware keyboard text input is delivered here; special keys are handled in pressesBegan.
10921095
*/
10931096
open func insertText(_ text: String) {
1094-
uitiLog("insertText(\"\(text)\") textInputStorage:\"\(textInputStorage)\"")
1097+
//uitiLog("insertText(\(text.debugDescription)) \(textInputStateDescription())")
1098+
1099+
if tryComposeKoreanFinal(text) {
1100+
return
1101+
}
1102+
1103+
beginTextInputEdit()
10951104

10961105
let rangeToReplace = _markedTextRange ?? _selectedTextRange
10971106
let rangeStartIndex = rangeToReplace.startPosition.offset
@@ -1100,6 +1109,8 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
11001109
let insertedPosition = TextPosition(offset: rangeStartIndex + text.count)
11011110
_selectedTextRange = TextRange(from: insertedPosition, to: insertedPosition)
11021111

1112+
endTextInputEdit()
1113+
11031114
if terminalAccessory?.controlModifier ?? false {
11041115
self.send(applyControlToEventCharacters(text))
11051116
terminalAccessory?.controlModifier = false
@@ -1115,14 +1126,77 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
11151126
queuePendingDisplay()
11161127
}
11171128

1129+
// this is necessary because something in the iOS IME seems to prevent
1130+
// the sequence "ㅇ", "ㅜ", "ㅇ" from becoming "웅", and instead
1131+
// it becomes "우" followed by "ㅇ"
1132+
private func tryComposeKoreanFinal(_ text: String) -> Bool {
1133+
guard let language = textInputMode?.primaryLanguage, language.hasPrefix("ko") else { return false }
1134+
guard _markedTextRange == nil else { return false }
1135+
guard _selectedTextRange.isEmpty, _selectedTextRange.endPosition.offset == textInputStorage.count else { return false }
1136+
guard text.count == 1, let jamo = text.first else { return false }
1137+
guard let finalIndex = koreanFinalIndex[jamo] else { return false }
1138+
guard let lastChar = textInputStorage.last else { return false }
1139+
guard let composed = composeHangulSyllable(base: lastChar, finalIndex: finalIndex) else { return false }
1140+
1141+
uitiLog("koreanComposeFinal base:\(lastChar) jamo:\(jamo) -> \(composed)")
1142+
1143+
beginTextInputEdit()
1144+
textInputStorage.removeLast()
1145+
textInputStorage.append(composed)
1146+
let newOffset = textInputStorage.count
1147+
_markedTextRange = nil
1148+
_selectedTextRange = TextRange(from: TextPosition(offset: newOffset), to: TextPosition(offset: newOffset))
1149+
endTextInputEdit()
1150+
1151+
send([backspaceSendsControlH ? 8 : 0x7f])
1152+
send(txt: String(composed))
1153+
queuePendingDisplay()
1154+
return true
1155+
}
1156+
1157+
private let koreanFinalIndex: [Character: Int] = [
1158+
"": 1, "": 2, "": 3,
1159+
"": 4, "": 5, "": 6,
1160+
"": 7,
1161+
"": 8, "": 9, "": 10, "": 11, "": 12, "": 13, "": 14, "": 15,
1162+
"": 16,
1163+
"": 17, "": 18,
1164+
"": 19, "": 20,
1165+
"": 21,
1166+
"": 22,
1167+
"": 23,
1168+
"": 24,
1169+
"": 25,
1170+
"": 26,
1171+
"": 27
1172+
]
1173+
1174+
private func composeHangulSyllable(base: Character, finalIndex: Int) -> Character? {
1175+
guard finalIndex > 0 && finalIndex < 28 else { return nil }
1176+
guard let scalar = base.unicodeScalars.first, base.unicodeScalars.count == 1 else { return nil }
1177+
let scalarValue = Int(scalar.value)
1178+
let sBase = 0xAC00
1179+
let sEnd = 0xD7A3
1180+
guard scalarValue >= sBase && scalarValue <= sEnd else { return nil }
1181+
let vCount = 21
1182+
let tCount = 28
1183+
let sIndex = scalarValue - sBase
1184+
let lIndex = sIndex / (vCount * tCount)
1185+
let vIndex = (sIndex % (vCount * tCount)) / tCount
1186+
let tIndex = sIndex % tCount
1187+
guard tIndex == 0 else { return nil }
1188+
let newScalarValue = sBase + (lIndex * vCount + vIndex) * tCount + finalIndex
1189+
guard let newScalar = UnicodeScalar(newScalarValue) else { return nil }
1190+
return Character(newScalar)
1191+
}
1192+
11181193
func ensureCaretIsVisible ()
11191194
{
11201195
contentOffset = CGPoint (x: 0, y: CGFloat (terminal.buffer.lines.count-terminal.rows)*cellDimension.height)
11211196
}
11221197

11231198
public func deleteBackward() {
1124-
uitiLog("deleteBackward() textInputStorage:\"\(textInputStorage)\" markedTextRange:\"\(_markedTextRange)\" selectedTextRange:\"\(_selectedTextRange)\"")
1125-
inputDelegate?.selectionWillChange(self)
1199+
uitiLog("deleteBackward() \(textInputStateDescription())")
11261200

11271201
// after backward deletion, marked range is always cleared, and length of selected range is always zero
11281202
let rangeToDelete = _markedTextRange ?? _selectedTextRange
@@ -1140,12 +1214,15 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
11401214
return
11411215
}
11421216

1217+
beginTextInputEdit()
1218+
11431219
rangeStartIndex -= 1
11441220
textInputStorage.remove(at: textInputStorage.index(textInputStorage.startIndex, offsetBy: rangeStartIndex))
11451221
rangeStartPosition = TextPosition(offset: rangeStartIndex)
11461222

11471223
self.send ([backspaceSendsControlH ? 8 : 0x7f])
11481224
} else {
1225+
beginTextInputEdit()
11491226
// Send as many backspaces that are in the range to delete. When on auto-repeat, after a some time
11501227
// pressing the backspace, it will delete chunks of text at a time.
11511228
let oldText = textInputStorage[rangeToDelete.fullRange(in: textInputStorage)]
@@ -1160,7 +1237,7 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
11601237
_markedTextRange = nil
11611238
_selectedTextRange = TextRange(from: rangeStartPosition, to: rangeStartPosition)
11621239

1163-
inputDelegate?.selectionDidChange(self)
1240+
endTextInputEdit()
11641241
}
11651242

11661243
enum SendData {
@@ -1211,9 +1288,15 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
12111288

12121289
public override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
12131290
var didHandleEvent = false
1291+
1292+
if _markedTextRange != nil {
1293+
super.pressesBegan(presses, with: event)
1294+
return
1295+
}
12141296

12151297
for press in presses {
12161298
guard let key = press.key else { continue }
1299+
uitiLog("pressesBegan keyCode:\(key.keyCode) chars:\(key.characters.debugDescription) ignoring:\(key.charactersIgnoringModifiers.debugDescription) modifiers:\(key.modifierFlags)")
12171300

12181301
var data: SendData? = nil
12191302

@@ -1281,19 +1364,13 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
12811364
case .keyboardDeleteForward:
12821365
data = .bytes (EscapeSequences.cmdDelKey)
12831366

1284-
case .keyboardDeleteOrBackspace:
1285-
data = .bytes ([backspaceSendsControlH ? 8 : 0x7f])
1286-
12871367
case .keyboardEscape:
12881368
data = .bytes ([0x1b])
12891369

12901370
case .keyboardInsert:
12911371
print (".keyboardInsert ignored")
12921372
break
12931373

1294-
case .keyboardReturn:
1295-
data = .bytes (returnByteSequence)
1296-
12971374
case .keyboardTab:
12981375
data = .bytes ([9])
12991376

@@ -1331,16 +1408,6 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
13311408
optionAsMetaKey.toggle()
13321409
} else if key.modifierFlags.contains (.alternate) && optionAsMetaKey {
13331410
data = .text("\u{1b}\(key.charactersIgnoringModifiers)")
1334-
} else if !key.modifierFlags.contains (.command){
1335-
if let keyboardLanguage = self.textInputMode?.primaryLanguage {
1336-
// Is the keyboard language one of the multi-input languages? Chinese, Japanese, Korean and Hindi-Transliteration
1337-
// If so, do not process the input yet (we'll do it later in unmarkText())
1338-
if (!keyboardLanguage.hasPrefix("hi") && !keyboardLanguage.hasPrefix("zh") && !keyboardLanguage.hasPrefix("ja") && !keyboardLanguage.hasPrefix("ko")) {
1339-
if key.characters.count > 0 {
1340-
data = .text (key.characters)
1341-
}
1342-
}
1343-
}
13441411
}
13451412
}
13461413
if let sendableData = data {

0 commit comments

Comments
 (0)