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