Skip to content

Commit fa9e3cb

Browse files
committed
Update to our keyboard input.
Our improved IME support came with a downside, now things like <space><space> become <space><dot> if the user has turned this on globally on iOS. Which is great for sending messages, but terrible for a Unix shell, or for Python users. This patch covers two problems: that the input accessories went behind the back of our backing store, so it was not aware of the new inserted character (so ps aux<space>|<space> was seem by the IME system as <space><space> replacing the "|" with a ".". The other problem is the more fundamental issue which is that <space><space> should be intercepted to prevent the system from sending a backspace, removing the space and adding the period.
1 parent ed46b4c commit fa9e3cb

3 files changed

Lines changed: 90 additions & 22 deletions

File tree

Sources/SwiftTerm/iOS/iOSAccessoryView.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,21 @@ public class TerminalAccessory: UIInputView, UIInputViewAudioFeedback {
6565
#endif
6666
terminalView?.send (data)
6767
}
68+
69+
func clickAndInsertText (_ text: String)
70+
{
71+
#if os(iOS)
72+
UIDevice.current.playInputClick()
73+
#endif
74+
terminalView?.insertTextFromAccessory(text)
75+
}
6876

6977
@objc func esc (_ sender: AnyObject) { clickAndSend ([0x1b]) }
7078
@objc func tab (_ sender: AnyObject) { clickAndSend ([0x9]) }
71-
@objc func tilde (_ sender: AnyObject) { clickAndSend ([UInt8 (ascii: "~")]) }
72-
@objc func pipe (_ sender: AnyObject) { clickAndSend ([UInt8 (ascii: "|")]) }
73-
@objc func slash (_ sender: AnyObject) { clickAndSend ([UInt8 (ascii: "/")]) }
74-
@objc func dash (_ sender: AnyObject) { clickAndSend ([UInt8 (ascii: "-")]) }
79+
@objc func tilde (_ sender: AnyObject) { clickAndInsertText ("~") }
80+
@objc func pipe (_ sender: AnyObject) { clickAndInsertText ("|") }
81+
@objc func slash (_ sender: AnyObject) { clickAndInsertText ("/") }
82+
@objc func dash (_ sender: AnyObject) { clickAndInsertText ("-") }
7583
@objc func f1 (_ sender: AnyObject) { clickAndSend (EscapeSequences.cmdF[0]) }
7684
@objc func f2 (_ sender: AnyObject) { clickAndSend (EscapeSequences.cmdF[1]) }
7785
@objc func f3 (_ sender: AnyObject) { clickAndSend (EscapeSequences.cmdF[2]) }

Sources/SwiftTerm/iOS/iOSTerminalView.swift

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
173173

174174
// We use this as temporary storage for UITextInput, which we send to the terminal on demand
175175
var textInputStorage: String = ""
176+
var pendingAutoPeriodDeleteWasSpace: Bool = false
176177

177178
// This tracks the marked text, part of the UITextInput protocol, which is used to flag temporary data entry, that might
178179
// be removed afterwards by the input system (input methods will insert approximiations, mark and change on demand)
@@ -1092,11 +1093,39 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
10921093
return !textInputStorage.isEmpty
10931094
}
10941095

