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.
This commit is contained in:
Jake Wharton
2025-01-24 00:25:38 -05:00
committed by GitHub
parent 44ef3f4706
commit fe34cd2f29
37 changed files with 888 additions and 352 deletions

View File

@ -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 fun close ()V
public final fun interrupt ()V public final fun getEvents ()Lkotlinx/coroutines/channels/ReceiveChannel;
public final fun read ([BII)I
public final fun readWithTimeout ([BIII)I
}
public final class com/jakewharton/mosaic/terminal/TerminalParser {
public fun <init> (Lcom/jakewharton/mosaic/terminal/StdinReader;)V
public final fun debugNext ()Lkotlin/Pair;
public final fun getKittyDisambiguateEscapeCodes ()Z public final fun getKittyDisambiguateEscapeCodes ()Z
public final fun getXtermExtendedUtf8Mouse ()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 setKittyDisambiguateEscapeCodes (Z)V
public final fun setXtermExtendedUtf8Mouse (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 final class com/jakewharton/mosaic/terminal/Tty {
public static final field INSTANCE Lcom/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 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 { 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 fun toString ()Ljava/lang/String;
} }
public final class com/jakewharton/mosaic/terminal/event/DebugEvent : com/jakewharton/mosaic/terminal/event/Event {
public fun <init> (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 final class com/jakewharton/mosaic/terminal/event/DecModeReportEvent : com/jakewharton/mosaic/terminal/event/Event {
public fun <init> (ILcom/jakewharton/mosaic/terminal/event/DecModeReportEvent$Setting;)V public fun <init> (ILcom/jakewharton/mosaic/terminal/event/DecModeReportEvent$Setting;)V
public fun equals (Ljava/lang/Object;)Z public fun equals (Ljava/lang/Object;)Z

View File

@ -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 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 <init>(com.jakewharton.mosaic.terminal.event/Event, kotlin/ByteArray) // com.jakewharton.mosaic.terminal.event/DebugEvent.<init>|<init>(com.jakewharton.mosaic.terminal.event.Event;kotlin.ByteArray){}[0]
final val bytes // com.jakewharton.mosaic.terminal.event/DebugEvent.bytes|{}bytes[0]
final fun <get-bytes>(): kotlin/ByteArray // com.jakewharton.mosaic.terminal.event/DebugEvent.bytes.<get-bytes>|<get-bytes>(){}[0]
final val event // com.jakewharton.mosaic.terminal.event/DebugEvent.event|{}event[0]
final fun <get-event>(): com.jakewharton.mosaic.terminal.event/Event // com.jakewharton.mosaic.terminal.event/DebugEvent.event.<get-event>|<get-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] final class com.jakewharton.mosaic.terminal.event/DecModeReportEvent : com.jakewharton.mosaic.terminal.event/Event { // com.jakewharton.mosaic.terminal.event/DecModeReportEvent|null[0]
constructor <init>(kotlin/Int, com.jakewharton.mosaic.terminal.event/DecModeReportEvent.Setting) // com.jakewharton.mosaic.terminal.event/DecModeReportEvent.<init>|<init>(kotlin.Int;com.jakewharton.mosaic.terminal.event.DecModeReportEvent.Setting){}[0] constructor <init>(kotlin/Int, com.jakewharton.mosaic.terminal.event/DecModeReportEvent.Setting) // com.jakewharton.mosaic.terminal.event/DecModeReportEvent.<init>|<init>(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 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 class com.jakewharton.mosaic.terminal/TerminalReader : kotlin/AutoCloseable { // com.jakewharton.mosaic.terminal/TerminalReader|null[0]
final fun close() // com.jakewharton.mosaic.terminal/StdinReader.close|close(){}[0] final val events // com.jakewharton.mosaic.terminal/TerminalReader.events|{}events[0]
final fun interrupt() // com.jakewharton.mosaic.terminal/StdinReader.interrupt|interrupt(){}[0] final fun <get-events>(): kotlinx.coroutines.channels/ReceiveChannel<com.jakewharton.mosaic.terminal.event/Event> // com.jakewharton.mosaic.terminal/TerminalReader.events.<get-events>|<get-events>(){}[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/TerminalParser { // com.jakewharton.mosaic.terminal/TerminalParser|null[0] final var kittyDisambiguateEscapeCodes // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes|{}kittyDisambiguateEscapeCodes[0]
constructor <init>(com.jakewharton.mosaic.terminal/StdinReader) // com.jakewharton.mosaic.terminal/TerminalParser.<init>|<init>(com.jakewharton.mosaic.terminal.StdinReader){}[0] final fun <get-kittyDisambiguateEscapeCodes>(): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes.<get-kittyDisambiguateEscapeCodes>|<get-kittyDisambiguateEscapeCodes>(){}[0]
final fun <set-kittyDisambiguateEscapeCodes>(kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalReader.kittyDisambiguateEscapeCodes.<set-kittyDisambiguateEscapeCodes>|<set-kittyDisambiguateEscapeCodes>(kotlin.Boolean){}[0]
final var xtermExtendedUtf8Mouse // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse|{}xtermExtendedUtf8Mouse[0]
final fun <get-xtermExtendedUtf8Mouse>(): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse.<get-xtermExtendedUtf8Mouse>|<get-xtermExtendedUtf8Mouse>(){}[0]
final fun <set-xtermExtendedUtf8Mouse>(kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalReader.xtermExtendedUtf8Mouse.<set-xtermExtendedUtf8Mouse>|<set-xtermExtendedUtf8Mouse>(kotlin.Boolean){}[0]
final var kittyDisambiguateEscapeCodes // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes|{}kittyDisambiguateEscapeCodes[0] final fun close() // com.jakewharton.mosaic.terminal/TerminalReader.close|close(){}[0]
final fun <get-kittyDisambiguateEscapeCodes>(): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes.<get-kittyDisambiguateEscapeCodes>|<get-kittyDisambiguateEscapeCodes>(){}[0] final fun interrupt() // com.jakewharton.mosaic.terminal/TerminalReader.interrupt|interrupt(){}[0]
final fun <set-kittyDisambiguateEscapeCodes>(kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalParser.kittyDisambiguateEscapeCodes.<set-kittyDisambiguateEscapeCodes>|<set-kittyDisambiguateEscapeCodes>(kotlin.Boolean){}[0] final fun runParseLoop() // com.jakewharton.mosaic.terminal/TerminalReader.runParseLoop|runParseLoop(){}[0]
final var xtermExtendedUtf8Mouse // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse|{}xtermExtendedUtf8Mouse[0]
final fun <get-xtermExtendedUtf8Mouse>(): kotlin/Boolean // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse.<get-xtermExtendedUtf8Mouse>|<get-xtermExtendedUtf8Mouse>(){}[0]
final fun <set-xtermExtendedUtf8Mouse>(kotlin/Boolean) // com.jakewharton.mosaic.terminal/TerminalParser.xtermExtendedUtf8Mouse.<set-xtermExtendedUtf8Mouse>|<set-xtermExtendedUtf8Mouse>(kotlin.Boolean){}[0]
final fun debugNext(): kotlin/Pair<com.jakewharton.mosaic.terminal.event/Event, kotlin/ByteArray> // 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 object com.jakewharton.mosaic.terminal/Tty { // com.jakewharton.mosaic.terminal/Tty|null[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 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]
} }

View File

@ -38,17 +38,23 @@ kotlin {
} }
if (it.name.endsWith("Test")) { if (it.name.endsWith("Test")) {
languageSettings { languageSettings {
optIn('com.jakewharton.mosaic.terminal.TestApi')
optIn('kotlin.ExperimentalStdlibApi') optIn('kotlin.ExperimentalStdlibApi')
optIn('kotlinx.coroutines.DelicateCoroutinesApi') optIn('kotlinx.coroutines.DelicateCoroutinesApi')
} }
} }
} }
commonMain {
dependencies {
api libs.kotlinx.coroutines.core
}
}
commonTest { commonTest {
dependencies { dependencies {
implementation libs.kotlin.test implementation libs.kotlin.test
implementation libs.kotlinx.coroutines.test
implementation libs.kotlinx.io implementation libs.kotlinx.io
implementation libs.kotlinx.coroutines.core
implementation libs.assertk implementation libs.assertk
} }
} }

View File

@ -4,4 +4,6 @@
#define likely(x) __builtin_expect(!!(x), 1) #define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0) #define unlikely(x) __builtin_expect(!!(x), 0)
#define UNUSED __attribute__((unused))
#endif // CUTILS_H #endif // CUTILS_H

View File

@ -15,6 +15,7 @@ typedef struct stdinReaderImpl {
int pipe[2]; int pipe[2];
fd_set fds; fd_set fds;
int nfds; int nfds;
platformEventHandler *handler;
} stdinReaderImpl; } stdinReaderImpl;
typedef struct stdinWriterImpl { typedef struct stdinWriterImpl {
@ -22,7 +23,7 @@ typedef struct stdinWriterImpl {
stdinReader *reader; stdinReader *reader;
} stdinWriterImpl; } stdinWriterImpl;
stdinReaderResult stdinReader_initWithFd(int stdinFd) { stdinReaderResult stdinReader_initWithFd(int stdinFd, platformEventHandler *handler) {
stdinReaderResult result = {}; stdinReaderResult result = {};
stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl)); 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. // 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. // 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->nfds = ((stdinFd > reader->pipe[0]) ? stdinFd : reader->pipe[0]) + 1;
reader->handler = handler;
result.reader = reader; result.reader = reader;
@ -51,8 +53,8 @@ stdinReaderResult stdinReader_initWithFd(int stdinFd) {
goto ret; goto ret;
} }
stdinReaderResult stdinReader_init() { stdinReaderResult stdinReader_init(platformEventHandler *handler) {
return stdinReader_initWithFd(STDIN_FILENO); return stdinReader_initWithFd(STDIN_FILENO, handler);
} }
stdinRead stdinReader_readInternal( stdinRead stdinReader_readInternal(
@ -139,7 +141,7 @@ platformError stdinReader_free(stdinReader *reader) {
return result; return result;
} }
stdinWriterResult stdinWriter_init() { stdinWriterResult stdinWriter_init(platformEventHandler *handler) {
stdinWriterResult result = {}; stdinWriterResult result = {};
stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl)); stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl));
@ -153,7 +155,7 @@ stdinWriterResult stdinWriter_init() {
goto err; goto err;
} }
stdinReaderResult readerResult = stdinReader_initWithFd(writer->pipe[0]); stdinReaderResult readerResult = stdinReader_initWithFd(writer->pipe[0], handler);
if (unlikely(readerResult.error)) { if (unlikely(readerResult.error)) {
result.error = readerResult.error; result.error = readerResult.error;
goto err; goto err;
@ -189,6 +191,23 @@ platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count) {
return errno; 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) { platformError stdinWriter_free(stdinWriter *writer) {
int *pipe = writer->pipe; int *pipe = writer->pipe;

View File

@ -8,6 +8,7 @@
typedef struct stdinReaderImpl { typedef struct stdinReaderImpl {
HANDLE waitHandles[2]; HANDLE waitHandles[2];
HANDLE readHandle; HANDLE readHandle;
platformEventHandler *handler;
} stdinReaderImpl; } stdinReaderImpl;
typedef struct stdinWriterImpl { typedef struct stdinWriterImpl {
@ -17,7 +18,11 @@ typedef struct stdinWriterImpl {
stdinReader *reader; stdinReader *reader;
} stdinWriterImpl; } stdinWriterImpl;
stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait) { stdinReaderResult stdinReader_initWithHandle(
HANDLE stdinRead,
HANDLE stdinWait,
platformEventHandler *handler
) {
stdinReaderResult result = {}; stdinReaderResult result = {};
stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl)); stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl));
@ -30,15 +35,17 @@ stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait)
result.error = GetLastError(); result.error = GetLastError();
goto err; goto err;
} }
reader->readHandle = stdinRead;
reader->waitHandles[0] = stdinWait;
HANDLE interruptEvent = CreateEvent(NULL, FALSE, FALSE, NULL); HANDLE interruptEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (unlikely(interruptEvent == NULL)) { if (unlikely(interruptEvent == NULL)) {
result.error = GetLastError(); result.error = GetLastError();
goto err; goto err;
} }
reader->waitHandles[0] = stdinWait;
reader->waitHandles[1] = interruptEvent; reader->waitHandles[1] = interruptEvent;
reader->readHandle = stdinRead;
reader->handler = handler;
result.reader = reader; result.reader = reader;
@ -50,9 +57,9 @@ stdinReaderResult stdinReader_initWithHandle(HANDLE stdinRead, HANDLE stdinWait)
goto ret; goto ret;
} }
stdinReaderResult stdinReader_init() { stdinReaderResult stdinReader_init(platformEventHandler *handler) {
HANDLE h = GetStdHandle(STD_INPUT_HANDLE); HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
return stdinReader_initWithHandle(h, h); return stdinReader_initWithHandle(h, h, handler);
} }
stdinRead stdinReader_read( stdinRead stdinReader_read(
@ -107,7 +114,7 @@ platformError stdinReader_free(stdinReader *reader) {
return result; return result;
} }
stdinWriterResult stdinWriter_init() { stdinWriterResult stdinWriter_init(platformEventHandler *handler) {
stdinWriterResult result = {}; stdinWriterResult result = {};
stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl)); stdinWriterImpl *writer = calloc(1, sizeof(stdinWriterImpl));
@ -128,7 +135,7 @@ stdinWriterResult stdinWriter_init() {
} }
writer->eventHandle = writeEvent; writer->eventHandle = writeEvent;
stdinReaderResult readerResult = stdinReader_initWithHandle(writer->readHandle, writer->eventHandle); stdinReaderResult readerResult = stdinReader_initWithHandle(writer->readHandle, writer->eventHandle, handler);
if (unlikely(readerResult.error)) { if (unlikely(readerResult.error)) {
result.error = readerResult.error; result.error = readerResult.error;
goto err; goto err;
@ -160,6 +167,26 @@ platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count) {
return GetLastError(); 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) { platformError stdinWriter_free(stdinWriter *writer) {
DWORD result = 0; DWORD result = 0;
if (unlikely(CloseHandle(writer->eventHandle) == 0)) { if (unlikely(CloseHandle(writer->eventHandle) == 0)) {

View File

@ -1,6 +1,8 @@
#ifndef MOSAIC_H #ifndef MOSAIC_H
#define MOSAIC_H #define MOSAIC_H
#include <stdbool.h>
#if defined(__APPLE__) || defined(__linux__) #if defined(__APPLE__) || defined(__linux__)
#include <termios.h> #include <termios.h>
@ -44,15 +46,33 @@ typedef struct stdinRead {
platformError error; platformError error;
} stdinRead; } 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_read(stdinReader *reader, void *buffer, int count);
stdinRead stdinReader_readWithTimeout(stdinReader *reader, void *buffer, int count, int timeoutMillis); stdinRead stdinReader_readWithTimeout(stdinReader *reader, void *buffer, int count, int timeoutMillis);
platformError stdinReader_interrupt(stdinReader* reader); platformError stdinReader_interrupt(stdinReader* reader);
platformError stdinReader_free(stdinReader *reader); platformError stdinReader_free(stdinReader *reader);
stdinWriterResult stdinWriter_init(); stdinWriterResult stdinWriter_init(platformEventHandler *handler);
stdinReader *stdinWriter_getReader(stdinWriter *writer); stdinReader *stdinWriter_getReader(stdinWriter *writer);
platformError stdinWriter_write(stdinWriter *writer, void *buffer, int count); 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); platformError stdinWriter_free(stdinWriter *writer);
#endif // MOSAIC_H #endif // MOSAIC_H

View File

@ -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<Event>,
) {
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))
}
}

