From 2aa8682c007d9834a19e1eb3c4f78d3e3d65bd1c Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Thu, 21 Nov 2024 23:28:49 -0500 Subject: [PATCH] Parse system theme events (#539) --- mosaic-terminal/api/mosaic-terminal.api | 16 +++++ mosaic-terminal/api/mosaic-terminal.klib.api | 22 +++++++ .../mosaic/terminal/TerminalParser.kt | 37 +++++++++++- .../com/jakewharton/mosaic/terminal/bytes.kt | 2 + .../mosaic/terminal/event/Event.kt | 10 +++- .../TerminalParserCsiSystemThemeEventTest.kt | 58 +++++++++++++++++++ .../src/commonMain/kotlin/example/main.kt | 1 + 7 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt diff --git a/mosaic-terminal/api/mosaic-terminal.api b/mosaic-terminal/api/mosaic-terminal.api index d8b57cb4..4e43403b 100644 --- a/mosaic-terminal/api/mosaic-terminal.api +++ b/mosaic-terminal/api/mosaic-terminal.api @@ -24,6 +24,14 @@ public final class com/jakewharton/mosaic/terminal/event/BracketedPasteEvent : c public fun toString ()Ljava/lang/String; } +public final class com/jakewharton/mosaic/terminal/event/DeviceStatusReportEvent : com/jakewharton/mosaic/terminal/event/Event { + public fun (Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class com/jakewharton/mosaic/terminal/event/Event { } @@ -43,6 +51,14 @@ public final class com/jakewharton/mosaic/terminal/event/PrimaryDeviceAttributes public fun toString ()Ljava/lang/String; } +public final class com/jakewharton/mosaic/terminal/event/SystemThemeEvent : com/jakewharton/mosaic/terminal/event/Event { + public fun (Z)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public final fun isDark ()Z + public fun toString ()Ljava/lang/String; +} + public final class com/jakewharton/mosaic/terminal/event/UnknownEvent : com/jakewharton/mosaic/terminal/event/Event { public fun (Ljava/lang/String;[B)V public fun equals (Ljava/lang/Object;)Z diff --git a/mosaic-terminal/api/mosaic-terminal.klib.api b/mosaic-terminal/api/mosaic-terminal.klib.api index ccb0ff2e..9a5ca41a 100644 --- a/mosaic-terminal/api/mosaic-terminal.klib.api +++ b/mosaic-terminal/api/mosaic-terminal.klib.api @@ -19,6 +19,17 @@ final class com.jakewharton.mosaic.terminal.event/BracketedPasteEvent : com.jake final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/BracketedPasteEvent.toString|toString(){}[0] } +final class com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent|null[0] + constructor (kotlin/String) // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.|(kotlin.String){}[0] + + final val data // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.data|{}data[0] + final fun (): kotlin/String // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.data.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/DeviceStatusReportEvent.toString|toString(){}[0] +} + final class com.jakewharton.mosaic.terminal.event/FocusEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/FocusEvent|null[0] constructor (kotlin/Boolean) // com.jakewharton.mosaic.terminal.event/FocusEvent.|(kotlin.Boolean){}[0] @@ -41,6 +52,17 @@ final class com.jakewharton.mosaic.terminal.event/PrimaryDeviceAttributesEvent : final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/PrimaryDeviceAttributesEvent.toString|toString(){}[0] } +final class com.jakewharton.mosaic.terminal.event/SystemThemeEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/SystemThemeEvent|null[0] + constructor (kotlin/Boolean) // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.|(kotlin.Boolean){}[0] + + final val isDark // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.isDark|{}isDark[0] + final fun (): kotlin/Boolean // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.isDark.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/SystemThemeEvent.toString|toString(){}[0] +} + final class com.jakewharton.mosaic.terminal.event/UnknownEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/UnknownEvent|null[0] constructor (kotlin/String, kotlin/ByteArray) // com.jakewharton.mosaic.terminal.event/UnknownEvent.|(kotlin.String;kotlin.ByteArray){}[0] diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt index 808e1398..bf5dc790 100644 --- a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt @@ -3,7 +3,7 @@ package com.jakewharton.mosaic.terminal import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent import com.jakewharton.mosaic.terminal.event.CodepointEvent import com.jakewharton.mosaic.terminal.event.DecModeReport -import com.jakewharton.mosaic.terminal.event.DeviceStatusReportString +import com.jakewharton.mosaic.terminal.event.DeviceStatusReportEvent import com.jakewharton.mosaic.terminal.event.Event import com.jakewharton.mosaic.terminal.event.FocusEvent import com.jakewharton.mosaic.terminal.event.KeyEscape @@ -11,6 +11,7 @@ import com.jakewharton.mosaic.terminal.event.KittyGraphicsEvent import com.jakewharton.mosaic.terminal.event.MouseEvent import com.jakewharton.mosaic.terminal.event.PrimaryDeviceAttributesEvent import com.jakewharton.mosaic.terminal.event.ResizeEvent +import com.jakewharton.mosaic.terminal.event.SystemThemeEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent private const val BufferSize = 8 * 1024 @@ -333,6 +334,38 @@ public class TerminalParser( ) } + 'n'.code -> { + if (buffer[b3Index].toInt() == '?'.code) { + val delimiter = buffer.indexOfOrDefault(';'.code.toByte(), start + 3, finalIndex, finalIndex) + when (buffer.parseIntDigits(start + 3, delimiter)) { + 997 -> { + if (delimiter + 2 == finalIndex) { + val p2 = buffer[delimiter + 1].toInt() + if (p2 == '1'.code) { + return SystemThemeEvent(isDark = true) + } + if (p2 == '2'.code) { + return SystemThemeEvent(isDark = false) + } + } + return UnknownEvent( + context = "CSI ? 997 ; p2 n sequence has invalid p2", + bytes = buffer.copyOfRange(start, end), + ) + } + else -> { + return DeviceStatusReportEvent( + data = buffer.decodeToString(start + 3, finalIndex), + ) + } + } + } + return UnknownEvent( + context = "CSI .. n sequence without leading ?", + bytes = buffer.copyOfRange(start, end), + ) + } + 'c'.code -> { if (buffer[b3Index].toInt() == '?'.code) { val data = buffer.decodeToString(start + 3, finalIndex) @@ -417,7 +450,7 @@ public class TerminalParser( buffer[start + 2].toInt() == '>'.code && buffer[start + 3].toInt() == '|'.code ) { - DeviceStatusReportString( + DeviceStatusReportEvent( data = buffer.decodeToString(start + 4, stIndex), ) } else { diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/bytes.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/bytes.kt index 79b13a2c..b04e3c52 100644 --- a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/bytes.kt +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/bytes.kt @@ -29,6 +29,8 @@ internal inline fun ByteArray.indexOfFirstOrElse( } internal fun ByteArray.parseIntDigits(start: Int, end: Int): Int { + // TODO This needs an orElse path for parsing failure on non-digits. + var value = 0 for (i in start until end) { value *= 10 diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/event/Event.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/event/Event.kt index eefc535d..52ff88f8 100644 --- a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/event/Event.kt +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/event/Event.kt @@ -84,8 +84,14 @@ public class PrimaryDeviceAttributesEvent( public val data: String, ) : Event -internal data class DeviceStatusReportString( - val data: String, +@Poko +public class DeviceStatusReportEvent( + public val data: String, +) : Event + +@Poko +public class SystemThemeEvent( + public val isDark: Boolean, ) : Event internal data class KittyGraphicsEvent( diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt new file mode 100644 index 00000000..de7e341d --- /dev/null +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt @@ -0,0 +1,58 @@ +package com.jakewharton.mosaic.terminal + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.terminal.event.SystemThemeEvent +import com.jakewharton.mosaic.terminal.event.UnknownEvent +import kotlin.test.AfterTest +import kotlin.test.Test + +@OptIn(ExperimentalStdlibApi::class) +class TerminalParserCsiSystemThemeEventTest { + private val writer = Tty.stdinWriter() + private val parser = TerminalParser(writer.reader, true) + + @AfterTest fun after() { + writer.close() + } + + @Test fun dark() { + writer.writeHex("1b5b3f3939373b316e") + assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = true)) + } + + @Test fun light() { + writer.writeHex("1b5b3f3939373b326e") + assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = false)) + } + + @Test fun missingP2() { + writer.writeHex("1b5b3f3939373b6e") + assertThat(parser.next()).isEqualTo( + UnknownEvent( + context = "CSI ? 997 ; p2 n sequence has invalid p2", + bytes = "1b5b3f3939373b6e".hexToByteArray(), + ), + ) + } + + @Test fun unknownP2() { + writer.writeHex("1b5b3f3939373b346e") + assertThat(parser.next()).isEqualTo( + UnknownEvent( + context = "CSI ? 997 ; p2 n sequence has invalid p2", + bytes = "1b5b3f3939373b346e".hexToByteArray(), + ), + ) + } + + @Test fun tooLongP2() { + writer.writeHex("1b5b3f3939373b31316e") + assertThat(parser.next()).isEqualTo( + UnknownEvent( + context = "CSI ? 997 ; p2 n sequence has invalid p2", + bytes = "1b5b3f3939373b31316e".hexToByteArray(), + ), + ) + } +} diff --git a/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt b/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt index d8906735..1ecc5390 100644 --- a/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt +++ b/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt @@ -75,6 +75,7 @@ private class RawModeEchoCommand : CliktCommand("raw-mode-echo") { } if (all || colorQuery) { print("\u001b[?996n") // Color scheme request + print("\u001b[?2031h") // Color scheme enable } val reader = Tty.stdinReader()