Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,39 +100,42 @@ internal class PosixEventParser(

val cmdStart = s.length - 1

// leading digits
repeat(3) {
if (ch in '0'..'9') {
read() ?: return null
}
}

// modifier
if (ch == ';') {
// Consume all parameter, intermediate, and final bytes of the CSI sequence.
// Final bytes are in the range 0x40..0x7E ('@' to '~'), plus '$' for rxvt.
while (ch.code !in 0x40..0x7E && ch != '$') {
if (ch.code < 0x20) break
read() ?: return null

if (ch in '0'..'9') {
read() ?: return null
}
}

// We buffered enough data, now extract code and modifier from it
val cmd = s.substring(cmdStart)
var match = Regex("""(\d\d?)(?:;(\d))?([~^$])|(\d{3}~)""").matchEntire(cmd)
var match = Regex("""27;(\d+);(\d+)~""").matchEntire(cmd)
if (match != null) {
if (match.groupValues[4].isNotEmpty()) {
code.append(match.groupValues[4])
} else {
code.append(match.groupValues[1] + match.groupValues[3])
modifier = (match.groupValues[2].toIntOrNull() ?: 1) - 1
}
modifier = (match.groupValues[1].toIntOrNull() ?: 1) - 1
name = keycodeToName(match.groupValues[2].toIntOrNull() ?: 0)
} else {
match = Regex("""((\d;)?(\d))?([A-Za-z])""").matchEntire(cmd)
match = Regex("""(\d+)(?:;(\d+))?u""").matchEntire(cmd)
if (match != null) {
code.append(match.groupValues[4])
modifier = (match.groupValues[3].toIntOrNull() ?: 1) - 1
modifier = (match.groupValues[2].toIntOrNull() ?: 1) - 1
name = keycodeToName(match.groupValues[1].toIntOrNull() ?: 0)
} else {
code.append(cmd)
match = Regex("""(\d\d?)(?:;(\d+))?([~^$])|(\d{3}~)""").matchEntire(cmd)
if (match != null) {
if (match.groupValues[4].isNotEmpty()) {
code.append(match.groupValues[4])
} else {
code.append(match.groupValues[1] + match.groupValues[3])
modifier = (match.groupValues[2].toIntOrNull() ?: 1) - 1
}
} else {
match = Regex("""((\d+;)?(\d+))?([A-Za-z])""").matchEntire(cmd)
if (match != null) {
code.append(match.groupValues[4])
modifier = (match.groupValues[3].toIntOrNull() ?: 1) - 1
} else {
code.append(cmd)
}
}
}
}
}
Expand All @@ -142,7 +145,7 @@ internal class PosixEventParser(
alt = (modifier and 10) != 0
shift = (modifier and 1) != 0

when (code.toString()) {
if (name == null) when (code.toString()) {
"[P" -> name = "F1"
"[Q" -> name = "F2"
"[R" -> name = "F3"
Expand Down Expand Up @@ -430,3 +433,15 @@ internal class PosixEventParser(
return readBytesAsUtf8 { readRawByte(timeout) }
}
}

private fun keycodeToName(keycode: Int): String {
return when (keycode) {
13 -> "Enter"
9 -> "Tab"
8, 127 -> "Backspace"
27 -> "Escape"
32 -> " "
in 33..126 -> keycode.toChar().toString()
else -> "Unidentified"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,39 @@ class PosixEventParserTest {
readEvent(parser) shouldBe KeyboardEvent("ArrowUp")
}

@[Test JsName("ghostty_shift_enter")]
fun `ghostty shift enter`() {
parse("$esc[27;2;13~") shouldBe KeyboardEvent("Enter", shift = true)
}

@[Test JsName("kitty_shift_enter")]
fun `kitty shift enter`() {
parse("$esc[13;2u") shouldBe KeyboardEvent("Enter", shift = true)
}

@[Test JsName("sequential_extended_csi_without_byte_leak")]
fun `sequential extended csi without byte leak`() {
val parser = parserFor("$esc[27;2;13~a")
readEvent(parser) shouldBe KeyboardEvent("Enter", shift = true)
readEvent(parser) shouldBe KeyboardEvent("a")
}

@[Test JsName("unsupported_csi_sequence_consumed_atomically")]
fun `unsupported csi sequence consumed atomically`() {
val parser = parserFor("$esc[?2004ha")
readEvent(parser) shouldBe KeyboardEvent("Unidentified")
readEvent(parser) shouldBe KeyboardEvent("a")
}

@[Test JsName("modify_other_keys_modifiers")]
fun `modify other keys modifiers`() = forAll(
row("ctrl+enter", "27;5;13~", KeyboardEvent("Enter", ctrl = true)),
row("alt+enter", "27;3;13~", KeyboardEvent("Enter", alt = true)),
row("shift+tab", "27;2;9~", KeyboardEvent("Tab", shift = true)),
) { _, seq, expected ->
parse("$esc[$seq") shouldBe expected
}

private fun parse(input: String): Any? = readEvent(parserFor(input))

private fun parserFor(input: String): PosixEventParser {
Expand Down