View File

@ -1,6 +1,7 @@
package com.jakewharton.mosaic.terminal package com.jakewharton.mosaic.terminal
import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent 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.DecModeReportEvent
import com.jakewharton.mosaic.terminal.event.Event import com.jakewharton.mosaic.terminal.event.Event
import com.jakewharton.mosaic.terminal.event.FocusEvent 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.UnknownEvent
import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent
import com.jakewharton.mosaic.terminal.event.XtermPixelSizeEvent 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 BufferSize = 8 * 1024
private const val BareEscapeDisambiguationReadTimeoutMillis = 100 private const val BareEscapeDisambiguationReadTimeoutMillis = 100
public class TerminalParser( public class TerminalReader internal constructor(
private val stdinReader: StdinReader, private val platformInput: PlatformInput,
) { events: Channel<Event>,
private val emitDebugEvents: Boolean,
) : AutoCloseable {
private val buffer = ByteArray(BufferSize) private val buffer = ByteArray(BufferSize)
private var offset = 0 private var offset = 0
private var limit = 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<Event> get() = _events
/** /**
* Indicate whether Kitty's * Indicate whether Kitty's
* [escape code disambiguation](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate-escape-codes) * [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 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<Event, ByteArray> { public fun runParseLoop() {
// 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 {
val buffer = buffer val buffer = buffer
var offset = offset
var limit = limit
while (true) { while (true) {
if (offset < limit) { if (offset < limit) {
parse(buffer, offset, limit)?.let { event -> val event = tryParse(buffer, offset, limit)
return event 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. // Underflow! Copy any data to start of buffer in preparation for a read.
buffer.copyInto(buffer, 0, startIndex = offset, endIndex = limit) buffer.copyInto(buffer, 0, startIndex = offset, endIndex = limit)
limit -= offset
// Do not write the new limit to the member property because the read code below will.
limit = limit - offset
offset = 0 offset = 0
this.offset = 0
if (kittyDisambiguateEscapeCodes || limit != 1 || buffer[0] != 0x1B.toByte()) { if (kittyDisambiguateEscapeCodes || limit != 1 || buffer[0] != 0x1B.toByte()) {
// Common case: we are using the Kitty keyboard protocol to disambiguate escape keys, or // 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. // the buffer contains anything other than a bare escape. Do a normal read for more data.
val read = stdinReader.read(buffer, limit, BufferSize - limit) val read = platformInput.read(buffer, limit, BufferSize - limit)
if (read == -1) break if (read == -1) break // EOF
if (read == 0) return // Interrupt
limit += read limit += read
this.limit = limit
continue continue
} }
// Otherwise, perform a quick read to see if we have any more bytes. This will allow us to // 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 // determine whether the bare escape was truly a legacy keyboard escape event, or just the
// start of some other escape sequence. // start of some other escape sequence.
val read = stdinReader.readWithTimeout( val read = platformInput.readWithTimeout(
buffer, buffer,
1, 1,
BufferSize - 1, BufferSize - 1,
BareEscapeDisambiguationReadTimeoutMillis, 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. // We know the offset is 0, so resetting the limit effectively consumes the byte.
this.limit = 0 0
return KeyboardEvent(0x1B) } else {
} else if (read == -1) { read + 1
break
} }
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 val b1 = buffer[start].toInt() and 0xff
if (b1 == 0x1B) { if (b1 == 0x1B) {
val b2Index = start + 1 val b2Index = start + 1
@ -797,4 +819,17 @@ public class TerminalParser(
return handler(b3Index, stIndex) return handler(b3Index, stIndex)
?: UnknownEvent(buffer.copyOfRange(start, end)) ?: 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()
}
} }

View File

@ -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

View File

@ -1,5 +1,7 @@
package com.jakewharton.mosaic.terminal package com.jakewharton.mosaic.terminal
import com.jakewharton.mosaic.terminal.event.DebugEvent
public expect object Tty { public expect object Tty {
/** /**
* Save the current terminal settings and enter "raw" mode. * 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, * 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 * 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 * 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 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. * supporting interruption.
* *
* Use with [enableRawMode] to read input byte-by-byte. * 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. * 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 * 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 * @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. * 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. * value is not validated.
* @see read * @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. */ /** Signal blocking calls to [read] to wake up and return 0. */
public fun interrupt() fun interrupt()
/** /**
* Free the resources associated with this reader. * Free the resources associated with this reader.
@ -63,13 +69,19 @@ public expect class StdinReader : AutoCloseable {
override fun close() override fun close()
} }
@TestApi
internal expect class StdinWriter : AutoCloseable { internal expect class StdinWriter : AutoCloseable {
val reader: StdinReader val reader: TerminalReader
// TODO Take ByteString once it migrates to stdlib, // TODO Take ByteString once it migrates to stdlib,
// or if Sink/RawSink migrates expose that as a val. // or if Sink/RawSink migrates expose that as a val.
// https://github.com/Kotlin/kotlinx-io/issues/354 // https://github.com/Kotlin/kotlinx-io/issues/354
fun write(buffer: ByteArray) 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() override fun close()
} }

View File

@ -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 @Poko
public class KeyboardEvent( public class KeyboardEvent(
public val codepoint: Int, public val codepoint: Int,

View File

@ -1,16 +1,34 @@
package com.jakewharton.mosaic.terminal package com.jakewharton.mosaic.terminal
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.Event
import kotlin.test.AfterTest 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 { abstract class BaseTerminalParserTest {
internal val writer = Tty.stdinWriter() 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() writer.close()
assertThat(parser.copyBuffer().toHexString()).isEqualTo("")
} }
internal fun StdinWriter.writeHex(hex: String) { internal fun StdinWriter.writeHex(hex: String) {
write(hex.hexToByteArray()) write(hex.hexToByteArray())
} }
internal suspend fun TerminalReader.next(): Event {
return events.receive()
}
} }

View File

@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
class StdinReaderTest { class StdinReaderTest {
private val writer = Tty.stdinWriter() private val writer = Tty.stdinWriter()
private val reader = writer.reader private val reader = writer.reader.platformInput()
@AfterTest fun after() { @AfterTest fun after() {
reader.close() reader.close()

View File

@ -4,14 +4,15 @@ import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent import com.jakewharton.mosaic.terminal.event.BracketedPasteEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiBracketedPasteEventTest : BaseTerminalParserTest() { class TerminalParserCsiBracketedPasteEventTest : BaseTerminalParserTest() {
@Test fun pasteStart() { @Test fun pasteStart() = runTest {
writer.writeHex("1b5b3230307e") writer.writeHex("1b5b3230307e")
assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = true)) assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = true))
} }
@Test fun pasteEnd() { @Test fun pasteEnd() = runTest {
writer.writeHex("1b5b3230317e") writer.writeHex("1b5b3230317e")
assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = false)) assertThat(parser.next()).isEqualTo(BracketedPasteEvent(start = false))
} }

View File

@ -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.DecModeReportEvent.Setting.Set
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() { class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() {
@Test fun settings() { @Test fun settings() = runTest {
writer.writeHex("1b5b3f313030343b302479") writer.writeHex("1b5b3f313030343b302479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
DecModeReportEvent( DecModeReportEvent(
@ -54,7 +55,7 @@ class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun minimal() { @Test fun minimal() = runTest {
writer.writeHex("1b5b3f313b302479") writer.writeHex("1b5b3f313b302479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
DecModeReportEvent( DecModeReportEvent(
@ -64,56 +65,56 @@ class TerminalParserCsiDecModeReportEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun unknownSetting() { @Test fun unknownSetting() = runTest {
writer.writeHex("1b5b313030343b352479") writer.writeHex("1b5b313030343b352479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b313030343b352479".hexToByteArray()), UnknownEvent("1b5b313030343b352479".hexToByteArray()),
) )
} }
@Test fun noQuestion() { @Test fun noQuestion() = runTest {
writer.writeHex("1b5b313030343b302479") writer.writeHex("1b5b313030343b302479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b313030343b302479".hexToByteArray()), UnknownEvent("1b5b313030343b302479".hexToByteArray()),
) )
} }
@Test fun noDollar() { @Test fun noDollar() = runTest {
writer.writeHex("1b5b3f313030343b3079") writer.writeHex("1b5b3f313030343b3079")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f313030343b3079".hexToByteArray()), UnknownEvent("1b5b3f313030343b3079".hexToByteArray()),
) )
} }
@Test fun noMode() { @Test fun noMode() = runTest {
writer.writeHex("1b5b3f3b3130302479") writer.writeHex("1b5b3f3b3130302479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f3b3130302479".hexToByteArray()), UnknownEvent("1b5b3f3b3130302479".hexToByteArray()),
) )
} }
@Test fun nonDigitMode() { @Test fun nonDigitMode() = runTest {
writer.writeHex("1b5b3f31302d32343b302479") writer.writeHex("1b5b3f31302d32343b302479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f31302d32343b302479".hexToByteArray()), UnknownEvent("1b5b3f31302d32343b302479".hexToByteArray()),
) )
} }
@Test fun noSetting() { @Test fun noSetting() = runTest {
writer.writeHex("1b5b3f313030343b2479") writer.writeHex("1b5b3f313030343b2479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f313030343b2479".hexToByteArray()), UnknownEvent("1b5b3f313030343b2479".hexToByteArray()),
) )
} }
@Test fun nonDigitSetting() { @Test fun nonDigitSetting() = runTest {
writer.writeHex("1b5b3f313030343b312d322479") writer.writeHex("1b5b3f313030343b312d322479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f313030343b312d322479".hexToByteArray()), UnknownEvent("1b5b3f313030343b312d322479".hexToByteArray()),
) )
} }
@Test fun noSemicolon() { @Test fun noSemicolon() = runTest {
writer.writeHex("1b5b3f313030342479") writer.writeHex("1b5b3f313030342479")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f313030342479".hexToByteArray()), UnknownEvent("1b5b3f313030342479".hexToByteArray()),

View File

@ -4,14 +4,15 @@ import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.FocusEvent import com.jakewharton.mosaic.terminal.event.FocusEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiFocusEventTest : BaseTerminalParserTest() { class TerminalParserCsiFocusEventTest : BaseTerminalParserTest() {
@Test fun focusedTrue() { @Test fun focusedTrue() = runTest {
writer.writeHex("1b5b49") writer.writeHex("1b5b49")
assertThat(parser.next()).isEqualTo(FocusEvent(focused = true)) assertThat(parser.next()).isEqualTo(FocusEvent(focused = true))
} }
@Test fun focusedFalse() { @Test fun focusedFalse() = runTest {
writer.writeHex("1b5b4f") writer.writeHex("1b5b4f")
assertThat(parser.next()).isEqualTo(FocusEvent(focused = false)) assertThat(parser.next()).isEqualTo(FocusEvent(focused = false))
} }

View File

@ -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.KeyboardEvent.Companion.Up
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiKeyboardEventTest : BaseTerminalParserTest() { class TerminalParserCsiKeyboardEventTest : BaseTerminalParserTest() {
@Test fun up() { @Test fun up() = runTest {
writer.writeHex("1b5b41") writer.writeHex("1b5b41")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up))
} }
@Test fun down() { @Test fun down() = runTest {
writer.writeHex("1b5b42") writer.writeHex("1b5b42")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Down)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Down))
} }
@Test fun right() { @Test fun right() = runTest {
writer.writeHex("1b5b43") writer.writeHex("1b5b43")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Right)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Right))
} }
@Test fun left() { @Test fun left() = runTest {
writer.writeHex("1b5b44") writer.writeHex("1b5b44")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Left)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Left))
} }
@Test fun begin() { @Test fun begin() = runTest {
writer.writeHex("1b5b45") writer.writeHex("1b5b45")
assertThat(parser.next()).isEqualTo(KeyboardEvent(KpBegin)) assertThat(parser.next()).isEqualTo(KeyboardEvent(KpBegin))
} }
@Test fun end() { @Test fun end() = runTest {
writer.writeHex("1b5b46") writer.writeHex("1b5b46")
assertThat(parser.next()).isEqualTo(KeyboardEvent(End)) assertThat(parser.next()).isEqualTo(KeyboardEvent(End))
} }
@Test fun home() { @Test fun home() = runTest {
writer.writeHex("1b5b48") writer.writeHex("1b5b48")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Home)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Home))
} }
@Test fun modifierShiftUp() { @Test fun modifierShiftUp() = runTest {
writer.writeHex("1b5b313b3241") writer.writeHex("1b5b313b3241")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierShift)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierShift))
} }
@Test fun modifierAltUp() { @Test fun modifierAltUp() = runTest {
writer.writeHex("1b5b313b3341") writer.writeHex("1b5b313b3341")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierAlt)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierAlt))
} }
@Test fun modifierCtrlUp() { @Test fun modifierCtrlUp() = runTest {
writer.writeHex("1b5b313b3541") writer.writeHex("1b5b313b3541")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCtrl))
} }
@Test fun modifierSuperUp() { @Test fun modifierSuperUp() = runTest {
writer.writeHex("1b5b313b3941") writer.writeHex("1b5b313b3941")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierSuper)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierSuper))
} }
@Test fun modifierHyperUp() { @Test fun modifierHyperUp() = runTest {
writer.writeHex("1b5b313b313741") writer.writeHex("1b5b313b313741")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierHyper)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierHyper))
} }
@Test fun modifierMetaUp() { @Test fun modifierMetaUp() = runTest {
writer.writeHex("1b5b313b333341") writer.writeHex("1b5b313b333341")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierMeta)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierMeta))
} }
@Test fun modifierCapsLockUp() { @Test fun modifierCapsLockUp() = runTest {
writer.writeHex("1b5b313b363541") writer.writeHex("1b5b313b363541")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCapsLock)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierCapsLock))
} }
@Test fun modifierNumLockUp() { @Test fun modifierNumLockUp() = runTest {
writer.writeHex("1b5b313b31323941") writer.writeHex("1b5b313b31323941")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierNumLock)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up, modifiers = ModifierNumLock))
} }
@Test fun non1p0() { @Test fun non1p0() = runTest {
writer.writeHex("1b5b323b3248") writer.writeHex("1b5b323b3248")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b323b3248".hexToByteArray()), UnknownEvent("1b5b323b3248".hexToByteArray()),
) )
} }
@Test fun emptyModifier() { @Test fun emptyModifier() = runTest {
writer.writeHex("1b5b313b48") writer.writeHex("1b5b313b48")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b313b48".hexToByteArray()), UnknownEvent("1b5b313b48".hexToByteArray()),
) )
} }
@Test fun nonDigitModifier() { @Test fun nonDigitModifier() = runTest {
writer.writeHex("1b5b313b2f48") writer.writeHex("1b5b313b2f48")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b313b2f48".hexToByteArray()), UnknownEvent("1b5b313b2f48".hexToByteArray()),

View File

@ -5,37 +5,38 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.KeyboardEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent
import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierShift import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierShift
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiKittyKeyboardEventTest : BaseTerminalParserTest() { class TerminalParserCsiKittyKeyboardEventTest : BaseTerminalParserTest() {
@Test fun h() { @Test fun h() = runTest {
writer.writeHex("1b5b31303475") writer.writeHex("1b5b31303475")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KeyboardEvent(0x68), KeyboardEvent(0x68),
) )
} }
@Test fun shiftH() { @Test fun shiftH() = runTest {
writer.writeHex("1b5b3130343b3275") writer.writeHex("1b5b3130343b3275")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KeyboardEvent(0x68, modifiers = ModifierShift), KeyboardEvent(0x68, modifiers = ModifierShift),
) )
} }
@Test fun shiftHWithAlternate() { @Test fun shiftHWithAlternate() = runTest {
writer.writeHex("1b5b3130343a37323b3275") writer.writeHex("1b5b3130343a37323b3275")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KeyboardEvent(0x68, 0x48, modifiers = ModifierShift), KeyboardEvent(0x68, 0x48, modifiers = ModifierShift),
) )
} }
@Test fun shiftHWithReleaseEventType() { @Test fun shiftHWithReleaseEventType() = runTest {
writer.writeHex("1b5b3130343b323a3375") writer.writeHex("1b5b3130343b323a3375")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KeyboardEvent(0x68, modifiers = ModifierShift, eventType = 3), KeyboardEvent(0x68, modifiers = ModifierShift, eventType = 3),
) )
} }
@Test fun hWithAssociatedText() { @Test fun hWithAssociatedText() = runTest {
writer.writeHex("1b5b3130343b3b31303475") writer.writeHex("1b5b3130343b3b31303475")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KeyboardEvent(0x68, text = "h"), KeyboardEvent(0x68, text = "h"),

View File

@ -5,31 +5,32 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.KittyKeyboardQueryEvent import com.jakewharton.mosaic.terminal.event.KittyKeyboardQueryEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiKittyKeyboardQueryEventTest : BaseTerminalParserTest() { class TerminalParserCsiKittyKeyboardQueryEventTest : BaseTerminalParserTest() {
@Test fun flagsNone() { @Test fun flagsNone() = runTest {
writer.writeHex("1b5b3f3075") writer.writeHex("1b5b3f3075")
assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(0)) assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(0))
} }
@Test fun flagsAll() { @Test fun flagsAll() = runTest {
writer.writeHex("1b5b3f333175") writer.writeHex("1b5b3f333175")
assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(31)) assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(31))
} }
@Test fun flagsUnknown() { @Test fun flagsUnknown() = runTest {
writer.writeHex("1b5b3f31323875") writer.writeHex("1b5b3f31323875")
assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(128)) assertThat(parser.next()).isEqualTo(KittyKeyboardQueryEvent(128))
} }
@Test fun flagsMissing() { @Test fun flagsMissing() = runTest {
writer.writeHex("1b5b3f75") writer.writeHex("1b5b3f75")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f75".hexToByteArray()), UnknownEvent("1b5b3f75".hexToByteArray()),
) )
} }
@Test fun flagsNonDigit() { @Test fun flagsNonDigit() = runTest {
writer.writeHex("1b5b3f312b2075") writer.writeHex("1b5b3f312b2075")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f312b2075".hexToByteArray()), UnknownEvent("1b5b3f312b2075".hexToByteArray()),

View File

@ -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.MouseEvent.Type
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiMouseEventTest : BaseTerminalParserTest() { class TerminalParserCsiMouseEventTest : BaseTerminalParserTest() {
@Test fun motion() { @Test fun motion() = runTest {
writer.writeHex("1b5b4d434837") writer.writeHex("1b5b4d434837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Motion, Button.None), MouseEvent(39, 22, Type.Motion, Button.None),
) )
} }
@Test fun click() { @Test fun click() = runTest {
writer.writeHex("1b5b4d204837") writer.writeHex("1b5b4d204837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Left), MouseEvent(39, 22, Type.Press, Button.Left),
) )
} }
@Test fun drag() { @Test fun drag() = runTest {
writer.writeHex("1b5b4d404837") writer.writeHex("1b5b4d404837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Drag, Button.Left), MouseEvent(39, 22, Type.Drag, Button.Left),
) )
} }
@Test fun clickMouseUp() { @Test fun clickMouseUp() = runTest {
writer.writeHex("1b5b4d234837") writer.writeHex("1b5b4d234837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.None), MouseEvent(39, 22, Type.Press, Button.None),
) )
} }
@Test fun shiftClick() { @Test fun shiftClick() = runTest {
writer.writeHex("1b5b4d244837") writer.writeHex("1b5b4d244837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Left, shift = true), MouseEvent(39, 22, Type.Press, Button.Left, shift = true),
) )
} }
@Test fun altClick() { @Test fun altClick() = runTest {
writer.writeHex("1b5b4d284837") writer.writeHex("1b5b4d284837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Left, alt = true), MouseEvent(39, 22, Type.Press, Button.Left, alt = true),
) )
} }
@Test fun ctrlClick() { @Test fun ctrlClick() = runTest {
writer.writeHex("1b5b4d304837") writer.writeHex("1b5b4d304837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Left, ctrl = true), MouseEvent(39, 22, Type.Press, Button.Left, ctrl = true),
) )
} }
@Test fun clickRight() { @Test fun clickRight() = runTest {
writer.writeHex("1b5b4d224837") writer.writeHex("1b5b4d224837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Right), MouseEvent(39, 22, Type.Press, Button.Right),
) )
} }
@Test fun clickMiddle() { @Test fun clickMiddle() = runTest {
writer.writeHex("1b5b4d214837") writer.writeHex("1b5b4d214837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Middle), MouseEvent(39, 22, Type.Press, Button.Middle),
) )
} }
@Test fun clickWheelUp() { @Test fun clickWheelUp() = runTest {
writer.writeHex("1b5b4d604837") writer.writeHex("1b5b4d604837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.WheelUp), MouseEvent(39, 22, Type.Press, Button.WheelUp),
) )
} }
@Test fun clickWheelDown() { @Test fun clickWheelDown() = runTest {
writer.writeHex("1b5b4d614837") writer.writeHex("1b5b4d614837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.WheelDown), MouseEvent(39, 22, Type.Press, Button.WheelDown),
) )
} }
@Test fun clickButton8() { @Test fun clickButton8() = runTest {
writer.writeHex("1b5b4da04837") writer.writeHex("1b5b4da04837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Button8), MouseEvent(39, 22, Type.Press, Button.Button8),
) )
} }
@Test fun clickButton9() { @Test fun clickButton9() = runTest {
writer.writeHex("1b5b4da14837") writer.writeHex("1b5b4da14837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Button9), MouseEvent(39, 22, Type.Press, Button.Button9),
) )
} }
@Test fun clickButton10() { @Test fun clickButton10() = runTest {
writer.writeHex("1b5b4da24837") writer.writeHex("1b5b4da24837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Button10), MouseEvent(39, 22, Type.Press, Button.Button10),
) )
} }
@Test fun clickButton11() { @Test fun clickButton11() = runTest {
writer.writeHex("1b5b4da34837") writer.writeHex("1b5b4da34837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
MouseEvent(39, 22, Type.Press, Button.Button11), MouseEvent(39, 22, Type.Press, Button.Button11),
) )
} }
@Test fun clickUtf8() { @Test fun clickUtf8() = runTest {
parser.xtermExtendedUtf8Mouse = true parser.xtermExtendedUtf8Mouse = true
writer.writeHex("1b5b4d20c28037") writer.writeHex("1b5b4d20c28037")
@ -125,7 +126,7 @@ class TerminalParserCsiMouseEventTest : BaseTerminalParserTest() {
// TODO all types & buttons utf-8 in both single-byte and multi-byte form // TODO all types & buttons utf-8 in both single-byte and multi-byte form
@Test fun lowercaseMDelimiterInvalid() { @Test fun lowercaseMDelimiterInvalid() = runTest {
writer.writeHex("1b5b6d204837") writer.writeHex("1b5b6d204837")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b6d".hexToByteArray()), UnknownEvent("1b5b6d".hexToByteArray()),

View File

@ -5,24 +5,25 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.OperatingStatusResponseEvent import com.jakewharton.mosaic.terminal.event.OperatingStatusResponseEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiOperatingStatusResponseEventTest : BaseTerminalParserTest() { class TerminalParserCsiOperatingStatusResponseEventTest : BaseTerminalParserTest() {
@Test fun ok() { @Test fun ok() = runTest {
writer.writeHex("1b5b306e") writer.writeHex("1b5b306e")
assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = true)) assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = true))
} }
@Test fun notOk() { @Test fun notOk() = runTest {
writer.writeHex("1b5b336e") writer.writeHex("1b5b336e")
assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = false)) assertThat(parser.next()).isEqualTo(OperatingStatusResponseEvent(ok = false))
} }
@Test fun unknownP1() { @Test fun unknownP1() = runTest {
writer.writeHex("1b5b316e") writer.writeHex("1b5b316e")
assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b316e".hexToByteArray())) assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b316e".hexToByteArray()))
} }
@Test fun nonDigitP1() { @Test fun nonDigitP1() = runTest {
writer.writeHex("1b5b2b6e") writer.writeHex("1b5b2b6e")
assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b2b6e".hexToByteArray())) assertThat(parser.next()).isEqualTo(UnknownEvent("1b5b2b6e".hexToByteArray()))
} }

View File

@ -5,21 +5,22 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.PrimaryDeviceAttributesEvent import com.jakewharton.mosaic.terminal.event.PrimaryDeviceAttributesEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiPrimaryDeviceAttributesEventTest : BaseTerminalParserTest() { class TerminalParserCsiPrimaryDeviceAttributesEventTest : BaseTerminalParserTest() {
@Test fun noLeadingQuestionMarkIsUnknown() { @Test fun noLeadingQuestionMarkIsUnknown() = runTest {
writer.writeHex("1b5b303063") writer.writeHex("1b5b303063")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b303063".hexToByteArray()), UnknownEvent("1b5b303063".hexToByteArray()),
) )
} }
@Test fun emptyData() { @Test fun emptyData() = runTest {
writer.writeHex("1b5b3f63") writer.writeHex("1b5b3f63")
assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = "")) assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = ""))
} }
@Test fun data() { @Test fun data() = runTest {
writer.writeHex("1b5b3f323b3263") writer.writeHex("1b5b3f323b3263")
assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = "2;2")) assertThat(parser.next()).isEqualTo(PrimaryDeviceAttributesEvent(data = "2;2"))
} }

View File

@ -5,36 +5,37 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.ResizeEvent import com.jakewharton.mosaic.terminal.event.ResizeEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiResizeEventTest : BaseTerminalParserTest() { class TerminalParserCsiResizeEventTest : BaseTerminalParserTest() {
@Test fun basic() { @Test fun basic() = runTest {
writer.writeHex("1b5b34383b313b323b333b3474") writer.writeHex("1b5b34383b313b323b333b3474")
assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3))
} }
@Test fun pixelSizeAsZero() { @Test fun pixelSizeAsZero() = runTest {
writer.writeHex("1b5b34383b313b323b303b3074") writer.writeHex("1b5b34383b313b323b303b3074")
assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 0, 0)) assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 0, 0))
} }
@Test fun subparametersIgnored() { @Test fun subparametersIgnored() = runTest {
writer.writeHex("1b5b34383b313a39393b323a39383a39373b333a39393a3a39373b343a39393a74") writer.writeHex("1b5b34383b313a39393b323a39383a39373b333a39393a3a39373b343a39393a74")
assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3))
} }
@Test fun emptySubparametersIgnored() { @Test fun emptySubparametersIgnored() = runTest {
writer.writeHex("1b5b34383b313a3b323a3b333a3b343a74") writer.writeHex("1b5b34383b313a3b323a3b333a3b343a74")
assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3)) assertThat(parser.next()).isEqualTo(ResizeEvent(2, 1, 4, 3))
} }
@Test fun emptyModeFails() { @Test fun emptyModeFails() = runTest {
writer.writeHex("1b5b3b313b323b333b3474") writer.writeHex("1b5b3b313b323b333b3474")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3b313b323b333b3474".hexToByteArray()), UnknownEvent("1b5b3b313b323b333b3474".hexToByteArray()),
) )
} }
@Test fun emptyParameterFails() { @Test fun emptyParameterFails() = runTest {
writer.writeHex("1b5b34383b3b323b333b3474") writer.writeHex("1b5b34383b3b323b333b3474")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b34383b3b323b333b3474".hexToByteArray()), UnknownEvent("1b5b34383b3b323b333b3474".hexToByteArray()),
@ -53,7 +54,7 @@ class TerminalParserCsiResizeEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun nonDigitParameterFails() { @Test fun nonDigitParameterFails() = runTest {
writer.writeHex("1b5b34383b312e303b323b333b3474") writer.writeHex("1b5b34383b312e303b323b333b3474")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b34383b312e303b323b333b3474".hexToByteArray()), UnknownEvent("1b5b34383b312e303b323b333b3474".hexToByteArray()),

View File

@ -5,40 +5,41 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.SystemThemeEvent import com.jakewharton.mosaic.terminal.event.SystemThemeEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiSystemThemeEventTest : BaseTerminalParserTest() { class TerminalParserCsiSystemThemeEventTest : BaseTerminalParserTest() {
@Test fun dark() { @Test fun dark() = runTest {
writer.writeHex("1b5b3f3939373b316e") writer.writeHex("1b5b3f3939373b316e")
assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = true)) assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = true))
} }
@Test fun light() { @Test fun light() = runTest {
writer.writeHex("1b5b3f3939373b326e") writer.writeHex("1b5b3f3939373b326e")
assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = false)) assertThat(parser.next()).isEqualTo(SystemThemeEvent(isDark = false))
} }
@Test fun missingP2() { @Test fun missingP2() = runTest {
writer.writeHex("1b5b3f3939373b6e") writer.writeHex("1b5b3f3939373b6e")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f3939373b6e".hexToByteArray()), UnknownEvent("1b5b3f3939373b6e".hexToByteArray()),
) )
} }
@Test fun unknownP2() { @Test fun unknownP2() = runTest {
writer.writeHex("1b5b3f3939373b346e") writer.writeHex("1b5b3f3939373b346e")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f3939373b346e".hexToByteArray()), UnknownEvent("1b5b3f3939373b346e".hexToByteArray()),
) )
} }
@Test fun nonDigitP2() { @Test fun nonDigitP2() = runTest {
writer.writeHex("1b5b3f3939373b2b6e") writer.writeHex("1b5b3f3939373b2b6e")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f3939373b2b6e".hexToByteArray()), UnknownEvent("1b5b3f3939373b2b6e".hexToByteArray()),
) )
} }
@Test fun tooLongP2() { @Test fun tooLongP2() = runTest {
writer.writeHex("1b5b3f3939373b31316e") writer.writeHex("1b5b3f3939373b31316e")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b3f3939373b31316e".hexToByteArray()), UnknownEvent("1b5b3f3939373b31316e".hexToByteArray()),

View File

@ -5,14 +5,15 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent import com.jakewharton.mosaic.terminal.event.XtermCharacterSizeEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiXtermCharacterSizeEventTest : BaseTerminalParserTest() { class TerminalParserCsiXtermCharacterSizeEventTest : BaseTerminalParserTest() {
@Test fun basic() { @Test fun basic() = runTest {
writer.writeHex("1b5b383b313b3274") writer.writeHex("1b5b383b313b3274")
assertThat(parser.next()).isEqualTo(XtermCharacterSizeEvent(1, 2)) assertThat(parser.next()).isEqualTo(XtermCharacterSizeEvent(1, 2))
} }
@Test fun emptyParameterFails() { @Test fun emptyParameterFails() = runTest {
writer.writeHex("1b5b383b3b3274") writer.writeHex("1b5b383b3b3274")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b383b3b3274".hexToByteArray()), UnknownEvent("1b5b383b3b3274".hexToByteArray()),
@ -23,7 +24,7 @@ class TerminalParserCsiXtermCharacterSizeEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun nonDigitParameterFails() { @Test fun nonDigitParameterFails() = runTest {
writer.writeHex("1b5b383b223b3274") writer.writeHex("1b5b383b223b3274")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b383b223b3274".hexToByteArray()), UnknownEvent("1b5b383b223b3274".hexToByteArray()),

View File

@ -5,14 +5,15 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import com.jakewharton.mosaic.terminal.event.XtermPixelSizeEvent import com.jakewharton.mosaic.terminal.event.XtermPixelSizeEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserCsiXtermPixelSizeEventTest : BaseTerminalParserTest() { class TerminalParserCsiXtermPixelSizeEventTest : BaseTerminalParserTest() {
@Test fun basic() { @Test fun basic() = runTest {
writer.writeHex("1b5b343b313b3274") writer.writeHex("1b5b343b313b3274")
assertThat(parser.next()).isEqualTo(XtermPixelSizeEvent(1, 2)) assertThat(parser.next()).isEqualTo(XtermPixelSizeEvent(1, 2))
} }
@Test fun emptyParameterFails() { @Test fun emptyParameterFails() = runTest {
writer.writeHex("1b5b343b3b3274") writer.writeHex("1b5b343b3b3274")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b343b3b3274".hexToByteArray()), UnknownEvent("1b5b343b3b3274".hexToByteArray()),
@ -23,7 +24,7 @@ class TerminalParserCsiXtermPixelSizeEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun nonDigitParameterFails() { @Test fun nonDigitParameterFails() = runTest {
writer.writeHex("1b5b343b223b3274") writer.writeHex("1b5b343b223b3274")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5b343b223b3274".hexToByteArray()), UnknownEvent("1b5b343b223b3274".hexToByteArray()),

View File

@ -4,14 +4,15 @@ import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.TerminalVersionEvent import com.jakewharton.mosaic.terminal.event.TerminalVersionEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserDcsTerminalVersionEventTest : BaseTerminalParserTest() { class TerminalParserDcsTerminalVersionEventTest : BaseTerminalParserTest() {
@Test fun empty() { @Test fun empty() = runTest {
writer.writeHex("1b503e7c1b5c") writer.writeHex("1b503e7c1b5c")
assertThat(parser.next()).isEqualTo(TerminalVersionEvent("")) assertThat(parser.next()).isEqualTo(TerminalVersionEvent(""))
} }
@Test fun text() { @Test fun text() = runTest {
writer.writeHex("1b503e7c68656c6c6f1b5c") writer.writeHex("1b503e7c68656c6c6f1b5c")
assertThat(parser.next()).isEqualTo(TerminalVersionEvent("hello")) assertThat(parser.next()).isEqualTo(TerminalVersionEvent("hello"))
} }

View File

@ -5,9 +5,10 @@ import assertk.assertions.isEqualTo
import com.jakewharton.mosaic.terminal.event.KeyboardEvent import com.jakewharton.mosaic.terminal.event.KeyboardEvent
import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserGroundKeyboardEventTest : BaseTerminalParserTest() { class TerminalParserGroundKeyboardEventTest : BaseTerminalParserTest() {
@Test fun graphic() { @Test fun graphic() = runTest {
for (codepoint in 0x20..0x7f) { for (codepoint in 0x20..0x7f) {
val hex = codepoint.toString(16) val hex = codepoint.toString(16)
writer.writeHex(hex) writer.writeHex(hex)
@ -15,172 +16,172 @@ class TerminalParserGroundKeyboardEventTest : BaseTerminalParserTest() {
} }
} }
@Test fun ctrlShiftAt() { @Test fun ctrlShiftAt() = runTest {
writer.writeHex("00") writer.writeHex("00")
assertThat(parser.next()).isEqualTo(KeyboardEvent('@'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('@'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlA() { @Test fun ctrlA() = runTest {
writer.writeHex("01") writer.writeHex("01")
assertThat(parser.next()).isEqualTo(KeyboardEvent('a'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('a'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlB() { @Test fun ctrlB() = runTest {
writer.writeHex("02") writer.writeHex("02")
assertThat(parser.next()).isEqualTo(KeyboardEvent('b'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('b'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlC() { @Test fun ctrlC() = runTest {
writer.writeHex("03") writer.writeHex("03")
assertThat(parser.next()).isEqualTo(KeyboardEvent('c'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('c'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlD() { @Test fun ctrlD() = runTest {
writer.writeHex("04") writer.writeHex("04")
assertThat(parser.next()).isEqualTo(KeyboardEvent('d'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('d'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlE() { @Test fun ctrlE() = runTest {
writer.writeHex("05") writer.writeHex("05")
assertThat(parser.next()).isEqualTo(KeyboardEvent('e'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('e'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlF() { @Test fun ctrlF() = runTest {
writer.writeHex("06") writer.writeHex("06")
assertThat(parser.next()).isEqualTo(KeyboardEvent('f'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('f'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlG() { @Test fun ctrlG() = runTest {
writer.writeHex("07") writer.writeHex("07")
assertThat(parser.next()).isEqualTo(KeyboardEvent('g'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('g'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlH() { @Test fun ctrlH() = runTest {
writer.writeHex("08") writer.writeHex("08")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x7f)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x7f))
} }
@Test fun ctrlI() { @Test fun ctrlI() = runTest {
writer.writeHex("09") writer.writeHex("09")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x09)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x09))
} }
@Test fun ctrlJ() { @Test fun ctrlJ() = runTest {
writer.writeHex("0a") writer.writeHex("0a")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d))
} }
@Test fun ctrlK() { @Test fun ctrlK() = runTest {
writer.writeHex("0b") writer.writeHex("0b")
assertThat(parser.next()).isEqualTo(KeyboardEvent('k'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('k'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlL() { @Test fun ctrlL() = runTest {
writer.writeHex("0c") writer.writeHex("0c")
assertThat(parser.next()).isEqualTo(KeyboardEvent('l'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('l'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlM() { @Test fun ctrlM() = runTest {
writer.writeHex("0d") writer.writeHex("0d")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x0d))
} }
@Test fun ctrlN() { @Test fun ctrlN() = runTest {
writer.writeHex("0e") writer.writeHex("0e")
assertThat(parser.next()).isEqualTo(KeyboardEvent('n'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('n'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlO() { @Test fun ctrlO() = runTest {
writer.writeHex("0f") writer.writeHex("0f")
assertThat(parser.next()).isEqualTo(KeyboardEvent('o'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('o'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlP() { @Test fun ctrlP() = runTest {
writer.writeHex("10") writer.writeHex("10")
assertThat(parser.next()).isEqualTo(KeyboardEvent('p'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('p'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlQ() { @Test fun ctrlQ() = runTest {
writer.writeHex("11") writer.writeHex("11")
assertThat(parser.next()).isEqualTo(KeyboardEvent('q'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('q'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlR() { @Test fun ctrlR() = runTest {
writer.writeHex("12") writer.writeHex("12")
assertThat(parser.next()).isEqualTo(KeyboardEvent('r'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('r'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlS() { @Test fun ctrlS() = runTest {
writer.writeHex("13") writer.writeHex("13")
assertThat(parser.next()).isEqualTo(KeyboardEvent('s'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('s'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlT() { @Test fun ctrlT() = runTest {
writer.writeHex("14") writer.writeHex("14")
assertThat(parser.next()).isEqualTo(KeyboardEvent('t'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('t'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlU() { @Test fun ctrlU() = runTest {
writer.writeHex("15") writer.writeHex("15")
assertThat(parser.next()).isEqualTo(KeyboardEvent('u'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('u'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlV() { @Test fun ctrlV() = runTest {
writer.writeHex("16") writer.writeHex("16")
assertThat(parser.next()).isEqualTo(KeyboardEvent('v'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('v'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlW() { @Test fun ctrlW() = runTest {
writer.writeHex("17") writer.writeHex("17")
assertThat(parser.next()).isEqualTo(KeyboardEvent('w'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('w'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlX() { @Test fun ctrlX() = runTest {
writer.writeHex("18") writer.writeHex("18")
assertThat(parser.next()).isEqualTo(KeyboardEvent('x'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('x'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlY() { @Test fun ctrlY() = runTest {
writer.writeHex("19") writer.writeHex("19")
assertThat(parser.next()).isEqualTo(KeyboardEvent('y'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('y'.code, modifiers = ModifierCtrl))
} }
@Test fun ctrlZ() { @Test fun ctrlZ() = runTest {
writer.writeHex("1a") writer.writeHex("1a")
assertThat(parser.next()).isEqualTo(KeyboardEvent('z'.code, modifiers = ModifierCtrl)) assertThat(parser.next()).isEqualTo(KeyboardEvent('z'.code, modifiers = ModifierCtrl))
} }
@Test fun bareEscape() { @Test fun bareEscape() = runTest {
writer.writeHex("1b") writer.writeHex("1b")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1b)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1b))
} }
@Test fun hex1c() { @Test fun hex1c() = runTest {
writer.writeHex("1c") writer.writeHex("1c")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1c)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1c))
} }
@Test fun hex1d() { @Test fun hex1d() = runTest {
writer.writeHex("1d") writer.writeHex("1d")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1d)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1d))
} }
@Test fun hex1e() { @Test fun hex1e() = runTest {
writer.writeHex("1e") writer.writeHex("1e")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1e)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1e))
} }
@Test fun hex1f() { @Test fun hex1f() = runTest {
writer.writeHex("1f") writer.writeHex("1f")
assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1f)) assertThat(parser.next()).isEqualTo(KeyboardEvent(0x1f))
} }
@Test fun utf8TwoBytes() { @Test fun utf8TwoBytes() = runTest {
writer.writeHex("cea9") writer.writeHex("cea9")
assertThat(parser.next()).isEqualTo(KeyboardEvent('Ω'.code)) assertThat(parser.next()).isEqualTo(KeyboardEvent('Ω'.code))
} }
@Test fun utf8ThreeBytes() { @Test fun utf8ThreeBytes() = runTest {
writer.writeHex("e28988") writer.writeHex("e28988")
assertThat(parser.next()).isEqualTo(KeyboardEvent('≈'.code)) assertThat(parser.next()).isEqualTo(KeyboardEvent('≈'.code))
} }

View File

@ -6,44 +6,45 @@ import com.jakewharton.mosaic.terminal.event.KittyPointerQueryNameEvent
import com.jakewharton.mosaic.terminal.event.KittyPointerQuerySupportEvent import com.jakewharton.mosaic.terminal.event.KittyPointerQuerySupportEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserOscKittyPointerQueryEventTest : BaseTerminalParserTest() { class TerminalParserOscKittyPointerQueryEventTest : BaseTerminalParserTest() {
@Test fun emptyFails() { @Test fun emptyFails() = runTest {
writer.writeHex("1b5d32323b1b5c") writer.writeHex("1b5d32323b1b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5d32323b1b5c".hexToByteArray()), UnknownEvent("1b5d32323b1b5c".hexToByteArray()),
) )
} }
@Test fun valuesSingleFalse() { @Test fun valuesSingleFalse() = runTest {
writer.writeHex("1b5d32323b301b5c") writer.writeHex("1b5d32323b301b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQuerySupportEvent(booleanArrayOf(false)), KittyPointerQuerySupportEvent(booleanArrayOf(false)),
) )
} }
@Test fun valuesSingleTrue() { @Test fun valuesSingleTrue() = runTest {
writer.writeHex("1b5d32323b311b5c") writer.writeHex("1b5d32323b311b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQuerySupportEvent(booleanArrayOf(true)), KittyPointerQuerySupportEvent(booleanArrayOf(true)),
) )
} }
@Test fun valuesSingleValueTrailingCommaFails() { @Test fun valuesSingleValueTrailingCommaFails() = runTest {
writer.writeHex("1b5d32323b312c1b5c") writer.writeHex("1b5d32323b312c1b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5d32323b312c1b5c".hexToByteArray()), UnknownEvent("1b5d32323b312c1b5c".hexToByteArray()),
) )
} }
@Test fun valuesMultiple() { @Test fun valuesMultiple() = runTest {
writer.writeHex("1b5d32323b302c302c312c312c301b5c") writer.writeHex("1b5d32323b302c302c312c312c301b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQuerySupportEvent(booleanArrayOf(false, false, true, true, false)), KittyPointerQuerySupportEvent(booleanArrayOf(false, false, true, true, false)),
) )
} }
@Test fun valuesTons() { @Test fun valuesTons() = runTest {
writer.writeHex("1b5d32323b302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c301b5c") writer.writeHex("1b5d32323b302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c302c302c312c312c301b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQuerySupportEvent( KittyPointerQuerySupportEvent(
@ -61,28 +62,28 @@ class TerminalParserOscKittyPointerQueryEventTest : BaseTerminalParserTest() {
) )
} }
@Test fun nameSingleDigit() { @Test fun nameSingleDigit() = runTest {
writer.writeHex("1b5d32323b321b5c") writer.writeHex("1b5d32323b321b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQueryNameEvent("2"), KittyPointerQueryNameEvent("2"),
) )
} }
@Test fun nameLeadingValueDigit() { @Test fun nameLeadingValueDigit() = runTest {
writer.writeHex("1b5d32323b30611b5c") writer.writeHex("1b5d32323b30611b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQueryNameEvent("0a"), KittyPointerQueryNameEvent("0a"),
) )
} }
@Test fun nameValidRange() { @Test fun nameValidRange() = runTest {
writer.writeHex("1b5d32323b6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f1b5c") writer.writeHex("1b5d32323b6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f1b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
KittyPointerQueryNameEvent("abcdefghijklmnopqrstuvwxyz0123456789-_"), KittyPointerQueryNameEvent("abcdefghijklmnopqrstuvwxyz0123456789-_"),
) )
} }
@Test fun nameInvalidRange() { @Test fun nameInvalidRange() = runTest {
writer.writeHex("1b5d32323b6162633132334142431b5c") writer.writeHex("1b5d32323b6162633132334142431b5c")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b5d32323b6162633132334142431b5c".hexToByteArray()), UnknownEvent("1b5d32323b6162633132334142431b5c".hexToByteArray()),

View File

@ -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.OperatingStatusResponseEvent
import com.jakewharton.mosaic.terminal.event.UnknownEvent import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.test.Test import kotlin.test.Test
import kotlinx.coroutines.test.runTest
class TerminalParserSs3KeyboardEventTest : BaseTerminalParserTest() { class TerminalParserSs3KeyboardEventTest : BaseTerminalParserTest() {
@Test fun up() { @Test fun up() = runTest {
writer.writeHex("1b4f41") writer.writeHex("1b4f41")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Up)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Up))
} }
@Test fun down() { @Test fun down() = runTest {
writer.writeHex("1b4f42") writer.writeHex("1b4f42")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Down)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Down))
} }
@Test fun right() { @Test fun right() = runTest {
writer.writeHex("1b4f43") writer.writeHex("1b4f43")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Right)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Right))
} }
@Test fun left() { @Test fun left() = runTest {
writer.writeHex("1b4f44") writer.writeHex("1b4f44")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Left)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Left))
} }
@Test fun end() { @Test fun end() = runTest {
writer.writeHex("1b4f46") writer.writeHex("1b4f46")
assertThat(parser.next()).isEqualTo(KeyboardEvent(End)) assertThat(parser.next()).isEqualTo(KeyboardEvent(End))
} }
@Test fun home() { @Test fun home() = runTest {
writer.writeHex("1b4f48") writer.writeHex("1b4f48")
assertThat(parser.next()).isEqualTo(KeyboardEvent(Home)) assertThat(parser.next()).isEqualTo(KeyboardEvent(Home))
} }
@Test fun f1() { @Test fun f1() = runTest {
writer.writeHex("1b4f50") writer.writeHex("1b4f50")
assertThat(parser.next()).isEqualTo(KeyboardEvent(F1)) assertThat(parser.next()).isEqualTo(KeyboardEvent(F1))
} }
@Test fun f2() { @Test fun f2() = runTest {
writer.writeHex("1b4f51") writer.writeHex("1b4f51")
assertThat(parser.next()).isEqualTo(KeyboardEvent(F2)) assertThat(parser.next()).isEqualTo(KeyboardEvent(F2))
} }
@Test fun f3() { @Test fun f3() = runTest {
writer.writeHex("1b4f52") writer.writeHex("1b4f52")
assertThat(parser.next()).isEqualTo(KeyboardEvent(F3)) assertThat(parser.next()).isEqualTo(KeyboardEvent(F3))
} }
@Test fun f4() { @Test fun f4() = runTest {
writer.writeHex("1b4f53") writer.writeHex("1b4f53")
assertThat(parser.next()).isEqualTo(KeyboardEvent(F4)) assertThat(parser.next()).isEqualTo(KeyboardEvent(F4))
} }
@Test fun invalidKey() { @Test fun invalidKey() = runTest {
writer.writeHex("1b4f75") writer.writeHex("1b4f75")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b4f75".hexToByteArray()), UnknownEvent("1b4f75".hexToByteArray()),
) )
} }
@Test fun keyIsEscapeDoesNotConsumeEscape() { @Test fun keyIsEscapeDoesNotConsumeEscape() = runTest {
writer.writeHex("1b4f1b5b306e") writer.writeHex("1b4f1b5b306e")
assertThat(parser.next()).isEqualTo( assertThat(parser.next()).isEqualTo(
UnknownEvent("1b4f".hexToByteArray()), UnknownEvent("1b4f".hexToByteArray()),

View File

@ -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 JNIEXPORT jlong JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInit(JNIEnv *env, jclass type) { Java_com_jakewharton_mosaic_terminal_Jni_platformEventHandlerInit(
stdinReaderResult result = stdinReader_init(); 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)) { if (likely(!result.error)) {
return (jlong) result.reader; return (jlong) result.reader;
} }
@ -62,7 +188,7 @@ JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderRead( Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderRead(
JNIEnv *env, JNIEnv *env,
jclass type, jclass type,
jlong ptr, jlong readerOpaque,
jbyteArray buffer, jbyteArray buffer,
jint offset, jint offset,
jint count jint count
@ -70,7 +196,8 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderRead(
jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL);
jbyte *nativeBufferAtOffset = nativeBuffer + offset; 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); (*env)->ReleaseByteArrayElements(env, buffer, nativeBuffer, 0);
@ -88,7 +215,7 @@ JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout( Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout(
JNIEnv *env, JNIEnv *env,
jclass type, jclass type,
jlong ptr, jlong readerOpaque,
jbyteArray buffer, jbyteArray buffer,
jint offset, jint offset,
jint count, jint count,
@ -97,8 +224,9 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout(
jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL);
jbyte *nativeBufferAtOffset = nativeBuffer + offset; jbyte *nativeBufferAtOffset = nativeBuffer + offset;
stdinReader *reader = (stdinReader *) readerOpaque;
stdinRead read = stdinReader_readWithTimeout( stdinRead read = stdinReader_readWithTimeout(
(stdinReader *) ptr, reader,
nativeBufferAtOffset, nativeBufferAtOffset,
count, count,
timeoutMillis timeoutMillis
@ -117,24 +245,27 @@ Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderReadWithTimeout(
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInterrupt(JNIEnv *env, jclass type, jlong ptr) { Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderInterrupt(JNIEnv *env, jclass type, jlong readerOpaque) {
platformError error = stdinReader_interrupt((stdinReader *) ptr); stdinReader *reader = (stdinReader *) readerOpaque;
platformError error = stdinReader_interrupt(reader);
if (unlikely(error)) { if (unlikely(error)) {
throwIse(env, error, "Unable to interrupt stdin reader"); throwIse(env, error, "Unable to interrupt stdin reader");
} }
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderFree(JNIEnv *env, jclass type, jlong ptr) { Java_com_jakewharton_mosaic_terminal_Jni_stdinReaderFree(JNIEnv *env, jclass type, jlong readerOpaque) {
platformError error = stdinReader_free((stdinReader *) ptr); stdinReader *reader = (stdinReader *) readerOpaque;
platformError error = stdinReader_free(reader);
if (unlikely(error)) { if (unlikely(error)) {
throwIse(env, error, "Unable to free stdin reader"); throwIse(env, error, "Unable to free stdin reader");
} }
} }
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterInit(JNIEnv *env, jclass type) { Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterInit(JNIEnv *env, jclass type, jlong handlerOpaque) {
stdinWriterResult result = stdinWriter_init(); platformEventHandler *handler = (platformEventHandler *) handlerOpaque;
stdinWriterResult result = stdinWriter_init(handler);
if (likely(!result.error)) { if (likely(!result.error)) {
return (jlong) result.writer; return (jlong) result.writer;
} }
@ -149,13 +280,14 @@ JNIEXPORT void JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterWrite( Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterWrite(
JNIEnv *env, JNIEnv *env,
jclass type, jclass type,
jlong ptr, jlong writerOpaque,
jbyteArray buffer jbyteArray buffer
) { ) {
jsize count = (*env)->GetArrayLength(env, buffer); jsize count = (*env)->GetArrayLength(env, buffer);
jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL); 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); (*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 JNIEXPORT jlong JNICALL
Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterGetReader(JNIEnv *env, jclass type, jlong ptr) { Java_com_jakewharton_mosaic_terminal_Jni_stdinWriterGetReader(JNIEnv *env, jclass type, jlong ptr) {
return (jlong) stdinWriter_getReader((stdinWriter *) ptr); return (jlong) stdinWriter_getReader((stdinWriter *) ptr);

View File

@ -19,7 +19,13 @@ internal object Jni {
external fun exitRawMode(savedConfig: Long) external fun exitRawMode(savedConfig: Long)
@JvmStatic @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 @JvmStatic
external fun stdinReaderRead( external fun stdinReaderRead(
@ -45,7 +51,7 @@ internal object Jni {
external fun stdinReaderFree(reader: Long) external fun stdinReaderFree(reader: Long)
@JvmStatic @JvmStatic
external fun stdinWriterInit(): Long external fun stdinWriterInit(handler: Long): Long
@JvmStatic @JvmStatic
external fun stdinWriterGetReader(writer: Long): Long external fun stdinWriterGetReader(writer: Long): Long
@ -53,6 +59,24 @@ internal object Jni {
@JvmStatic @JvmStatic
external fun stdinWriterWrite(writer: Long, buffer: ByteArray) 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 @JvmStatic
external fun stdinWriterFree(writer: Long) external fun stdinWriterFree(writer: Long)

View File

@ -2,15 +2,24 @@ package com.jakewharton.mosaic.terminal
import com.jakewharton.mosaic.terminal.Jni.enterRawMode import com.jakewharton.mosaic.terminal.Jni.enterRawMode
import com.jakewharton.mosaic.terminal.Jni.exitRawMode 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.stdinReaderFree
import com.jakewharton.mosaic.terminal.Jni.stdinReaderInit import com.jakewharton.mosaic.terminal.Jni.stdinReaderInit
import com.jakewharton.mosaic.terminal.Jni.stdinReaderInterrupt import com.jakewharton.mosaic.terminal.Jni.stdinReaderInterrupt
import com.jakewharton.mosaic.terminal.Jni.stdinReaderRead import com.jakewharton.mosaic.terminal.Jni.stdinReaderRead
import com.jakewharton.mosaic.terminal.Jni.stdinReaderReadWithTimeout 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.stdinWriterFree
import com.jakewharton.mosaic.terminal.Jni.stdinWriterGetReader import com.jakewharton.mosaic.terminal.Jni.stdinWriterGetReader
import com.jakewharton.mosaic.terminal.Jni.stdinWriterInit 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.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 { public actual object Tty {
@JvmStatic @JvmStatic
@ -29,53 +38,94 @@ public actual object Tty {
} }
@JvmStatic @JvmStatic
public actual fun stdinReader(): StdinReader { public actual fun terminalReader(emitDebugEvents: Boolean): TerminalReader {
val reader = stdinReaderInit() val events = Channel<Event>(UNLIMITED)
if (reader == 0L) throw OutOfMemoryError() val handlerPtr = platformEventHandlerInit(PlatformEventHandler(events))
return StdinReader(reader) 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. @JvmSynthetic // Hide from Java callers.
internal actual fun stdinWriter(): StdinWriter { internal actual fun stdinWriter(emitDebugEvents: Boolean): StdinWriter {
val writer = stdinWriterInit() val events = Channel<Event>(UNLIMITED)
if (writer == 0L) throw OutOfMemoryError() val handlerPtr = platformEventHandlerInit(PlatformEventHandler(events))
val reader = stdinWriterGetReader(writer) if (handlerPtr != 0L) {
return StdinWriter(writer, reader) 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( // TODO @JvmSynthetic https://youtrack.jetbrains.com/issue/KT-24981
private val readerPtr: Long, internal actual class PlatformInput internal constructor(
private var readerPtr: Long,
private val handlerPtr: Long,
) : AutoCloseable { ) : 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) 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) return stdinReaderReadWithTimeout(readerPtr, buffer, offset, count, timeoutMillis)
} }
public actual fun interrupt() { actual fun interrupt() {
stdinReaderInterrupt(readerPtr) stdinReaderInterrupt(readerPtr)
} }
public actual override fun close() { actual override fun close() {
stdinReaderFree(readerPtr) if (readerPtr != 0L) {
stdinReaderFree(readerPtr)
readerPtr = 0
platformEventHandlerFree(handlerPtr)
}
} }
} }
// TODO @JvmSynthetic https://youtrack.jetbrains.com/issue/KT-24981 // TODO @JvmSynthetic https://youtrack.jetbrains.com/issue/KT-24981
internal actual class StdinWriter internal constructor( internal actual class StdinWriter internal constructor(
private val writerPtr: Long, private var writerPtr: Long,
readerPtr: Long, actual val reader: TerminalReader,
) : AutoCloseable { ) : AutoCloseable {
actual val reader: StdinReader = StdinReader(readerPtr)
actual fun write(buffer: ByteArray) { actual fun write(buffer: ByteArray) {
stdinWriterWrite(writerPtr, buffer) 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() { actual override fun close() {
stdinWriterFree(writerPtr) reader.close()
if (writerPtr != 0L) {
stdinWriterFree(writerPtr)
writerPtr = 0
}
} }
} }

View File

@ -2,3 +2,11 @@
-keep,allowoptimization class com.jakewharton.mosaic.terminal.Jni { -keep,allowoptimization class com.jakewharton.mosaic.terminal.Jni {
native <methods>; native <methods>;
} }
# These members are interacted with through native code.
-keep,allowoptimization class com.jakewharton.mosaic.terminal.PlatformEventHandler {
void onFocus(...);
void onKey(...);
void onMouse(...);
void onResize(...);
}

View File

@ -1,9 +1,20 @@
package com.jakewharton.mosaic.terminal package com.jakewharton.mosaic.terminal
import com.jakewharton.mosaic.terminal.event.Event
import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.CPointer import kotlinx.cinterop.CPointer
import kotlinx.cinterop.StableRef
import kotlinx.cinterop.addressOf 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.useContents
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
public actual object Tty { public actual object Tty {
public actual fun enableRawMode(): AutoCloseable { public actual fun enableRawMode(): AutoCloseable {
@ -24,21 +35,61 @@ public actual object Tty {
} }
} }
public actual fun stdinReader(): StdinReader { public actual fun terminalReader(emitDebugEvents: Boolean): TerminalReader {
val reader = stdinReader_init().useContents { val events = Channel<Event>(UNLIMITED)
val handler = PlatformEventHandler(events)
val handlerRef = StableRef.create(handler)
val handlerPtr = nativeHeap.alloc<platformEventHandler> {
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" } 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 { internal actual fun stdinWriter(emitDebugEvents: Boolean): StdinWriter {
val writer = stdinWriter_init().useContents { val events = Channel<Event>(UNLIMITED)
// TODO Fix all this duplication, ownership
val handler = PlatformEventHandler(events)
val handlerRef = StableRef.create(handler)
val handlerPtr = nativeHeap.alloc<platformEventHandler> {
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" } 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 { internal fun throwError(error: UInt): Nothing {
@ -46,38 +97,45 @@ public actual object Tty {
} }
} }
public actual class StdinReader internal constructor( internal actual class PlatformInput internal constructor(
private var ref: CPointer<stdinReader>?, ptr: CPointer<stdinReader>,
private val handlerPtr: CPointer<platformEventHandler>?,
private val handlerRef: StableRef<PlatformEventHandler>?,
) : AutoCloseable { ) : AutoCloseable {
public actual fun read(buffer: ByteArray, offset: Int, count: Int): Int { private var ptr: CPointer<stdinReader>? = ptr
actual fun read(buffer: ByteArray, offset: Int, count: Int): Int {
buffer.usePinned { buffer.usePinned {
stdinReader_read(ref, it.addressOf(offset), count).useContents { stdinReader_read(ptr, it.addressOf(offset), count).useContents {
if (error == 0U) return this.count if (error == 0U) return this.count
Tty.throwError(error) 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 { 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 if (error == 0U) return this.count
Tty.throwError(error) Tty.throwError(error)
} }
} }
} }
public actual fun interrupt() { actual fun interrupt() {
val error = stdinReader_interrupt(ref) val error = stdinReader_interrupt(ptr)
if (error == 0U) return if (error == 0U) return
Tty.throwError(error) Tty.throwError(error)
} }
public actual override fun close() { actual override fun close() {
ref?.let { ref -> ptr?.let { ptr ->
this.ref = null this.ptr = null
val error = stdinReader_free(ptr)
handlerPtr?.let(nativeHeap::free)
handlerRef?.dispose()
val error = stdinReader_free(ref)
if (error == 0U) return if (error == 0U) return
Tty.throwError(error) Tty.throwError(error)
} }
@ -85,22 +143,36 @@ public actual class StdinReader internal constructor(
} }
internal actual class StdinWriter internal constructor( internal actual class StdinWriter internal constructor(
private var ref: CPointer<stdinWriter>?, private var ptr: CPointer<stdinWriter>?,
readerRef: CPointer<stdinReader>, actual val reader: TerminalReader,
) : AutoCloseable { ) : AutoCloseable {
actual val reader: StdinReader = StdinReader(readerRef)
actual fun write(buffer: ByteArray) { actual fun write(buffer: ByteArray) {
val error = buffer.usePinned { val error = buffer.usePinned {
stdinWriter_write(ref, it.addressOf(0), buffer.size) stdinWriter_write(ptr, it.addressOf(0), buffer.size)
} }
if (error == 0U) return if (error == 0U) return
Tty.throwError(error) 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() { actual override fun close() {
ref?.let { ref -> ptr?.let { ref ->
this.ref = null this.ptr = null
reader.close() reader.close()
@ -111,3 +183,23 @@ internal actual class StdinWriter internal constructor(
} }
} }
} }
private fun onFocusCallback(opaque: COpaquePointer?, focused: Boolean) {
val handler = opaque!!.asStableRef<PlatformEventHandler>().get()
handler.onFocus(focused)
}
private fun onKeyCallback(opaque: COpaquePointer?) {
val handler = opaque!!.asStableRef<PlatformEventHandler>().get()
handler.onKey()
}
private fun onMouseCallback(opaque: COpaquePointer?) {
val handler = opaque!!.asStableRef<PlatformEventHandler>().get()
handler.onMouse()
}
private fun onResizeCallback(opaque: COpaquePointer?, columns: Int, rows: Int, width: Int, height: Int) {
val handler = opaque!!.asStableRef<PlatformEventHandler>().get()
handler.onResize(columns, rows, width, height)
}

View File

@ -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.options.option
import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.enum
import com.jakewharton.finalization.withFinalizationHook import com.jakewharton.finalization.withFinalizationHook
import com.jakewharton.mosaic.terminal.TerminalParser
import com.jakewharton.mosaic.terminal.Tty 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
import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl import com.jakewharton.mosaic.terminal.event.KeyboardEvent.Companion.ModifierCtrl
import com.jakewharton.mosaic.terminal.event.UnknownEvent
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.awaitCancellation 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.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -89,7 +85,7 @@ private class RawModeEchoCommand : CliktCommand("raw-mode-echo") {
print("\u001b]4;$i;?\u001b\\") 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 // 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. // 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<String>(UNLIMITED)
launch(Dispatchers.IO) { 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) { when (mode) {
Mode.Hex -> { Mode.Hex -> print((events.receive() as DebugEvent).bytes.toHexString())
val buffer = ByteArray(1024) Mode.Event -> print(event.toString())
while (job.isActive) { }
val read = reader.read(buffer, 0, 1024)
if (read > 0) { if (event is KeyboardEvent &&
val hex = buffer.toHexString(endIndex = read) event.codepoint == 0x63 &&
inputs.trySend(hex) event.modifiers == ModifierCtrl
if (hex == "03" || hex == "1b5b39393b3575") { ) {
break 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
}
}
}
} }
inputs.close()
} }
print(inputs.receive())
for (input in inputs) {
print("\r\n")
print(input)
}
print("\r\n") print("\r\n")
readerInterruptJob.cancel() readerInterruptJob.cancel()
}, },