empty client-server game

This commit is contained in:
Tenari
2026-01-31 09:59:03 -06:00
commit 565c1562c6
32 changed files with 13104 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
#include "../base/all.h"
// https://stackoverflow.com/questions/1098897/what-is-the-largest-safe-udp-packet-size-on-the-internet
#define UDP_MAX_MESSAGE_LEN 508
#ifndef NET_OUTGOING_MESSAGE_QUEUE_LEN
#define NET_OUTGOING_MESSAGE_QUEUE_LEN 16
#endif
typedef struct sockaddr_in SocketAddress;
typedef struct UDPServer {
bool ready;
SocketAddress server_address;
i32 server_socket;
} UDPServer;
typedef struct UDPClient {
bool ready;
SocketAddress server_address;
u16 client_port;
i32 socket;
} UDPClient;
typedef struct UDPMessage {
u16 bytes_len;
SocketAddress address;
u8 bytes[UDP_MAX_MESSAGE_LEN];
} UDPMessage;
typedef struct OutgoingMessageQueue {
UDPMessage items[NET_OUTGOING_MESSAGE_QUEUE_LEN];
u32 head;
u32 tail;
u32 count;
Mutex mutex;
Cond not_empty;
Cond not_full;
} OutgoingMessageQueue;
fn OutgoingMessageQueue* newOutgoingMessageQueue(Arena* a) {
OutgoingMessageQueue* result = arenaAlloc(a, sizeof(OutgoingMessageQueue));
MemoryZero(result, (sizeof *result));
result->mutex = newMutex();
result->not_full = newCond();
result->not_empty = newCond();
return result;
}
fn void outgoingMessageQueuePush(OutgoingMessageQueue* queue, UDPMessage* msg) {
lockMutex(&queue->mutex); {
while (queue->count == NET_OUTGOING_MESSAGE_QUEUE_LEN) {
waitForCondSignal(&queue->not_full, &queue->mutex);
}
MemoryCopy(&queue->items[queue->tail], msg, (sizeof *msg));
queue->tail = (queue->tail + 1) % NET_OUTGOING_MESSAGE_QUEUE_LEN;
queue->count++;
signalCond(&queue->not_empty);
} unlockMutex(&queue->mutex);
}
fn UDPMessage* outgoingMessageNonblockingQueuePop(OutgoingMessageQueue* q, UDPMessage* copy_target) {
// immediately returns NULL if there's nothing in the ThreadQueue
// copies the ParsedClientCommand into `copy_target` if there is something in the queue
// and marks it as popped from the queue
UDPMessage* result = NULL;
lockMutex(&q->mutex); {
if (q->count > 0) {
result = &q->items[q->head];
MemoryCopy(copy_target, result, (sizeof *copy_target));
q->head = (q->head + 1) % NET_OUTGOING_MESSAGE_QUEUE_LEN;
q->count--;
signalCond(&q->not_full);
}
} unlockMutex(&q->mutex);
return result;
}
fn UDPMessage* outgoingMessageQueuePop(OutgoingMessageQueue* q, UDPMessage* copy_target) {
UDPMessage* result = NULL;
lockMutex(&q->mutex); {
while (q->count == 0) {
waitForCondSignal(&q->not_empty, &q->mutex);
}
result = &q->items[q->head];
MemoryCopy(copy_target, result, (sizeof *copy_target));
q->head = (q->head + 1) % NET_OUTGOING_MESSAGE_QUEUE_LEN;
q->count--;
signalCond(&q->not_full);
} unlockMutex(&q->mutex);
return result;
}
fn bool socketAddressEqual(SocketAddress a, SocketAddress b) {
return a.sin_addr.s_addr == b.sin_addr.s_addr
&& a.sin_port == b.sin_port;
}
// ONLY WORKS ON POSIX. taken from https://gist.github.com/miekg/a61d55a8ec6560ad6c4a2747b21e6128
// the only real difference between a udp "server" and a "client" is the bind() syscall
// that the server makes in order to specify a port/address that it's listening on
UDPServer createUDPServer(u16 server_port) {
UDPServer result = {0};
// define the address we'll be listening on
result.server_address.sin_family = AF_INET;
result.server_address.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(INADDR_ANY);
result.server_address.sin_port = htons(server_port);
// get a FileDescriptor number from the OS to use for our socket
result.server_socket = socket(PF_INET, SOCK_DGRAM, 0);
if (result.server_socket < 0) {
return result;
}
// to let us immediately kill and restart server
i32 optval = 1;
setsockopt(result.server_socket, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(i32));
result.ready = bind(result.server_socket, (struct sockaddr *)&result.server_address, sizeof(result.server_address)) >= 0;
return result;
}
UDPClient createUDPClient(u16 server_port, str addr) {
UDPClient result = {0};
// define the address we'll be listening on
result.server_address.sin_family = AF_INET;
if (addr == 0) {
result.server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
} else {
result.server_address.sin_addr.s_addr = inet_addr(addr);
}
result.server_address.sin_port = htons(server_port);
// get a FileDescriptor number from the OS to use for our socket
result.socket = socket(PF_INET, SOCK_DGRAM, 0);
if (result.socket < 0) {
return result;
}
struct sockaddr_in client_address = {0};
client_address.sin_family = AF_INET;
client_address.sin_addr.s_addr = htonl(INADDR_ANY);
client_address.sin_port = 0;
result.ready = bind(result.socket, (struct sockaddr *)&client_address, sizeof(client_address)) >= 0;
struct sockaddr_in empty_addr;
socklen_t addr_len = sizeof(empty_addr);
getsockname(result.socket, (struct sockaddr *)&empty_addr, &addr_len);
result.client_port = ntohs(empty_addr.sin_port);
return result;
}
void infiniteReadUDPServer(UDPServer* server, void (*handleMessage)(u8* udp_message, u32 udp_len, SocketAddress sending_address, i32 socket)) {
u8 message_buffer[UDP_MAX_MESSAGE_LEN] = {0};
i32 bytes_recieved = 0;
SocketAddress client_address = {0};
i32 addrlen = sizeof(struct sockaddr);
while (true) {
bytes_recieved = recvfrom(server->server_socket, message_buffer, UDP_MAX_MESSAGE_LEN, 0, (struct sockaddr *)&client_address, (socklen_t*)&addrlen);
//gethostbyaddr: determine who sent the datagram
//struct hostent* hostp = gethostbyaddr(
// (const char *)&client_address.sin_addr.s_addr,
// sizeof(client_address.sin_addr.s_addr),
// AF_INET
//);
//ptr printable_host_IP_address_string = inet_ntoa(client_address.sin_addr);
handleMessage(message_buffer, bytes_recieved, client_address, server->server_socket);
MemoryZero(message_buffer, UDP_MAX_MESSAGE_LEN);
}
}
// TODO: sendall() to handle cases when the sendto() bytes return value is less than the intended bytes to send... stupid kernel fuckin wit us.
i32 sendUDPu8List(i32 using_socket, SocketAddress* to, u8List* message) {
return sendto(
using_socket,
message->items,
message->length,
0,
(const struct sockaddr *)to,
sizeof(struct sockaddr)
);
}
i32 sendUDPMessage(UDPServer* to, u8* message, u32 len) {
return sendto(to->server_socket, message, len, 0, (struct sockaddr *)&to->server_address, sizeof(struct sockaddr));
}
+7988
View File
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
#include "thread.h"
typedef struct ThreadQueue {
void* items;
u32 type_size;
u32 max;
u32 head;
u32 tail;
u32 count;
Mutex mutex;
Cond not_empty;
Cond not_full;
} ThreadQueue;
Thread spawnThread(void * (*threadFn)(void *), void* thread_arg) {
pthread_t thread;
pthread_create(&thread, NULL, threadFn, thread_arg);
Thread result = { thread };
return result;
}
Mutex newMutex() {
Mutex result = { PTHREAD_MUTEX_INITIALIZER };
return result;
}
Cond newCond() {
Cond result = { 0 };
pthread_cond_init(&result.cond, NULL);
return result;
}
void lockMutex(Mutex* m) {
pthread_mutex_lock(&m->mutex);
}
void unlockMutex(Mutex* m) {
pthread_mutex_unlock(&m->mutex);
}
void signalCond(Cond* cond) {
pthread_cond_signal(&cond->cond);
}
void waitForCondSignal(Cond* cond, Mutex* mutex) {
pthread_cond_wait(&cond->cond, &mutex->mutex);
}
fn ThreadQueue newThreadQueue(Arena* a, u32 type_size, u32 items_max) {
ThreadQueue result = {0};
result.type_size = type_size;
result.max = items_max;
result.items = arenaAllocArraySized(a, type_size, items_max);
result.mutex = newMutex();
result.not_full = newCond();
result.not_empty = newCond();
return result;
}
fn void threadSafeQueuePush(ThreadQueue* queue, void* item) {
lockMutex(&queue->mutex); {
while (queue->count == queue->max) {
waitForCondSignal(&queue->not_full, &queue->mutex);
}
memcpy(queue->items + (queue->tail * (sizeof item)), item, queue->type_size);
queue->tail = (queue->tail + 1) % queue->max;
queue->count++;
signalCond(&queue->not_empty);
} unlockMutex(&queue->mutex);
}
fn void* threadSafeQueuePop(ThreadQueue* q) {
void* result = NULL;
lockMutex(&q->mutex); {
while (q->count == 0) {
waitForCondSignal(&q->not_empty, &q->mutex);
}
result = &q->items[q->head];
q->head = (q->head + 1) % q->max;
q->count--;
signalCond(&q->not_full);
} unlockMutex(&q->mutex);
return result;
}
// immediately returns NULL if there's nothing in the ThreadQueue
fn void* threadSafeNonblockingQueuePop(ThreadQueue* q, void* copy_target, u64 len) {
void* result = NULL;
lockMutex(&q->mutex); {
if (q->count > 0) {
result = &q->items[q->head];
MemoryCopy(copy_target, result, len);
q->head = (q->head + 1) % q->max;
q->count--;
signalCond(&q->not_full);
}
} unlockMutex(&q->mutex);
return result;
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef LIB_THREAD_H
#define LIB_THREAD_H
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "../base/include.h"
typedef struct Thread {
pthread_t thread;
} Thread;
typedef struct Mutex {
pthread_mutex_t mutex;
} Mutex;
typedef struct Cond {
pthread_cond_t cond;
} Cond;
Thread spawnThread(void * (*threadFn)(void *), void* thread_arg);
Mutex newMutex();
Cond newCond();
void lockMutex(Mutex* m);
void unlockMutex(Mutex* m);
void signalCond(Cond* cond);
void waitForCondSignal(Cond* cond, Mutex* mutex);
#endif //LIB_THREAD_H
+579
View File
@@ -0,0 +1,579 @@
#include "../base/all.h"
#include "../string_chunk.h"
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#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)
///// 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;
///// Functions()
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 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);
}