Skip to content

Commit ce51984

Browse files
committed
feat: ignore escape sequences from special keys
1 parent 017d922 commit ce51984

File tree

6 files changed

+368
-104
lines changed

6 files changed

+368
-104
lines changed

cli/Sources/Noora/Components/TextPrompt.swift

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct TextPrompt {
1313
let collapseOnAnswer: Bool
1414
let renderer: Rendering
1515
let standardPipelines: StandardPipelines
16+
let keyStrokeListener: KeyStrokeListening
1617
let logger: Logger?
1718
let validationRules: [ValidatableRule]
1819
let validator: InputValidating
@@ -26,39 +27,44 @@ struct TextPrompt {
2627
fatalError("'\(prompt)' can't be prompted in a non-interactive session.")
2728
}
2829

29-
var input = ""
30-
31-
func isReturn(_ character: Character) -> Bool {
32-
#if os(Windows)
33-
return character.unicodeScalars.first?.value == 10 || character.unicodeScalars.first?.value == 13
34-
#else
35-
return character == "\n"
36-
#endif
37-
}
30+
var input = [Character]()
31+
var cursorIndex = 0
3832

3933
terminal.withoutCursor {
40-
render(input: input, errors: errors)
41-
while let character = terminal.readCharacter(), !isReturn(character) {
42-
#if os(Windows)
43-
// Handle Ctrl+C (character code 3)
44-
// On Windows, Ctrl+C generates character code 3
45-
// while "getch" is running it doesn't emit a signal
46-
if character.unicodeScalars.first?.value == 3 {
47-
exit(0)
34+
render(input: String(input), cursorIndex: cursorIndex, errors: errors)
35+
keyStrokeListener.listen(terminal: terminal) { keyStroke in
36+
switch keyStroke {
37+
case .returnKey:
38+
return .abort
39+
case let .printable(character):
40+
input.insert(character, at: cursorIndex)
41+
cursorIndex += 1
42+
case .backspace:
43+
if cursorIndex > 0 {
44+
cursorIndex -= 1
45+
input.remove(at: cursorIndex)
4846
}
49-
50-
let isBackspace = character.unicodeScalars.first?.value == 8 || character.unicodeScalars.first?.value == 127
51-
#else
52-
let isBackspace = character == "\u{08}" || character == "\u{7F}"
53-
#endif
54-
if isBackspace { // Handle Backspace (Delete Last Character)
55-
if !input.isEmpty {
56-
input.removeLast() // Remove last character from input
47+
case .delete:
48+
if cursorIndex < input.count {
49+
input.remove(at: cursorIndex)
50+
}
51+
case .leftArrowKey:
52+
if cursorIndex > 0 {
53+
cursorIndex -= 1
5754
}
58-
} else {
59-
input.append(character)
55+
case .rightArrowKey:
56+
if cursorIndex < input.count {
57+
cursorIndex += 1
58+
}
59+
case .home:
60+
cursorIndex = 0
61+
case .end:
62+
cursorIndex = input.count
63+
default:
64+
return .continue
6065
}
61-
render(input: input)
66+
render(input: String(input), cursorIndex: cursorIndex)
67+
return .continue
6268
}
6369
}
6470

@@ -68,14 +74,14 @@ struct TextPrompt {
6874
if input.isEmpty, let defaultValue {
6975
resolvedInput = defaultValue
7076
} else {
71-
resolvedInput = input
77+
resolvedInput = String(input)
7278
}
7379

7480
let validationResult = validator.validate(input: resolvedInput, rules: validationRules)
7581

7682
switch validationResult {
7783
case .success:
78-
render(input: input, withCursor: false)
84+
render(input: String(input), cursorIndex: cursorIndex, withCursor: false)
7985
case let .failure(error):
8086
return run(errors: error.errors)
8187
}
@@ -87,7 +93,12 @@ struct TextPrompt {
8793
return resolvedInput
8894
}
8995

90-
private func render(input: String, withCursor: Bool = true, errors: [ValidatableError] = []) {
96+
private func render(
97+
input: String,
98+
cursorIndex: Int = 0,
99+
withCursor: Bool = true,
100+
errors: [ValidatableError] = []
101+
) {
91102
let titleOffset = title != nil ? " " : ""
92103

93104
var message = ""
@@ -96,7 +107,14 @@ struct TextPrompt {
96107
.boldIfColoredTerminal(terminal)
97108
}
98109

99-
let inputDisplay = "\(input)\(withCursor ? "" : "")".hexIfColoredTerminal(theme.secondary, terminal)
110+
let inputDisplay: String
111+
if withCursor {
112+
let prefix = String(input.prefix(cursorIndex))
113+
let suffix = String(input.dropFirst(cursorIndex))
114+
inputDisplay = "\(prefix)\(suffix)".hexIfColoredTerminal(theme.secondary, terminal)
115+
} else {
116+
inputDisplay = input.hexIfColoredTerminal(theme.secondary, terminal)
117+
}
100118

101119
message += "\(title != nil ? "\n" : "")\(titleOffset)\(prompt.formatted(theme: theme, terminal: terminal)) \(inputDisplay)"
102120

cli/Sources/Noora/Noora.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ public final class Noora: Noorable {
637637
collapseOnAnswer: collapseOnAnswer,
638638
renderer: renderer,
639639
standardPipelines: standardPipelines,
640+
keyStrokeListener: keyStrokeListener,
640641
logger: logger,
641642
validationRules: validationRules,
642643
validator: validator

0 commit comments

Comments
 (0)