Read the keyboard: implement GET, GETKEY and SCNCLR
The poll_key half of group E, against the akbasic_InputBackend record. The
libakgl implementation behind it is akgl_controller_poll_key(), which drains a
ring the library fills from SDL events the host pumps -- so the interpreter can
answer "is there a key waiting" without owning an event loop, which is what goal
3 requires.
GET and GETKEY differ in one way and it is the interesting one. GET takes
whatever is there including nothing, and an empty buffer is success with the
empty string rather than an error -- that is what happens on most iterations of
every GET loop ever written, and upstream is explicit that its poll reports it
the same way. GETKEY waits, and since the library may not block, waiting is
spelled as holding the step loop: the verb sets a flag and akbasic_runtime_step()
declines to advance until a key arrives. Every step still returns and a bounded
run() still comes back, so a host keeps its frame rate; the program simply does
not move past the GETKEY.
The PLAY queue is serviced before that check on purpose -- music should keep
playing while a program waits for a keypress. Withdrawing the input device while
a GETKEY is holding releases it rather than wedging the script on a device that
no longer exists.
Both verbs accept an integer variable as well as a string one and give it the raw
key code, which is what a program testing for cursor or function keys needs. No
key is code zero, matching what a C128 reports. A float variable is refused: it
is neither a character nor a code.
SCNCLR goes through the text sink rather than a device, because the sink is where
PRINT already goes and is the only thing that knows what a screen means for this
host. The stdio sink treats it as a no-op; clearing a pipe means nothing.
One thing worth knowing before writing any bounded-run test, and now commented in
tests/input_verbs.c: source lines are stored indexed by line number and the
cursor starts at zero, so a step is spent on each empty slot along the way. A
program at line 10 needs eleven steps before it has run anything.
70/70 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:35:05 -04:00
|
|
|
/**
|
|
|
|
|
* @file runtime_input.c
|
|
|
|
|
* @brief The console input verbs: GET, GETKEY and SCNCLR.
|
|
|
|
|
*
|
|
|
|
|
* Same rule as the other two device files -- no SDL here, everything through the
|
|
|
|
|
* akbasic_InputBackend record. The libakgl implementation behind it is
|
|
|
|
|
* akgl_controller_poll_key(), which drains a ring the library fills from SDL
|
|
|
|
|
* events the *host* pumps, so the interpreter answers "is there a key waiting"
|
|
|
|
|
* without owning an event loop.
|
|
|
|
|
*
|
|
|
|
|
* GET and GETKEY differ in exactly one way and it is the interesting one. GET
|
|
|
|
|
* takes whatever is there, including nothing. GETKEY waits -- and since the
|
|
|
|
|
* library may not block, waiting is spelled as holding the step loop rather than
|
|
|
|
|
* as sitting on the keyboard. See TODO.md section 5.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.
Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.
verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.
Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.
DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.
Refs libakstdlib#26, libakstdlib#38
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
|
|
|
#include <akstdlib.h>
|
Read the keyboard: implement GET, GETKEY and SCNCLR
The poll_key half of group E, against the akbasic_InputBackend record. The
libakgl implementation behind it is akgl_controller_poll_key(), which drains a
ring the library fills from SDL events the host pumps -- so the interpreter can
answer "is there a key waiting" without owning an event loop, which is what goal
3 requires.
GET and GETKEY differ in one way and it is the interesting one. GET takes
whatever is there including nothing, and an empty buffer is success with the
empty string rather than an error -- that is what happens on most iterations of
every GET loop ever written, and upstream is explicit that its poll reports it
the same way. GETKEY waits, and since the library may not block, waiting is
spelled as holding the step loop: the verb sets a flag and akbasic_runtime_step()
declines to advance until a key arrives. Every step still returns and a bounded
run() still comes back, so a host keeps its frame rate; the program simply does
not move past the GETKEY.
The PLAY queue is serviced before that check on purpose -- music should keep
playing while a program waits for a keypress. Withdrawing the input device while
a GETKEY is holding releases it rather than wedging the script on a device that
no longer exists.
Both verbs accept an integer variable as well as a string one and give it the raw
key code, which is what a program testing for cursor or function keys needs. No
key is code zero, matching what a C128 reports. A float variable is refused: it
is neither a character nor a code.
SCNCLR goes through the text sink rather than a device, because the sink is where
PRINT already goes and is the only thing that knows what a screen means for this
host. The stdio sink treats it as a no-op; clearing a pipe means nothing.
One thing worth knowing before writing any bounded-run test, and now commented in
tests/input_verbs.c: source lines are stored indexed by line number and the
cursor starts at zero, so a step is spent on each empty slot along the way. A
program at line 10 needs eleven steps before it has run anything.
70/70 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:35:05 -04:00
|
|
|
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/input.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
|
|
|
|
|
#include "verbs.h"
|
|
|
|
|
|
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
|
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
|
|
|
do { \
|
|
|
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
|
|
|
} while ( 0 )
|
|
|
|
|
|
|
|
|
|
/** @brief Refuse politely when the host lent us no input device. */
|
|
|
|
|
static akerr_ErrorContext *require_input(akbasic_Runtime *obj, const char *verb)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in require_input");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->input != NULL), AKBASIC_ERR_DEVICE,
|
|
|
|
|
"%s needs an input device and this runtime has none", verb);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Identify the variable a GET or GETKEY assigns into.
|
|
|
|
|
*
|
|
|
|
|
* A string variable receives the character; a numeric one receives the key code,
|
|
|
|
|
* which is what a program testing for cursor keys or function keys wants.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, akbasic_Variable **dest, bool *numeric)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL && numeric != NULL),
|
|
|
|
|
AKERR_NULLPOINTER, "NULL argument in target_variable");
|
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
|
|
|
if ( arg == NULL ) {
|
|
|
|
|
arg = expr->right;
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"%s expected a variable", verb);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(arg->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ||
|
|
|
|
|
arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT),
|
|
|
|
|
AKBASIC_ERR_TYPE,
|
|
|
|
|
"%s expected a string or integer variable", verb);
|
|
|
|
|
*numeric = (arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT);
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"%s could not reach the variable %s", verb, arg->identifier);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @brief Write a keystroke, or the absence of one, into the target variable. */
|
|
|
|
|
static akerr_ErrorContext *store_key(akbasic_Variable *variable, bool numeric, int keycode, bool available)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
|
char text[2];
|
|
|
|
|
|
|
|
|
|
if ( numeric ) {
|
|
|
|
|
/* No key is code zero, which is what a C128 reports too. */
|
|
|
|
|
PASS(errctx, akbasic_variable_set_integer(variable, available ? (int64_t)keycode : 0,
|
|
|
|
|
zerosubscript, 1));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
text[0] = (available && keycode > 0 && keycode < 128) ? (char)keycode : '\0';
|
|
|
|
|
text[1] = '\0';
|
|
|
|
|
PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------- GET --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_get(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
bool numeric = false;
|
|
|
|
|
bool available = false;
|
|
|
|
|
int keycode = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_input(obj, "GET"));
|
|
|
|
|
PASS(errctx, target_variable(obj, expr, "GET", &variable, &numeric));
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* An empty buffer is success with the empty string, not an error. That is
|
|
|
|
|
* what happens on most iterations of any GET loop ever written, and libakgl
|
|
|
|
|
* is explicit that its poll reports it the same way.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
|
|
|
|
|
PASS(errctx, store_key(variable, numeric, keycode, available));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* -------------------------------------------------------------- GETKEY --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_getkey(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
bool numeric = false;
|
|
|
|
|
bool available = false;
|
|
|
|
|
int keycode = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_input(obj, "GETKEY"));
|
|
|
|
|
PASS(errctx, target_variable(obj, expr, "GETKEY", &variable, &numeric));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
|
|
|
|
|
if ( available ) {
|
|
|
|
|
obj->input_state.waiting = false;
|
|
|
|
|
PASS(errctx, store_key(variable, numeric, keycode, true));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Nothing typed yet. A C128 would sit here; the library may not, so instead
|
|
|
|
|
* the step loop is told to stop advancing the program counter. The line
|
|
|
|
|
* re-executes on each step until a key turns up, which looks the same to the
|
|
|
|
|
* program and leaves the host its frame rate. TODO.md section 5.
|
|
|
|
|
*/
|
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
|
|
|
if ( arg == NULL ) {
|
|
|
|
|
arg = expr->right;
|
|
|
|
|
}
|
|
|
|
|
obj->input_state.waiting = true;
|
|
|
|
|
obj->input_state.numeric = numeric;
|
Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.
Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.
verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.
Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.
DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.
Refs libakstdlib#26, libakstdlib#38
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:41:49 -04:00
|
|
|
PASS(errctx, aksl_strcpy(obj->input_state.variable,
|
|
|
|
|
sizeof(obj->input_state.variable), arg->identifier));
|
Read the keyboard: implement GET, GETKEY and SCNCLR
The poll_key half of group E, against the akbasic_InputBackend record. The
libakgl implementation behind it is akgl_controller_poll_key(), which drains a
ring the library fills from SDL events the host pumps -- so the interpreter can
answer "is there a key waiting" without owning an event loop, which is what goal
3 requires.
GET and GETKEY differ in one way and it is the interesting one. GET takes
whatever is there including nothing, and an empty buffer is success with the
empty string rather than an error -- that is what happens on most iterations of
every GET loop ever written, and upstream is explicit that its poll reports it
the same way. GETKEY waits, and since the library may not block, waiting is
spelled as holding the step loop: the verb sets a flag and akbasic_runtime_step()
declines to advance until a key arrives. Every step still returns and a bounded
run() still comes back, so a host keeps its frame rate; the program simply does
not move past the GETKEY.
The PLAY queue is serviced before that check on purpose -- music should keep
playing while a program waits for a keypress. Withdrawing the input device while
a GETKEY is holding releases it rather than wedging the script on a device that
no longer exists.
Both verbs accept an integer variable as well as a string one and give it the raw
key code, which is what a program testing for cursor or function keys needs. No
key is code zero, matching what a C128 reports. A float variable is refused: it
is neither a character nor a code.
SCNCLR goes through the text sink rather than a device, because the sink is where
PRINT already goes and is the only thing that knows what a screen means for this
host. The stdio sink treats it as a no-op; clearing a pipe means nothing.
One thing worth knowing before writing any bounded-run test, and now commented in
tests/input_verbs.c: source lines are stored indexed by line number and the
cursor starts at zero, so a step is spent on each empty slot along the way. A
program at line 10 needs eleven steps before it has run anything.
70/70 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:35:05 -04:00
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_input_service(akbasic_Runtime *obj, bool *blocked)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
bool available = false;
|
|
|
|
|
int keycode = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in input_service");
|
|
|
|
|
*blocked = false;
|
|
|
|
|
if ( !obj->input_state.waiting ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* A host that takes the input device away while a GETKEY is holding would
|
|
|
|
|
* otherwise wedge the script forever. Release it instead -- the variable
|
|
|
|
|
* keeps whatever it had, which is the empty string.
|
|
|
|
|
*/
|
|
|
|
|
if ( obj->input == NULL ) {
|
|
|
|
|
obj->input_state.waiting = false;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->input->poll_key(obj->input, &keycode, &available));
|
|
|
|
|
if ( !available ) {
|
|
|
|
|
*blocked = true;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj->input_state.waiting = false;
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, obj->input_state.variable, &variable));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"GETKEY could not reach the variable %s", obj->input_state.variable);
|
|
|
|
|
PASS(errctx, store_key(variable, obj->input_state.numeric, keycode, true));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* -------------------------------------------------------------- SCNCLR --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_scnclr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in SCNCLR");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Clearing the text screen is the sink's job, not a device's -- the sink is
|
|
|
|
|
* where PRINT already goes, and it is the only thing that knows what a
|
|
|
|
|
* "screen" means for this host. The stdio sink treats it as a no-op, since
|
|
|
|
|
* clearing a pipe means nothing.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, obj->sink->clear(obj->sink));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|