/** * @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); } 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; /* * 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; } SUCCEED_RETURN(errctx); }