/** * @file sink_tee.c * @brief A text sink that writes to two others. * * The reference's Write() and Println() (basicruntime_graphics.go:140,148) put * every line on stdout *and* on an SDL surface. TODO.md section 1.5 turned that * pair of hardcoded calls into a vtable specifically so the interpreter would * never carry the second one, and section 3 item 5 says it again for the * standalone AKGL driver: do not put a hardcoded second write in the * interpreter. This is where the second write lives instead. * * No SDL and no libakgl. Composing two function-pointer records needs neither, * so this stays in the core library where the stdio-only suite can test it. */ #include #include #include #include /** @brief Fetch and validate the state behind a tee sink. */ static akerr_ErrorContext AKERR_NOIGNORE *tee_state(akbasic_TextSink *self, akbasic_TeeSink **dest) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in tee sink"); *dest = (akbasic_TeeSink *)self->self; FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER, "tee sink has no state"); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *tee_write(akbasic_TextSink *self, const char *text) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); PASS(errctx, state->primary->write(state->primary, text)); PASS(errctx, state->mirror->write(state->mirror, text)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *tee_writeln(akbasic_TextSink *self, const char *text) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); PASS(errctx, state->primary->writeln(state->primary, text)); PASS(errctx, state->mirror->writeln(state->mirror, text)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *tee_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); FAIL_ZERO_RETURN(errctx, (dest != NULL && eof != NULL), AKERR_NULLPOINTER, "NULL argument in tee sink readline"); if ( state->reader == NULL ) { /* * A pair with no reader reports end of input rather than raising, which * is the contract sink.h states: running off the end is how RUNSTREAM * mode finishes normally and INPUT already handles it. */ if ( len > 0 ) { dest[0] = '\0'; } *eof = true; SUCCEED_RETURN(errctx); } PASS(errctx, state->reader->readline(state->reader, dest, len, eof)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *tee_clear(akbasic_TextSink *self) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); PASS(errctx, state->primary->clear(state->primary)); PASS(errctx, state->mirror->clear(state->mirror)); SUCCEED_RETURN(errctx); } /** * @brief Forward a cursor move to whichever half has a cursor. * * Only one of them will: an akgl sink has a character grid and a stdio sink does * not. Forwarding to both would be wrong anyway -- there is one cursor, and it * belongs to the half that draws. */ static akerr_ErrorContext *tee_moveto(akbasic_TextSink *self, int col, int row) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in moveto"); state = (akbasic_TeeSink *)self->self; FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "tee sink has no state"); if ( state->primary != NULL && state->primary->moveto != NULL ) { PASS(errctx, state->primary->moveto(state->primary, col, row)); } if ( state->mirror != NULL && state->mirror->moveto != NULL ) { PASS(errctx, state->mirror->moveto(state->mirror, col, row)); } SUCCEED_RETURN(errctx); } /** * @brief Constrain whichever half has a grid, for WINDOW. * * Same shape as tee_moveto() above it, and for the same reason: a stdio half has * no grid to constrain and a windowed half does, so the call goes to whoever can * take it and the other is left alone. * * This was missing, and its absence made `WINDOW` unreachable in the one build * that has a character grid -- the standalone AKGL frontend runs the interpreter * against a tee, so the fully implemented sink_window() underneath answered * nothing. `WINDOW` refused with "needs a text device with a character grid" on * the build that had one. TODO.md section 6 item 31. */ static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top, int right, int bottom) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); if ( state->primary != NULL && state->primary->window != NULL ) { PASS(errctx, state->primary->window(state->primary, left, top, right, bottom)); } if ( state->mirror != NULL && state->mirror->window != NULL ) { PASS(errctx, state->mirror->window(state->mirror, left, top, right, bottom)); } SUCCEED_RETURN(errctx); } /** * @brief Report the grid of whichever half has one. * * The first half that answers wins rather than both being consulted, because * there is one grid and only one half can have it -- the same reasoning * tee_moveto() gives about there being one cursor. */ static akerr_ErrorContext *tee_grid(akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh) { PREPARE_ERROR(errctx); akbasic_TeeSink *state = NULL; PASS(errctx, tee_state(self, &state)); if ( state->primary != NULL && state->primary->grid != NULL ) { PASS(errctx, state->primary->grid(state->primary, columns, rows, cellw, cellh)); SUCCEED_RETURN(errctx); } if ( state->mirror != NULL && state->mirror->grid != NULL ) { PASS(errctx, state->mirror->grid(state->mirror, columns, rows, cellw, cellh)); SUCCEED_RETURN(errctx); } FAIL_RETURN(errctx, AKBASIC_ERR_STATE, "neither half of this tee has a character grid"); } akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink *state, akbasic_TextSink *primary, akbasic_TextSink *mirror, akbasic_TextSink *reader) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER, "NULL argument in sink_init_tee"); FAIL_ZERO_RETURN(errctx, (primary != NULL && mirror != NULL), AKERR_NULLPOINTER, "A tee sink needs two sinks to write to"); /* * Refused rather than quietly accepted. A third sink here would read from * somewhere nothing writes to, which is the sort of wiring mistake that * shows up much later as a program that never sees its own input. */ FAIL_ZERO_RETURN(errctx, (reader == NULL || reader == primary || reader == mirror), AKBASIC_ERR_VALUE, "A tee sink's reader must be one of the two sinks it writes to"); state->primary = primary; state->mirror = mirror; state->reader = reader; obj->self = state; obj->write = tee_write; obj->writeln = tee_writeln; obj->readline = tee_readline; obj->clear = tee_clear; /* * Cleared first, because the two below are conditional and an initializer * that leaves a field alone is not an initializer: a caller with a sink on * the stack would get whatever was in that memory, and `CHAR` decides * whether it can move a cursor by testing this pointer for NULL. */ obj->moveto = NULL; obj->window = NULL; obj->grid = NULL; /* * Offered only when a half can actually do it, so CHAR's refusal against a * stdio-only driver still reads correctly through a tee. */ if ( (primary != NULL && primary->moveto != NULL) || (mirror != NULL && mirror->moveto != NULL) ) { obj->moveto = tee_moveto; } /* Same rule for WINDOW, so its refusal against a grid-less pair still reads correctly through a tee. */ if ( (primary != NULL && primary->window != NULL) || (mirror != NULL && mirror->window != NULL) ) { obj->window = tee_window; } /* And for the grid RWINDOW and RGR read. */ if ( (primary != NULL && primary->grid != NULL) || (mirror != NULL && mirror->grid != NULL) ) { obj->grid = tee_grid; } SUCCEED_RETURN(errctx); }