218 lines
8.0 KiB
C
218 lines
8.0 KiB
C
|
|
/**
|
||
|
|
* @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 <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
strncpy(obj->input_state.variable, arg->identifier,
|
||
|
|
sizeof(obj->input_state.variable) - 1);
|
||
|
|
obj->input_state.variable[sizeof(obj->input_state.variable) - 1] = '\0';
|
||
|
|
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);
|
||
|
|
}
|