From fe34cd2f29eeaddbabc8dc31aa84e938b70c8032 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Fri, 24 Jan 2025 00:25:38 -0500 Subject: [PATCH] Rewrite native input handling This will allow out-of-band events to come in, such as a resize from SIGWINCH, or focus and resize from Windows' console events in a future change. --- mosaic-terminal/api/mosaic-terminal.api | 26 ++- mosaic-terminal/api/mosaic-terminal.klib.api | 44 ++-- mosaic-terminal/build.gradle | 8 +- mosaic-terminal/src/c/cutils.h | 2 + mosaic-terminal/src/c/mosaic-stdin-posix.c | 29 ++- mosaic-terminal/src/c/mosaic-stdin-windows.c | 41 +++- mosaic-terminal/src/c/mosaic.h | 24 +- .../mosaic/terminal/PlatformEventHandler.kt | 26 +++ .../{TerminalParser.kt => TerminalReader.kt} | 115 ++++++---- .../jakewharton/mosaic/terminal/TestApi.kt | 9 + .../com/jakewharton/mosaic/terminal/Tty.kt | 30 ++- .../mosaic/terminal/event/Event.kt | 7 + .../mosaic/terminal/BaseTerminalParserTest.kt | 22 +- .../mosaic/terminal/StdinReaderTest.kt | 2 +- ...erminalParserCsiBracketedPasteEventTest.kt | 5 +- ...TerminalParserCsiDecModeReportEventTest.kt | 21 +- .../TerminalParserCsiFocusEventTest.kt | 5 +- .../TerminalParserCsiKeyboardEventTest.kt | 37 ++-- ...TerminalParserCsiKittyKeyboardEventTest.kt | 11 +- ...nalParserCsiKittyKeyboardQueryEventTest.kt | 11 +- .../TerminalParserCsiMouseEventTest.kt | 35 +-- ...rserCsiOperatingStatusResponseEventTest.kt | 9 +- ...rserCsiPrimaryDeviceAttributesEventTest.kt | 7 +- .../TerminalParserCsiResizeEventTest.kt | 15 +- .../TerminalParserCsiSystemThemeEventTest.kt | 13 +- ...nalParserCsiXtermCharacterSizeEventTest.kt | 7 +- ...erminalParserCsiXtermPixelSizeEventTest.kt | 7 +- ...rminalParserDcsTerminalVersionEventTest.kt | 5 +- .../TerminalParserGroundKeyboardEventTest.kt | 71 +++--- ...inalParserOscKittyPointerQueryEventTest.kt | 21 +- .../TerminalParserSs3KeyboardEventTest.kt | 25 ++- mosaic-terminal/src/jvmMain/jni/mosaic-jni.c | 205 ++++++++++++++++-- .../com/jakewharton/mosaic/terminal/Jni.kt | 28 ++- .../com/jakewharton/mosaic/terminal/Tty.kt | 92 ++++++-- .../com.jakewharton.mosaic-terminal.pro | 8 + .../com/jakewharton/mosaic/terminal/Tty.kt | 148 ++++++++++--- .../src/commonMain/kotlin/example/main.kt | 69 ++---- 37 files changed, 888 insertions(+), 352 deletions(-) create mode 100644 mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/PlatformEventHandler.kt rename mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/{TerminalParser.kt => TerminalReader.kt} (91%) create mode 100644 mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TestApi.kt diff --git a/mosaic-terminal/api/mosaic-terminal.api b/mosaic-terminal/api/mosaic-terminal.api index 753e3cd4..27bde10a 100644 --- a/mosaic-terminal/api/mosaic-terminal.api +++ b/mosaic-terminal/api/mosaic-terminal.api @@ -1,16 +1,10 @@ -public final class com/jakewharton/mosaic/terminal/StdinReader : java/lang/AutoCloseable { +public final class com/jakewharton/mosaic/terminal/TerminalReader : java/lang/AutoCloseable { public fun close ()V - public final fun interrupt ()V - public final fun read ([BII)I - public final fun readWithTimeout ([BIII)I -} - -public final class com/jakewharton/mosaic/terminal/TerminalParser { - public fun (Lcom/jakewharton/mosaic/terminal/StdinReader;)V - public final fun debugNext ()Lkotlin/Pair; + public final fun getEvents ()Lkotlinx/coroutines/channels/ReceiveChannel; public final fun getKittyDisambiguateEscapeCodes ()Z public final fun getXtermExtendedUtf8Mouse ()Z - public final fun next ()Lcom/jakewharton/mosaic/terminal/event/Event; + public final fun interrupt ()V + public final fun runParseLoop ()V public final fun setKittyDisambiguateEscapeCodes (Z)V public final fun setXtermExtendedUtf8Mouse (Z)V } @@ -18,7 +12,8 @@ public final class com/jakewharton/mosaic/terminal/TerminalParser { public final class com/jakewharton/mosaic/terminal/Tty { public static final field INSTANCE Lcom/jakewharton/mosaic/terminal/Tty; public static final fun enableRawMode ()Ljava/lang/AutoCloseable; - public static final fun stdinReader ()Lcom/jakewharton/mosaic/terminal/StdinReader; + public static final fun terminalReader (Z)Lcom/jakewharton/mosaic/terminal/TerminalReader; + public static synthetic fun terminalReader$default (ZILjava/lang/Object;)Lcom/jakewharton/mosaic/terminal/TerminalReader; } public final class com/jakewharton/mosaic/terminal/event/BracketedPasteEvent : com/jakewharton/mosaic/terminal/event/Event { @@ -29,6 +24,15 @@ public final class com/jakewharton/mosaic/terminal/event/BracketedPasteEvent : c public fun toString ()Ljava/lang/String; } +public final class com/jakewharton/mosaic/terminal/event/DebugEvent : com/jakewharton/mosaic/terminal/event/Event { + public fun (Lcom/jakewharton/mosaic/terminal/event/Event;[B)V + public fun equals (Ljava/lang/Object;)Z + public final fun getBytes ()[B + public final fun getEvent ()Lcom/jakewharton/mosaic/terminal/event/Event; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/jakewharton/mosaic/terminal/event/DecModeReportEvent : com/jakewharton/mosaic/terminal/event/Event { public fun (ILcom/jakewharton/mosaic/terminal/event/DecModeReportEvent$Setting;)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 9bfd350f..e86dbf81 100644 --- a/mosaic-terminal/api/mosaic-terminal.klib.api +++ b/mosaic-terminal/api/mosaic-terminal.klib.api @@ -21,6 +21,19 @@ 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/DebugEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/DebugEvent|null[0] + constructor (com.jakewharton.mosaic.terminal.event/Event, kotlin/ByteArray) // com.jakewharton.mosaic.terminal.event/DebugEvent.|(com.jakewharton.mosaic.terminal.event.Event;kotlin.ByteArray){}[0] + + final val bytes // com.jakewharton.mosaic.terminal.event/DebugEvent.bytes|{}bytes[0] + final fun (): kotlin/ByteArray // com.jakewharton.mosaic.terminal.event/DebugEvent.bytes.|(){}[0] + final val event // com.jakewharton.mosaic.terminal.event/DebugEvent.event|{}event[0] + final fun (): com.jakewharton.mosaic.terminal.event/Event // com.jakewharton.mosaic.terminal.event/DebugEvent.event.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // com.jakewharton.mosaic.terminal.event/DebugEvent.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.jakewharton.mosaic.terminal.event/DebugEvent.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/DebugEvent.toString|toString(){}[0] +} + final class com.jakewharton.mosaic.terminal.event/DecModeReportEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/DecModeReportEvent|null[0] constructor (kotlin/Int, com.jakewharton.mosaic.terminal.event/DecModeReportEvent.Setting) // com.jakewharton.mosaic.terminal.event/DecModeReportEvent.|(kotlin.Int;com.jakewharton.mosaic.terminal.event.DecModeReportEvent.Setting){}[0] @@ -300,28 +313,23 @@ final class com.jakewharton.mosaic.terminal.event/XtermPixelSizeEvent : com.jake final fun toString(): kotlin/String // com.jakewharton.mosaic.terminal.event/XtermPixelSizeEvent.toString|toString(){}[0] } -final class com.jakewharton.mosaic.terminal/StdinReader : kotlin/AutoCloseable { // com.jakewharton.mosaic.terminal/StdinReader|null[0] - final fun close() // com.jakewharton.mosaic.terminal/StdinReader.close|close(){}[0] - final fun interrupt() // com.jakewharton.mosaic.terminal/StdinReader.interrupt|interrupt(){}[0] - final fun read(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.terminal/StdinReader.read|read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] - final fun readWithTimeout(kotlin/ByteArray, kotlin/Int, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.terminal/StdinReader.readWithTimeout|readWithTimeout(kotlin.ByteArray;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -} +final class com.jakewharton.mosaic.terminal/TerminalReader : kotlin/AutoCloseable { // com.jakewharton.mosaic.terminal/TerminalReader|null[0] + final val events // com.jakewharton.mosaic.terminal/TerminalReader.events|{}events[0] + final fun (): kotlinx.coroutines.channels/ReceiveChannel // com.jakewharton.mosaic.terminal/TerminalReader.events.|(){}[0] -final class com.jakewharton.mosaic.terminal/TerminalParser { // com.jakewharton.mosaic.terminal/TerminalParser|null[0] - constructor (com.jakewharton.mosaic.terminal/StdinReader) // com.jakewharton.mosaic.terminal/TerminalParser.|(com.jakewharton.mosaic.terminal.StdinReader){}[0] + final var kittyDisambiguateEscapeCodes // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes|{}kittyDisambiguateEscapeCodes[0] + final fun (): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes.|(){}[0] + final fun (kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes.|(kotlin.Boolean){}[0] + final var xtermExtendedUtf8Mouse // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse|{}xtermExtendedUtf8Mouse[0] + final fun (): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse.|(){}[0] + final fun (kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse.|(kotlin.Boolean){}[0] - final var kittyDisambiguateEscapeCodes // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes|{}kittyDisambiguateEscapeCodes[0] - final fun (): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes.|(){}[0] - final fun (kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes.|(kotlin.Boolean){}[0] - final var xtermExtendedUtf8Mouse // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse|{}xtermExtendedUtf8Mouse[0] - final fun (): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse.|(){}[0] - final fun (kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse.|(kotlin.Boolean){}[0] - - final fun debugNext(): kotlin/Pair // com.jakewharton.mosaic.terminal/TerminalParser.debugNext|debugNext(){}[0] - final fun next(): com.jakewharton.mosaic.terminal.event/Event // com.jakewharton.mosaic.terminal/TerminalParser.next|next(){}[0] + final fun close() // com.jakewharton.mosaic.terminal/TerminalReader.close|close(){}[0] + final fun interrupt() // com.jakewharton.mosaic.terminal/TerminalReader.interrupt|interrupt(){}[0] + final fun runParseLoop() // com.jakewharton.mosaic.terminal/TerminalReader.runParseLoop|runParseLoop(){}[0] } final object com.jakewharton.mosaic.terminal/Tty { // com.jakewharton.mosaic.terminal/Tty|null[0] final fun enableRawMode(): kotlin/AutoCloseable // com.jakewharton.mosaic.terminal/Tty.enableRawMode|enableRawMode(){}[0] - final fun stdinReader(): com.jakewharton.mosaic.terminal/StdinReader // com.jakewharton.mosaic.terminal/Tty.stdinReader|stdinReader(){}[0] + final fun terminalReader(kotlin/Boolean = ...): com.jakewharton.mosaic.terminal/TerminalReader // com.jakewharton.mosaic.terminal/Tty.terminalReader|terminalReader(kotlin.Boolean){}[0] } diff --git a/mosaic-terminal/build.gradle b/mosaic-terminal/build.gradle index 3616efc6..1bef9231 100644 --- a/mosaic-terminal/build.gradle +++ b/mosaic-terminal/build.gradle @@ -38,17 +38,23 @@ kotlin { } if (it.name.endsWith("Test")) { languageSettings { + optIn('com.jakewharton.mosaic.terminal.TestApi') optIn('kotlin.ExperimentalStdlibApi') optIn('kotlinx.coroutines.DelicateCoroutinesApi') } } } + commonMain { + dependencies { + api libs.kotlinx.coroutines.core + } + } commonTest { dependencies { implementation libs.kotlin.test + implementation libs.kotlinx.coroutines.test implementation libs.kotlinx.io - implementation libs.kotlinx.coroutines.core implementation libs.assertk } } diff --git a/mosaic-terminal/src/c/cutils.h b/mosaic-terminal/src/c/cutils.h index 96a18646..3a01ba34 100644 --- a/mosaic-terminal/src/c/cutils.h +++ b/mosaic-terminal/src/c/cutils.h @@ -4,4 +4,6 @@ #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) +#define UNUSED __attribute__((unused)) + #endif // CUTILS_H diff --git a/mosaic-terminal/src/c/mosaic-stdin-posix.c b/mosaic-terminal/src/c/mosaic-stdin-posix.c index 55a15be5..96216b98 100644 --- a/mosaic-terminal/src/c/mosaic-stdin-posix.c +++ b/mosaic-terminal/src/c/mosaic-stdin-posix.c @@ -15,6 +15,7 @@ typedef struct stdinReaderImpl { int pipe[2]; fd_set fds; int nfds; + platformEventHandler *handler; } stdinReaderImpl; typedef struct stdinWriterImpl { @@ -22,7 +23,7 @@ typedef struct stdinWriterImpl { stdinReader *reader; } stdinWriterImpl; -stdinReaderResult stdinReader_initWithFd(int stdinFd) { +stdinReaderResult stdinReader_initWithFd(int stdinFd, platformEventHandler *handler) { stdinReaderResult result = {}; stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl)); @@ -40,6 +41,7 @@ stdinReaderResult stdinReader_initWithFd(int stdinFd) { // TODO Consider forcing the writer pipe to always be lower than this pipe. // If we did this, we could always assume pipe[0] + 1 is the value for nfds. reader->nfds = ((stdinFd > reader->pipe[0]) ? stdinFd : reader->pipe[0]) + 1; + reader->handler = handler; result.reader = reader; @@ -51,8 +53,8 @@ stdinReaderResult stdinReader_initWithFd(int stdinFd) { goto ret; } -stdinReaderResult stdinReader_init() { - return stdinReader_initWithFd(STDIN_FILENO); +stdinReaderResult stdinReader_init(platformEventHandler *handler) { + return stdinReader_initWithFd(STDIN_FILENO, handler); } stdinRead stdinReader_readInternal( @@ -139,7 +141,7 @@ platformError stdinReader_free(stdinReader *reader) { return result; } -stdinWriterResult stdinWriter_init() { +stdinWriterResult stdinWriter_init(platformEventHandler *handler) { stdinWriterResult result = {}; stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl)); @@ -153,7 +155,7 @@ stdinWriterResult stdinWriter_init() { goto err; } - stdinReaderResult readerResult = stdinReader_initWithFd(writer->pipe[0]); + stdinReaderResult readerResult = stdinReader_initWithFd(writer->pipe[0], handler); if (unlikely(readerResult.error)) { result.error = readerResult.error; goto err; @@ -189,6 +191,23 @@ platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count) { return errno; } +void stdinWriter_focusEvent(stdinWriter *writer UNUSED, bool focused UNUSED) { + // Focus events are delivered through VT sequences. +} + +void stdinWriter_keyEvent(stdinWriter *writer UNUSED) { + // Key events are delivered through VT sequences. +} + +void stdinWriter_mouseEvent(stdinWriter *writer UNUSED) { + // Mouse events are delivered through VT sequences. +} + +void stdinWriter_resizeEvent(stdinWriter *writer, int columns, int rows, int width, int height) { + platformEventHandler *handler = writer->reader->handler; + handler->onResize(handler->opaque, columns, rows, width, height); +} + platformError stdinWriter_free(stdinWriter *writer) { int *pipe = writer->pipe; diff --git a/mosaic-terminal/src/c/mosaic-stdin-windows.c b/mosaic-terminal/src/c/mosaic-stdin-windows.c index 3c0112c4..c9e0a144 100644 --- a/mosaic-terminal/src/c/mosaic-stdin-windows.c +++ b/mosaic-terminal/src/c/mosaic-stdin-windows.c @@ -8,6 +8,7 @@ typedef struct stdinReaderImpl { HANDLE waitHandles[2]; HANDLE readHandle; + platformEventHandler *handler; } stdinReaderImpl; typedef struct stdinWriterImpl { @@ -17,7 +18,11 @@ typedef struct stdinWriterImpl { stdinReader *reader; } stdinWriterImpl; -stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait) { +stdinReaderResult stdinReader_initWithHandle( + HANDLE stdinRead, + HANDLE stdinWait, + platformEventHandler *handler +) { stdinReaderResult result = {}; stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl)); @@ -30,15 +35,17 @@ stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait) result.error = GetLastError(); goto err; } - reader->readHandle = stdinRead; - reader->waitHandles[0] = stdinWait; HANDLE interruptEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (unlikely(interruptEvent == NULL)) { result.error = GetLastError(); goto err; } + + reader->waitHandles[0] = stdinWait; reader->waitHandles[1] = interruptEvent; + reader->readHandle = stdinRead; + reader->handler = handler; result.reader = reader; @@ -50,9 +57,9 @@ stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait) goto ret; } -stdinReaderResult stdinReader_init() { +stdinReaderResult stdinReader_init(platformEventHandler *handler) { HANDLE h = GetStdHandle(STD_INPUT_HANDLE); - return stdinReader_initWithHandle(h, h); + return stdinReader_initWithHandle(h, h, handler); } stdinRead stdinReader_read( @@ -107,7 +114,7 @@ platformError stdinReader_free(stdinReader *reader) { return result; } -stdinWriterResult stdinWriter_init() { +stdinWriterResult stdinWriter_init(platformEventHandler *handler) { stdinWriterResult result = {}; stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl)); @@ -128,7 +135,7 @@ stdinWriterResult stdinWriter_init() { } writer->eventHandle = writeEvent; - stdinReaderResult readerResult = stdinReader_initWithHandle(writer->readHandle, writer->eventHandle); + stdinReaderResult readerResult = stdinReader_initWithHandle(writer->readHandle, writer->eventHandle, handler); if (unlikely(readerResult.error)) { result.error = readerResult.error; goto err; @@ -160,6 +167,26 @@ platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count) { return GetLastError(); } +void stdinWriter_focusEvent(stdinWriter *writer, bool focused) { + platformEventHandler *handler = writer->reader->handler; + handler->onFocus(handler->opaque, focused); + } + + void stdinWriter_keyEvent(stdinWriter *writer) { + platformEventHandler *handler = writer->reader->handler; + handler->onKey(handler->opaque); + } + + void stdinWriter_mouseEvent(stdinWriter *writer) { + platformEventHandler *handler = writer->reader->handler; + handler->onMouse(handler->opaque); + } + + void stdinWriter_resizeEvent(stdinWriter *writer, int columns, int rows, int width, int height) { + platformEventHandler *handler = writer->reader->handler; + handler->onResize(handler->opaque, columns, rows, width, height); + } + platformError stdinWriter_free(stdinWriter *writer) { DWORD result = 0; if (unlikely(CloseHandle(writer->eventHandle) == 0)) { diff --git a/mosaic-terminal/src/c/mosaic.h b/mosaic-terminal/src/c/mosaic.h index edba7acd..47d9e889 100644 --- a/mosaic-terminal/src/c/mosaic.h +++ b/mosaic-terminal/src/c/mosaic.h @@ -1,6 +1,8 @@ #ifndef MOSAIC_H #define MOSAIC_H +#include + #if defined(__APPLE__) || defined(__linux__) #include @@ -44,15 +46,33 @@ typedef struct stdinRead { platformError error; } stdinRead; -stdinReaderResult stdinReader_init(); +typedef void PlatformEventHandlerOnRead(void *opaque); +typedef void PlatformEventHandlerOnFocus(void *opaque, bool focused); +typedef void PlatformEventHandlerOnKey(void *opaque); // TODO params +typedef void PlatformEventHandlerOnMouse(void *opaque); // TODO params +typedef void PlatformEventHandlerOnResize(void *opaque, int columns, int rows, int width, int height); + +typedef struct platformEventHandler { + void *opaque; + PlatformEventHandlerOnFocus *onFocus; + PlatformEventHandlerOnKey *onKey; + PlatformEventHandlerOnMouse *onMouse; + PlatformEventHandlerOnResize *onResize; +} platformEventHandler; + +stdinReaderResult stdinReader_init(platformEventHandler *handler); stdinRead stdinReader_read(stdinReader *reader, void *buffer, int count); stdinRead stdinReader_readWithTimeout(stdinReader *reader, void *buffer, int count, int timeoutMillis); platformError stdinReader_interrupt(stdinReader* reader); platformError stdinReader_free(stdinReader *reader); -stdinWriterResult stdinWriter_init(); +stdinWriterResult stdinWriter_init(platformEventHandler *handler); stdinReader *stdinWriter_getReader(stdinWriter *writer); platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count); +void stdinWriter_focusEvent(stdinWriter *writer, bool focused); +void stdinWriter_keyEvent(stdinWriter *writer); +void stdinWriter_mouseEvent(stdinWriter *writer); +void stdinWriter_resizeEvent(stdinWriter *writer, int columns, int rows, int width, int height); platformError stdinWriter_free(stdinWriter *writer); #endif // MOSAIC_H diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/PlatformEventHandler.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/PlatformEventHandler.kt new file mode 100644 index 00000000..3f57c339 --- /dev/null +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/PlatformEventHandler.kt @@ -0,0 +1,26 @@ +package com.jakewharton.mosaic.terminal + +import com.jakewharton.mosaic.terminal.event.Event +import com.jakewharton.mosaic.terminal.event.FocusEvent +import com.jakewharton.mosaic.terminal.event.ResizeEvent +import kotlinx.coroutines.channels.SendChannel + +internal class PlatformEventHandler( + private val events: SendChannel, +) { + fun onFocus(focused: Boolean) { + events.trySend(FocusEvent(focused)) + } + + fun onKey() { + return + } + + fun onMouse() { + return + } + + fun onResize(columns: Int, rows: Int, width: Int, height: Int) { + events.trySend(ResizeEvent(columns, rows, width, height)) + } +} diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalReader.kt similarity index 91% rename from mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt rename to mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalReader.kt index d6b54aae..4615fd44 100644 --- a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalParser.kt +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TerminalReader.kt @@ -1,6 +1,7 @@ package com.jakewharton.mosaic.terminal import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent +import com.jakewharton.mosaic.terminal.event.DebugEvent import com.jakewharton.mosaic.terminal.event.DecModeReportEvent import com.jakewharton.mosaic.terminal.event.Event import com.jakewharton.mosaic.terminal.event.FocusEvent @@ -20,17 +21,32 @@ import com.jakewharton.mosaic.terminal.event.TerminalVersionEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent import com.jakewharton.mosaic.terminal.event.XtermPixelSizeEvent +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel private const val BufferSize = 8 * 1024 private const val BareEscapeDisambiguationReadTimeoutMillis = 100 -public class TerminalParser( - private val stdinReader: StdinReader, -) { +public class TerminalReader internal constructor( + private val platformInput: PlatformInput, + events: Channel, + private val emitDebugEvents: Boolean, +) : AutoCloseable { private val buffer = ByteArray(BufferSize) private var offset = 0 private var limit = 0 + @TestApi + internal fun copyBuffer() = buffer.copyOfRange(offset, limit) + + @TestApi + internal fun platformInput() = platformInput + + private val _events = events + + /** Events read as a result of calls to [tryReadEvents]. */ + public val events: ReceiveChannel get() = _events + /** * Indicate whether Kitty's * [escape code disambiguation](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate-escape-codes) @@ -57,77 +73,83 @@ public class TerminalParser( public var xtermExtendedUtf8Mouse: Boolean = false /** - * A version of [next] which also returns the bytes that produced the event. + * Perform a blocking read from stdin to try and parse events. Calls to this function are not + * guaranteed to read an event, nor are they guaranteed to read only one event. Events + * which are read will be placed into [events]. * - * **WARNING** This function is expensive, and should only be used for debugging. + * It is expected that this function will be called repeatedly in a loop. + * + * @return False if returning due to [interrupt] being called. True means some data was read, + * but not necessarily that any events were put into the [events] channel. This could be because + * not enough bytes were available to parse the entire event, for example. */ - public fun debugNext(): Pair { - // Move any existing data to index 0 of the buffer. This will ensure we can capture all the - // bytes consumed (even across multiple reads) since the original offset will always be 0. - buffer.copyInto(buffer, 0, startIndex = offset, endIndex = limit) - limit = limit - offset - offset = 0 - - val event = next() - val bytes = buffer.copyOfRange(0, offset) - return event to bytes - } - - public fun next(): Event { + public fun runParseLoop() { val buffer = buffer - var offset = offset - var limit = limit while (true) { if (offset < limit) { - parse(buffer, offset, limit)?.let { event -> - return event + val event = tryParse(buffer, offset, limit) + if (event != null) { + _events.trySend(event) + if (!emitDebugEvents) continue + + // In debug event mode, parsing starts at index 0 of the buffer (see below). Leverage + // this to capture the consumed bytes by looking at where the next parse would start. + val eventBytes = buffer.copyOfRange(0, offset) + _events.trySend(DebugEvent(event, eventBytes)) + + // Move remaining data to the start of the buffer to maintain the parse from 0 invariant. + buffer.copyInto(buffer, 0, startIndex = offset, endIndex = limit) + limit -= offset + offset = 0 + + continue } } // Underflow! Copy any data to start of buffer in preparation for a read. buffer.copyInto(buffer, 0, startIndex = offset, endIndex = limit) - - // Do not write the new limit to the member property because the read code below will. - limit = limit - offset - + limit -= offset offset = 0 - this.offset = 0 if (kittyDisambiguateEscapeCodes || limit != 1 || buffer[0] != 0x1B.toByte()) { // Common case: we are using the Kitty keyboard protocol to disambiguate escape keys, or // the buffer contains anything other than a bare escape. Do a normal read for more data. - val read = stdinReader.read(buffer, limit, BufferSize - limit) - if (read == -1) break + val read = platformInput.read(buffer, limit, BufferSize - limit) + if (read == -1) break // EOF + if (read == 0) return // Interrupt + limit += read - this.limit = limit continue } // Otherwise, perform a quick read to see if we have any more bytes. This will allow us to // determine whether the bare escape was truly a legacy keyboard escape event, or just the // start of some other escape sequence. - val read = stdinReader.readWithTimeout( + val read = platformInput.readWithTimeout( buffer, 1, BufferSize - 1, BareEscapeDisambiguationReadTimeoutMillis, ) - if (read == 0) { + if (read == -1) break + + limit = if (read == 0) { + _events.trySend(KeyboardEvent(0x1B)) // We know the offset is 0, so resetting the limit effectively consumes the byte. - this.limit = 0 - return KeyboardEvent(0x1B) - } else if (read == -1) { - break + 0 + } else { + read + 1 } - limit += read - this.limit = limit } - throw RuntimeException("stdin eof") + if (limit > 0) { + _events.trySend(UnknownEvent(buffer.copyOfRange(0, limit))) + } + _events.close() } - private fun parse(buffer: ByteArray, start: Int, limit: Int): Event? { + private fun tryParse(buffer: ByteArray, start: Int, limit: Int): Event? { val b1 = buffer[start].toInt() and 0xff if (b1 == 0x1B) { val b2Index = start + 1 @@ -797,4 +819,17 @@ public class TerminalParser( return handler(b3Index, stIndex) ?: UnknownEvent(buffer.copyOfRange(start, end)) } + + public fun interrupt() { + platformInput.interrupt() + } + + /** + * Free the resources associated with this reader. + * + * This call can be omitted if your process is exiting. + */ + override fun close() { + platformInput.close() + } } diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TestApi.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TestApi.kt new file mode 100644 index 00000000..d8e1dd24 --- /dev/null +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/TestApi.kt @@ -0,0 +1,9 @@ +package com.jakewharton.mosaic.terminal + +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY + +@RequiresOptIn +@Target(CLASS, FUNCTION, PROPERTY) +internal annotation class TestApi diff --git a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt index 09798867..f9792270 100644 --- a/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt +++ b/mosaic-terminal/src/commonMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt @@ -1,5 +1,7 @@ package com.jakewharton.mosaic.terminal +import com.jakewharton.mosaic.terminal.event.DebugEvent + public expect object Tty { /** * Save the current terminal settings and enter "raw" mode. @@ -15,22 +17,26 @@ public expect object Tty { * In addition to the flags required for entering "raw" mode, on POSIX-compliant platforms, * this function will change the standard input stream to block indefinitely until a minimum * of 1 byte is available to read. This allows the reader thread to fully be suspended rather - * than consuming CPU. Use [stdinReader] to read in a manner that can still be interrupted. + * than consuming CPU. Use [terminalReader] to read in a manner that can still be interrupted. */ public fun enableRawMode(): AutoCloseable /** - * Create a [StdinReader] which will read from this process' stdin stream while also + * Create a [TerminalReader] which will read from this process' stdin stream while also * supporting interruption. * * Use with [enableRawMode] to read input byte-by-byte. + * + * @param emitDebugEvents When true, each event sent to [TerminalReader.events] will be followed + * by a [DebugEvent] that contains the original event and the bytes which produced it. */ - public fun stdinReader(): StdinReader + public fun terminalReader(emitDebugEvents: Boolean = false): TerminalReader - internal fun stdinWriter(): StdinWriter + @TestApi + internal fun stdinWriter(emitDebugEvents: Boolean = false): StdinWriter } -public expect class StdinReader : AutoCloseable { +internal expect class PlatformInput : AutoCloseable { /** * Read up to [count] bytes into [buffer] at [offset]. The number of bytes read will be returned. * 0 will be returned if [interrupt] is called while waiting for input. -1 will be returned if @@ -38,7 +44,7 @@ public expect class StdinReader : AutoCloseable { * * @see readWithTimeout */ - public fun read(buffer: ByteArray, offset: Int, count: Int): Int + fun read(buffer: ByteArray, offset: Int, count: Int): Int /** * Read up to [count] bytes into [buffer] at [offset]. The number of bytes read will be returned. @@ -50,10 +56,10 @@ public expect class StdinReader : AutoCloseable { * value is not validated. * @see read */ - public fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int + fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int /** Signal blocking calls to [read] to wake up and return 0. */ - public fun interrupt() + fun interrupt() /** * Free the resources associated with this reader. @@ -63,13 +69,19 @@ public expect class StdinReader : AutoCloseable { override fun close() } +@TestApi internal expect class StdinWriter : AutoCloseable { - val reader: StdinReader + val reader: TerminalReader // TODO Take ByteString once it migrates to stdlib, // or if Sink/RawSink migrates expose that as a val. // https://github.com/Kotlin/kotlinx-io/issues/354 fun write(buffer: ByteArray) + fun focusEvent(focused: Boolean) + fun keyEvent() + fun mouseEvent() + fun resizeEvent(columns: Int, rows: Int, width: Int, height: Int) + override fun close() } 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 c50afe4b..5c805f41 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 @@ -21,6 +21,13 @@ public class UnknownEvent( } } +@Poko +public class DebugEvent( + public val event: Event, + // TODO ByteString once it moves into the stdlib. + @ReadArrayContent public val bytes: ByteArray, +) : Event + @Poko public class KeyboardEvent( public val codepoint: Int, diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/BaseTerminalParserTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/BaseTerminalParserTest.kt index c6886da0..4a761278 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/BaseTerminalParserTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/BaseTerminalParserTest.kt @@ -1,16 +1,34 @@ package com.jakewharton.mosaic.terminal +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.terminal.event.Event import kotlin.test.AfterTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest abstract class BaseTerminalParserTest { internal val writer = Tty.stdinWriter() - internal val parser = TerminalParser(writer.reader) + internal val parser = writer.reader + private val runLoop = GlobalScope.launch(Dispatchers.IO) { + parser.runParseLoop() + } - @AfterTest fun after() { + @AfterTest fun after() = runTest { + parser.interrupt() + runLoop.join() writer.close() + assertThat(parser.copyBuffer().toHexString()).isEqualTo("") } internal fun StdinWriter.writeHex(hex: String) { write(hex.hexToByteArray()) } + + internal suspend fun TerminalReader.next(): Event { + return events.receive() + } } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/StdinReaderTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/StdinReaderTest.kt index 3fb48996..d39667b2 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/StdinReaderTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/StdinReaderTest.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch class StdinReaderTest { private val writer = Tty.stdinWriter() - private val reader = writer.reader + private val reader = writer.reader.platformInput() @AfterTest fun after() { reader.close() diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiBracketedPasteEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiBracketedPasteEventTest.kt index 3aea939d..a932e69e 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiBracketedPasteEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiBracketedPasteEventTest.kt @@ -4,14 +4,15 @@ import assertk.assertThat import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiBracketedPasteEventTest : BaseTerminalParserTest() { - @Test fun pasteStart() { + @Test fun pasteStart() = runTest { writer.writeHex("1b5b3230307e") assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = true)) } - @Test fun pasteEnd() { + @Test fun pasteEnd() = runTest { writer.writeHex("1b5b3230317e") assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = false)) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiDecModeReportEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiDecModeReportEventTest.kt index bb0de920..9ac4cc11 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiDecModeReportEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiDecModeReportEventTest.kt @@ -10,9 +10,10 @@ import com.jakewharton.mosaic.terminal.event.DecModeReportEvent.Setting.Reset import com.jakewharton.mosaic.terminal.event.DecModeReportEvent.Setting.Set import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() { - @Test fun settings() { + @Test fun settings() = runTest { writer.writeHex("1b5b3f313030343b302479") assertThat(parser.next()).isEqualTo( DecModeReportEvent( @@ -54,7 +55,7 @@ class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() { ) } - @Test fun minimal() { + @Test fun minimal() = runTest { writer.writeHex("1b5b3f313b302479") assertThat(parser.next()).isEqualTo( DecModeReportEvent( @@ -64,56 +65,56 @@ class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() { ) } - @Test fun unknownSetting() { + @Test fun unknownSetting() = runTest { writer.writeHex("1b5b313030343b352479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b313030343b352479".hexToByteArray()), ) } - @Test fun noQuestion() { + @Test fun noQuestion() = runTest { writer.writeHex("1b5b313030343b302479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b313030343b302479".hexToByteArray()), ) } - @Test fun noDollar() { + @Test fun noDollar() = runTest { writer.writeHex("1b5b3f313030343b3079") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f313030343b3079".hexToByteArray()), ) } - @Test fun noMode() { + @Test fun noMode() = runTest { writer.writeHex("1b5b3f3b3130302479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f3b3130302479".hexToByteArray()), ) } - @Test fun nonDigitMode() { + @Test fun nonDigitMode() = runTest { writer.writeHex("1b5b3f31302d32343b302479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f31302d32343b302479".hexToByteArray()), ) } - @Test fun noSetting() { + @Test fun noSetting() = runTest { writer.writeHex("1b5b3f313030343b2479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f313030343b2479".hexToByteArray()), ) } - @Test fun nonDigitSetting() { + @Test fun nonDigitSetting() = runTest { writer.writeHex("1b5b3f313030343b312d322479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f313030343b312d322479".hexToByteArray()), ) } - @Test fun noSemicolon() { + @Test fun noSemicolon() = runTest { writer.writeHex("1b5b3f313030342479") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f313030342479".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiFocusEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiFocusEventTest.kt index 97efb2bf..706d98d0 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiFocusEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiFocusEventTest.kt @@ -4,14 +4,15 @@ import assertk.assertThat import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.FocusEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiFocusEventTest : BaseTerminalParserTest() { - @Test fun focusedTrue() { + @Test fun focusedTrue() = runTest { writer.writeHex("1b5b49") assertThat(parser.next()).isEqualTo(FocusEvent(focused = true)) } - @Test fun focusedFalse() { + @Test fun focusedFalse() = runTest { writer.writeHex("1b5b4f") assertThat(parser.next()).isEqualTo(FocusEvent(focused = false)) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKeyboardEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKeyboardEventTest.kt index 82fbc5f8..91b23fda 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKeyboardEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKeyboardEventTest.kt @@ -20,98 +20,99 @@ import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.Right import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.Up import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiKeyboardEventTest : BaseTerminalParserTest() { - @Test fun up() { + @Test fun up() = runTest { writer.writeHex("1b5b41") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up)) } - @Test fun down() { + @Test fun down() = runTest { writer.writeHex("1b5b42") assertThat(parser.next()).isEqualTo(KeyboardEvent(Down)) } - @Test fun right() { + @Test fun right() = runTest { writer.writeHex("1b5b43") assertThat(parser.next()).isEqualTo(KeyboardEvent(Right)) } - @Test fun left() { + @Test fun left() = runTest { writer.writeHex("1b5b44") assertThat(parser.next()).isEqualTo(KeyboardEvent(Left)) } - @Test fun begin() { + @Test fun begin() = runTest { writer.writeHex("1b5b45") assertThat(parser.next()).isEqualTo(KeyboardEvent(KpBegin)) } - @Test fun end() { + @Test fun end() = runTest { writer.writeHex("1b5b46") assertThat(parser.next()).isEqualTo(KeyboardEvent(End)) } - @Test fun home() { + @Test fun home() = runTest { writer.writeHex("1b5b48") assertThat(parser.next()).isEqualTo(KeyboardEvent(Home)) } - @Test fun modifierShiftUp() { + @Test fun modifierShiftUp() = runTest { writer.writeHex("1b5b313b3241") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierShift)) } - @Test fun modifierAltUp() { + @Test fun modifierAltUp() = runTest { writer.writeHex("1b5b313b3341") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierAlt)) } - @Test fun modifierCtrlUp() { + @Test fun modifierCtrlUp() = runTest { writer.writeHex("1b5b313b3541") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCtrl)) } - @Test fun modifierSuperUp() { + @Test fun modifierSuperUp() = runTest { writer.writeHex("1b5b313b3941") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierSuper)) } - @Test fun modifierHyperUp() { + @Test fun modifierHyperUp() = runTest { writer.writeHex("1b5b313b313741") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierHyper)) } - @Test fun modifierMetaUp() { + @Test fun modifierMetaUp() = runTest { writer.writeHex("1b5b313b333341") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierMeta)) } - @Test fun modifierCapsLockUp() { + @Test fun modifierCapsLockUp() = runTest { writer.writeHex("1b5b313b363541") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCapsLock)) } - @Test fun modifierNumLockUp() { + @Test fun modifierNumLockUp() = runTest { writer.writeHex("1b5b313b31323941") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierNumLock)) } - @Test fun non1p0() { + @Test fun non1p0() = runTest { writer.writeHex("1b5b323b3248") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b323b3248".hexToByteArray()), ) } - @Test fun emptyModifier() { + @Test fun emptyModifier() = runTest { writer.writeHex("1b5b313b48") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b313b48".hexToByteArray()), ) } - @Test fun nonDigitModifier() { + @Test fun nonDigitModifier() = runTest { writer.writeHex("1b5b313b2f48") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b313b2f48".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardEventTest.kt index cd017119..303262a4 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardEventTest.kt @@ -5,37 +5,38 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.KeyboardEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierShift import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiKittyKeyboardEventTest : BaseTerminalParserTest() { - @Test fun h() { + @Test fun h() = runTest { writer.writeHex("1b5b31303475") assertThat(parser.next()).isEqualTo( KeyboardEvent(0x68), ) } - @Test fun shiftH() { + @Test fun shiftH() = runTest { writer.writeHex("1b5b3130343b3275") assertThat(parser.next()).isEqualTo( KeyboardEvent(0x68, modifiers = ModifierShift), ) } - @Test fun shiftHWithAlternate() { + @Test fun shiftHWithAlternate() = runTest { writer.writeHex("1b5b3130343a37323b3275") assertThat(parser.next()).isEqualTo( KeyboardEvent(0x68, 0x48, modifiers = ModifierShift), ) } - @Test fun shiftHWithReleaseEventType() { + @Test fun shiftHWithReleaseEventType() = runTest { writer.writeHex("1b5b3130343b323a3375") assertThat(parser.next()).isEqualTo( KeyboardEvent(0x68, modifiers = ModifierShift, eventType = 3), ) } - @Test fun hWithAssociatedText() { + @Test fun hWithAssociatedText() = runTest { writer.writeHex("1b5b3130343b3b31303475") assertThat(parser.next()).isEqualTo( KeyboardEvent(0x68, text = "h"), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardQueryEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardQueryEventTest.kt index 558ba2d9..dcd0a5d0 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardQueryEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiKittyKeyboardQueryEventTest.kt @@ -5,31 +5,32 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.KittyKeyboardQueryEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiKittyKeyboardQueryEventTest : BaseTerminalParserTest() { - @Test fun flagsNone() { + @Test fun flagsNone() = runTest { writer.writeHex("1b5b3f3075") assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(0)) } - @Test fun flagsAll() { + @Test fun flagsAll() = runTest { writer.writeHex("1b5b3f333175") assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(31)) } - @Test fun flagsUnknown() { + @Test fun flagsUnknown() = runTest { writer.writeHex("1b5b3f31323875") assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(128)) } - @Test fun flagsMissing() { + @Test fun flagsMissing() = runTest { writer.writeHex("1b5b3f75") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f75".hexToByteArray()), ) } - @Test fun flagsNonDigit() { + @Test fun flagsNonDigit() = runTest { writer.writeHex("1b5b3f312b2075") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f312b2075".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiMouseEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiMouseEventTest.kt index 4dc3d507..4d4d57ed 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiMouseEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiMouseEventTest.kt @@ -7,114 +7,115 @@ import com.jakewharton.mosaic.terminal.event.MouseEvent.Button import com.jakewharton.mosaic.terminal.event.MouseEvent.Type import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiMouseEventTest : BaseTerminalParserTest() { - @Test fun motion() { + @Test fun motion() = runTest { writer.writeHex("1b5b4d434837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Motion, Button.None), ) } - @Test fun click() { + @Test fun click() = runTest { writer.writeHex("1b5b4d204837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Left), ) } - @Test fun drag() { + @Test fun drag() = runTest { writer.writeHex("1b5b4d404837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Drag, Button.Left), ) } - @Test fun clickMouseUp() { + @Test fun clickMouseUp() = runTest { writer.writeHex("1b5b4d234837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.None), ) } - @Test fun shiftClick() { + @Test fun shiftClick() = runTest { writer.writeHex("1b5b4d244837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Left, shift = true), ) } - @Test fun altClick() { + @Test fun altClick() = runTest { writer.writeHex("1b5b4d284837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Left, alt = true), ) } - @Test fun ctrlClick() { + @Test fun ctrlClick() = runTest { writer.writeHex("1b5b4d304837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Left, ctrl = true), ) } - @Test fun clickRight() { + @Test fun clickRight() = runTest { writer.writeHex("1b5b4d224837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Right), ) } - @Test fun clickMiddle() { + @Test fun clickMiddle() = runTest { writer.writeHex("1b5b4d214837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Middle), ) } - @Test fun clickWheelUp() { + @Test fun clickWheelUp() = runTest { writer.writeHex("1b5b4d604837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.WheelUp), ) } - @Test fun clickWheelDown() { + @Test fun clickWheelDown() = runTest { writer.writeHex("1b5b4d614837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.WheelDown), ) } - @Test fun clickButton8() { + @Test fun clickButton8() = runTest { writer.writeHex("1b5b4da04837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Button8), ) } - @Test fun clickButton9() { + @Test fun clickButton9() = runTest { writer.writeHex("1b5b4da14837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Button9), ) } - @Test fun clickButton10() { + @Test fun clickButton10() = runTest { writer.writeHex("1b5b4da24837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Button10), ) } - @Test fun clickButton11() { + @Test fun clickButton11() = runTest { writer.writeHex("1b5b4da34837") assertThat(parser.next()).isEqualTo( MouseEvent(39, 22, Type.Press, Button.Button11), ) } - @Test fun clickUtf8() { + @Test fun clickUtf8() = runTest { parser.xtermExtendedUtf8Mouse = true writer.writeHex("1b5b4d20c28037") @@ -125,7 +126,7 @@ class TerminalParserCsiMouseEventTest : BaseTerminalParserTest() { // TODO all types & buttons utf-8 in both single-byte and multi-byte form - @Test fun lowercaseMDelimiterInvalid() { + @Test fun lowercaseMDelimiterInvalid() = runTest { writer.writeHex("1b5b6d204837") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b6d".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiOperatingStatusResponseEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiOperatingStatusResponseEventTest.kt index 7753900f..d4b7959e 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiOperatingStatusResponseEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiOperatingStatusResponseEventTest.kt @@ -5,24 +5,25 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.OperatingStatusResponseEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiOperatingStatusResponseEventTest : BaseTerminalParserTest() { - @Test fun ok() { + @Test fun ok() = runTest { writer.writeHex("1b5b306e") assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = true)) } - @Test fun notOk() { + @Test fun notOk() = runTest { writer.writeHex("1b5b336e") assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = false)) } - @Test fun unknownP1() { + @Test fun unknownP1() = runTest { writer.writeHex("1b5b316e") assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b316e".hexToByteArray())) } - @Test fun nonDigitP1() { + @Test fun nonDigitP1() = runTest { writer.writeHex("1b5b2b6e") assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b2b6e".hexToByteArray())) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiPrimaryDeviceAttributesEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiPrimaryDeviceAttributesEventTest.kt index 68ae9d61..e9b819d8 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiPrimaryDeviceAttributesEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiPrimaryDeviceAttributesEventTest.kt @@ -5,21 +5,22 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.PrimaryDeviceAttributesEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiPrimaryDeviceAttributesEventTest : BaseTerminalParserTest() { - @Test fun noLeadingQuestionMarkIsUnknown() { + @Test fun noLeadingQuestionMarkIsUnknown() = runTest { writer.writeHex("1b5b303063") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b303063".hexToByteArray()), ) } - @Test fun emptyData() { + @Test fun emptyData() = runTest { writer.writeHex("1b5b3f63") assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = "")) } - @Test fun data() { + @Test fun data() = runTest { writer.writeHex("1b5b3f323b3263") assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = "2;2")) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiResizeEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiResizeEventTest.kt index aaefd225..3665845c 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiResizeEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiResizeEventTest.kt @@ -5,36 +5,37 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.ResizeEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiResizeEventTest : BaseTerminalParserTest() { - @Test fun basic() { + @Test fun basic() = runTest { writer.writeHex("1b5b34383b313b323b333b3474") assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) } - @Test fun pixelSizeAsZero() { + @Test fun pixelSizeAsZero() = runTest { writer.writeHex("1b5b34383b313b323b303b3074") assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 0, 0)) } - @Test fun subparametersIgnored() { + @Test fun subparametersIgnored() = runTest { writer.writeHex("1b5b34383b313a39393b323a39383a39373b333a39393a3a39373b343a39393a74") assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) } - @Test fun emptySubparametersIgnored() { + @Test fun emptySubparametersIgnored() = runTest { writer.writeHex("1b5b34383b313a3b323a3b333a3b343a74") assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) } - @Test fun emptyModeFails() { + @Test fun emptyModeFails() = runTest { writer.writeHex("1b5b3b313b323b333b3474") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3b313b323b333b3474".hexToByteArray()), ) } - @Test fun emptyParameterFails() { + @Test fun emptyParameterFails() = runTest { writer.writeHex("1b5b34383b3b323b333b3474") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b34383b3b323b333b3474".hexToByteArray()), @@ -53,7 +54,7 @@ class TerminalParserCsiResizeEventTest : BaseTerminalParserTest() { ) } - @Test fun nonDigitParameterFails() { + @Test fun nonDigitParameterFails() = runTest { writer.writeHex("1b5b34383b312e303b323b333b3474") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b34383b312e303b323b333b3474".hexToByteArray()), 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 index 74a782ea..f03539bf 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiSystemThemeEventTest.kt @@ -5,40 +5,41 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.SystemThemeEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiSystemThemeEventTest : BaseTerminalParserTest() { - @Test fun dark() { + @Test fun dark() = runTest { writer.writeHex("1b5b3f3939373b316e") assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = true)) } - @Test fun light() { + @Test fun light() = runTest { writer.writeHex("1b5b3f3939373b326e") assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = false)) } - @Test fun missingP2() { + @Test fun missingP2() = runTest { writer.writeHex("1b5b3f3939373b6e") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f3939373b6e".hexToByteArray()), ) } - @Test fun unknownP2() { + @Test fun unknownP2() = runTest { writer.writeHex("1b5b3f3939373b346e") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f3939373b346e".hexToByteArray()), ) } - @Test fun nonDigitP2() { + @Test fun nonDigitP2() = runTest { writer.writeHex("1b5b3f3939373b2b6e") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f3939373b2b6e".hexToByteArray()), ) } - @Test fun tooLongP2() { + @Test fun tooLongP2() = runTest { writer.writeHex("1b5b3f3939373b31316e") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b3f3939373b31316e".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermCharacterSizeEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermCharacterSizeEventTest.kt index 3b8e2c83..2b35e077 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermCharacterSizeEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermCharacterSizeEventTest.kt @@ -5,14 +5,15 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiXtermCharacterSizeEventTest : BaseTerminalParserTest() { - @Test fun basic() { + @Test fun basic() = runTest { writer.writeHex("1b5b383b313b3274") assertThat(parser.next()).isEqualTo(XtermCharacterSizeEvent(1, 2)) } - @Test fun emptyParameterFails() { + @Test fun emptyParameterFails() = runTest { writer.writeHex("1b5b383b3b3274") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b383b3b3274".hexToByteArray()), @@ -23,7 +24,7 @@ class TerminalParserCsiXtermCharacterSizeEventTest : BaseTerminalParserTest() { ) } - @Test fun nonDigitParameterFails() { + @Test fun nonDigitParameterFails() = runTest { writer.writeHex("1b5b383b223b3274") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b383b223b3274".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermPixelSizeEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermPixelSizeEventTest.kt index 2f13f897..db327748 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermPixelSizeEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserCsiXtermPixelSizeEventTest.kt @@ -5,14 +5,15 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.XtermPixelSizeEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserCsiXtermPixelSizeEventTest : BaseTerminalParserTest() { - @Test fun basic() { + @Test fun basic() = runTest { writer.writeHex("1b5b343b313b3274") assertThat(parser.next()).isEqualTo(XtermPixelSizeEvent(1, 2)) } - @Test fun emptyParameterFails() { + @Test fun emptyParameterFails() = runTest { writer.writeHex("1b5b343b3b3274") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b343b3b3274".hexToByteArray()), @@ -23,7 +24,7 @@ class TerminalParserCsiXtermPixelSizeEventTest : BaseTerminalParserTest() { ) } - @Test fun nonDigitParameterFails() { + @Test fun nonDigitParameterFails() = runTest { writer.writeHex("1b5b343b223b3274") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5b343b223b3274".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserDcsTerminalVersionEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserDcsTerminalVersionEventTest.kt index f82d18f2..f67faa9c 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserDcsTerminalVersionEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserDcsTerminalVersionEventTest.kt @@ -4,14 +4,15 @@ import assertk.assertThat import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.TerminalVersionEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserDcsTerminalVersionEventTest : BaseTerminalParserTest() { - @Test fun empty() { + @Test fun empty() = runTest { writer.writeHex("1b503e7c1b5c") assertThat(parser.next()).isEqualTo(TerminalVersionEvent("")) } - @Test fun text() { + @Test fun text() = runTest { writer.writeHex("1b503e7c68656c6c6f1b5c") assertThat(parser.next()).isEqualTo(TerminalVersionEvent("hello")) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserGroundKeyboardEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserGroundKeyboardEventTest.kt index 8a24fac5..d718fdf2 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserGroundKeyboardEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserGroundKeyboardEventTest.kt @@ -5,9 +5,10 @@ import assertk.assertions.isEqualTo import com.jakewharton.mosaic.terminal.event.KeyboardEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserGroundKeyboardEventTest : BaseTerminalParserTest() { - @Test fun graphic() { + @Test fun graphic() = runTest { for (codepoint in 0x20..0x7f) { val hex = codepoint.toString(16) writer.writeHex(hex) @@ -15,172 +16,172 @@ class TerminalParserGroundKeyboardEventTest : BaseTerminalParserTest() { } } - @Test fun ctrlShiftAt() { + @Test fun ctrlShiftAt() = runTest { writer.writeHex("00") assertThat(parser.next()).isEqualTo(KeyboardEvent('@'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlA() { + @Test fun ctrlA() = runTest { writer.writeHex("01") assertThat(parser.next()).isEqualTo(KeyboardEvent('a'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlB() { + @Test fun ctrlB() = runTest { writer.writeHex("02") assertThat(parser.next()).isEqualTo(KeyboardEvent('b'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlC() { + @Test fun ctrlC() = runTest { writer.writeHex("03") assertThat(parser.next()).isEqualTo(KeyboardEvent('c'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlD() { + @Test fun ctrlD() = runTest { writer.writeHex("04") assertThat(parser.next()).isEqualTo(KeyboardEvent('d'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlE() { + @Test fun ctrlE() = runTest { writer.writeHex("05") assertThat(parser.next()).isEqualTo(KeyboardEvent('e'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlF() { + @Test fun ctrlF() = runTest { writer.writeHex("06") assertThat(parser.next()).isEqualTo(KeyboardEvent('f'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlG() { + @Test fun ctrlG() = runTest { writer.writeHex("07") assertThat(parser.next()).isEqualTo(KeyboardEvent('g'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlH() { + @Test fun ctrlH() = runTest { writer.writeHex("08") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x7f)) } - @Test fun ctrlI() { + @Test fun ctrlI() = runTest { writer.writeHex("09") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x09)) } - @Test fun ctrlJ() { + @Test fun ctrlJ() = runTest { writer.writeHex("0a") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d)) } - @Test fun ctrlK() { + @Test fun ctrlK() = runTest { writer.writeHex("0b") assertThat(parser.next()).isEqualTo(KeyboardEvent('k'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlL() { + @Test fun ctrlL() = runTest { writer.writeHex("0c") assertThat(parser.next()).isEqualTo(KeyboardEvent('l'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlM() { + @Test fun ctrlM() = runTest { writer.writeHex("0d") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d)) } - @Test fun ctrlN() { + @Test fun ctrlN() = runTest { writer.writeHex("0e") assertThat(parser.next()).isEqualTo(KeyboardEvent('n'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlO() { + @Test fun ctrlO() = runTest { writer.writeHex("0f") assertThat(parser.next()).isEqualTo(KeyboardEvent('o'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlP() { + @Test fun ctrlP() = runTest { writer.writeHex("10") assertThat(parser.next()).isEqualTo(KeyboardEvent('p'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlQ() { + @Test fun ctrlQ() = runTest { writer.writeHex("11") assertThat(parser.next()).isEqualTo(KeyboardEvent('q'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlR() { + @Test fun ctrlR() = runTest { writer.writeHex("12") assertThat(parser.next()).isEqualTo(KeyboardEvent('r'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlS() { + @Test fun ctrlS() = runTest { writer.writeHex("13") assertThat(parser.next()).isEqualTo(KeyboardEvent('s'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlT() { + @Test fun ctrlT() = runTest { writer.writeHex("14") assertThat(parser.next()).isEqualTo(KeyboardEvent('t'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlU() { + @Test fun ctrlU() = runTest { writer.writeHex("15") assertThat(parser.next()).isEqualTo(KeyboardEvent('u'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlV() { + @Test fun ctrlV() = runTest { writer.writeHex("16") assertThat(parser.next()).isEqualTo(KeyboardEvent('v'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlW() { + @Test fun ctrlW() = runTest { writer.writeHex("17") assertThat(parser.next()).isEqualTo(KeyboardEvent('w'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlX() { + @Test fun ctrlX() = runTest { writer.writeHex("18") assertThat(parser.next()).isEqualTo(KeyboardEvent('x'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlY() { + @Test fun ctrlY() = runTest { writer.writeHex("19") assertThat(parser.next()).isEqualTo(KeyboardEvent('y'.code, modifiers = ModifierCtrl)) } - @Test fun ctrlZ() { + @Test fun ctrlZ() = runTest { writer.writeHex("1a") assertThat(parser.next()).isEqualTo(KeyboardEvent('z'.code, modifiers = ModifierCtrl)) } - @Test fun bareEscape() { + @Test fun bareEscape() = runTest { writer.writeHex("1b") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1b)) } - @Test fun hex1c() { + @Test fun hex1c() = runTest { writer.writeHex("1c") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1c)) } - @Test fun hex1d() { + @Test fun hex1d() = runTest { writer.writeHex("1d") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1d)) } - @Test fun hex1e() { + @Test fun hex1e() = runTest { writer.writeHex("1e") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1e)) } - @Test fun hex1f() { + @Test fun hex1f() = runTest { writer.writeHex("1f") assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1f)) } - @Test fun utf8TwoBytes() { + @Test fun utf8TwoBytes() = runTest { writer.writeHex("cea9") assertThat(parser.next()).isEqualTo(KeyboardEvent('Ω'.code)) } - @Test fun utf8ThreeBytes() { + @Test fun utf8ThreeBytes() = runTest { writer.writeHex("e28988") assertThat(parser.next()).isEqualTo(KeyboardEvent('≈'.code)) } diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserOscKittyPointerQueryEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserOscKittyPointerQueryEventTest.kt index a4225bee..6d1ae167 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserOscKittyPointerQueryEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserOscKittyPointerQueryEventTest.kt @@ -6,44 +6,45 @@ import com.jakewharton.mosaic.terminal.event.KittyPointerQueryNameEvent import com.jakewharton.mosaic.terminal.event.KittyPointerQuerySupportEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserOscKittyPointerQueryEventTest : BaseTerminalParserTest() { - @Test fun emptyFails() { + @Test fun emptyFails() = runTest { writer.writeHex("1b5d32323b1b5c") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5d32323b1b5c".hexToByteArray()), ) } - @Test fun valuesSingleFalse() { + @Test fun valuesSingleFalse() = runTest { writer.writeHex("1b5d32323b301b5c") assertThat(parser.next()).isEqualTo( KittyPointerQuerySupportEvent(booleanArrayOf(false)), ) } - @Test fun valuesSingleTrue() { + @Test fun valuesSingleTrue() = runTest { writer.writeHex("1b5d32323b311b5c") assertThat(parser.next()).isEqualTo( KittyPointerQuerySupportEvent(booleanArrayOf(true)), ) } - @Test fun valuesSingleValueTrailingCommaFails() { + @Test fun valuesSingleValueTrailingCommaFails() = runTest { writer.writeHex("1b5d32323b312c1b5c") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5d32323b312c1b5c".hexToByteArray()), ) } - @Test fun valuesMultiple() { + @Test fun valuesMultiple() = runTest { writer.writeHex("1b5d32323b302c302c312c312c301b5c") assertThat(parser.next()).isEqualTo( KittyPointerQuerySupportEvent(booleanArrayOf(false, false, true, true, false)), ) } - @Test fun valuesTons() { + @Test fun valuesTons() = runTest { writer.writeHex("1b5d32323b302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c301b5c") assertThat(parser.next()).isEqualTo( KittyPointerQuerySupportEvent( @@ -61,28 +62,28 @@ class TerminalParserOscKittyPointerQueryEventTest : BaseTerminalParserTest() { ) } - @Test fun nameSingleDigit() { + @Test fun nameSingleDigit() = runTest { writer.writeHex("1b5d32323b321b5c") assertThat(parser.next()).isEqualTo( KittyPointerQueryNameEvent("2"), ) } - @Test fun nameLeadingValueDigit() { + @Test fun nameLeadingValueDigit() = runTest { writer.writeHex("1b5d32323b30611b5c") assertThat(parser.next()).isEqualTo( KittyPointerQueryNameEvent("0a"), ) } - @Test fun nameValidRange() { + @Test fun nameValidRange() = runTest { writer.writeHex("1b5d32323b6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f1b5c") assertThat(parser.next()).isEqualTo( KittyPointerQueryNameEvent("abcdefghijklmnopqrstuvwxyz0123456789-_"), ) } - @Test fun nameInvalidRange() { + @Test fun nameInvalidRange() = runTest { writer.writeHex("1b5d32323b6162633132334142431b5c") assertThat(parser.next()).isEqualTo( UnknownEvent("1b5d32323b6162633132334142431b5c".hexToByteArray()), diff --git a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserSs3KeyboardEventTest.kt b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserSs3KeyboardEventTest.kt index 31a476b5..2d06088e 100644 --- a/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserSs3KeyboardEventTest.kt +++ b/mosaic-terminal/src/commonTest/kotlin/com/jakewharton/mosaic/terminal/TerminalParserSs3KeyboardEventTest.kt @@ -16,66 +16,67 @@ import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.Up import com.jakewharton.mosaic.terminal.event.OperatingStatusResponseEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.test.Test +import kotlinx.coroutines.test.runTest class TerminalParserSs3KeyboardEventTest : BaseTerminalParserTest() { - @Test fun up() { + @Test fun up() = runTest { writer.writeHex("1b4f41") assertThat(parser.next()).isEqualTo(KeyboardEvent(Up)) } - @Test fun down() { + @Test fun down() = runTest { writer.writeHex("1b4f42") assertThat(parser.next()).isEqualTo(KeyboardEvent(Down)) } - @Test fun right() { + @Test fun right() = runTest { writer.writeHex("1b4f43") assertThat(parser.next()).isEqualTo(KeyboardEvent(Right)) } - @Test fun left() { + @Test fun left() = runTest { writer.writeHex("1b4f44") assertThat(parser.next()).isEqualTo(KeyboardEvent(Left)) } - @Test fun end() { + @Test fun end() = runTest { writer.writeHex("1b4f46") assertThat(parser.next()).isEqualTo(KeyboardEvent(End)) } - @Test fun home() { + @Test fun home() = runTest { writer.writeHex("1b4f48") assertThat(parser.next()).isEqualTo(KeyboardEvent(Home)) } - @Test fun f1() { + @Test fun f1() = runTest { writer.writeHex("1b4f50") assertThat(parser.next()).isEqualTo(KeyboardEvent(F1)) } - @Test fun f2() { + @Test fun f2() = runTest { writer.writeHex("1b4f51") assertThat(parser.next()).isEqualTo(KeyboardEvent(F2)) } - @Test fun f3() { + @Test fun f3() = runTest { writer.writeHex("1b4f52") assertThat(parser.next()).isEqualTo(KeyboardEvent(F3)) } - @Test fun f4() { + @Test fun f4() = runTest { writer.writeHex("1b4f53") assertThat(parser.next()).isEqualTo(KeyboardEvent(F4)) } - @Test fun invalidKey() { + @Test fun invalidKey() = runTest { writer.writeHex("1b4f75") assertThat(parser.next()).isEqualTo( UnknownEvent("1b4f75".hexToByteArray()), ) } - @Test fun keyIsEscapeDoesNotConsumeEscape() { + @Test fun keyIsEscapeDoesNotConsumeEscape() = runTest { writer.writeHex("1b4f1b5b306e") assertThat(parser.next()).isEqualTo( UnknownEvent("1b4f".hexToByteArray()), diff --git a/mosaic-terminal/src/jvmMain/jni/mosaic-jni.c b/mosaic-terminal/src/jvmMain/jni/mosaic-jni.c index 330740be..c385b1ee 100644 --- a/mosaic-terminal/src/jvmMain/jni/mosaic-jni.c +++ b/mosaic-terminal/src/jvmMain/jni/mosaic-jni.c @@ -45,9 +45,135 @@ Java_com_jakewharton_mosaic_terminal_Jni_exitRawMode(JNIEnv *env, jclass type, j } } +typedef struct jniPlatformEventHandler { + JNIEnv *env; + jobject instance; + jclass clazz; + jmethodID onFocus; + jmethodID onKey; + jmethodID onMouse; + jmethodID onResize; +} jniPlatformEventHandler; + +void invokeOnFocusHandler(void *opaque, bool focused) { + jniPlatformEventHandler *handler = (jniPlatformEventHandler *) opaque; + (*handler->env)->CallNonvirtualVoidMethod( + handler->env, + handler->instance, + handler->clazz, + handler->onFocus, + focused + ); +} + +void invokeOnKeyHandler(void *opaque) { + jniPlatformEventHandler *handler = (jniPlatformEventHandler *) opaque; + (*handler->env)->CallNonvirtualVoidMethod( + handler->env, + handler->instance, + handler->clazz, + handler->onKey + ); +} + +void invokeOnMouseHandler(void *opaque) { + jniPlatformEventHandler *handler = (jniPlatformEventHandler *) opaque; + (*handler->env)->CallNonvirtualVoidMethod( + handler->env, + handler->instance, + handler->clazz, + handler->onMouse + ); +} + +void invokeOnResizeHandler(void *opaque, int columns, int rows, int width, int height) { + jniPlatformEventHandler *handler = (jniPlatformEventHandler *) opaque; + (*handler->env)->CallNonvirtualVoidMethod( + handler->env, + handler->instance, + handler->clazz, + handler->onResize, + columns, + rows, + width, + height + ); +} + JNIEXPORT jlong JNICALL -Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInit(JNIEnv *env, jclass type) { - stdinReaderResult result = stdinReader_init(); +Java_com_jakewharton_mosaic_terminal_Jni_platformEventHandlerInit( + JNIEnv *env, + jclass type, + jobject instance +) { + jobject globalInstance = (*env)->NewGlobalRef(env, instance); + if (unlikely(globalInstance == NULL)) { + return 0; + } + jclass clazz = (*env)->FindClass(env, "com/jakewharton/mosaic/terminal/PlatformEventHandler"); + if (unlikely(clazz == NULL)) { + return 0; + } + jmethodID onFocus = (*env)->GetMethodID(env, clazz, "onFocus", "(Z)V"); + if (unlikely(onFocus == NULL)) { + return 0; + } + jmethodID onKey = (*env)->GetMethodID(env, clazz, "onKey", "()V"); + if (unlikely(onKey == NULL)) { + return 0; + } + jmethodID onMouse = (*env)->GetMethodID(env, clazz, "onMouse", "()V"); + if (unlikely(onMouse == NULL)) { + return 0; + } + jmethodID onResize = (*env)->GetMethodID(env, clazz, "onResize", "(IIII)V"); + if (unlikely(onResize == NULL)) { + return 0; + } + + jniPlatformEventHandler *jniHandler = malloc(sizeof(jniPlatformEventHandler)); + if (unlikely(!jniHandler)) { + return 0; + } + jniHandler->env = env; + jniHandler->instance = globalInstance; + jniHandler->clazz = clazz; + jniHandler->onFocus = onFocus; + jniHandler->onKey = onKey; + jniHandler->onMouse = onMouse; + jniHandler->onResize = onResize; + + platformEventHandler *handler = malloc(sizeof(platformEventHandler)); + if (unlikely(!handler)) { + return 0; + } + handler->opaque = jniHandler; + handler->onFocus = invokeOnFocusHandler; + handler->onKey = invokeOnKeyHandler; + handler->onMouse = invokeOnMouseHandler; + handler->onResize = invokeOnResizeHandler; + + return (jlong) handler; +} + +JNIEXPORT void JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_platformEventHandlerFree( + JNIEnv *env, + jclass type, + jlong handlerOpaque +) { + platformEventHandler *handler = (platformEventHandler *) handlerOpaque; + jniPlatformEventHandler *jniHandler = handler->opaque; + jobject instance = jniHandler->instance; + free(handler); + free(jniHandler); + (*env)->DeleteGlobalRef(env, instance); +} + +JNIEXPORT jlong JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInit(JNIEnv *env, jclass type, jlong handlerOpaque) { + platformEventHandler *handler = (platformEventHandler *) handlerOpaque; + stdinReaderResult result = stdinReader_init(handler); if (likely(!result.error)) { return (jlong) result.reader; } @@ -62,7 +188,7 @@ JNIEXPORT jint JNICALL Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderRead( JNIEnv *env, jclass type, - jlong ptr, + jlong readerOpaque, jbyteArray buffer, jint offset, jint count @@ -70,7 +196,8 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderRead( jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); jbyte *nativeBufferAtOffset = nativeBuffer + offset; - stdinRead read = stdinReader_read((stdinReader *) ptr, nativeBufferAtOffset, count); + stdinReader *reader = (stdinReader *) readerOpaque; + stdinRead read = stdinReader_read(reader, nativeBufferAtOffset, count); (*env)->ReleaseByteArrayElements(env, buffer, nativeBuffer, 0); @@ -88,7 +215,7 @@ JNIEXPORT jint JNICALL Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout( JNIEnv *env, jclass type, - jlong ptr, + jlong readerOpaque, jbyteArray buffer, jint offset, jint count, @@ -97,8 +224,9 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout( jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); jbyte *nativeBufferAtOffset = nativeBuffer + offset; + stdinReader *reader = (stdinReader *) readerOpaque; stdinRead read = stdinReader_readWithTimeout( - (stdinReader *) ptr, + reader, nativeBufferAtOffset, count, timeoutMillis @@ -117,24 +245,27 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout( } JNIEXPORT void JNICALL -Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInterrupt(JNIEnv *env, jclass type, jlong ptr) { - platformError error = stdinReader_interrupt((stdinReader *) ptr); +Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInterrupt(JNIEnv *env, jclass type, jlong readerOpaque) { + stdinReader *reader = (stdinReader *) readerOpaque; + platformError error = stdinReader_interrupt(reader); if (unlikely(error)) { throwIse(env, error, "Unable to interrupt stdin reader"); } } JNIEXPORT void JNICALL -Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderFree(JNIEnv *env, jclass type, jlong ptr) { - platformError error = stdinReader_free((stdinReader *) ptr); +Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderFree(JNIEnv *env, jclass type, jlong readerOpaque) { + stdinReader *reader = (stdinReader *) readerOpaque; + platformError error = stdinReader_free(reader); if (unlikely(error)) { throwIse(env, error, "Unable to free stdin reader"); } } JNIEXPORT jlong JNICALL -Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterInit(JNIEnv *env, jclass type) { - stdinWriterResult result = stdinWriter_init(); +Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterInit(JNIEnv *env, jclass type, jlong handlerOpaque) { + platformEventHandler *handler = (platformEventHandler *) handlerOpaque; + stdinWriterResult result = stdinWriter_init(handler); if (likely(!result.error)) { return (jlong) result.writer; } @@ -149,13 +280,14 @@ JNIEXPORT void JNICALL Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterWrite( JNIEnv *env, jclass type, - jlong ptr, + jlong writerOpaque, jbyteArray buffer ) { jsize count = (*env)->GetArrayLength(env, buffer); jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); - platformError error = stdinWriter_write((stdinWriter *) ptr, nativeBuffer, count); + stdinWriter *writer = (stdinWriter *) writerOpaque; + platformError error = stdinWriter_write(writer, nativeBuffer, count); (*env)->ReleaseByteArrayElements(env, buffer, nativeBuffer, 0); @@ -165,6 +297,51 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterWrite( } } +JNIEXPORT void JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterFocusEvent( + JNIEnv *env, + jclass type, + jlong writerOpaque, + bool focused +) { + stdinWriter *writer = (stdinWriter *) writerOpaque; + stdinWriter_focusEvent(writer, focused); +} + +JNIEXPORT void JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterKeyEvent( + JNIEnv *env, + jclass type, + jlong writerOpaque +) { + stdinWriter *writer = (stdinWriter *) writerOpaque; + stdinWriter_keyEvent(writer); +} + +JNIEXPORT void JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterMouseEvent( + JNIEnv *env, + jclass type, + jlong writerOpaque +) { + stdinWriter *writer = (stdinWriter *) writerOpaque; + stdinWriter_mouseEvent(writer); +} + +JNIEXPORT void JNICALL +Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterResizeEvent( + JNIEnv *env, + jclass type, + jlong writerOpaque, + jint columns, + jint rows, + jint width, + jint height +) { + stdinWriter *writer = (stdinWriter *) writerOpaque; + stdinWriter_resizeEvent(writer, columns, rows, width, height); +} + JNIEXPORT jlong JNICALL Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterGetReader(JNIEnv *env, jclass type, jlong ptr) { return (jlong) stdinWriter_getReader((stdinWriter *) ptr); diff --git a/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Jni.kt b/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Jni.kt index 656fffaf..279b59e0 100644 --- a/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Jni.kt +++ b/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Jni.kt @@ -19,7 +19,13 @@ internal object Jni { external fun exitRawMode(savedConfig: Long) @JvmStatic - external fun stdinReaderInit(): Long + external fun platformEventHandlerInit(handler: PlatformEventHandler): Long + + @JvmStatic + external fun platformEventHandlerFree(handler: Long) + + @JvmStatic + external fun stdinReaderInit(handler: Long): Long @JvmStatic external fun stdinReaderRead( @@ -45,7 +51,7 @@ internal object Jni { external fun stdinReaderFree(reader: Long) @JvmStatic - external fun stdinWriterInit(): Long + external fun stdinWriterInit(handler: Long): Long @JvmStatic external fun stdinWriterGetReader(writer: Long): Long @@ -53,6 +59,24 @@ internal object Jni { @JvmStatic external fun stdinWriterWrite(writer: Long, buffer: ByteArray) + @JvmStatic + external fun stdinWriterFocusEvent(writer: Long, focused: Boolean) + + @JvmStatic + external fun stdinWriterKeyEvent(writer: Long) + + @JvmStatic + external fun stdinWriterMouseEvent(writer: Long) + + @JvmStatic + external fun stdinWriterResizeEvent( + writer: Long, + columns: Int, + rows: Int, + width: Int, + height: Int, + ) + @JvmStatic external fun stdinWriterFree(writer: Long) diff --git a/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt b/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt index 00a4d424..3d36fba5 100644 --- a/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt +++ b/mosaic-terminal/src/jvmMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt @@ -2,15 +2,24 @@ package com.jakewharton.mosaic.terminal import com.jakewharton.mosaic.terminal.Jni.enterRawMode import com.jakewharton.mosaic.terminal.Jni.exitRawMode +import com.jakewharton.mosaic.terminal.Jni.platformEventHandlerFree +import com.jakewharton.mosaic.terminal.Jni.platformEventHandlerInit import com.jakewharton.mosaic.terminal.Jni.stdinReaderFree import com.jakewharton.mosaic.terminal.Jni.stdinReaderInit import com.jakewharton.mosaic.terminal.Jni.stdinReaderInterrupt import com.jakewharton.mosaic.terminal.Jni.stdinReaderRead import com.jakewharton.mosaic.terminal.Jni.stdinReaderReadWithTimeout +import com.jakewharton.mosaic.terminal.Jni.stdinWriterFocusEvent import com.jakewharton.mosaic.terminal.Jni.stdinWriterFree import com.jakewharton.mosaic.terminal.Jni.stdinWriterGetReader import com.jakewharton.mosaic.terminal.Jni.stdinWriterInit +import com.jakewharton.mosaic.terminal.Jni.stdinWriterKeyEvent +import com.jakewharton.mosaic.terminal.Jni.stdinWriterMouseEvent +import com.jakewharton.mosaic.terminal.Jni.stdinWriterResizeEvent import com.jakewharton.mosaic.terminal.Jni.stdinWriterWrite +import com.jakewharton.mosaic.terminal.event.Event +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED public actual object Tty { @JvmStatic @@ -29,53 +38,94 @@ public actual object Tty { } @JvmStatic - public actual fun stdinReader(): StdinReader { - val reader = stdinReaderInit() - if (reader == 0L) throw OutOfMemoryError() - return StdinReader(reader) + public actual fun terminalReader(emitDebugEvents: Boolean): TerminalReader { + val events = Channel(UNLIMITED) + val handlerPtr = platformEventHandlerInit(PlatformEventHandler(events)) + if (handlerPtr != 0L) { + val readerPtr = stdinReaderInit(handlerPtr) + if (readerPtr != 0L) { + val platformInput = PlatformInput(readerPtr, handlerPtr) + return TerminalReader(platformInput, events, emitDebugEvents) + } + platformEventHandlerFree(handlerPtr) + } + throw OutOfMemoryError() } @JvmSynthetic // Hide from Java callers. - internal actual fun stdinWriter(): StdinWriter { - val writer = stdinWriterInit() - if (writer == 0L) throw OutOfMemoryError() - val reader = stdinWriterGetReader(writer) - return StdinWriter(writer, reader) + internal actual fun stdinWriter(emitDebugEvents: Boolean): StdinWriter { + val events = Channel(UNLIMITED) + val handlerPtr = platformEventHandlerInit(PlatformEventHandler(events)) + if (handlerPtr != 0L) { + val writerPtr = stdinWriterInit(handlerPtr) + if (writerPtr != 0L) { + val readerPtr = stdinWriterGetReader(writerPtr) + val platformInput = PlatformInput(readerPtr, handlerPtr) + val terminalReader = TerminalReader(platformInput, events, emitDebugEvents) + return StdinWriter(writerPtr, terminalReader) + } + platformEventHandlerFree(handlerPtr) + } + throw OutOfMemoryError() } } -public actual class StdinReader internal constructor( - private val readerPtr: Long, +// TODO @JvmSynthetic https://youtrack.jetbrains.com/issue/KT-24981 +internal actual class PlatformInput internal constructor( + private var readerPtr: Long, + private val handlerPtr: Long, ) : AutoCloseable { - public actual fun read(buffer: ByteArray, offset: Int, count: Int): Int { + actual fun read(buffer: ByteArray, offset: Int, count: Int): Int { return stdinReaderRead(readerPtr, buffer, offset, count) } - public actual fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int { + actual fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int { return stdinReaderReadWithTimeout(readerPtr, buffer, offset, count, timeoutMillis) } - public actual fun interrupt() { + actual fun interrupt() { stdinReaderInterrupt(readerPtr) } - public actual override fun close() { - stdinReaderFree(readerPtr) + actual override fun close() { + if (readerPtr != 0L) { + stdinReaderFree(readerPtr) + readerPtr = 0 + platformEventHandlerFree(handlerPtr) + } } } // TODO @JvmSynthetic https://youtrack.jetbrains.com/issue/KT-24981 internal actual class StdinWriter internal constructor( - private val writerPtr: Long, - readerPtr: Long, + private var writerPtr: Long, + actual val reader: TerminalReader, ) : AutoCloseable { - actual val reader: StdinReader = StdinReader(readerPtr) - actual fun write(buffer: ByteArray) { stdinWriterWrite(writerPtr, buffer) } + actual fun focusEvent(focused: Boolean) { + stdinWriterFocusEvent(writerPtr, focused) + } + + actual fun keyEvent() { + stdinWriterKeyEvent(writerPtr) + } + + actual fun mouseEvent() { + stdinWriterMouseEvent(writerPtr) + } + + actual fun resizeEvent(columns: Int, rows: Int, width: Int, height: Int) { + stdinWriterResizeEvent(writerPtr, columns, rows, width, height) + } + actual override fun close() { - stdinWriterFree(writerPtr) + reader.close() + if (writerPtr != 0L) { + stdinWriterFree(writerPtr) + writerPtr = 0 + } } } diff --git a/mosaic-terminal/src/jvmMain/resources/META-INF/proguard/com.jakewharton.mosaic-terminal.pro b/mosaic-terminal/src/jvmMain/resources/META-INF/proguard/com.jakewharton.mosaic-terminal.pro index 5640342b..62fc7f95 100644 --- a/mosaic-terminal/src/jvmMain/resources/META-INF/proguard/com.jakewharton.mosaic-terminal.pro +++ b/mosaic-terminal/src/jvmMain/resources/META-INF/proguard/com.jakewharton.mosaic-terminal.pro @@ -2,3 +2,11 @@ -keep,allowoptimization class com.jakewharton.mosaic.terminal.Jni { native ; } + +# These members are interacted with through native code. +-keep,allowoptimization class com.jakewharton.mosaic.terminal.PlatformEventHandler { + void onFocus(...); + void onKey(...); + void onMouse(...); + void onResize(...); +} diff --git a/mosaic-terminal/src/nativeMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt b/mosaic-terminal/src/nativeMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt index 02c53dd5..537e8487 100644 --- a/mosaic-terminal/src/nativeMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt +++ b/mosaic-terminal/src/nativeMain/kotlin/com/jakewharton/mosaic/terminal/Tty.kt @@ -1,9 +1,20 @@ package com.jakewharton.mosaic.terminal +import com.jakewharton.mosaic.terminal.event.Event +import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.CPointer +import kotlinx.cinterop.StableRef import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.free +import kotlinx.cinterop.nativeHeap +import kotlinx.cinterop.ptr +import kotlinx.cinterop.staticCFunction import kotlinx.cinterop.useContents import kotlinx.cinterop.usePinned +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED public actual object Tty { public actual fun enableRawMode(): AutoCloseable { @@ -24,21 +35,61 @@ public actual object Tty { } } - public actual fun stdinReader(): StdinReader { - val reader = stdinReader_init().useContents { + public actual fun terminalReader(emitDebugEvents: Boolean): TerminalReader { + val events = Channel(UNLIMITED) + + val handler = PlatformEventHandler(events) + val handlerRef = StableRef.create(handler) + val handlerPtr = nativeHeap.alloc { + opaque = handlerRef.asCPointer() + onFocus = staticCFunction(::onFocusCallback) + onKey = staticCFunction(::onKeyCallback) + onMouse = staticCFunction(::onMouseCallback) + onResize = staticCFunction(::onResizeCallback) + }.ptr + + val readerPtr = stdinReader_init(handlerPtr).useContents { + reader?.let { return@useContents it } + + nativeHeap.free(handlerPtr) + handlerRef.dispose() + check(error == 0U) { "Unable to create stdin reader: $error" } - reader ?: throw OutOfMemoryError() + throw OutOfMemoryError() } - return StdinReader(reader) + + val reader = PlatformInput(readerPtr, handlerPtr, handlerRef) + return TerminalReader(reader, events, emitDebugEvents) } - internal actual fun stdinWriter(): StdinWriter { - val writer = stdinWriter_init().useContents { + internal actual fun stdinWriter(emitDebugEvents: Boolean): StdinWriter { + val events = Channel(UNLIMITED) + + // TODO Fix all this duplication, ownership + val handler = PlatformEventHandler(events) + val handlerRef = StableRef.create(handler) + val handlerPtr = nativeHeap.alloc { + opaque = handlerRef.asCPointer() + onFocus = staticCFunction(::onFocusCallback) + onKey = staticCFunction(::onKeyCallback) + onMouse = staticCFunction(::onMouseCallback) + onResize = staticCFunction(::onResizeCallback) + }.ptr + + val writerPtr = stdinWriter_init(handlerPtr).useContents { + writer?.let { return@useContents it } + + nativeHeap.free(handlerPtr) + handlerRef.dispose() + check(error == 0U) { "Unable to create stdin writer: $error" } - writer ?: throw OutOfMemoryError() + throw OutOfMemoryError() } - val reader = stdinWriter_getReader(writer)!! - return StdinWriter(writer, reader) + + val readerPtr = stdinWriter_getReader(writerPtr)!! + val platformInput = PlatformInput(readerPtr, handlerPtr, handlerRef) + val terminalReader = TerminalReader(platformInput, events, emitDebugEvents) + return StdinWriter(writerPtr, terminalReader) } internal fun throwError(error: UInt): Nothing { @@ -46,38 +97,45 @@ public actual object Tty { } } -public actual class StdinReader internal constructor( - private var ref: CPointer?, +internal actual class PlatformInput internal constructor( + ptr: CPointer, + private val handlerPtr: CPointer?, + private val handlerRef: StableRef?, ) : AutoCloseable { - public actual fun read(buffer: ByteArray, offset: Int, count: Int): Int { + private var ptr: CPointer? = ptr + + actual fun read(buffer: ByteArray, offset: Int, count: Int): Int { buffer.usePinned { - stdinReader_read(ref, it.addressOf(offset), count).useContents { + stdinReader_read(ptr, it.addressOf(offset), count).useContents { if (error == 0U) return this.count Tty.throwError(error) } } } - public actual fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int { + actual fun readWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int { buffer.usePinned { - stdinReader_readWithTimeout(ref, it.addressOf(offset), count, timeoutMillis).useContents { + stdinReader_readWithTimeout(ptr, it.addressOf(offset), count, timeoutMillis).useContents { if (error == 0U) return this.count Tty.throwError(error) } } } - public actual fun interrupt() { - val error = stdinReader_interrupt(ref) + actual fun interrupt() { + val error = stdinReader_interrupt(ptr) if (error == 0U) return Tty.throwError(error) } - public actual override fun close() { - ref?.let { ref -> - this.ref = null + actual override fun close() { + ptr?.let { ptr -> + this.ptr = null + + val error = stdinReader_free(ptr) + handlerPtr?.let(nativeHeap::free) + handlerRef?.dispose() - val error = stdinReader_free(ref) if (error == 0U) return Tty.throwError(error) } @@ -85,22 +143,36 @@ public actual class StdinReader internal constructor( } internal actual class StdinWriter internal constructor( - private var ref: CPointer?, - readerRef: CPointer, + private var ptr: CPointer?, + actual val reader: TerminalReader, ) : AutoCloseable { - actual val reader: StdinReader = StdinReader(readerRef) - actual fun write(buffer: ByteArray) { val error = buffer.usePinned { - stdinWriter_write(ref, it.addressOf(0), buffer.size) + stdinWriter_write(ptr, it.addressOf(0), buffer.size) } if (error == 0U) return Tty.throwError(error) } + actual fun focusEvent(focused: Boolean) { + stdinWriter_focusEvent(ptr, focused) + } + + actual fun keyEvent() { + stdinWriter_keyEvent(ptr) + } + + actual fun mouseEvent() { + stdinWriter_mouseEvent(ptr) + } + + actual fun resizeEvent(columns: Int, rows: Int, width: Int, height: Int) { + stdinWriter_resizeEvent(ptr, columns, rows, width, height) + } + actual override fun close() { - ref?.let { ref -> - this.ref = null + ptr?.let { ref -> + this.ptr = null reader.close() @@ -111,3 +183,23 @@ internal actual class StdinWriter internal constructor( } } } + +private fun onFocusCallback(opaque: COpaquePointer?, focused: Boolean) { + val handler = opaque!!.asStableRef().get() + handler.onFocus(focused) +} + +private fun onKeyCallback(opaque: COpaquePointer?) { + val handler = opaque!!.asStableRef().get() + handler.onKey() +} + +private fun onMouseCallback(opaque: COpaquePointer?) { + val handler = opaque!!.asStableRef().get() + handler.onMouse() +} + +private fun onResizeCallback(opaque: COpaquePointer?, columns: Int, rows: Int, width: Int, height: Int) { + val handler = opaque!!.asStableRef().get() + handler.onResize(columns, rows, width, height) +} 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 91796bbe..63f06d78 100644 --- a/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt +++ b/tools/raw-mode-echo/src/commonMain/kotlin/example/main.kt @@ -9,19 +9,15 @@ import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.enum import com.jakewharton.finalization.withFinalizationHook -import com.jakewharton.mosaic.terminal.TerminalParser import com.jakewharton.mosaic.terminal.Tty +import com.jakewharton.mosaic.terminal.event.DebugEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl -import com.jakewharton.mosaic.terminal.event.UnknownEvent import kotlin.jvm.JvmName import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -89,7 +85,7 @@ private class RawModeEchoCommand : CliktCommand("raw-mode-echo") { print("\u001b]4;$i;?\u001b\\") } - val reader = Tty.stdinReader() + val reader = Tty.terminalReader(emitDebugEvents = mode == Mode.Hex) // Upon receiving a signal, this block's job will be canceled. Use that to wake up the // blocking stdin read so it loops and checks if its job is still active or not. @@ -101,52 +97,31 @@ private class RawModeEchoCommand : CliktCommand("raw-mode-echo") { } } - val inputs = Channel(UNLIMITED) launch(Dispatchers.IO) { - val job = coroutineContext.job + reader.runParseLoop() + } + + var first = true + val events = reader.events + while (true) { + val event = events.receive() + + if (!first) print("\r\n") + first = false + when (mode) { - Mode.Hex -> { - val buffer = ByteArray(1024) - while (job.isActive) { - val read = reader.read(buffer, 0, 1024) - if (read > 0) { - val hex = buffer.toHexString(endIndex = read) - inputs.trySend(hex) - if (hex == "03" || hex == "1b5b39393b3575") { - break - } - } - } - } - Mode.Event -> { - val parser = TerminalParser(reader) - while (job.isActive) { - val (event, bytes) = parser.debugNext() - if (event is UnknownEvent) { - // The bytes are already displayed by this event. - inputs.trySend("$event\r\n") - } else { - val hex = bytes.toHexString() - inputs.trySend("$event\r\n $hex\r\n") - } - - if (event is KeyboardEvent && - event.codepoint == 0x63 && - event.modifiers == ModifierCtrl - ) { - break - } - } - } + Mode.Hex -> print((events.receive() as DebugEvent).bytes.toHexString()) + Mode.Event -> print(event.toString()) + } + + if (event is KeyboardEvent && + event.codepoint == 0x63 && + event.modifiers == ModifierCtrl + ) { + break } - inputs.close() } - print(inputs.receive()) - for (input in inputs) { - print("\r\n") - print(input) - } print("\r\n") readerInterruptJob.cancel() },