Skip to content

Commit

Permalink
Add TTY functions to write stdout and stderr (#738)
Browse files Browse the repository at this point in the history
In the future these will be used to write bytes instead of chars and to intercept 'println' usage.
  • Loading branch information
JakeWharton authored Feb 25, 2025
1 parent 7ab9bfd commit 32a91ba
Show file tree
Hide file tree
Showing 17 changed files with 349 additions and 153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public class TerminalReader internal constructor(
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 = tty.read(buffer, limit, BufferSize - limit)
val read = tty.readInput(buffer, limit, BufferSize - limit)
if (read == -1) break // EOF
if (read == 0) return // Interrupt

Expand All @@ -203,7 +203,7 @@ public class TerminalReader internal constructor(
// 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 = tty.readWithTimeout(
val read = tty.readInputWithTimeout(
buffer,
1,
BufferSize - 1,
Expand Down Expand Up @@ -955,7 +955,7 @@ public class TerminalReader internal constructor(
}

public fun interrupt() {
tty.interrupt()
tty.interruptRead()
}

/**
Expand Down
8 changes: 5 additions & 3 deletions mosaic-tty/api/mosaic-tty.api
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public final class com/jakewharton/mosaic/tty/Tty : java/lang/AutoCloseable {
public final fun currentSize ()[I
public final fun enableRawMode ()V
public final fun enableWindowResizeEvents ()V
public final fun interrupt ()V
public final fun read ([BII)I
public final fun readWithTimeout ([BIII)I
public final fun interruptRead ()V
public final fun readInput ([BII)I
public final fun readInputWithTimeout ([BIII)I
public final fun writeError ([BII)I
public final fun writeOutput ([BII)I
}

public abstract interface class com/jakewharton/mosaic/tty/Tty$Callback {
Expand Down
12 changes: 5 additions & 7 deletions mosaic-tty/api/mosaic-tty.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

// Library unique name: <com.jakewharton.mosaic:mosaic-tty>
final class com.jakewharton.mosaic.tty/TestTty : kotlin/AutoCloseable { // com.jakewharton.mosaic.tty/TestTty|null[0]
constructor <init>(kotlinx.cinterop/CPointer<cnames.structs/MosaicTestTtyImpl>?, com.jakewharton.mosaic.tty/Tty) // com.jakewharton.mosaic.tty/TestTty.<init>|<init>(kotlinx.cinterop.CPointer<cnames.structs.MosaicTestTtyImpl>?;com.jakewharton.mosaic.tty.Tty){}[0]

final val tty // com.jakewharton.mosaic.tty/TestTty.tty|{}tty[0]
final fun <get-tty>(): com.jakewharton.mosaic.tty/Tty // com.jakewharton.mosaic.tty/TestTty.tty.<get-tty>|<get-tty>(){}[0]

Expand All @@ -25,15 +23,15 @@ final class com.jakewharton.mosaic.tty/TestTty : kotlin/AutoCloseable { // com.j
}

final class com.jakewharton.mosaic.tty/Tty : kotlin/AutoCloseable { // com.jakewharton.mosaic.tty/Tty|null[0]
constructor <init>(kotlinx.cinterop/CPointer<cnames.structs/MosaicTtyImpl>, kotlinx.cinterop/CPointer<com.jakewharton.mosaic.tty/MosaicTtyCallback>, kotlinx.cinterop/StableRef<com.jakewharton.mosaic.tty/Tty.Callback>) // com.jakewharton.mosaic.tty/Tty.<init>|<init>(kotlinx.cinterop.CPointer<cnames.structs.MosaicTtyImpl>;kotlinx.cinterop.CPointer<com.jakewharton.mosaic.tty.MosaicTtyCallback>;kotlinx.cinterop.StableRef<com.jakewharton.mosaic.tty.Tty.Callback>){}[0]

final fun close() // com.jakewharton.mosaic.tty/Tty.close|close(){}[0]
final fun currentSize(): kotlin/IntArray // com.jakewharton.mosaic.tty/Tty.currentSize|currentSize(){}[0]
final fun enableRawMode() // com.jakewharton.mosaic.tty/Tty.enableRawMode|enableRawMode(){}[0]
final fun enableWindowResizeEvents() // com.jakewharton.mosaic.tty/Tty.enableWindowResizeEvents|enableWindowResizeEvents(){}[0]
final fun interrupt() // com.jakewharton.mosaic.tty/Tty.interrupt|interrupt(){}[0]
final fun read(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/Tty.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.tty/Tty.readWithTimeout|readWithTimeout(kotlin.ByteArray;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun interruptRead() // com.jakewharton.mosaic.tty/Tty.interruptRead|interruptRead(){}[0]
final fun readInput(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/Tty.readInput|readInput(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final fun readInputWithTimeout(kotlin/ByteArray, kotlin/Int, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/Tty.readInputWithTimeout|readInputWithTimeout(kotlin.ByteArray;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun writeError(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/Tty.writeError|writeError(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final fun writeOutput(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/Tty.writeOutput|writeOutput(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]

abstract interface Callback { // com.jakewharton.mosaic.tty/Tty.Callback|null[0]
abstract fun onFocus(kotlin/Boolean) // com.jakewharton.mosaic.tty/Tty.Callback.onFocus|onFocus(kotlin.Boolean){}[0]
Expand Down
27 changes: 18 additions & 9 deletions mosaic-tty/src/commonMain/c/mosaic-test-tty-posix.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#include <unistd.h>

typedef struct MosaicTestTtyImpl {
int pipe[2];
int stdin_write_fd;
MosaicTty *tty;
} MosaicTestTtyImpl;

Expand All @@ -23,16 +23,24 @@ MosaicTestTtyInitResult testTty_init(MosaicTtyCallback *callback) {
goto ret;
}

if (unlikely(pipe(testTty->pipe)) != 0) {
int stdinPipe[2];
if (unlikely(pipe(stdinPipe)) != 0) {
result.error = errno;
goto err;
}
int stdinReadFd = stdinPipe[0];
int stdinWriteFd = stdinPipe[1];

MosaicTtyInitResult ttyInitResult = tty_initWithFd(testTty->pipe[0], callback);
int stdoutWriteFd = STDOUT_FILENO;
int stderrWriteFd = STDERR_FILENO;

MosaicTtyInitResult ttyInitResult = tty_initWithFds(stdinReadFd, stdoutWriteFd, stderrWriteFd, callback);
if (unlikely(ttyInitResult.error)) {
result.error = ttyInitResult.error;
goto err;
}

testTty->stdin_write_fd = stdinWriteFd;
testTty->tty = ttyInitResult.tty;

result.testTty = testTty;
Expand All @@ -50,9 +58,9 @@ MosaicTty *testTty_getTty(MosaicTestTty *testTty) {
}

uint32_t testTty_write(MosaicTestTty *testTty, char *buffer, int count) {
int pipeOut = testTty->pipe[1];
int stdinWriteFd = testTty->stdin_write_fd;
while (count > 0) {
int result = write(pipeOut, buffer, count);
int result = write(stdinWriteFd, buffer, count);
if (unlikely(result == -1)) {
goto err;
}
Expand Down Expand Up @@ -86,16 +94,17 @@ uint32_t testTty_resizeEvent(MosaicTestTty *testTty, int columns, int rows, int
}

uint32_t testTty_free(MosaicTestTty *testTty) {
int *pipe = testTty->pipe;
uint32_t result = 0;

int result = 0;
if (unlikely(close(pipe[0]) != 0)) {
if (unlikely(close(testTty->stdin_write_fd) != 0)) {
result = errno;
}
if (unlikely(close(pipe[1]) != 0 && result != 0)) {
if (unlikely(close(testTty->tty->stdin_read_fd) != 0 && result != 0)) {
result = errno;
}

free(testTty);

return result;
}

Expand Down
6 changes: 5 additions & 1 deletion mosaic-tty/src/commonMain/c/mosaic-test-tty-windows.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ MosaicTestTtyInitResult testTty_init(MosaicTtyCallback *callback) {
// Ensure we don't start with existing records in the buffer.
FlushConsoleInputBuffer(writerConin);

MosaicTtyInitResult ttyInitResult = tty_initWithHandle(writerConin, callback);
HANDLE stdout = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE stderr = GetStdHandle(STD_ERROR_HANDLE);

MosaicTtyInitResult ttyInitResult = tty_initWithHandles(writerConin, stdout, stderr, callback);
if (unlikely(ttyInitResult.error)) {
result.error = ttyInitResult.error;
goto err;
Expand All @@ -65,6 +68,7 @@ MosaicTty *testTty_getTty(MosaicTestTty *testTty) {

uint32_t testTty_write(MosaicTestTty *testTty UNUSED, char *buffer, int count) {
uint32_t result = 0;

INPUT_RECORD *records = calloc(count, sizeof(INPUT_RECORD));
if (!records) {
result = ERROR_NOT_ENOUGH_MEMORY;
Expand Down
108 changes: 69 additions & 39 deletions mosaic-tty/src/commonMain/c/mosaic-tty-posix.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
#include <time.h>
#include <unistd.h>

MosaicTtyInitResult tty_initWithFd(int stdinFd, MosaicTtyCallback *callback) {
MosaicTtyInitResult tty_initWithFds(
int stdinReadFd,
int stdoutWriteFd,
int stderrWriteFd,
MosaicTtyCallback *callback
) {
MosaicTtyInitResult result = {};

MosaicTtyImpl *tty = calloc(1, sizeof(MosaicTtyImpl));
Expand All @@ -22,15 +27,17 @@ MosaicTtyInitResult tty_initWithFd(int stdinFd, MosaicTtyCallback *callback) {
goto ret;
}

if (unlikely(pipe(tty->pipe)) != 0) {
int interrupt_pipe[2];
if (unlikely(pipe(interrupt_pipe)) != 0) {
result.error = errno;
goto err;
}

tty->stdinFd = 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.
tty->nfds = ((stdinFd > tty->pipe[0]) ? stdinFd : tty->pipe[0]) + 1;
tty->stdin_read_fd = stdinReadFd;
tty->stdout_write_fd = stdoutWriteFd;
tty->stderr_write_fd = stderrWriteFd;
tty->interrupt_read_fd = interrupt_pipe[0];
tty->interrupt_write_fd = interrupt_pipe[1];
tty->callback = callback;

result.tty = tty;
Expand All @@ -44,37 +51,39 @@ MosaicTtyInitResult tty_initWithFd(int stdinFd, MosaicTtyCallback *callback) {
}

MosaicTtyInitResult tty_init(MosaicTtyCallback *callback) {
return tty_initWithFd(STDIN_FILENO, callback);
return tty_initWithFds(STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, callback);
}

MosaicTtyIoResult tty_readInternal(
MosaicTtyIoResult tty_readInputInternal(
MosaicTty *tty,
char *buffer,
int count,
struct timeval *timeout
) {
int stdinFd = tty->stdinFd;
FD_SET(stdinFd, &tty->fds);
MosaicTtyIoResult result = {};

int pipeIn = tty->pipe[0];
FD_SET(pipeIn, &tty->fds);
int stdinReadFd = tty->stdin_read_fd;
int interruptReadFd = tty->interrupt_read_fd;

MosaicTtyIoResult result = {};
fd_set fds;
FD_ZERO(&fds);
FD_SET(stdinReadFd, &fds);
FD_SET(interruptReadFd, &fds);

// TODO Consider setting up fd_set once in the struct and doing a stack copy here.
if (likely(select(tty->nfds, &tty->fds, NULL, NULL, timeout) >= 0)) {
if (likely(FD_ISSET(stdinFd, &tty->fds) != 0)) {
int c = read(stdinFd, buffer, count);
int nfds = 1 + ((stdinReadFd > interruptReadFd) ? stdinReadFd : interruptReadFd);
if (likely(select(nfds, &fds, NULL, NULL, timeout) >= 0)) {
if (likely(FD_ISSET(stdinReadFd, &fds) != 0)) {
int c = read(stdinReadFd, buffer, count);
if (likely(c > 0)) {
result.count = c;
} else if (c == 0) {
result.count = -1; // EOF
} else {
goto err;
}
} else if (unlikely(FD_ISSET(pipeIn, &tty->fds) != 0)) {
} else if (unlikely(FD_ISSET(interruptReadFd, &fds) != 0)) {
// Consume the single notification byte to clear the ready state for the next call.
int c = read(pipeIn, buffer, 1);
int c = read(interruptReadFd, buffer, 1);
if (unlikely(c < 0)) {
goto err;
}
Expand All @@ -92,11 +101,11 @@ MosaicTtyIoResult tty_readInternal(
goto ret;
}

MosaicTtyIoResult tty_read(MosaicTty *tty, char *buffer, int count) {
return tty_readInternal(tty, buffer, count, NULL);
MosaicTtyIoResult tty_readInput(MosaicTty *tty, char *buffer, int count) {
return tty_readInputInternal(tty, buffer, count, NULL);
}

MosaicTtyIoResult tty_readWithTimeout(
MosaicTtyIoResult tty_readInputWithTimeout(
MosaicTty *tty,
char *buffer,
int count,
Expand All @@ -106,23 +115,41 @@ MosaicTtyIoResult tty_readWithTimeout(
timeout.tv_sec = 0;
timeout.tv_usec = timeoutMillis * 1000;

return tty_readInternal(tty, buffer, count, &timeout);
return tty_readInputInternal(tty, buffer, count, &timeout);
}

uint32_t tty_interrupt(MosaicTty *tty) {
int pipeOut = tty->pipe[1];
int result = write(pipeOut, " ", 1);
return unlikely(result == -1)
? errno
: 0;
MosaicTtyIoResult tty_writeInternal(int writeFd, char *buffer, int count) {
MosaicTtyIoResult result = {};

int written = write(writeFd, buffer, count);
if (written != -1) {
result.count = written;
} else {
result.error = errno;
}

return result;
}

uint32_t tty_interruptRead(MosaicTty *tty) {
MosaicTtyIoResult result = tty_writeInternal(tty->interrupt_write_fd, " ", 1);
return result.error;
}

MosaicTtyIoResult tty_writeOutput(MosaicTty *tty, char *buffer, int count) {
return tty_writeInternal(tty->stdout_write_fd, buffer, count);
}

MosaicTtyIoResult tty_writeError(MosaicTty *tty, char *buffer, int count) {
return tty_writeInternal(tty->stderr_write_fd, buffer, count);
}

// TODO Make sure this is only written once. Probably just one instance per process at a time.
MosaicTty *globalTty = NULL;

void sigwinchHandler(int value UNUSED) {
struct winsize size;
if (ioctl(globalTty->stdinFd, TIOCGWINSZ, &size) != -1) {
if (ioctl(globalTty->stdin_read_fd, TIOCGWINSZ, &size) != -1) {
MosaicTtyCallback *callback = globalTty->callback;
callback->onResize(callback->opaque, size.ws_col, size.ws_row, size.ws_xpixel, size.ws_ypixel);
}
Expand All @@ -142,7 +169,7 @@ uint32_t tty_enableRawMode(MosaicTty *tty) {
goto ret;
}

if (unlikely(tcgetattr(STDIN_FILENO, saved) != 0)) {
if (unlikely(tcgetattr(tty->stdin_read_fd, saved) != 0)) {
result = errno;
goto err;
}
Expand All @@ -160,10 +187,10 @@ uint32_t tty_enableRawMode(MosaicTty *tty) {
current.c_cc[VMIN] = 1;
current.c_cc[VTIME] = 0;

if (unlikely(tcsetattr(STDIN_FILENO, TCSAFLUSH, &current) != 0)) {
if (unlikely(tcsetattr(tty->stdin_read_fd, TCSAFLUSH, &current) != 0)) {
result = errno;
// Try to restore the saved config.
tcsetattr(STDIN_FILENO, TCSAFLUSH, saved);
tcsetattr(tty->stdin_read_fd, TCSAFLUSH, saved);
goto err;
}

Expand Down Expand Up @@ -200,7 +227,7 @@ MosaicTtyTerminalSizeResult tty_currentTerminalSize(MosaicTty *tty) {
MosaicTtyTerminalSizeResult result = {};

struct winsize size;
if (ioctl(tty->stdinFd, TIOCGWINSZ, &size) != -1) {
if (ioctl(tty->stdin_read_fd, TIOCGWINSZ, &size) != -1) {
result.columns = size.ws_col;
result.rows = size.ws_row;
result.width = size.ws_xpixel;
Expand All @@ -213,25 +240,28 @@ MosaicTtyTerminalSizeResult tty_currentTerminalSize(MosaicTty *tty) {
}

uint32_t tty_free(MosaicTty *tty) {
int *pipe = tty->pipe;
uint32_t result = 0;

int result = 0;
if (unlikely(close(pipe[0]) != 0)) {
if (unlikely(close(tty->interrupt_read_fd) != 0)) {
result = errno;
}
if (unlikely(close(pipe[1]) != 0 && result != 0)) {
if (unlikely(close(tty->interrupt_write_fd) != 0 && result != 0)) {
result = errno;
}

if (tty->sigwinch && signal(SIGWINCH, SIG_DFL) == SIG_ERR && result != 0) {
result = errno;
}

if (tty->saved) {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, tty->saved) && result != 0) {
if (tcsetattr(tty->stdin_read_fd, TCSAFLUSH, tty->saved) && result != 0) {
result = errno;
}
free(tty->saved);
}

free(tty);

return result;
}

Expand Down
Loading

0 comments on commit 32a91ba

Please sign in to comment.