From 2973df584978d9aae6f164ce0204179c8a28b3ae Mon Sep 17 00:00:00 2001 From: 2lag <96544487+2lag@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:33:36 +0200 Subject: console, textbox changes, some other shit --- src/util/console.h | 883 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100644 src/util/console.h (limited to 'src/util/console.h') 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 +#include +#include +#include +#include +#include +#include +#include + +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 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 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 history{}; + U64 revision{}; +}; + +struct CONSOLE_COMMAND { + U8 unterminated; + LIST 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 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& 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; +} -- cgit v1.2.3