436 lines
11 KiB
C
436 lines
11 KiB
C
/**
|
|
* Copyright (c) 2025 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "console.h"
|
|
#include "assert/assert.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#include "input/input.h"
|
|
|
|
#include "console/cmd/cmdquit.h"
|
|
#include "console/cmd/cmdecho.h"
|
|
#include "console/cmd/cmdset.h"
|
|
#include "console/cmd/cmdget.h"
|
|
#include "console/cmd/cmdexec.h"
|
|
#include "console/cmd/cmdbind.h"
|
|
#include "console/cmd/cmdtoggleconsole.h"
|
|
#include "console/cmd/cmdalias.h"
|
|
#include "console/cmd/cmdscene.h"
|
|
|
|
console_t CONSOLE;
|
|
|
|
void consoleInit() {
|
|
memoryZero(&CONSOLE, sizeof(console_t));
|
|
|
|
// Register the get and set command.
|
|
CONSOLE.cmdGet = consoleRegCmd("get", cmdGet);
|
|
CONSOLE.cmdSet = consoleRegCmd("set", cmdSet);
|
|
consoleRegCmd("quit", cmdQuit);
|
|
consoleRegCmd("echo", cmdEcho);
|
|
consoleRegCmd("exec", cmdExec);
|
|
consoleRegCmd("bind", cmdBind);
|
|
consoleRegCmd("toggleconsole", cmdToggleConsole);
|
|
consoleRegCmd("alias", cmdAlias);
|
|
consoleRegCmd("scene", cmdScene);
|
|
|
|
#if CONSOLE_POSIX
|
|
threadInit(&CONSOLE.thread, consoleInputThread);
|
|
threadMutexInit(&CONSOLE.execMutex);
|
|
threadStartRequest(&CONSOLE.thread);
|
|
#endif
|
|
}
|
|
|
|
consolecmd_t * consoleRegCmd(const char_t *name, consolecmdfunc_t function) {
|
|
consolecmd_t *cmd = &CONSOLE.commands[CONSOLE.commandCount++];
|
|
consoleCmdInit(cmd, name, function);
|
|
return cmd;
|
|
}
|
|
|
|
consolevar_t * consoleRegVar(
|
|
const char_t *name,
|
|
const char_t *value,
|
|
consolevarchanged_t event
|
|
) {
|
|
consolevar_t *var;
|
|
|
|
// Existing?
|
|
var = consoleVarGet(name);
|
|
if(var != NULL) return var;
|
|
|
|
assertTrue(
|
|
CONSOLE.variableCount < CONSOLE_VARIABLES_MAX,
|
|
"Too many console variables registered."
|
|
);
|
|
|
|
// Create
|
|
var = &CONSOLE.variables[CONSOLE.variableCount++];
|
|
consoleVarInitListener(var, name, value, event);
|
|
return var;
|
|
}
|
|
|
|
consolevar_t * consoleVarGet(const char_t *name) {
|
|
assertNotNull(name, "name must not be NULL");
|
|
for(uint32_t i = 0; i < CONSOLE.variableCount; i++) {
|
|
consolevar_t *var = &CONSOLE.variables[i];
|
|
if(stringCompare(var->name, name) == 0) return var;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void consolePrint(const char_t *message, ...) {
|
|
char_t buffer[CONSOLE_LINE_MAX];
|
|
|
|
va_list args;
|
|
va_start(args, message);
|
|
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
|
va_end(args);
|
|
|
|
// Move all lines back
|
|
memoryMove(
|
|
CONSOLE.line[0],
|
|
CONSOLE.line[1],
|
|
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
|
);
|
|
|
|
// Copy the new line
|
|
memoryCopy(
|
|
CONSOLE.line[CONSOLE_HISTORY_MAX - 1],
|
|
buffer,
|
|
len + 1
|
|
);
|
|
printf("%s\n", buffer);
|
|
}
|
|
|
|
void consoleExec(const char_t *line) {
|
|
#if CONSOLE_POSIX
|
|
threadMutexLock(&CONSOLE.execMutex);
|
|
#endif
|
|
|
|
assertNotNull(line, "line must not be NULL");
|
|
assertTrue(
|
|
CONSOLE.execBufferCount < CONSOLE_EXEC_BUFFER_MAX,
|
|
"Too many commands in the buffer."
|
|
);
|
|
|
|
char_t buffer[CONSOLE_LINE_MAX];
|
|
size_t i = 0, j = 0;
|
|
char_t c;
|
|
consoleexecstate_t state = CONSOLE_EXEC_STATE_INITIAL;
|
|
consolecmdexec_t *exec = NULL;
|
|
|
|
while(state != CONSOLE_EXEC_STATE_FULLY_PARSED) {
|
|
c = line[i];
|
|
|
|
switch(state) {
|
|
case CONSOLE_EXEC_STATE_INITIAL:
|
|
assertTrue(j == 0, "Buffer not empty?");
|
|
|
|
if(c == '\0') {
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
break;
|
|
}
|
|
|
|
if(stringIsWhitespace(c) || c == ';') {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
state = CONSOLE_EXEC_STATE_PARSE_CMD;
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_PARSE_CMD:
|
|
if(stringIsWhitespace(c) || c == '\0' || c == ';') {
|
|
state = CONSOLE_EXEC_STATE_CMD_PARSED;
|
|
continue;
|
|
}
|
|
|
|
if(c == '"') {
|
|
// Can't handle quotes within the command.
|
|
consolePrint("Invalid command");
|
|
while(c != '\0' && c != ';') c = line[++i];
|
|
continue;
|
|
}
|
|
|
|
buffer[j++] = c;
|
|
i++;
|
|
|
|
if(j >= CONSOLE_LINE_MAX) {
|
|
consolePrint("Command too long");
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_CMD_PARSED:
|
|
if(j == 0) {
|
|
state = CONSOLE_EXEC_STATE_INITIAL;
|
|
continue;
|
|
}
|
|
|
|
// Create exec
|
|
assertNull(exec, "Existing command parsing?");
|
|
|
|
exec = &CONSOLE.execBuffer[CONSOLE.execBufferCount];
|
|
memoryZero(exec, sizeof(consolecmdexec_t));
|
|
|
|
buffer[j] = '\0';
|
|
stringCopy(exec->command, buffer, CONSOLE_LINE_MAX);
|
|
state = CONSOLE_EXEC_STATE_FIND_ARG;
|
|
|
|
j = 0;// Free up buffer
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_FIND_ARG:
|
|
if(c == '\0' || c == ';') {
|
|
state = CONSOLE_EXEC_STATE_CMD_FINISHED;
|
|
continue;
|
|
}
|
|
|
|
if(stringIsWhitespace(c)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if(c == '"') {
|
|
state = CONSOLE_EXEC_STATE_PARSE_ARG_QUOTED;
|
|
i++;
|
|
} else {
|
|
state = CONSOLE_EXEC_STATE_PARSE_ARG;
|
|
}
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_PARSE_ARG:
|
|
if(stringIsWhitespace(c) || c == '\0' || c == ';') {
|
|
state = CONSOLE_EXEC_STATE_ARG_PARSED;
|
|
continue;
|
|
}
|
|
|
|
buffer[j++] = c;
|
|
i++;
|
|
|
|
if(j >= CONSOLE_LINE_MAX) {
|
|
consolePrint("Arg too long");
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_PARSE_ARG_QUOTED:
|
|
if(c == '"') {
|
|
state = CONSOLE_EXEC_STATE_ARG_PARSED;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if(c == '\0' || c == ';') {
|
|
consolePrint("Unterminated quote");
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
continue;
|
|
}
|
|
|
|
if(c == '\\') {
|
|
c = line[++i];
|
|
|
|
if(c == '\0' || c == ';') {
|
|
consolePrint("Unterminated quote");
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
buffer[j++] = c;
|
|
i++;
|
|
|
|
if(j >= CONSOLE_LINE_MAX) {
|
|
consolePrint("Arg too long");
|
|
state = CONSOLE_EXEC_STATE_FULLY_PARSED;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_ARG_PARSED:
|
|
buffer[j] = '\0';
|
|
stringCopy(exec->argv[exec->argc++], buffer, CONSOLE_LINE_MAX);
|
|
state = CONSOLE_EXEC_STATE_FIND_ARG;
|
|
j = 0;// Free up buffer
|
|
break;
|
|
|
|
case CONSOLE_EXEC_STATE_CMD_FINISHED:
|
|
assertNotNull(exec, "No command found?");
|
|
|
|
// Now, is there a command that matches?
|
|
for(uint32_t k = 0; k < CONSOLE.commandCount; k++) {
|
|
consolecmd_t *cmd = &CONSOLE.commands[k];
|
|
if(stringCompare(cmd->name, exec->command) != 0) continue;
|
|
exec->cmd = cmd;
|
|
break;
|
|
}
|
|
|
|
if(exec->cmd == NULL) {
|
|
// Command wasn't found, is there a variable that matches?
|
|
for(uint32_t k = 0; k < CONSOLE.variableCount; k++) {
|
|
consolevar_t *var = &CONSOLE.variables[k];
|
|
if(stringCompare(var->name, exec->command) != 0) continue;
|
|
|
|
// Matching variable found, is this a GET or a SET?
|
|
if(exec->argc == 0) {
|
|
exec->cmd = CONSOLE.cmdGet;
|
|
stringCopy(exec->argv[0], exec->command, CONSOLE_LINE_MAX);
|
|
exec->argc = 1;
|
|
} else {
|
|
exec->cmd = CONSOLE.cmdSet;
|
|
stringCopy(exec->argv[1], exec->argv[0], CONSOLE_LINE_MAX);
|
|
stringCopy(exec->argv[0], exec->command, CONSOLE_LINE_MAX);
|
|
exec->argc = 2;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Variable not found, is there an alias that matches?
|
|
bool_t aliasFound = false;
|
|
for(uint32_t k = 0; k < CONSOLE.aliasCount; k++) {
|
|
consolealias_t *alias = &CONSOLE.aliases[k];
|
|
if(stringCompare(alias->alias, exec->command) != 0) continue;
|
|
|
|
// Matching alias found, we unlock the mutex and recursively call
|
|
// consoleExec to handle the alias command.
|
|
#if CONSOLE_POSIX
|
|
threadMutexUnlock(&CONSOLE.execMutex);
|
|
#endif
|
|
consoleExec(alias->command);
|
|
#if CONSOLE_POSIX
|
|
threadMutexLock(&CONSOLE.execMutex);
|
|
#endif
|
|
|
|
aliasFound = true;
|
|
break;
|
|
}
|
|
|
|
if(!aliasFound && exec->cmd == NULL) {
|
|
consolePrint("Command \"%s\" not found", exec->command);
|
|
exec = NULL;
|
|
state = CONSOLE_EXEC_STATE_INITIAL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Prep for next command.
|
|
exec = NULL;
|
|
state = CONSOLE_EXEC_STATE_INITIAL;
|
|
CONSOLE.execBufferCount++;
|
|
break;
|
|
|
|
default:
|
|
assertUnreachable("Invalid state.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if CONSOLE_POSIX
|
|
threadMutexUnlock(&CONSOLE.execMutex);
|
|
#endif
|
|
}
|
|
|
|
void consoleUpdate() {
|
|
#if CONSOLE_POSIX
|
|
threadMutexLock(&CONSOLE.execMutex);
|
|
#endif
|
|
|
|
// Toggle console
|
|
// if(inputPressed(INPUT_ACTION_CONSOLE)) {
|
|
// CONSOLE.visible = !CONSOLE.visible;
|
|
// }
|
|
|
|
// Anything to exec?
|
|
if(CONSOLE.execBufferCount == 0) {
|
|
#if CONSOLE_POSIX
|
|
threadMutexUnlock(&CONSOLE.execMutex);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// Copy the exec buffer, this allows exec command to work
|
|
consolecmdexec_t execBuffer[CONSOLE_EXEC_BUFFER_MAX];
|
|
uint32_t execBufferCount = CONSOLE.execBufferCount;
|
|
memoryCopy(
|
|
execBuffer,
|
|
CONSOLE.execBuffer,
|
|
sizeof(consolecmdexec_t) * execBufferCount
|
|
);
|
|
|
|
// Clear the exec buffer and unlock so new commands can be added while we
|
|
// process the current ones.
|
|
CONSOLE.execBufferCount = 0;
|
|
#if CONSOLE_POSIX
|
|
threadMutexUnlock(&CONSOLE.execMutex);
|
|
#endif
|
|
|
|
// Exec pending buffer.
|
|
for(uint32_t i = 0; i < execBufferCount; i++) {
|
|
consolecmdexec_t *exec = &execBuffer[i];
|
|
assertNotNull(exec->cmd, "Command execution has no command.");
|
|
exec->cmd->function(exec);
|
|
}
|
|
}
|
|
|
|
void consoleDispose(void) {
|
|
#if CONSOLE_POSIX
|
|
threadStop(&CONSOLE.thread);
|
|
threadMutexDispose(&CONSOLE.execMutex);
|
|
#endif
|
|
}
|
|
|
|
#if CONSOLE_POSIX
|
|
void consoleInputThread(thread_t *thread) {
|
|
assertNotNull(thread, "Thread cannot be NULL.");
|
|
|
|
char_t line[CONSOLE_LINE_MAX];
|
|
size_t cap = 0;
|
|
|
|
struct pollfd pfd = {
|
|
.fd = STDIN_FILENO,
|
|
.events = POLLIN
|
|
};
|
|
|
|
while(!threadShouldStop(thread) && ENGINE.running) {
|
|
int32_t rc = poll(&pfd, 1, CONSOLE_POSIX_POLL_RATE);
|
|
|
|
if(rc == 0) continue;
|
|
if(rc < 0) {
|
|
if(errno == EINTR) continue; // Interrupted by signal, retry
|
|
assertUnreachable("poll() failed with unexpected error.");
|
|
}
|
|
|
|
// Check for errors or input
|
|
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) break;
|
|
if (!(pfd.revents & POLLIN)) {
|
|
pfd.revents = 0;
|
|
continue;
|
|
}
|
|
|
|
// Read a line from stdin
|
|
if(!fgets(line, CONSOLE_LINE_MAX, stdin)) {
|
|
if (feof(stdin)) break;
|
|
clearerr(stdin);
|
|
continue;
|
|
}
|
|
|
|
// Did we read a full line or did it get truncated?
|
|
size_t len = strlen(line);
|
|
int32_t fullLine = len > 0 && line[len - 1] == '\n';
|
|
|
|
// Strip trailing newline/CR
|
|
while(len && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
|
line[--len] = '\0';
|
|
}
|
|
|
|
if(len > 0) consoleExec(line);
|
|
|
|
pfd.revents = 0;
|
|
}
|
|
}
|
|
#endif |