#include "../base/all.h" #include #include #include #include #define UTF8_MAX_WIDTH 4 #define ANSI_HP_RED (196) #define ANSI_MP_BLUE (33) #define ANSI_LIGHT_GREEN (82) #define ANSI_MID_GREEN (70) #define ANSI_DARK_GREEN (35) #define ANSI_BLACK (16) #define ANSI_WHITE (15) #define ANSI_BROWN (130) #define ANSI_DULL_RED (1) #define ANSI_HIGHLIGHT_RED (9) #define ANSI_DULL_GREEN (2) #define ANSI_HIGHLIGHT_GREEN (10) #define ANSI_DULL_YELLOW (3) #define ANSI_HIGHLIGHT_YELLOW (11) #define ANSI_DULL_BLUE (4) #define ANSI_HIGHLIGHT_BLUE (12) #define ANSI_DULL_GRAY (7) #define ANSI_HIGHLIGHT_GRAY (16) #define MAX_COMMAND_PALETTE_COMMANDS (1000) #ifndef SYSTEM_MESSAGES_LEN # define SYSTEM_MESSAGES_LEN (32) #endif #ifndef MAX_SYSTEM_MESSAGE_LEN # define MAX_SYSTEM_MESSAGE_LEN (512) #endif ///// TYPES typedef struct Pixel { u8 foreground; u8 background; u8 bytes[UTF8_MAX_WIDTH]; } Pixel; typedef struct TuiState { bool redraw; ptr writeable_output_ansi_string; Pixel* frame_buffer; Pixel* back_buffer; u64 buffer_len; // how many Pixels Pos2 cursor; Pos2 prev_cursor; Dim2 screen_dimensions; Dim2 prev_screen_dimensions; } TuiState; typedef struct RGB { u8 r; u8 g; u8 b; } RGB; typedef struct CommandPaletteCommand { u32 id; ptr display_name; ptr description; ptr* tags; } CommandPaletteCommand; typedef struct CommandPaletteCommandList { u32 length; CommandPaletteCommand* items; } CommandPaletteCommandList; typedef struct StringSearchScore { bool name_matched; bool description_matched; u16 tag_match_count; u32 score; u32 name_match_start; u32 name_match_len; u32 description_match_start; u32 description_match_len; } StringSearchScore; ///// GLOBALS global u8List system_messages[SYSTEM_MESSAGES_LEN] = {0}; global u8 system_message_index = 0; ///// Functions() fn void initSystemMessages(Arena* a) { for (i32 i = 0; i < SYSTEM_MESSAGES_LEN; i++) { system_messages[i].capacity = MAX_SYSTEM_MESSAGE_LEN; system_messages[i].length = 0; system_messages[i].items = arenaAllocArraySized(a, sizeof(u8), MAX_SYSTEM_MESSAGE_LEN); } } fn void addSystemMessage(u8* msg) { // save the message to our system_messages ring buffer memset(system_messages[system_message_index].items, 0, SYSTEM_MESSAGES_LEN); sprintf((char*)system_messages[system_message_index].items, "%s", msg); system_messages[system_message_index].length = strlen((char*)system_messages[system_message_index].items); system_message_index += 1; if (system_message_index == SYSTEM_MESSAGES_LEN) { system_message_index = 0; } } fn void renderSystemMessages(Pixel* buf, Dim2 screen_dimensions, Box sys_msg_box) { i32 printable_lines = sys_msg_box.height - 2; if (printable_lines > SYSTEM_MESSAGES_LEN) { printable_lines = SYSTEM_MESSAGES_LEN; } for (i32 i = 0; i < printable_lines; i++) { i32 index = (system_message_index - 1 - i); if (index < 0) { index = SYSTEM_MESSAGES_LEN + index; } u32 y = sys_msg_box.y + (sys_msg_box.height - i) - 1; u8List sys_msg = system_messages[index]; for (i32 j = 0; j < MAX_SYSTEM_MESSAGE_LEN && j < sys_msg_box.width-4; j++) { u32 pos = (sys_msg_box.x + 2+j) + (screen_dimensions.width * y); if (j < sys_msg.length) { if (sys_msg.items[j] != '\n') { buf[pos].bytes[0] = sys_msg.items[j]; } } } } } fn u32 rgbToNum(RGB rgb) { return ((rgb.r<<16) | (rgb.g<<8) | rgb.b); } fn u8 rgbToAnsi(RGB color) { u8 r = color.r / (255 / 5); u8 g = color.g / (255 / 5); u8 b = color.b / (255 / 5); return 16 + (36*r) + (6*g) + b; } fn RGB rgbDarken(RGB color, f32 factor) { RGB result = { .r = ((f32)color.r) * factor, .g = ((f32)color.g) * factor, .b = ((f32)color.b) * factor, }; return result; } /* fn RGB ansiToRGB(u8 ansi) { ansi -= 16; u8 b = ansi % 51; ansi -= () u8 g = } */ fn bool rgbEq(RGB a, RGB b) { return a.r == b.r && a.g == b.g && a.b == b.b; } fn bool isPixelEq(Pixel a, Pixel b) { return a.background == b.background && a.foreground == b.foreground && a.bytes[0] == b.bytes[0] && a.bytes[1] == b.bytes[1] && a.bytes[2] == b.bytes[2] && a.bytes[3] == b.bytes[3] ; } fn TuiState tuiInit(Arena* a, u64 buffer_len) { TuiState result = { .redraw = false, .writeable_output_ansi_string = arenaAlloc(a, MB(1)), .buffer_len = buffer_len, .back_buffer = arenaAllocArray(a, Pixel, buffer_len), // allocate biggest possible dimensions .frame_buffer = arenaAllocArray(a, Pixel, buffer_len), // allocate biggest possible dimensions }; MemoryZero(result.back_buffer, buffer_len * sizeof(Pixel)); MemoryZero(result.frame_buffer, buffer_len * sizeof(Pixel)); return result; } fn void copyStr(u8* bytes, str cstring) { for (u32 i = 0; i < strlen(cstring); i++) { bytes[i] = cstring[i]; } } fn void drawAnsiBox(Pixel* buf, Box box, Dim2 sd, bool bold) { str items[] = {"┌","─","┐","│","└","┘"}; str b_items[] = {"┏","━","┓","┃","┗","┛"}; ptr* use = (ptr*)items; if (bold) { use = (ptr*)b_items; } u32 pos = XYToPos(box.x, box.y, sd.width); // print the upper box border copyStr(buf[pos].bytes, use[0]); for (i32 i = 0; i < box.width; i++) { copyStr(buf[pos+1+i].bytes, use[1]); } copyStr(buf[pos+1+box.width].bytes, use[2]); // start printing the rows for (i32 i = 0; i < box.height; i++) { pos = XYToPos(box.x, (box.y+1+i), sd.width); // move cursor to beginning of the row copyStr(buf[pos].bytes, use[3]); copyStr(buf[pos+1+box.width].bytes, use[3]); } // print the bottom box border pos = XYToPos(box.x, (box.y+1+box.height), sd.width); copyStr(buf[pos].bytes, use[4]); for (i32 i = 0; i < box.width; i++) { copyStr(buf[pos+1+i].bytes, use[1]); } copyStr(buf[pos+1+box.width].bytes, use[5]); } fn void renderUtf8CharToBuffer(Pixel* buf, u16 x, u16 y, str text, Dim2 screen_dimensions) { assert(strlen(text) <= UTF8_MAX_WIDTH); u32 pos = x + (screen_dimensions.width*y); for (u32 i = 0; i < strlen(text); i++) { buf[pos].bytes[i] = text[i]; } } fn void renderStrToBufferMaxWidth(Pixel* buf, u16 x, u16 y, str text, u16 width, Dim2 screen_dimensions) { u32 pos = x + (screen_dimensions.width*y); for (u32 i = 0; i < strlen(text); i++) { buf[pos + (i % width)].background = 0; buf[pos + (i % width)].foreground = 0; buf[pos + (i % width)].bytes[0] = text[i]; if (i % width == (width-1)) { // on last char i inside width pos += screen_dimensions.width; // move pos to next line } } } fn void renderStrToBufferMaxWidthWithoutChangingColor(Pixel* buf, u16 x, u16 y, str text, u16 width, Dim2 screen_dimensions) { u32 pos = x + (screen_dimensions.width*y); for (u32 i = 0; i < strlen(text); i++) { buf[pos + (i % width)].bytes[0] = text[i]; if (i % width == (width-1)) { // on last char i inside width pos += screen_dimensions.width; // move pos to next line } } } fn void renderStrToBuffer(Pixel* buf, u16 x, u16 y, str text, Dim2 screen_dimensions) { u32 pos = x + (screen_dimensions.width*y); for (u32 i = 0; i < strlen(text); i++) { buf[pos+i].bytes[0] = text[i]; } } fn void renderStringChunkList(TuiState* tui, StringChunkList* list, u16 x, u16 y) { u32 pos = XYToPos(x, y, tui->screen_dimensions.width); StringChunk* chunk = list->first; for (u32 i = 0; i < list->total_size; i++) { if (i > 0 && i % STRING_CHUNK_PAYLOAD_SIZE == 0) { chunk = chunk->next; } tui->frame_buffer[pos+i].bytes[0] = *((char*)(chunk + 1) + (i%STRING_CHUNK_PAYLOAD_SIZE)); } } fn void renderPercentBar(TuiState* tui, u16 x, u16 y, u16 width, u8 ansi_color, u64 value, u64 max) { Pixel* buf = tui->frame_buffer; Dim2 screen_dimensions = tui->screen_dimensions; u16 pos = x + (screen_dimensions.width * y); buf[pos].bytes[0] = '['; pos = x+width + (screen_dimensions.width * y); buf[pos].bytes[0] = ']'; pos = x+1 + (screen_dimensions.width * y); f32 base_ratio = 0; if (max != 0) { base_ratio = ((f32)value / (f32)max); } f32 raw_ratio = base_ratio * (width-2); u32 full_spaces_count = (u32) raw_ratio; f32 remainder = raw_ratio - full_spaces_count; for (u32 i = 0; i < full_spaces_count; i++) { buf[pos+i].foreground = ansi_color; renderUtf8CharToBuffer(buf, x+1+i, y, "█", screen_dimensions); } buf[pos+full_spaces_count].foreground = ansi_color; if (value == max) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "█", screen_dimensions); } else if (remainder > 0.875) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▉", screen_dimensions); } else if (remainder > 0.75) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▊", screen_dimensions); } else if (remainder > 0.625) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▋", screen_dimensions); } else if (remainder > 0.5) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▌", screen_dimensions); } else if (remainder > 0.375) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▍", screen_dimensions); } else if (remainder > 0.25) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▎", screen_dimensions); } else if (remainder > 0.125) { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, "▏", screen_dimensions); } else { renderUtf8CharToBuffer(buf, x+1+full_spaces_count, y, " ", screen_dimensions); } } fn void renderStaticAssetToPixelBuffer(TuiState* tui, u8* asset, u32 len, u16 x, u16 y) { u16 line = 0; u16 x_in_line = 0; u16 pos = x + (tui->screen_dimensions.width * y); for (u32 i = 0; i < len; i++, x_in_line++) { if (asset[i] == '\n') { line += 1; x_in_line = 0; pos = x + (tui->screen_dimensions.width * (y+line)); } else if (asset[i] == ' ') { // do nothing, we skip spaces in our assets } else { tui->frame_buffer[pos+x_in_line].bytes[0] = asset[i]; } } } fn u32 sprintfAnsiMoveCursorTo(ptr output, u16 x, u16 y) { return sprintf(output, "\x1b[%d;%df",y,x); } fn void printfBufferAndSwap(TuiState* tui) { Pixel* old = tui->back_buffer; Pixel* next = tui->frame_buffer; u32 length = tui->screen_dimensions.height * tui->screen_dimensions.width; bool screen_dimensions_changed = tui->screen_dimensions.height != tui->prev_screen_dimensions.height || tui->screen_dimensions.width != tui->prev_screen_dimensions.width; bool should_redraw_whole_screen = screen_dimensions_changed || tui->redraw; // "quick" exit this fn if old == next if (!should_redraw_whole_screen) { bool old_equals_new = true; for (u32 i = 0; i < length; i++) { if (old[i].background != next[i].background || old[i].foreground != next[i].foreground || old[i].bytes[0] != next[i].bytes[0] || old[i].bytes[1] != next[i].bytes[1] || old[i].bytes[2] != next[i].bytes[2] || old[i].bytes[3] != next[i].bytes[3]) { old_equals_new = false; break; } } if (old_equals_new && tui->prev_cursor.x == tui->cursor.x && tui->prev_cursor.y == tui->cursor.y ) { length += 0; // debugging helper line return; // skip all the write() and sprintf() calls, since the frames are the same. } } ptr output = tui->writeable_output_ansi_string; u8 bg = 0; u8 fg = 0; u16 x = 1; u16 y = 1; u16 last_x = 0; u16 last_y = 0; str RESET_STYLES = "\033[0m"; str CLEAR_SCREEN = "\033[2J"; if (should_redraw_whole_screen) { output += sprintf(output, "\033[0m\033[2J"); output += sprintfAnsiMoveCursorTo(output, x, y); bool printed_last = false; for (u32 i = 0; i < length; i++) { x = (i % tui->screen_dimensions.width) + 1; y = (i / tui->screen_dimensions.width) + 1; if (next[i].bytes[0] != 0) { if (printed_last == false) { output += sprintfAnsiMoveCursorTo(output, x, y); } char bytes[5] = {0}; // do this nonsense to ensure null-terminated characters bytes[0] = next[i].bytes[0]; bytes[1] = next[i].bytes[1]; bytes[2] = next[i].bytes[2]; bytes[3] = next[i].bytes[3]; if (next[i].background != bg || next[i].foreground != fg) { bg = next[i].background; fg = next[i].foreground; if (bg == 0 && fg == 0) { output += sprintf(output, "%s%s", RESET_STYLES, bytes); } else if (bg != 0 && fg == 0) { output += sprintf(output, "\033[48;5;%dm%s", bg, bytes); } else if (bg == 0 && fg != 0) { output += sprintf(output, "\033[38;5;%dm%s", fg, bytes); } else { output += sprintf(output, "\033[48;5;%d;38;5;%dm%s", bg, fg, bytes); } } else { output += sprintf(output, "%s", bytes); } printed_last = true; } else { printed_last = false; } } } else { // clearing pass, to overwrite things that were there on the last frame, but are no longer present // we do this before the "rendering" pass so that multi-space characters (emojis) are easier to deal with output += sprintf(output, "%s", RESET_STYLES); output += sprintfAnsiMoveCursorTo(output, x, y); for (u32 i = 0; i < length; i++) { x = (i % tui->screen_dimensions.width) + 1; y = (i / tui->screen_dimensions.width) + 1; bool needs_clearing = (next[i].bytes[0] == 0 && old[i].bytes[0] != 0) || (next[i].background == 0 && old[i].background != 0) || (next[i].foreground == 0 && old[i].foreground != 0); if (needs_clearing) { bool last_printed_pos_is_adjacent_to_current_x_y = last_x+1 == x && last_y == y; if (!last_printed_pos_is_adjacent_to_current_x_y) { output += sprintfAnsiMoveCursorTo(output, x, y); } output += sprintf(output, " "); last_x = x; last_y = y; } } // rendering pass last_x = 0; last_y = 0; for (u32 i = 0; i < length; i++) { x = (i % tui->screen_dimensions.width) + 1; y = (i / tui->screen_dimensions.width) + 1; if (!isPixelEq(old[i], next[i])) { if (next[i].bytes[0] != 0) { bool last_printed_pos_is_adjacent_to_current_x_y = last_x+1 == x && last_y == y; if (!last_printed_pos_is_adjacent_to_current_x_y) { output += sprintfAnsiMoveCursorTo(output, x, y); } u8 bytes[5] = {0}; // do this nonsense to ensure null-terminated characters bytes[0] = next[i].bytes[0]; bytes[1] = next[i].bytes[1]; bytes[2] = next[i].bytes[2]; bytes[3] = next[i].bytes[3]; if (next[i].background != bg || next[i].foreground != fg) { bg = next[i].background; fg = next[i].foreground; if (bg == 0 && fg == 0) { output += sprintf(output, "%s%s", RESET_STYLES, bytes); } else if (bg != 0 && fg == 0) { output += sprintf(output, "\033[48;5;%dm%s", bg, bytes); } else if (bg == 0 && fg != 0) { output += sprintf(output, "\033[38;5;%dm%s", fg, bytes); } else { output += sprintf(output, "\033[48;5;%d;38;5;%dm%s", bg, fg, bytes); } } else { output += sprintf(output, "%s", bytes); } last_x = x; last_y = y; } } } } output += sprintfAnsiMoveCursorTo(output, tui->cursor.x+1, tui->cursor.y+1); // re-position the cursor according to what the rendering logic set it to // finally write our whole string to the terminal i64 count = output - tui->writeable_output_ansi_string; osBlitToTerminal(tui->writeable_output_ansi_string, count); // swap our buffers Pixel* tmp = tui->back_buffer; tui->back_buffer = tui->frame_buffer; tui->frame_buffer = tmp; // reset the `redraw` flag tui->redraw = false; } fn u32 matchCommandPaletteCommands(String current_search, CommandPaletteCommandList commands, u32 menu_index, u32* scores, StringSearchScore* score_details) { // returns the `commands` id that matches the `menu_index` for (u32 i = 0; i < commands.length; i++) { CommandPaletteCommand* cmd = &commands.items[i]; bool name_matches = false; for (u32 j = 0; j < strlen(cmd->display_name); j++) { if (current_search.length > 0 && lowerAscii(cmd->display_name[j]) == lowerAscii(current_search.bytes[0])) { for (u32 k = 0; k < current_search.length && k+j < strlen(cmd->display_name); k++) { if (lowerAscii(current_search.bytes[k]) != lowerAscii(cmd->display_name[j+k])) { break; } if (k == current_search.length - 1) { name_matches = true; score_details[i].name_match_start = j; score_details[i].name_match_len = current_search.length; } } } } bool description_matches = false; u32 tag_match_count = 0; scores[i] = (name_matches * 100 * MAX_COMMAND_PALETTE_COMMANDS) + (description_matches * 10 * MAX_COMMAND_PALETTE_COMMANDS) + (tag_match_count * MAX_COMMAND_PALETTE_COMMANDS) + cmd->id; score_details[i].score = scores[i]; score_details[i].name_matched = name_matches; score_details[i].description_matched = description_matches; score_details[i].tag_match_count = tag_match_count; } u32Quicksort(scores, 0, commands.length - 1); u32ReverseArray(scores, commands.length); return (scores[menu_index] % MAX_COMMAND_PALETTE_COMMANDS); } fn Pos2 renderCommandPalette(TuiState* tui, String current_search, CommandPaletteCommandList commands, u32 menu_index) { // returns the cursor position as Pos2 ScratchMem scratch = scratchGet(); Dim2 sd = tui->screen_dimensions; Pos2 result = { 0 }; // draw the outline Box outline = { .width = sd.width / 2, .height = sd.height / 2, }; outline.x = outline.width / 2; outline.y = outline.height / 2; drawAnsiBox(tui->frame_buffer, outline, sd, true); // draw the "search bar" result.x = outline.x + 1 + current_search.length; result.y = outline.y + 1; renderStrToBufferMaxWidth(tui->frame_buffer, outline.x+1, outline.y+1, current_search.bytes, outline.width - 2, sd); for (u32 i = 1; i < outline.width-1; i++) { u32 pos = XYToPos(outline.x+1, outline.y+2, sd.width); copyStr(tui->frame_buffer[pos].bytes, "━"); } // sort the command options u32* scores = arenaAllocArray(&scratch.arena, u32, commands.length); StringSearchScore* score_details = arenaAllocArray(&scratch.arena, StringSearchScore, commands.length); matchCommandPaletteCommands(current_search, commands, menu_index, scores, score_details); // draw the command options u32 x = outline.x + 1; u32 y = outline.y + 3; for (u32 i = 0; (y-outline.y) < (outline.height-1) && i < commands.length; i++, y+=2) { if (i == menu_index) { for (u32 ii = 0; ii < outline.width-1; ii++) { u32 pos = XYToPos(x+ii, y, tui->screen_dimensions.width); tui->frame_buffer[pos].bytes[0] = ' '; tui->frame_buffer[pos].foreground = ANSI_BLACK; tui->frame_buffer[pos].background = ANSI_WHITE; pos = XYToPos(x+ii, y+1, tui->screen_dimensions.width); tui->frame_buffer[pos].bytes[0] = ' '; tui->frame_buffer[pos].foreground = ANSI_BLACK; tui->frame_buffer[pos].background = ANSI_WHITE; } } u32 id = scores[i] % MAX_COMMAND_PALETTE_COMMANDS; StringSearchScore score = score_details[id]; CommandPaletteCommand* cmd = NULL; for (u32 j = 0; j < commands.length; j++) { if (commands.items[j].id == id) { cmd = &commands.items[j]; break; } } assert(cmd != NULL); if (score.name_matched) { u32 end_idx = score.name_match_len + score.name_match_start; for (u32 ii = 0; ii < strlen(cmd->display_name); ii++) { u32 pos = XYToPos(x+ii, y, tui->screen_dimensions.width); if (ii >= score.name_match_start && ii < end_idx) { if (i == menu_index) { tui->frame_buffer[pos].foreground = ANSI_DULL_RED; } else { tui->frame_buffer[pos].foreground = ANSI_HIGHLIGHT_YELLOW; tui->frame_buffer[pos].background = 0; } } tui->frame_buffer[pos].bytes[0] = cmd->display_name[ii]; } } else { renderStrToBufferMaxWidthWithoutChangingColor(tui->frame_buffer, x, y, cmd->display_name, outline.width - 2, sd); } renderStrToBufferMaxWidthWithoutChangingColor(tui->frame_buffer, x, y+1, cmd->description, outline.width - 2, sd); } scratchReturn(&scratch); return result; } fn void renderChoiceMenu(TuiState* tui, u16 x, u16 y, ptr options[], u32 len, bool choosable, u32 selected_index, u8* colors) { for (u32 i = 0; i < len; i++) { u32 pos = x + (tui->screen_dimensions.width*(y+i)); if (choosable && selected_index == i) { tui->cursor.x = x; tui->cursor.y = y+i; } u8 bytes[80] = {0}; sprintf((char*)bytes, "- %d. ", i+1); u32 offset = strlen((char*)bytes); u32 bytes_remaining = (80 - offset); for ( u32 j = 0; j < strlen(options[i]) && j < bytes_remaining; j++ ) { bytes[offset+j] = options[i][j]; } // write our `bytes` buffer into the Pixel* buf for (u32 j = 0; j < 80; j++) { tui->frame_buffer[pos+j].bytes[0] = bytes[j]; if (choosable && selected_index == i) { tui->frame_buffer[pos+j].foreground = ANSI_BLACK; tui->frame_buffer[pos+j].background = ANSI_WHITE; } else { tui->frame_buffer[pos+j].foreground = colors == NULL ? ANSI_WHITE : colors[i]; tui->frame_buffer[pos+j].background = ANSI_BLACK; } } } } fn void infiniteUILoop( u32 max_screen_width, u32 max_screen_height, u64 goal_input_loop_us, void* state, // updateAndRender should return a bool `should_quit` bool (*updateAndRender)(TuiState* tui, void* state, u8* input_buffer, u64 loop_count) ) { bool should_quit = false; Arena permanent_arena = {0}; arenaInit(&permanent_arena); // set up the TUI incantations TermIOs old_terminal_attributes = osStartTUI(false); TuiState tui = tuiInit(&permanent_arena, max_screen_width*max_screen_height); tui.screen_dimensions = osGetTerminalDimensions(); // ui loop (read input, simulate next frame, render) u8 input_buffer[5] = {0}; u64 loop_start; u64 loop_count = 0; while (!should_quit) { loop_count += 1; loop_start = osTimeMicrosecondsNow(); // both reads local input from the keyboard AND from the network osReadConsoleInput(input_buffer, 4); // prep rendering MemoryZero(tui.frame_buffer, tui.buffer_len * sizeof(Pixel)); tui.prev_screen_dimensions = tui.screen_dimensions; if (loop_count % 17 == 0) { // every 17 frames, update our terminal_dimensions tui.screen_dimensions = osGetTerminalDimensions(); } tui.prev_cursor = tui.cursor;// save last frame's cursor // operate on input + render new tui.frame_buffer should_quit = updateAndRender(&tui, state, input_buffer, loop_count); printfBufferAndSwap(&tui); // loop timing u32 loop_duration = osTimeMicrosecondsNow() - loop_start; i32 remaining_time = goal_input_loop_us - loop_duration; if (remaining_time > 0) { osSleepMicroseconds(remaining_time); } } // cleanup terminal TUI incantations osEndTUI(old_terminal_attributes); }