summaryrefslogtreecommitdiff
path: root/src/util/console.h
diff options
context:
space:
mode:
authoraura <nw@moneybot.cc>2026-08-11 19:10:16 +0200
committeraura <nw@moneybot.cc>2026-08-11 19:10:16 +0200
commit17c07b5f884453130d8be3a6d91d730d80d4f8ae (patch)
tree6ec11e5e5f7f0be9ed899196c2897aa76a907de2 /src/util/console.h
parent1aa17de9188790f049c0edc4281597cb4e535627 (diff)
parent0a9d7ce6a0764a87766ebb23cc1ece4c26c8c678 (diff)
Merge remote-tracking branch 'origin/day'
Diffstat (limited to 'src/util/console.h')
-rw-r--r--src/util/console.h883
1 files changed, 883 insertions, 0 deletions
diff --git a/src/util/console.h b/src/util/console.h
new file mode 100644
index 0000000..4a62561
--- /dev/null
+++ b/src/util/console.h
@@ -0,0 +1,883 @@
+#pragma once
+
+#include "../game/vars.h"
+#include "string.h"
+
+#include <math.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+enum CONSOLE_ROUTE : U8 {
+ CONSOLE_ROUTE_NONE = 0,
+ CONSOLE_ROUTE_VIEW = 1 << 0,
+ CONSOLE_ROUTE_STDOUT = 1 << 1,
+ CONSOLE_ROUTE_BOTH = CONSOLE_ROUTE_VIEW | CONSOLE_ROUTE_STDOUT
+};
+
+inline constexpr U64 CONSOLE_BYTES_MAX = 10 * 1024 * 1024;
+
+struct CONSOLE_DATA_RING {
+ LIST<STR> slots{64};
+
+ U32 first{};
+ U32 count{};
+
+ U64 max_bytes{CONSOLE_BYTES_MAX};
+ U64 revision{};
+ U64 total_pushed{};
+ U64 bytes{};
+
+ static U64 line_bytes( const STR& line ) {
+ return (U64)line.size + 1;
+ }
+
+ STR& operator[]( U32 logical_idx ) {
+ U32 physical_idx = ( first + logical_idx ) % slots.capacity;
+ return slots.data[physical_idx];
+ }
+
+ const STR& operator[]( U32 logical_idx ) const {
+ U32 physical_idx = ( first + logical_idx ) % slots.capacity;
+ return slots.data[physical_idx];
+ }
+
+ void grow() {
+ U32 new_capacity = slots.capacity
+ ? slots.capacity * 2
+ : 64;
+
+ LIST<STR> grown{new_capacity};
+
+ for( U32 idx = 0; idx < count; ++idx )
+ grown.push( (*this)[idx] );
+
+ slots = grown;
+ first = 0;
+ }
+
+ void pop_front() {
+ if( !count )
+ return;
+
+ STR& oldest = slots.data[first];
+ bytes -= line_bytes( oldest );
+ oldest = STR();
+
+ first = ( first + 1 ) % slots.capacity;
+
+ --count;
+ if( !count )
+ first = 0;
+ }
+
+ U8 make_room_for( U64 required_bytes ) {
+ if( required_bytes > max_bytes )
+ return 0;
+
+ U8 removed = 0;
+
+ while( count && bytes > max_bytes - required_bytes ) {
+ pop_front();
+ removed = 1;
+ }
+
+ if( removed )
+ ++revision;
+
+ return bytes <= max_bytes - required_bytes;
+ }
+
+ void push( const STR& src ) {
+ if( !max_bytes )
+ return;
+
+ U64 src_bytes = line_bytes( src );
+
+ if( src_bytes > max_bytes ) {
+ U32 keep = max_bytes > 1
+ ? (U32)( max_bytes - 1 )
+ : 0;
+
+ keep = keep < src.size
+ ? keep
+ : src.size;
+
+ STR clipped( keep, src.data + src.size - keep );
+ push( clipped );
+ return;
+ }
+
+ if( !make_room_for( src_bytes ) )
+ return;
+
+ if( count == slots.capacity )
+ grow();
+
+ U32 tail = ( first + count ) % slots.capacity;
+
+ if( tail < slots.size )
+ slots[tail] = src;
+ else
+ slots.push( src );
+
+ ++count;
+ ++total_pushed;
+ bytes += src_bytes;
+ }
+
+ void clear() {
+ slots.clear();
+ slots.reserve( 64 );
+ bytes = 0;
+ count = 0;
+ first = 0;
+ }
+};
+
+struct CONSOLE_STDOUT_CAPTURE {
+ I32 saved_stdout{-1};
+ I32 read_fd{-1};
+ U8 active{};
+};
+
+struct CONSOLE_DATA {
+ CONSOLE_STDOUT_CAPTURE stdout_capture{};
+ CONSOLE_DATA_RING lines{};
+ STR partial_stdout_line{};
+ U8 pending_carriage{};
+ LIST<STR> history{};
+ U64 revision{};
+};
+
+struct CONSOLE_COMMAND {
+ U8 unterminated;
+ LIST<STR> args;
+};
+
+inline U8 console_append_partial_bytes(
+ CONSOLE_DATA* state,
+ const char* text,
+ U32 length
+) {
+ if( !state || !text )
+ return 0;
+
+ if( !length )
+ return 1;
+
+ STR& partial = state->partial_stdout_line;
+ U64 required_bytes = (U64)partial.size + (U64)length + 1;
+
+ if( !state->lines.make_room_for( required_bytes ) )
+ return 0;
+
+ for( U32 idx = 0; idx < length; ++idx )
+ partial.push( text[idx] );
+
+ return 1;
+}
+
+inline void console_clear_partial_line( CONSOLE_DATA* state ) {
+ if( !state )
+ return;
+
+ STR& partial = state->partial_stdout_line;
+ partial.size = 0;
+
+ if( partial.data )
+ partial.data[0] = 0;
+}
+
+inline I32 console_clear_output( CONSOLE_DATA* state ) {
+ if( !state )
+ return 0;
+ state->lines.clear();
+ console_clear_partial_line( state );
+ state->pending_carriage = 0;
+ ++state->revision;
+ return 1;
+}
+extern I32 console_clear_cb( CVAR* self, LIST<STR> cmdline );
+
+inline void console_finish_partial_line( CONSOLE_DATA* state ) {
+ if( !state )
+ return;
+
+ state->lines.push( state->partial_stdout_line );
+ console_clear_partial_line( state );
+}
+
+inline STR console_command_token(
+ const char* text,
+ U32 start,
+ U32 end
+) {
+ STR token;
+ token.reserve( end - start + 1 );
+
+ for( U32 idx = start; idx < end; ++idx ) {
+ if( text[idx] == '"' )
+ continue;
+ token.push( text[idx] );
+ }
+ return token;
+}
+
+inline void console_parse_command(
+ const char* text,
+ CONSOLE_COMMAND* command
+) {
+ if( !command )
+ return;
+
+ command->args.clear();
+ command->unterminated = 0;
+
+ if( !text )
+ return;
+
+ U32 token_start = 0;
+ U8 in_string = 0;
+ U32 cursor = 0;
+
+ while( text[cursor] ) {
+ char c = text[cursor];
+
+ if( c == '\"' )
+ in_string ^= 1;
+
+ if( c == ' ' && !in_string ) {
+ if( cursor > token_start ) {
+ command->args.push(
+ console_command_token(
+ text, token_start, cursor
+ )
+ );
+ }
+
+ do {
+ ++cursor;
+ } while( text[cursor] == ' ' );
+
+ token_start = cursor;
+ continue;
+ }
+
+ ++cursor;
+ }
+
+ if( cursor > token_start ) {
+ command->args.push(
+ console_command_token(
+ text, token_start, cursor
+ )
+ );
+ }
+
+ command->unterminated = in_string;
+}
+
+inline void console_append_text(
+ CONSOLE_DATA* state,
+ const char* text,
+ U32 len
+) {
+ if( !state || !text || !len )
+ return;
+
+ ++state->revision;
+
+ for( U32 idx = 0; idx < len; ++idx ) {
+ char c = text[idx];
+
+ if( state->pending_carriage ) {
+ state->pending_carriage = 0;
+
+ if( c == '\n' ) {
+ console_finish_partial_line( state );
+ continue;
+ }
+ console_clear_partial_line( state );
+ }
+
+ if( c == '\r' ) {
+ state->pending_carriage = 1;
+ continue;
+ }
+
+ if( c == '\n' ) {
+ console_finish_partial_line( state );
+ continue;
+ }
+
+ if( c == '\t' ) {
+ console_append_partial_bytes(
+ state, " ", 2
+ );
+ continue;
+ }
+
+ console_append_partial_bytes( state, &c, 1 );
+ }
+}
+
+inline void console_write_fd( I32 fd, const char* text, U32 len ) {
+ if( fd < 0 || !text )
+ return;
+ U32 written = 0;
+ while( written < len ) {
+ ssize_t result = write( fd, text + written, len - written );
+ if( result > 0 ) {
+ written += (U32)result;
+ continue;
+ }
+ if( result < 0 && errno == EINTR )
+ continue;
+ break;
+ }
+}
+
+inline void console_write_original_stdout(
+ CONSOLE_DATA* state,
+ const char* text,
+ U32 len
+) {
+ I32 fd = state && state->stdout_capture.active
+ ? state->stdout_capture.saved_stdout
+ : STDOUT_FILENO;
+ console_write_fd( fd, text, len );
+}
+
+inline void console_write(
+ CONSOLE_DATA* state,
+ U8 routes,
+ const char* text,
+ U32 len
+) {
+ if( !text || !len )
+ return;
+ if( routes & CONSOLE_ROUTE_VIEW )
+ console_append_text( state, text, len );
+ if( routes & CONSOLE_ROUTE_STDOUT )
+ console_write_original_stdout( state, text, len );
+}
+
+inline void console_print(
+ CONSOLE_DATA* state,
+ U8 routes,
+ const char* fmt,
+ ...
+) {
+ if( !state || !fmt )
+ return;
+
+ va_list args;
+ va_list count_args;
+ va_start( args, fmt );
+ va_copy( count_args, args );
+ I32 len = vsnprintf( 0, 0, fmt, count_args );
+ va_end( count_args );
+
+ if( len < 0 ) {
+ va_end( args );
+ return;
+ }
+
+ STR text;
+ text.reserve( (U32)len + 1 );
+ vsnprintf(
+ text.data, (U32)len + 1,
+ fmt, args
+ );
+ va_end( args );
+
+ text.size = (U32)len;
+ text.data[text.size] = 0;
+ console_write(
+ state,
+ routes,
+ text.data,
+ text.size
+ );
+}
+
+inline U8 console_parse_i32(
+ const STR& text,
+ I32* output
+) {
+ if( !output || !text.data || !text.size )
+ return 0;
+
+ char* end = 0;
+ errno = 0;
+ long parsed = strtol( text.data, &end, 10 );
+
+ if( errno == ERANGE
+ || end == text.data
+ || *end
+ || parsed < INT_MIN
+ || parsed > INT_MAX
+ ) return 0;
+
+ *output = (I32)parsed;
+ return 1;
+}
+
+inline U8 console_parse_f32(
+ const STR& text,
+ F32* output
+) {
+ if( !output || !text.data || !text.size )
+ return 0;
+
+ char* end = 0;
+ errno = 0;
+ F32 parsed = strtof( text.data, &end );
+
+ if( errno == ERANGE
+ || end == text.data
+ || *end
+ || !isfinite( parsed )
+ ) return 0;
+
+ *output = parsed;
+ return 1;
+}
+
+inline U8 console_parse_color_component(
+ const STR& text,
+ F32* output
+) {
+ I32 integer = 0;
+ if( console_parse_i32( text, &integer ) ) {
+ if( integer < 0 || integer > 255 )
+ return 0;
+ *output = (F32)integer / 255.f;
+ return 1;
+ }
+
+ F32 decimal = 0.f;
+ if( !console_parse_f32( text, &decimal )
+ || decimal < 0.f
+ || decimal > 1.f
+ ) return 0;
+
+ *output = decimal;
+ return 1;
+}
+
+inline U8 console_execute_cvar(
+ CONSOLE_DATA* state,
+ CVAR* cvar,
+ LIST<STR>& args
+) {
+ if( cvar->type != CVAR_TYPE_FN && args.size == 0 )
+ return 0;
+
+ switch( cvar->type ) {
+ case CVAR_TYPE_FN: {
+ if( var_call( cvar, args ) )
+ return 1;
+
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s", "[error] failed to run cvar\n"
+ );
+ return 0;
+ }
+ case CVAR_TYPE_INT: {
+ I32 value = 0;
+
+ if( args.size != 1
+ || !console_parse_i32( args[0], &value )
+ ) break;
+
+ var_set( cvar, value );
+ return 1;
+ }
+ case CVAR_TYPE_FLOAT: {
+ F32 value = 0.f;
+
+ if( args.size != 1
+ || !console_parse_f32( args[0], &value )
+ ) break;
+
+ var_set( cvar, value );
+ return 1;
+ } break;
+ case CVAR_TYPE_COLOR: {
+ if( args.size == 1 ) {
+ F32 value = 0.f;
+
+ if( !console_parse_color_component( args[0], &value ) )
+ break;
+
+ var_set( cvar, value );
+ return 1;
+ }
+
+ if( args.size != 3 && args.size != 4 )
+ break;
+
+ F32 components[4] = {
+ cvar->clr_v.r,
+ cvar->clr_v.g,
+ cvar->clr_v.b,
+ cvar->clr_v.a
+ };
+
+ for( U32 idx = 0; idx < args.size; ++idx ) {
+ if(
+ !console_parse_color_component(
+ args[idx],
+ &components[idx]
+ )
+ ) break;
+
+ if( idx + 1 != args.size )
+ continue;
+
+ var_set( cvar,
+ CLR(
+ components[0],
+ components[1],
+ components[2],
+ components[3]
+ )
+ );
+ return 1;
+ }
+ } break;
+ case CVAR_TYPE_STRING: {
+ if( args.size != 1
+ || args[0].size >= sizeof( cvar->str_v )
+ ) break;
+
+ var_set( cvar, args[0] );
+ return 1;
+ }
+ default: {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s", "[error] unhandled cvar type\n"
+ );
+ return 0;
+ }
+ }
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "[error] failed to set %s\n",
+ cvar->name.data
+ );
+ return 0;
+}
+
+inline U8 console_print_i32_help(
+ CONSOLE_DATA* state,
+ CVAR* cvar
+) {
+ if( !state || !cvar || cvar->type != CVAR_TYPE_INT )
+ return 0;
+
+ if( cvar->flags & CVAR_FLAG_MINMAX ) {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%d) current(%d) min(%d) max(%d)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.i_v,
+ cvar->i_v,
+ cvar->min_v.i_v,
+ cvar->max_v.i_v
+ );
+ } else {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%d) current(%d)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.i_v,
+ cvar->i_v
+ );
+ }
+ return 1;
+}
+inline U8 console_print_f32_help(
+ CONSOLE_DATA* state,
+ CVAR* cvar
+) {
+ if( !state || !cvar || cvar->type != CVAR_TYPE_FLOAT )
+ return 0;
+
+ if( cvar->flags & CVAR_FLAG_MINMAX ) {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%f) current(%f) min(%f) max(%f)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.fl_v,
+ cvar->fl_v,
+ cvar->min_v.fl_v,
+ cvar->max_v.fl_v
+ );
+ } else {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%f) current(%f)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.fl_v,
+ cvar->fl_v
+ );
+ }
+ return 1;
+}
+
+inline U8 console_print_cvar_help(
+ CONSOLE_DATA* state,
+ CVAR* cvar
+) {
+ switch( cvar->type ) {
+ case CVAR_TYPE_FN: return 1;
+ case CVAR_TYPE_INT:
+ return console_print_i32_help( state, cvar );
+ case CVAR_TYPE_FLOAT:
+ return console_print_f32_help( state, cvar );
+ case CVAR_TYPE_COLOR: {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%f %f %f %f) current(%f %f %f %f)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.clr_v.r,
+ cvar->default_v.clr_v.g,
+ cvar->default_v.clr_v.b,
+ cvar->default_v.clr_v.a,
+ cvar->clr_v.r,
+ cvar->clr_v.g,
+ cvar->clr_v.b,
+ cvar->clr_v.a
+ );
+ } break;
+ case CVAR_TYPE_STRING: {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s - %s. default(%s) current(%s)\n",
+ cvar->name.data,
+ cvar->desc.data,
+ cvar->default_v.str_v,
+ cvar->str_v
+ );
+ } break;
+ default: {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s", "[error] unhandled cvar type\n"
+ );
+ return 0;
+ }
+ }
+ return 1;
+}
+
+inline U8 console_execute_command(
+ CONSOLE_DATA* state,
+ CONSOLE_COMMAND* command
+) {
+ if( !command || !command->args.size )
+ return 1;
+
+ if( command->unterminated ) {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "%s", "[error] unterminated string\n"
+ );
+ return 0;
+ }
+
+ STR command_name = command->args.pop_front();
+ CVAR* cvar = var_find( command_name.data );
+
+ if( !cvar ) {
+ console_print(
+ state, CONSOLE_ROUTE_BOTH,
+ "[error] unknown command: %s\n",
+ command_name.data
+ );
+ return 0;
+ }
+
+ if( !command->args.size && cvar->type != CVAR_TYPE_FN )
+ return console_print_cvar_help( state, cvar );
+ return console_execute_cvar(
+ state,
+ cvar,
+ command->args
+ );
+}
+
+inline U8 console_execute_segment(
+ CONSOLE_DATA* state,
+ const char* text,
+ U32 length
+) {
+ if( !length )
+ return 1;
+
+ STR segment( length, text );
+ CONSOLE_COMMAND command{};
+
+ console_parse_command(
+ segment.data,
+ &command
+ );
+
+ return console_execute_command( state, &command );
+}
+
+inline void console_execute( CONSOLE_DATA* state, const char* cmd ) {
+ if( !state || !cmd )
+ return;
+
+ U32 command_start = 0;
+ U8 in_string = 0;
+
+ for( U32 cursor = 0;; ++cursor ) {
+ char c = cmd[cursor];
+
+ if( c == '"' )
+ in_string ^= 1;
+
+ U8 at_end = !c;
+ U8 at_separator = c == ';' && !in_string;
+
+ if( !at_end && !at_separator )
+ continue;
+
+ if(
+ !console_execute_segment(
+ state,
+ cmd + command_start,
+ cursor - command_start
+ )
+ ) return;
+
+ if( at_end )
+ return;
+
+ command_start = cursor + 1;
+ }
+}
+
+inline U8 console_submit(
+ CONSOLE_DATA* state,
+ const char* cmd
+) {
+ if( !state || !cmd || !cmd[0] )
+ return 0;
+
+ STR submitted( "%s", cmd );
+ state->history.push( submitted );
+
+ console_print(
+ state,
+ CONSOLE_ROUTE_BOTH,
+ "] %s\n",
+ submitted.data
+ );
+
+ console_execute( state, submitted.data );
+ return 1;
+}
+
+inline U8 console_capture_stdout_begin( CONSOLE_DATA* state ) {
+ if( !state )
+ return 0;
+
+ CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture;
+
+ if( capture.active )
+ return 1;
+
+ I32 saved_stdout = dup( STDOUT_FILENO );
+ if( saved_stdout < 0 )
+ return 0;
+
+ I32 pipe_fds[2];
+ if( pipe( pipe_fds ) ) {
+ close( saved_stdout );
+ return 0;
+ }
+
+ I32 flags = fcntl( pipe_fds[0], F_GETFL, 0 );
+ if( flags < 0
+ || fcntl( pipe_fds[0], F_SETFL, flags | O_NONBLOCK ) < 0 ) {
+ close( pipe_fds[0] );
+ close( pipe_fds[1] );
+ close( saved_stdout );
+ return 0;
+ }
+
+ fflush( stdout );
+ setvbuf( stdout, 0, _IONBF, 0 );
+ if( dup2( pipe_fds[1], STDOUT_FILENO ) < 0 ) {
+ close( pipe_fds[0] );
+ close( pipe_fds[1] );
+ close( saved_stdout );
+ return 0;
+ }
+ close( pipe_fds[1] );
+ capture.saved_stdout = saved_stdout;
+ capture.read_fd = pipe_fds[0];
+ capture.active = 1;
+ return 1;
+}
+
+inline void console_pump_stdout( CONSOLE_DATA* state ) {
+ if( !state )
+ return;
+
+ CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture;
+ if( !capture.active || capture.read_fd < 0 )
+ return;
+ char buffer[4096];
+ for( ;; ) {
+ ssize_t count = read( capture.read_fd, buffer, sizeof( buffer ) );
+ if( count > 0 ) {
+ console_write_fd(
+ capture.saved_stdout,
+ buffer, (U32)count
+ );
+ console_append_text( state, buffer, (U32)count );
+ continue;
+ }
+ if( count < 0 && errno == EINTR )
+ continue;
+ if( count < 0
+ && ( errno == EAGAIN || errno == EWOULDBLOCK )
+ ) break;
+ break;
+ }
+}
+
+inline void console_capture_stdout_end( CONSOLE_DATA* state ) {
+ if( !state )
+ return;
+
+ CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture;
+ if( !capture.active )
+ return;
+
+ fflush( stdout );
+ dup2( capture.saved_stdout, STDOUT_FILENO );
+ console_pump_stdout( state );
+ close( capture.read_fd );
+ close( capture.saved_stdout );
+ capture.read_fd = capture.saved_stdout = -1;
+ capture.active = 0;
+}