diff --git a/TODO.md b/TODO.md index b519f89..159dd11 100644 --- a/TODO.md +++ b/TODO.md @@ -672,7 +672,7 @@ Also on the queue, from the reference's own defect list: `tests/language/statements/multiple_per_line.bas` asserts what actually runs. **One thing about it needed a decision rather than a transcription**, and it is recorded as - deviation 29 in §5: BASIC 7.0 scopes everything after `THEN` to the condition, so + deviation 31 in §5: BASIC 7.0 scopes everything after `THEN` to the condition, so `IF C THEN A : B` must run neither `A` nor `B` when `C` is false. The parser takes exactly one statement per arm, so the rest of the line reaches the statement loop as ordinary statements and the branch has to tell the loop whether to run them. @@ -844,22 +844,46 @@ deviations from the reference's *program*: `main.go` and the SDL half of exactly, and the whole golden corpus is driven through this loop to prove it. What it changes for a *user* is that the window closes when asked. -26. **The frame is not cleared between frames.** The reference blits a text surface onto the - window surface and calls `UpdateSurface`. Here the graphics verbs draw straight to the - renderer rather than into a display list, so a `SDL_RenderClear` at the top of each frame - would erase every `DRAW` at the end of the frame it was issued in. The text layer is drawn - over whatever is already there. **What this costs:** text that has scrolled away leaves its - pixels behind wherever a graphics verb has drawn, because nothing repaints that region. A - `SCNCLR` does not repaint it either — it clears the text grid, not the bitmap. `GRAPHIC 0` - is the verb that wipes the drawing surface. +26. **The frame is not cleared, but the text layer erases its own rows.** The reference blits a + text surface onto the window surface and calls `UpdateSurface`. Here the graphics verbs draw + straight to the renderer rather than into a display list, so a `SDL_RenderClear` at the top + of each frame would erase every `DRAW` at the end of the frame it was issued in. The host + therefore does not clear. -27. **The window is closed by the host, and that is not `QUIT`.** Closing the window stops the + **The sink erases row by row instead**, filling each row it is about to draw — and each row + that has emptied since it last drew — with its background colour first. That makes the text + layer authoritative over the rows it occupies and leaves every other pixel alone, which is + how a C128's text plane sits over its bitmap. + + **This was originally "the text layer is drawn over whatever is already there", and that was + a bug rather than a deviation.** The grid was always right and the screen kept the previous + frame, which surfaced as three separate-looking faults reported together: a backspaced + character that stayed on screen, a scroll that appeared to overwrite the display with + garbage, and the editing cursor smearing an underscore along every column it had passed + through. One cause, three symptoms. `tests/akgl_frontend.c` asserts the fix at pixel level — + grid assertions could never have caught it, because the grid was never wrong. + + **What it still costs:** a program that draws a picture and then prints loses the strip of + picture behind each text row it uses. That is the deliberate half of the trade — the + alternative was clearing the whole frame, which loses the picture entirely. + +27. **There is no cursor.** The reference draws one as a literal underscore glyph (`drawCursor`, + `basicruntime_graphics.go:33`) and this did too. Removed on request: it sat under the text + being typed and made it hard to read. + + Worth separating the two reasons it was unbearable, because only one of them is gone. It + *smeared* — an underscore left at every column the cursor passed through — and that was + deviation 26's missing erase, now fixed. It also simply sits under the text, which is what + an underscore cursor does in a font whose glyphs reach the baseline. A cursor could come back + and behave now; a block or a different colour would read better than an underscore. + +28. **The window is closed by the host, and that is not `QUIT`.** Closing the window stops the frontend and leaves `obj->mode` alone; it does not set `AKBASIC_MODE_QUIT`. The distinction is invisible to the standalone program, which exits either way, and load-bearing for an embedding host: a game that closes its own window has not decided that the script is finished, and a script that ran `QUIT` has. `tests/akgl_frontend.c` asserts both halves. -28. **Piped input still works in an AKGL build.** With a terminal on stdin the window is the +29. **Piped input still works in an AKGL build.** With a terminal on stdin the window is the console and lines come from its editor; piped or redirected, they come from the pipe. This is not in the reference, which always reads `os.Stdin` in REPL mode. It exists so `basic < program.bas` behaves the same in both builds — and so the golden corpus can be @@ -879,7 +903,7 @@ deviations from the reference's *program*: `main.go` and the SDL half of ### Deviations in statement separation -29. **A branch decides who owns the rest of its line.** BASIC 7.0 scopes every statement after +31. **A branch decides who owns the rest of its line.** BASIC 7.0 scopes every statement after `THEN` to the condition, and the reference has no opinion on the matter because it never consumed the `COLON` token at all. The parser here takes exactly one statement for each arm, so the rest of the line arrives at the statement loop as ordinary top-level statements and diff --git a/include/akbasic/akgl.h b/include/akbasic/akgl.h index 7187340..3da923c 100644 --- a/include/akbasic/akgl.h +++ b/include/akbasic/akgl.h @@ -86,6 +86,13 @@ typedef struct akgl_RenderBackend *renderer; TTF_Font *font; SDL_Color color; + /** + * What a text cell is erased to before its glyphs are drawn. Opaque black + * by default, which is a Commodore console and is what makes the text layer + * *authoritative* over the rows it occupies rather than transparent over + * them -- see akbasic_sink_akgl_render(). + */ + SDL_Color background; int x; /* pixel origin of the text area */ int y; @@ -105,6 +112,14 @@ typedef struct */ char text[64][256]; + /** + * Which rows carried glyphs when this sink last rendered. A row that has + * emptied since -- scrolled away, cleared, or shortened -- still has to be + * erased, and after it is erased there is no longer anything in the grid to + * say it once had something in it. + */ + bool drawn[64]; + /* * The line editor. Nothing here is live except while readline is running, * and `editing` is what says so -- render() draws a cursor only then, and diff --git a/src/sink_akgl.c b/src/sink_akgl.c index ba4a813..96b5f62 100644 --- a/src/sink_akgl.c +++ b/src/sink_akgl.c @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -131,18 +132,38 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text static void echo_line(akbasic_AkglSink *state) { int i = 0; + int erasedto = 0; + int endedat = 0; state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->echolen; i++ ) { putchar_at(state, ' '); } + erasedto = state->cursorrow; + state->cursorrow = state->editrow; state->cursorcol = state->editcol; for ( i = 0; i < state->editlen; i++ ) { putchar_at(state, state->editline[i]); } + endedat = state->cursorrow; state->echolen = state->editlen; + + /* + * A line that got shorter can leave whole rows holding nothing but the + * spaces the erase pass just wrote -- which is what backspacing back across + * a wrap does to the row below. Truncate them, so such a row reports itself + * *empty* rather than merely blank-looking. + * + * That distinction is load-bearing rather than tidiness: render() decides + * what to erase and redraw from whether a row has text, and a row of spaces + * is text. Left alone it would be repainted forever, and anything the + * program had drawn underneath it would stay erased. + */ + for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) { + state->text[i][0] = '\0'; + } } /** @brief Append one byte to the line being typed, if there is room for it. */ @@ -395,6 +416,11 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi state->color.g = 0xff; state->color.b = 0xff; state->color.a = 0xff; + /* Opaque, not transparent: the text layer owns the rows it draws. */ + state->background.r = 0x00; + state->background.g = 0x00; + state->background.b = 0x00; + state->background.a = 0xff; state->x = 0; state->y = 0; state->width = w; @@ -431,6 +457,24 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj) "akgl sink has no state"); /* + * **Erase each row before drawing it.** Without this the sink paints glyphs + * on top of whatever the previous frame left, and the host deliberately does + * not clear the frame either -- §5 deviation 26, so that a DRAW survives + * past the frame it was issued in. The grid was always right and the screen + * kept the last frame, which showed up as three separate-looking bugs: a + * backspaced character that would not go away, a scroll that appeared to + * overwrite the screen with garbage, and the editing cursor smearing an + * underscore along every column it passed through. + * + * Row by row rather than one clear over the whole text area, and that is the + * deliberate part: the text layer is authoritative over the rows it occupies + * and leaves every other pixel alone. A program that draws a picture and + * prints one line loses one line's strip of it, the way a C128's text plane + * sits over its bitmap -- rather than losing the picture. + * + * `drawn` is what makes a row that has *emptied* get erased. Once it is + * cleared there is nothing left in the grid to say it ever held anything. + * * A loop, so no CATCH and no _BREAK macros: they expand to a C break, which * would escape the loop with an error still pending. PASS only. * @@ -439,7 +483,20 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj) * put characters somewhere the cursor does not think they are. */ for ( row = 0; row < state->rows; row++ ) { - if ( state->text[row][0] == '\0' ) { + bool hastext = (state->text[row][0] != '\0'); + SDL_FRect cells; + + if ( !hastext && !state->drawn[row] ) { + continue; + } + cells.x = (float)state->x; + cells.y = (float)(state->y + (row * state->cellh)); + cells.w = (float)state->width; + cells.h = (float)state->cellh; + PASS(errctx, akgl_draw_filled_rect(state->renderer, &cells, state->background)); + + state->drawn[row] = hastext; + if ( !hastext ) { continue; } PASS(errctx, akgl_text_rendertextat(state->font, state->text[row], @@ -449,18 +506,13 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj) } /* - * The cursor, drawn only while a line is being typed. The reference draws it - * the same way, as a literal underscore glyph (drawCursor, - * basicruntime_graphics.go:33), rather than as a filled rectangle -- which - * means it needs no draw primitive and cannot be a different shape from the - * text it sits in. + * No cursor glyph. The reference draws one as a literal underscore + * (drawCursor, basicruntime_graphics.go:33) and this did too, until it turned + * out to sit *under* the text being typed and make it hard to read. Removed + * on request; §5 records it, and the smearing that made it unbearable is + * fixed by the erase above rather than by the removal -- so a cursor could + * come back and behave, if one is wanted. */ - if ( state->editing ) { - PASS(errctx, akgl_text_rendertextat(state->font, "_", - state->color, 0, - state->x + (state->cursorcol * state->cellw), - state->y + (state->cursorrow * state->cellh))); - } SUCCEED_RETURN(errctx); } diff --git a/tests/akgl_frontend.c b/tests/akgl_frontend.c index 5f95d99..6217d3d 100644 --- a/tests/akgl_frontend.c +++ b/tests/akgl_frontend.c @@ -458,6 +458,257 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_text_input_is_started(void) SUCCEED_RETURN(errctx); } +/** @brief True when every pixel of one character cell is the background. */ +static bool cell_is_blank(SDL_Surface *shot, akbasic_AkglSink *sink, int col, int row) +{ + return !anything_drawn(shot, sink->x + (col * sink->cellw), + sink->y + (row * sink->cellh), + sink->cellw, sink->cellh); +} + +/** + * @brief What the sink drew last frame is erased before it draws again. + * + * **Three reported symptoms, one cause.** The sink used to paint glyphs on top + * of whatever was already on the renderer and never erase anything, and the + * frontend deliberately does not clear the frame (§5 deviation 26, so a DRAW + * survives). The grid was always right; the *screen* kept the previous frame: + * + * - a backspaced character stayed on screen, so backspace looked broken + * - a scrolled line left its old pixels behind, "papered over with garbage" + * - the editing cursor left an underscore at every column it passed through, + * which read as an underline under the whole line being typed + * + * These assertions are about *pixels*, deliberately. Every other sink test + * checks the character grid, and the grid was never the problem. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_render_erases_what_it_drew(void) +{ + PREPARE_ERROR(errctx); + SDL_Surface *shot = NULL; + akbasic_AkglSink *sink = NULL; + int row = 0; + + PASS(errctx, start_frontend(NULL)); + sink = &FRONTEND.akglstate; + + /* Something on screen to erase. */ + PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "XYZ")); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "XYZ should have been drawn"); + SDL_DestroySurface(shot); + + /* + * A clear must reach the glass, not just the grid. This is SCNCLR, and it is + * the simplest shape of the bug: the grid empties and the pixels stay. + */ + PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink)); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + for ( row = 0; row < 3; row++ ) { + TEST_REQUIRE(cell_is_blank(shot, sink, 0, row), + "row %d still shows its old pixels after a clear", row); + } + SDL_DestroySurface(shot); + + /* A line that gets *shorter* erases the tail it no longer occupies. */ + PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "ABCD")); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink)); + PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "AB")); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "AB should still be drawn"); + TEST_REQUIRE(cell_is_blank(shot, sink, 2, 0), + "the third cell held a C and must now be blank"); + TEST_REQUIRE(cell_is_blank(shot, sink, 3, 0), + "the fourth cell held a D and must now be blank"); + SDL_DestroySurface(shot); + + stop_frontend(); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Scrolling reaches the glass: the bottom row is blank after it scrolls. + * + * `scroll()` shifts the grid up and clears the last row, and that was always + * correct. What was not correct was the screen: nothing erased the row's old + * pixels, so the text appeared to be overwritten with garbage rather than + * scrolled. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_scroll_reaches_the_screen(void) +{ + PREPARE_ERROR(errctx); + SDL_Surface *shot = NULL; + akbasic_AkglSink *sink = NULL; + int i = 0; + + PASS(errctx, start_frontend(NULL)); + sink = &FRONTEND.akglstate; + + /* + * Fill every row *and render the bottom one* before scrolling it away. The + * first version of this test wrote all the rows in one loop, which scrolled + * the bottom row off before it had ever been drawn -- so there were no stale + * pixels to find and the test passed whether or not the bug was there. It is + * only a test of the scroll if the row reaches the glass first. + */ + for ( i = 0; i < sink->rows - 1; i++ ) { + PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, "FULL")); + } + PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "BOTTOM")); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + TEST_REQUIRE(!cell_is_blank(shot, sink, 0, sink->rows - 1), + "the bottom row should have been drawn before we scroll it"); + SDL_DestroySurface(shot); + + /* Now scroll it away. The grid clears the bottom row; the screen must too. */ + PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, "")); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + TEST_REQUIRE_STR(sink->text[sink->rows - 1], ""); + TEST_REQUIRE(cell_is_blank(shot, sink, 0, sink->rows - 1), + "the bottom row scrolled away but its pixels are still on screen"); + SDL_DestroySurface(shot); + + stop_frontend(); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Backspace erases across a line wrap, taking the cursor to the row above. + * + * Called out explicitly in the report: backspace has to clear the character and + * step back "at all times, including when the backspace key would move the + * cursor up to the previous line on the Y axis". A line long enough to wrap puts + * the tail of the edit on the row below its anchor, and backspacing past the + * wrap has to erase there and return the cursor to the row above. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_backspace_across_a_wrap(void) +{ + PREPARE_ERROR(errctx); + SDL_Surface *shot = NULL; + akbasic_AkglSink *sink = NULL; + char line[256]; + bool eof = true; + int over = 0; + int i = 0; + + PASS(errctx, start_frontend(NULL)); + sink = &FRONTEND.akglstate; + PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input)); + + /* Two characters past the right-hand edge, so the edit wraps onto row 1. */ + over = sink->columns + 2; + for ( i = 0; i < over; i++ ) { + push_text_key('x', "x"); + } + push_key('\r'); + PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof)); + TEST_REQUIRE_INT((int)strlen(line), over); + TEST_REQUIRE(sink->text[1][0] != '\0', "a line past the edge must wrap onto the next row"); + /* + * Render it, so row 1 genuinely has pixels to leave behind. Without this the + * pixel assertions below pass whether or not anything erases, because the + * wrapped row would never have been drawn in the first place. + */ + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + + /* + * Now type the same over-long line again and backspace back past the wrap. + * The editor is re-entered, so the anchor is row 2 by now; what matters is + * that the wrapped tail is erased and the cursor comes back a row. + */ + PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink)); + for ( i = 0; i < over; i++ ) { + push_text_key('y', "y"); + } + for ( i = 0; i < 4; i++ ) { + push_key(SDLK_BACKSPACE); + } + push_key('\r'); + PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof)); + TEST_REQUIRE_INT((int)strlen(line), over - 4); + /* Back inside the first row: the wrap is gone, so row 1 holds nothing. */ + TEST_REQUIRE_STR(sink->text[1], ""); + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + TEST_REQUIRE(cell_is_blank(shot, sink, 0, 1), + "the wrapped tail was backspaced away but is still on screen"); + /* And the last surviving character is where it should be, on row 0. */ + TEST_REQUIRE(!cell_is_blank(shot, sink, over - 5, 0), + "the character before the backspaces should still be drawn"); + TEST_REQUIRE(cell_is_blank(shot, sink, over - 4, 0), + "the first backspaced character should be gone"); + SDL_DestroySurface(shot); + + stop_frontend(); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Nothing is drawn under the text being typed. + * + * The editing cursor used to be an underscore glyph drawn at the cursor cell. + * With nothing erasing the previous frame it accumulated at every column the + * cursor had passed through, which read as an underline beneath the whole line + * and made typed text hard to read. Removed outright; §5 records it. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_no_cursor_underline(void) +{ + PREPARE_ERROR(errctx); + SDL_Surface *shot = NULL; + akbasic_AkglSink *sink = NULL; + int col = 0; + int y = 0; + int lit = 0; + + PASS(errctx, start_frontend(NULL)); + sink = &FRONTEND.akglstate; + + /* + * Rendered *while editing*, which is the only state the cursor was ever + * drawn in -- an earlier version of this test rendered after readline had + * returned, by which time `editing` is false and a re-added cursor would + * sail straight through it. + */ + PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "ab")); + sink->editing = true; + PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink)); + + /* + * The bottom scanline of each cell the text occupies and the one after it. + * An underscore lands there; "a" and "b" in this font do not, so anything + * lit on that line is the cursor and nothing else. + */ + shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + y = sink->y + sink->cellh - 1; + for ( col = 0; col < 4; col++ ) { + if ( anything_drawn(shot, sink->x + (col * sink->cellw), y, sink->cellw, 1) ) { + lit += 1; + } + } + TEST_REQUIRE_INT(lit, 0); + SDL_DestroySurface(shot); + sink->editing = false; + + stop_frontend(); + SUCCEED_RETURN(errctx); +} + /** * @brief A whole REPL session typed at the window, ending with QUIT. * @@ -535,6 +786,10 @@ int main(void) CATCH(errctx, test_window_close_stops()); CATCH(errctx, test_line_editor()); CATCH(errctx, test_text_input_is_started()); + CATCH(errctx, test_render_erases_what_it_drew()); + CATCH(errctx, test_scroll_reaches_the_screen()); + CATCH(errctx, test_backspace_across_a_wrap()); + CATCH(errctx, test_no_cursor_underline()); CATCH(errctx, test_repl_session()); } CLEANUP { stop_frontend(); diff --git a/tests/akgl_typing.sh b/tests/akgl_typing.sh index 46a897a..56afadc 100755 --- a/tests/akgl_typing.sh +++ b/tests/akgl_typing.sh @@ -108,8 +108,37 @@ if ! await "lower" 15; then fail "lower case did not survive the keyboard: composed text is not reaching the editor" fi +# Backspace, end to end and through what it produces rather than what it draws. +# Typing PRINX, rubbing out the X and finishing the word only runs if backspace +# actually removed a character from the line buffer -- otherwise this is a +# syntax error. Reported as "the backspace key does not clear the character +# underneath it". +type_line '30 PRINX'$'\b''T "BS OK"' +type_line 'RUN' +if ! await "BS OK" 15; then + echo "--- what the driver actually wrote ---" + tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p' + fail "backspace did not remove a character: the line reached the parser unedited" +fi + +# More lines than the window has rows, so the text has to scroll. Checked +# through stdout, which proves the interpreter kept running across the scroll; +# that the *screen* scrolls correctly is asserted at pixel level in +# tests/akgl_frontend.c, which can read the render target back and this cannot. +type_line '40 FOR I# = 1 TO 40' +type_line '50 PRINT "SCROLL"' +type_line '60 NEXT I#' +type_line 'RUN 40' +if ! await "SCROLL" 20; then + echo "--- what the driver actually wrote ---" + tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p' + fail "a program long enough to scroll the window produced nothing" +fi +COUNT="$(tr -d '\r' < "${OUT}" | grep -c '^SCROLL$')" +[ "${COUNT}" -eq 40 ] || fail "expected 40 scrolled lines, got ${COUNT}" + type_line 'QUIT' await "" 1 >/dev/null 2>&1 || true -echo "PASS: typed at a real focused window and the interpreter ran it" +echo "PASS: typed at a real focused window; backspace, scrolling and RUN all worked" exit 0