1095-
/*
1096-
Soft keyboard input. Hardware keyboard text input is delivered here; special keys are handled in pressesBegan.
1097-
*/
1098-
open func insertText(_ text: String) {
1099-
//uitiLog("insertText(\(text.debugDescription)) \(textInputStateDescription())")
1096+
func normalizedAutoPeriodReplacementText(_ text: String, oldText: Substring, rangeToReplace: TextRange) -> String? {
1097+
if text == ". " && pendingAutoPeriodDeleteWasSpace {
1098+
pendingAutoPeriodDeleteWasSpace = false
1099+
return " "
1100+
}
1101+
guard text == ". " else { return nil }
1102+
guard rangeToReplace.endPosition.offset == textInputStorage.count else { return nil }
1103+
guard oldText.count <= 2 else { return nil }
1104+
guard oldText.allSatisfy({ $0 == " " }) else { return nil }
1105+
if oldText.count == 1 {
1106+
return " "
1107+
}
1108+
return String(oldText)
1109+
}
1110+
1111+
private func normalizedAutoPeriodInsertionText(_ text: String, rangeToReplace: TextRange, hadPendingAutoPeriodDelete: Bool) -> String? {
1112+
guard text == ". " else { return nil }
1113+
if hadPendingAutoPeriodDelete {
1114+
pendingAutoPeriodDeleteWasSpace = false
1115+
return " "
1116+
}
1117+
pendingAutoPeriodDeleteWasSpace = false
1118+
guard rangeToReplace.isEmpty else { return nil }
1119+
guard rangeToReplace.endPosition.offset == textInputStorage.count else { return nil }
1120+
guard textInputStorage.last == " " else { return nil }
1121+
return " "
1122+
}
1123+
1124+
private func commitTextInput(_ text: String, applyControlModifier: Bool) {
1125+
let hadPendingAutoPeriodDelete = pendingAutoPeriodDeleteWasSpace
1126+
if text != ". " {
1127+
pendingAutoPeriodDeleteWasSpace = false
1128+
}
11001129

11011130
if tryComposeKoreanFinal(text) {
11021131
return
@@ -1105,29 +1134,46 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
11051134
beginTextInputEdit()
11061135

11071136
let rangeToReplace = _markedTextRange ?? _selectedTextRange
1137+
var textToInsert = text
1138+
if let normalized = normalizedAutoPeriodInsertionText(text, rangeToReplace: rangeToReplace, hadPendingAutoPeriodDelete: hadPendingAutoPeriodDelete) {
1139+
textToInsert = normalized
1140+
}
1141+
11081142
let rangeStartIndex = rangeToReplace.startPosition.offset
1109-
textInputStorage.replaceSubrange(rangeToReplace.fullRange(in: textInputStorage), with: text)
1143+
textInputStorage.replaceSubrange(rangeToReplace.fullRange(in: textInputStorage), with: textToInsert)
11101144
_markedTextRange = nil
1111-
let insertedPosition = TextPosition(offset: rangeStartIndex + text.count)
1145+
let insertedPosition = TextPosition(offset: rangeStartIndex + textToInsert.count)
11121146
_selectedTextRange = TextRange(from: insertedPosition, to: insertedPosition)
11131147

11141148
endTextInputEdit()
11151149

1116-
if terminalAccessory?.controlModifier ?? false {
1117-
self.send(applyControlToEventCharacters(text))
1150+
if applyControlModifier && (terminalAccessory?.controlModifier ?? false) {
1151+
self.send(applyControlToEventCharacters(textToInsert))
11181152
terminalAccessory?.controlModifier = false
11191153
} else {
1120-
if text == "\n" {
1154+
if textToInsert == "\n" {
11211155
resetInputBuffer()
11221156
self.send(data: returnByteSequence [0...])
11231157
} else {
1124-
self.send(txt: text)
1158+
self.send(txt: textToInsert)
11251159
}
11261160
}
1127-
1161+
11281162
queuePendingDisplay()
11291163
}
11301164

1165+
func insertTextFromAccessory(_ text: String) {
1166+
commitTextInput(text, applyControlModifier: false)
1167+
}
1168+
1169+
/*
1170+
Soft keyboard input. Hardware keyboard text input is delivered here; special keys are handled in pressesBegan.
1171+
*/
1172+
open func insertText(_ text: String) {
1173+
//uitiLog("insertText(\(text.debugDescription)) \(textInputStateDescription())")
1174+
commitTextInput(text, applyControlModifier: true)
1175+
}
1176+
11311177
// this is necessary because something in the iOS IME seems to prevent
11321178
// the sequence "ㅇ", "ㅜ", "ㅇ" from becoming "웅", and instead
11331179
// it becomes "우" followed by "ㅇ"
@@ -1212,6 +1258,7 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
12121258
// This is the case when the user hits backspace, but there is no text in the
12131259
// text input buffer. This happens for example when text has been pasted.
12141260
// In that scenario, we should just send the backspace character to the terminal
1261+
pendingAutoPeriodDeleteWasSpace = false
12151262
self.send ([backspaceSendsControlH ? 8 : 0x7f])
12161263
uitiLog("deleteBackward() no text to delete, sending backspace")
12171264
return
@@ -1220,11 +1267,16 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
12201267
beginTextInputEdit()
12211268

12221269
rangeStartIndex -= 1
1223-
textInputStorage.remove(at: textInputStorage.index(textInputStorage.startIndex, offsetBy: rangeStartIndex))
1270+
let deleteIndex = textInputStorage.index(textInputStorage.startIndex, offsetBy: rangeStartIndex)
1271+
let deletedChar = textInputStorage[deleteIndex]
1272+
let deletingAtEnd = rangeStartPosition.offset == textInputStorage.count
1273+
pendingAutoPeriodDeleteWasSpace = deletingAtEnd && deletedChar == " " && _markedTextRange == nil
1274+
textInputStorage.remove(at: deleteIndex)
12241275
rangeStartPosition = TextPosition(offset: rangeStartIndex)
12251276

12261277
self.send ([backspaceSendsControlH ? 8 : 0x7f])
12271278
} else {
1279+
pendingAutoPeriodDeleteWasSpace = false
12281280
beginTextInputEdit()
12291281
// Send as many backspaces that are in the range to delete. When on auto-repeat, after a some time
12301282
// pressing the backspace, it will delete chunks of text at a time.

Sources/SwiftTerm/iOS/iOSTextInput.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,25 +114,32 @@ extension TerminalView: UITextInput {
114114

115115
// Send the edits to the terminal
116116
// Delete the old by sending as many backspaces as needed
117-
let oldText = textInputStorage [r.fullRange(in: textInputStorage)]
117+
let oldText = textInputStorage[r.fullRange(in: textInputStorage)]
118+
if text != ". " {
119+
pendingAutoPeriodDeleteWasSpace = false
120+
}
121+
var replacementText = text
122+
if let normalized = normalizedAutoPeriodReplacementText(text, oldText: oldText, rangeToReplace: r) {
123+
replacementText = normalized
124+
}
118125
let backspaces = oldText.count
119126
for _ in 0..<backspaces {
120127
self.send ([0x7f])
121128
}
122-
self.send (txt: text)
129+
self.send (txt: replacementText)
123130

124131
let insertionIndex = r.startPosition.offset
125-
textInputStorage.replaceSubrange(r.fullRange(in: textInputStorage), with: text)
132+
textInputStorage.replaceSubrange(r.fullRange(in: textInputStorage), with: replacementText)
126133
if r.endPosition.offset <= _selectedTextRange.startPosition.offset {
127134
let selectionOffset = _selectedTextRange.startPosition.offset - insertionIndex
128-
let newSelectionOffset = selectionOffset - r.length + text.count
135+
let newSelectionOffset = selectionOffset - r.length + replacementText.count
129136
let newSelectionIndex = newSelectionOffset + insertionIndex
130137
_selectedTextRange = TextRange(from: TextPosition(offset:newSelectionIndex),
131138
to: TextPosition(offset: newSelectionIndex + _selectedTextRange.length))
132139
} else if r.startPosition.offset >= _selectedTextRange.endPosition.offset {
133140
// NOOP
134141
} else {
135-
let insertionEndPosition = TextPosition(offset:insertionIndex + text.count)
142+
let insertionEndPosition = TextPosition(offset:insertionIndex + replacementText.count)
136143
_selectedTextRange = TextRange(from: insertionEndPosition, to: insertionEndPosition)
137144
}
138145

@@ -219,6 +226,7 @@ extension TerminalView: UITextInput {
219226
{
220227
uitiLog("resetInputBuffer() from \(loc) \(textInputStateDescription())")
221228
beginTextInputEdit()
229+
pendingAutoPeriodDeleteWasSpace = false
222230
textInputStorage = ""
223231
_selectedTextRange = TextRange (from: TextPosition(offset: 0), to: TextPosition(offset: 0))
224232
_markedTextRange = nil

0 commit comments

Comments
 (0)