/** * 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