diff options
author | Raymaekers Luca <raymaekers.luca@gmail.com> | 2024-11-18 00:13:44 +0100 |
---|---|---|
committer | Raymaekers Luca <raymaekers.luca@gmail.com> | 2024-11-18 20:50:27 +0100 |
commit | 0d635bc20467b3d789091f14affe8c499c74d2ea (patch) | |
tree | 74e06b8130d50c46b633ae70af22364646a20692 | |
parent | 0ce18f9b70d907c8c50d45c4b5b279b8bd9275f1 (diff) |
Added markdown support for messages
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | chatty.c | 380 | ||||
-rw-r--r-- | server.c | 8 | ||||
-rw-r--r-- | ui.c | 291 |
4 files changed, 461 insertions, 219 deletions
@@ -13,6 +13,7 @@ The idea is the following: - [ ] bug: wrapping does not work and displays nothing if there is no screen space - [ ] bug: reconnect does not work when server does not know id - [ ] markup for messages +- [ ] convert tabs to spaces ## server - [ ] check that fds arena does not overflow @@ -3,6 +3,7 @@ #include "chatty.h" #include "protocol.h" +#include "ui.c" #include <arpa/inet.h> #include <assert.h> @@ -29,11 +30,11 @@ enum { FDS_BI = 0, // for one-way communication with the server (eg. TextMessage FDS_MAX }; typedef struct { - u8 author[AUTHOR_LEN]; - ID id; + u8 Author[AUTHOR_LEN]; + ID ID; } User; #define USER_FMT "[%s](%lu)" -#define USER_ARG(client) client.author, client.id +#define USER_ARG(client) client.Author, client.ID // User used by chatty global_variable User user = {0}; @@ -42,10 +43,10 @@ global_variable struct sockaddr_in address; // fill str array with char void -fillstr(u32* str, u32 ch, u32 len) +fillstr(u32* Str, u32 ch, u32 Len) { - for (u32 i = 0; i < len; i++) - str[i] = ch; + for (u32 i = 0; i < Len; i++) + Str[i] = ch; } // Centered popup displaying message in the appropriate cololrs @@ -61,15 +62,15 @@ popup(u32 fg, u32 bg, char* text) // Returns user if the id was the user's ID // Returns 0 if nothing was found User* -getUserByID(Arena* clientsArena, ID id) +get_user_by_id(Arena* clientsArena, ID id) { // User is not in the clientsArena - if (id == user.id) return &user; + if (id == user.ID) return &user; User* clients = clientsArena->addr; for (u64 i = 0; i < (clientsArena->pos / sizeof(*clients)); i++) { - if (clients[i].id == id) + if (clients[i].ID == id) return clients + i; } return 0; @@ -78,11 +79,11 @@ getUserByID(Arena* clientsArena, ID id) // Request information of client from fd byd id and add it to clientsArena // Returns pointer to added client User* -addUserInfo(Arena* clientsArena, s32 fd, u64 id) +add_user_info(Arena* clientsArena, s32 fd, u64 id) { // Request information about ID HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); - header.id = user.id; + header.id = user.ID; IDMessage message = {id}; s32 nsend = sendAnyMessage(fd, header, &message); assert(nsend != -1); @@ -93,8 +94,8 @@ addUserInfo(Arena* clientsArena, s32 fd, u64 id) // Add the information User* client = ArenaPush(clientsArena, sizeof(*client)); - memcpy(client->author, introduction_message.author, AUTHOR_LEN); - client->id = id; + memcpy(client->Author, introduction_message.author, AUTHOR_LEN); + client->ID = id; loggingf("Got " USER_FMT "\n", USER_ARG((*client))); return client; @@ -102,7 +103,7 @@ addUserInfo(Arena* clientsArena, s32 fd, u64 id) // Tries to connect to address and populates resulting file descriptors in ConnectionResult. s32 -getConnection(struct sockaddr_in* address) +get_connection(struct sockaddr_in* address) { s32 fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) return -1; @@ -120,10 +121,10 @@ u32 authenticate(User* user, s32 fd) { /* Scenario 1: Already have an ID */ - if (user->id) + if (user->ID) { HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); - IDMessage message = {user->id}; + IDMessage message = {user->ID}; s32 nsend = sendAnyMessage(fd, header, &message); assert(nsend != -1); @@ -144,14 +145,14 @@ authenticate(User* user, s32 fd) { HeaderMessage header = HEADER_INIT(HEADER_TYPE_INTRODUCTION); IntroductionMessage message; - memcpy(message.author, user->author, AUTHOR_LEN); + memcpy(message.author, user->Author, AUTHOR_LEN); s32 nsend = sendAnyMessage(fd, header, &message); assert(nsend != -1); IDMessage id_message; s32 nrecv = recvAnyMessageType(fd, &header, &id_message, HEADER_TYPE_ID); assert(nrecv != -1); - user->id = id_message.id; + user->ID = id_message.id; return 1; } } @@ -164,7 +165,7 @@ authenticate(User* user, s32 fd) // Returns 0. #define Miliseconds(s) (s*1000*1000) void* -threadReconnect(void* fds_ptr) +thread_reconnect(void* fds_ptr) { s32 unifd, bifd; struct pollfd* fds = fds_ptr; @@ -175,13 +176,13 @@ threadReconnect(void* fds_ptr) // timeout nanosleep(&t, &t); - bifd = getConnection(&address); + bifd = get_connection(&address); if (bifd == -1) { loggingf("errno: %d\n", errno); continue; } - unifd = getConnection(&address); + unifd = get_connection(&address); if (unifd == -1) { loggingf("errno: %d\n", errno); @@ -212,115 +213,19 @@ threadReconnect(void* fds_ptr) return 0; } -// Print `text` wrapped to limit_x. It will print no more than limit_y lines. x, y, fg and -// bg will be passed to the tb_printf() function calls. -// pfx is a string that will be printed first and will not be wrapped on characters like msg->text, -// this is useful when for example: printing messages and wanting to have consistent -// timestamp+author name. -// Returns the number of lines printed. -// TODO: (bug) text after pfx is wrapped one too soon -// TODO: text == 0 to know how many lines *would* be printed -// - no this should be a separate function -// TODO: check if text[i] goes out of bounds -u32 -tb_printf_wrap(u32 x, u32 y, u32 fg, u32 bg, u32* text, s32 text_len, u32 fg_pfx, u32 bg_pfx, u8* pfx, s32 limit_x, u32 limit_y) -{ - assert(limit_x > 0); - - // lines y, incremented after each wrap - s32 ly = y; - // character the text is split on - u32 t = 0; - // index used for searching in string - s32 i = limit_x; - // previous i for windowing through the text - s32 offset = 0; - // used when retrying to get a longer limit - u32 failed = 0; - - // NOTE: We can assume that we need to wrap, therefore print a newline after the prefix string - if (pfx != 0) - { - tb_printf(x, ly, fg_pfx, bg_pfx, "%s", pfx); - - // If the text fits on one line print the text and return - // Otherwise print the text on the next line - s32 pfx_len = strlen((char*)pfx); - if (limit_x > pfx_len + text_len) - { - tb_printf(x + pfx_len, y, fg, bg, "%ls", text); - return 1; - } - else - { - ly++; - } - } - - /// Algorithm - // 1. Start at limit - // 2. Look backwards for whitespace - // 3. Whitespace found? - // n) failed++ - // i = limit + limit*failed - // step 2. - // y) step 4. - // 4. failed = 0 - // 5. terminate text at i found - // 6. print text - // 7. restore text[i] - // 8. step 2. until i >= text_len - // 9. print remaining part of the string - - while (i < text_len && ly - y < limit_y) - { - // search backwards for whitespace - while (i > offset && text[i] != L' ') - i--; - - // retry with bigger limit - if (i == offset) - { - offset = i; - failed++; - i += limit_x + failed * limit_x; - continue; - } - else - { - failed = 0; - } - - t = text[i]; - text[i] = 0; - tb_printf(x, ly, fg, bg, "%ls", text + offset); - text[i] = t; - - i++; // after the space - ly++; - - offset = i; - i += limit_x; - } - if ((u32)ly <= limit_y) - { - tb_printf(x, ly, fg, bg, "%ls", text + offset); - ly++; - } - - return ly - y; -} - // home screen, the first screen the user sees // it displays a prompt with the user input of input_len wide characters // and the received messages from msgsArena void -screen_home(Arena* msgsArena, u32 nmessages, Arena* clientsArena, struct pollfd* fds, u32 input[], u32 input_len) +screen_home(Arena* ScratchArena, + Arena* MessagesArena, u32 MessagesNum, + Arena* ClientsArena, struct pollfd* fds, + u32 Input[], u32 InputLen) { // config options const s32 box_max_len = 80; const s32 box_x = 0, box_y = global.height - 3, box_pad_x = 1, box_mar_x = 1, box_bwith = 1, box_height = 3; - const u32 prompt_x = box_x + box_pad_x + box_mar_x + box_bwith + input_len; + const u32 prompt_x = box_x + box_pad_x + box_mar_x + box_bwith + InputLen; // the minimum height required is the hight for the box prompt // the minimum width required is that one character should fit in the box prompt @@ -342,37 +247,40 @@ screen_home(Arena* msgsArena, u32 nmessages, Arena* clientsArena, struct pollfd* // Looks like this: // 03:24:29 [1234567890ab] hello homes how are // you doing? - // 03:24:33 [TlasT] I am fine + // 03:24:33 [TlasT] │ I am fine + // 03:24:33 [Fin] │ I am too { - u32 free_y = global.height - box_height; - if (free_y <= 0) + u32 VerticalBarOffset = TIMESTAMP_LEN + AUTHOR_LEN + 2; + + u32 FreeHeight = global.height - box_height; + if (FreeHeight <= 0) goto draw_prompt; - u8* addr = msgsArena->addr; - assert(addr != 0); - // on what line to print the current message, used for scrolling - u32 msg_y = 0; + // Used to go to the next message in MessagesArena by incrementing with the messages' size. + u8* MessageAddress = MessagesArena->addr; + assert(MessageAddress != 0); - u32 offs = (nmessages > free_y) ? nmessages - free_y : 0; - // skip offs ccount messages - for (u32 i = 0; i < offs; i++) + // Skip messages if there is not enough space to display them all + u32 MessagesOffset = (MessagesNum > FreeHeight) ? MessagesNum - FreeHeight : 0; + for (u32 MessageIndex = 0; MessageIndex < MessagesOffset; MessageIndex++) { - HeaderMessage* header = (HeaderMessage*)addr; - addr += sizeof(*header); + HeaderMessage* header = (HeaderMessage*)MessageAddress; + MessageAddress += sizeof(*header); + switch (header->type) { case HEADER_TYPE_TEXT: { - TextMessage* message = (TextMessage*)addr; - addr += TEXTMESSAGE_SIZE; - addr += message->len * sizeof(*message->text); + TextMessage* message = (TextMessage*)MessageAddress; + MessageAddress += TEXTMESSAGE_SIZE; + MessageAddress += message->len * sizeof(*message->text); break; } case HEADER_TYPE_PRESENCE: - addr += sizeof(PresenceMessage); + MessageAddress += sizeof(PresenceMessage); break; case HEADER_TYPE_HISTORY: - addr += sizeof(HistoryMessage); + MessageAddress += sizeof(HistoryMessage); break; default: // unhandled message type @@ -380,37 +288,35 @@ screen_home(Arena* msgsArena, u32 nmessages, Arena* clientsArena, struct pollfd* } } - // In each case statement advance the addr pointer by the size of the message - for (u32 i = offs; i < nmessages && msg_y < free_y; i++) + u32 MessageY = 0; + + for (u32 i = MessagesOffset; + i < MessagesNum; + i++) { - HeaderMessage* header = (HeaderMessage*)addr; - addr += sizeof(*header); + if (MessageY >= FreeHeight) break; - // Get User for message - User* client; - switch (header->type) + HeaderMessage* header = (HeaderMessage*)MessageAddress; + MessageAddress += sizeof(*header); + + User* client = get_user_by_id(ClientsArena, header->id); + if (!client) { - case HEADER_TYPE_TEXT: - case HEADER_TYPE_PRESENCE: - client = getUserByID(clientsArena, header->id); - if (!client) - { - loggingf("User not known, requesting from server\n"); - client = addUserInfo(clientsArena, fds[FDS_BI].fd, header->id); - } - assert(client); - break; + loggingf("User not known, requesting from server\n"); + client = add_user_info(ClientsArena, fds[FDS_BI].fd, header->id); } + assert(client); switch (header->type) { case HEADER_TYPE_TEXT: { - TextMessage* message = (TextMessage*)addr; + TextMessage* message = (TextMessage*)MessageAddress; + // Color own messages u32 fg = 0; - if (user.id == header->id) + if (user.ID == header->id) { fg = TB_CYAN; } @@ -420,35 +326,73 @@ screen_home(Arena* msgsArena, u32 nmessages, Arena* clientsArena, struct pollfd* } // prefix is of format "HH:MM:SS [<author>] ", create it - u8 pfx[AUTHOR_LEN - 1 + TIMESTAMP_LEN - 1 + 4 + 1] = {0}; u8 timestamp[TIMESTAMP_LEN]; formatTimestamp(timestamp, message->timestamp); - sprintf((char*)pfx, "%s [%s] ", timestamp, client->author); - msg_y += tb_printf_wrap(0, msg_y, TB_WHITE, 0, (u32*)&message->text, message->len, fg, 0, pfx, global.width, free_y - msg_y); + tb_printf(0, MessageY, TB_WHITE, 0, "%s", timestamp); + tb_printf(TIMESTAMP_LEN, MessageY, fg, 0, "[%s]", client->Author); + + // Only display when there is enough space + if (global.width > VerticalBarOffset + 2) + { + raw_result RawText = markdown_to_raw(ScratchArena, (u32*)&message->text, message->len); + markdown_formatoptions MDFormat = preprocess_markdown(ScratchArena, + (u32*)&message->text, + message->len); + + u32 timesWrapped = tb_print_wrapped_with_markdown(VerticalBarOffset + 2, MessageY, fg, 0, + RawText.Text, RawText.Len, + global.width, global.height, MDFormat); + + // Free the memory + ScratchArena->pos = 0; + + MessageY += timesWrapped; + } + else + { + // We still displayed the timestamp so we need to increment the Y. + MessageY++; + } u32 message_size = TEXTMESSAGE_SIZE + message->len * sizeof(*message->text); - addr += message_size; + MessageAddress += message_size; } break; case HEADER_TYPE_PRESENCE: { - PresenceMessage* message = (PresenceMessage*)addr; - tb_printf(0, msg_y, 0, 0, " [%s] *%s*", client->author, presenceTypeString(message->type)); - msg_y++; - addr += sizeof(*message); + PresenceMessage* message = (PresenceMessage*)MessageAddress; + tb_printf(TIMESTAMP_LEN, MessageY, TB_MAGENTA, 0, "[%s]", client->Author); + + // Wrap Text in '*' + u8 *Text = presenceTypeString(message->type); + u32 Len = 0; + while(Text[Len]) Len++; + u32 FormattedText[Len+2]; + FormattedText[0] = '*'; + FormattedText[Len+1] = '*'; + for (u32 i = 1; i < Len + 1; i++) FormattedText[i] = Text[i-1]; + + tb_print_markdown(VerticalBarOffset + 2, MessageY, 0, 0, FormattedText, Len + 2); + + MessageY++; + MessageAddress += sizeof(*message); } break; case HEADER_TYPE_HISTORY: { - HistoryMessage* message = (HistoryMessage*)addr; - addr += sizeof(*message); + HistoryMessage* message = (HistoryMessage*)MessageAddress; + MessageAddress += sizeof(*message); // TODO: implement } break; default: - tb_printf(0, msg_y, 0, 0, "%s", headerTypeString(header->type)); - msg_y++; + tb_printf(0, MessageY, 0, 0, "%s", headerTypeString(header->type)); + MessageY++; break; } } + + // Print vertical bar + for (u32 Y = 0; Y < FreeHeight; Y++) + tb_print(VerticalBarOffset, Y, 0, 0, "│"); draw_prompt: // Draw prompt box which is a box made out of @@ -500,16 +444,16 @@ screen_home(Arena* msgsArena, u32 nmessages, Arena* clientsArena, struct pollfd* if (freesp <= 0) return; - if (input_len > freesp) + if (InputLen > freesp) { - u32* text_offs = input + (input_len - freesp); + u32* text_offs = Input + (InputLen - freesp); tb_printf(box_x + box_mar_x + box_pad_x + box_bwith, box_y + 1, 0, 0, "%ls", text_offs); global.cursor_x = box_x + box_pad_x + box_mar_x + box_bwith + freesp; } else { global.cursor_x = prompt_x; - tb_printf(box_x + box_mar_x + box_pad_x + box_bwith, box_y + 1, 0, 0, "%ls", input); + tb_printf(box_x + box_mar_x + box_pad_x + box_bwith, box_y + 1, 0, 0, "%ls", Input); } } @@ -532,21 +476,23 @@ main(int argc, char** argv) u32 arg_len = strlen(argv[1]); assert(arg_len <= AUTHOR_LEN - 1); - memcpy(user.author, argv[1], arg_len); - user.author[arg_len] = '\0'; + memcpy(user.Author, argv[1], arg_len); + user.Author[arg_len] = '\0'; s32 err = 0; // error code for functions - u32 nmessages = 0; // Number of messages in msgsArena + u32 MessagesNum = 0; // Number of messages in msgsArena s32 nrecv = 0; // number of bytes received - u32 input[INPUT_LIMIT] = {0}; // input buffer - u32 ninput = 0; // number of characters in input + u32 Input[INPUT_LIMIT] = {0}; // input buffer + u32 InputLen = 0; // number of characters in input - Arena msgsArena; - Arena clientsArena; - ArenaAlloc(&msgsArena, Megabytes(64)); // Messages received & sent - ArenaAlloc(&clientsArena, Megabytes(1)); // Arena for storing clients + Arena ScratchArena; + Arena MessagesArena; + Arena ClientsArena; + ArenaAlloc(&MessagesArena, Megabytes(64)); // Messages received & sent + ArenaAlloc(&ClientsArena, Megabytes(1)); // Arena for storing clients + ArenaAlloc(&ScratchArena, Megabytes(1)); // Arena for storing clients struct tb_event ev; // event fork keypress & resize u8 quit = 0; // boolean to indicate if we want to quit the main loop @@ -585,13 +531,13 @@ main(int argc, char** argv) /* Authentication */ { s32 unifd, bifd; - bifd = getConnection(&address); + bifd = get_connection(&address); if (bifd == -1) { loggingf("errno: %d\n", errno); return 1; } - unifd = getConnection(&address); + unifd = get_connection(&address); if (unifd == -1) { loggingf("errno: %d\n", errno); @@ -617,7 +563,7 @@ main(int argc, char** argv) write(idfile, &user.id, sizeof(user.id)); #endif - loggingf("Got ID: %lu\n", user.id); + loggingf("Got ID: %lu\n", user.ID); // for wide character printing assert(setlocale(LC_ALL, "") != 0); @@ -626,7 +572,7 @@ main(int argc, char** argv) tb_init(); tb_get_fds(&fds[FDS_TTY].fd, &fds[FDS_RESIZE].fd); - screen_home(&msgsArena, nmessages, &clientsArena, fds, input, ninput); + screen_home(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputLen); tb_present(); // main loop @@ -653,7 +599,7 @@ main(int argc, char** argv) assert(err == 0); fds[FDS_UNI].fd = -1; // ignore // start trying to reconnect in a thread - err = pthread_create(&thr_rec, 0, &threadReconnect, (void*)fds); + err = pthread_create(&thr_rec, 0, &thread_reconnect, (void*)fds); assert(err == 0); } else @@ -664,22 +610,22 @@ main(int argc, char** argv) continue; } - void* addr = ArenaPush(&msgsArena, sizeof(header)); + void* addr = ArenaPush(&MessagesArena, sizeof(header)); memcpy(addr, &header, sizeof(header)); // Messages handled from server switch (header.type) { case HEADER_TYPE_TEXT: - recvTextMessage(&msgsArena, fds[FDS_UNI].fd); - nmessages++; + recvTextMessage(&MessagesArena, fds[FDS_UNI].fd); + MessagesNum++; break; case HEADER_TYPE_PRESENCE:; - PresenceMessage* message = ArenaPush(&msgsArena, sizeof(*message)); + PresenceMessage* message = ArenaPush(&MessagesArena, sizeof(*message)); nrecv = recv(fds[FDS_UNI].fd, message, sizeof(*message), 0); assert(nrecv != -1); assert(nrecv == sizeof(*message)); - nmessages++; + MessagesNum++; break; default: loggingf("Got unhandled message: %s\n", headerTypeString(header.type)); @@ -697,24 +643,24 @@ main(int argc, char** argv) { case TB_KEY_CTRL_W: // delete consecutive whitespace - while (ninput) + while (InputLen) { - if (input[ninput - 1] == L' ') + if (Input[InputLen - 1] == L' ') { - input[ninput - 1] = 0; - ninput--; + Input[InputLen - 1] = 0; + InputLen--; continue; } break; } // delete until whitespace - while (ninput) + while (InputLen) { - if (input[ninput - 1] == L' ') + if (Input[InputLen - 1] == L' ') break; // erase - input[ninput - 1] = 0; - ninput--; + Input[InputLen - 1] = 0; + InputLen--; } break; case TB_KEY_CTRL_Z: @@ -730,7 +676,7 @@ main(int argc, char** argv) quit = 1; break; case TB_KEY_CTRL_M: // send message - if (ninput == 0) + if (InputLen == 0) // do not send empty message break; if (fds[FDS_UNI].fd == -1) @@ -738,43 +684,43 @@ main(int argc, char** argv) break; // null terminate - input[ninput] = 0; - ninput++; + Input[InputLen] = 0; + InputLen++; // Save header - HeaderMessage* header = ArenaPush(&msgsArena, sizeof(*header)); + HeaderMessage* header = ArenaPush(&MessagesArena, sizeof(*header)); header->version = PROTOCOL_VERSION; header->type = HEADER_TYPE_TEXT; - header->id = user.id; + header->id = user.ID; // Save message - TextMessage* sendmsg = ArenaPush(&msgsArena, TEXTMESSAGE_SIZE); + TextMessage* sendmsg = ArenaPush(&MessagesArena, TEXTMESSAGE_SIZE); sendmsg->timestamp = time(0); - sendmsg->len = ninput; + sendmsg->len = InputLen; - u32 text_size = ninput * sizeof(*input); - ArenaPush(&msgsArena, text_size); - memcpy(&sendmsg->text, input, text_size); + u32 text_size = InputLen * sizeof(*Input); + ArenaPush(&MessagesArena, text_size); + memcpy(&sendmsg->text, Input, text_size); sendAnyMessage(fds[FDS_UNI].fd, *header, sendmsg); - nmessages++; + MessagesNum++; // also clear input case TB_KEY_CTRL_U: // clear input - bzero(input, ninput * sizeof(*input)); - ninput = 0; + bzero(Input, InputLen * sizeof(*Input)); + InputLen = 0; break; default: if (ev.ch == 0) break; // TODO: show error - if (ninput == INPUT_LIMIT - 1) // last byte reserved for \0 + if (InputLen == INPUT_LIMIT - 1) // last byte reserved for \0 break; // append key to input buffer - input[ninput] = ev.ch; - ninput++; + Input[InputLen] = ev.ch; + InputLen++; } if (quit) break; @@ -787,7 +733,7 @@ main(int argc, char** argv) tb_poll_event(&ev); } - screen_home(&msgsArena, nmessages, &clientsArena, fds, input, ninput); + screen_home(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputLen); tb_present(); } @@ -2,6 +2,7 @@ #include "protocol.h" #include <assert.h> +#include <errno.h> #include <fcntl.h> #include <netinet/in.h> #include <poll.h> @@ -15,7 +16,7 @@ // timeout on polling #define TIMEOUT 60 * 1000 // max pending connections -#define MAX_CONNECTIONS 16 +#define MAX_CONNECTIONS 1600 // Get number of connections from arena position // NOTE: this is somewhat wrong, because of when disconnections happen #define FDS_SIZE (fdsArena.pos / sizeof(struct pollfd)) @@ -442,7 +443,10 @@ main(int argc, char** argv) // We received a message, try to parse the header HeaderMessage header; s32 nrecv = recv(fds[conn].fd, &header, sizeof(header), 0); - assert(nrecv != -1); + if(nrecv == -1) + { + loggingf("Received error from fd: %d, errno: %d\n", fds[conn].fd, errno); + }; Client* client; if (nrecv != sizeof(header)) @@ -0,0 +1,291 @@ +#define DEBUG + +// Format option at a position in raw text, used when iterating to know when to toggle a color +// option. +typedef struct { + u32 Position; + u32 Color; +} format_option; + +// Array of format options and length of said array +typedef struct { + format_option* Options; + u32 Len; +} markdown_formatoptions; + +typedef struct { + u32* Text; + u32 Len; +} raw_result; + +// Return True if ch is whitespace +Bool +is_whitespace(u32 ch) +{ + if (ch == L' ') + return True; + return False; +} + +// Return True if ch is a supported markdown markup character +// TODO: tilde +Bool +is_markdown(u32 ch) +{ + if (ch == L'_' || + ch == L'*') + return True; + return False; +} + +// Print `Text`, `Len` characters long with markdown +// NOTE: This function has no wrapping support +void +tb_print_markdown(u32 X, u32 Y, u32 fg, u32 bg, u32* Text, u32 Len) +{ + for (u32 ch = 0; ch < Len; ch++) + { + if (Text[ch] == L'_') + { + if (ch < Len - 1 && Text[ch + 1] == L'_') + { + fg ^= TB_UNDERLINE; + ch++; + } + else + { + fg ^= TB_ITALIC; + } + } + else if (Text[ch] == L'*') + { + if (ch < Len - 1 && Text[ch + 1] == L'*') + { + fg ^= TB_BOLD; + ch++; + } + else + { + fg ^= TB_ITALIC; + } + } + else + { + tb_printf(X, Y, fg, bg, "%lc", Text[ch]); +#ifdef DEBUG + tb_present(); +#endif + X++; + } + } +} + +// Print `Text`, `Len` characters long as a string wrapped at `XLimit` width and `YLimit` height. +void +tb_print_wrapped(u32 X, u32 Y, u32 XLimit, u32 YLimit, u32* Text, u32 Len) +{ + // Iterator in text + assert(XLimit > 0); + assert(YLimit > 0); + u32 i = XLimit; + + u32 PrevI = 0; + + // For printing + u32 t = 0; + + while(i < Len) + { + // Search backwards for whitespace + while (!is_whitespace(Text[i])) + { + i--; + + // Failed to find whitespace, break on limit at character + if (i == PrevI) + { + i += XLimit; + break; + } + } + + t = Text[i]; + Text[i] = 0; + tb_printf(X, Y++, 0, 0, "%ls", Text + PrevI); +#ifdef DEBUG + tb_present(); +#endif + + Text[i] = t; + + if (is_whitespace(Text[i])) i++; + + PrevI = i; + i += XLimit; + + if (Y >= YLimit - 1) + { + break; + } + } + tb_printf(X, Y++, 0, 0, "%ls", Text + PrevI); +} + +// Print raw string with markdown format options in `MDFormat`, wrapped at +// `XLimit` and `YLimit`. The string is offset by `XOffset` and `YOffset`. +// `fg` and `bg` are passed to `tb_printf`. +// `Len` is the length of the string not including a null terminator +// The wrapping algorithm searches for a whitespace backwards and if none are found it wraps at +// `XLimit`. +// This function first builds an array of positions where to wrap and then prints `Text` by +// character using the array in `MDFormat.Options` and `WrapPositions` to know when to act. +// Returns how many times wrapped +u32 +tb_print_wrapped_with_markdown(u32 XOffset, u32 YOffset, u32 fg, u32 bg, + u32* Text, u32 Len, + u32 XLimit, u32 YLimit, + markdown_formatoptions MDFormat) +{ + XLimit -= XOffset; + YLimit -= YOffset; + assert(YLimit > 0); + assert(XLimit > 0); + + u32 TextIndex = XLimit; + u32 PrevTextIndex = 0; + + u32 WrapPositions[Len/XLimit + 1]; + u32 WrapPositionsLen = 0; + + // Get wrap positions + while (TextIndex < Len) + { + while (!is_whitespace(Text[TextIndex])) + { + TextIndex--; + + if (TextIndex == PrevTextIndex) + { + TextIndex += XLimit; + break; + } + } + + WrapPositions[WrapPositionsLen] = TextIndex; + WrapPositionsLen++; + + PrevTextIndex = TextIndex; + TextIndex += XLimit; + } + + u32 MDFormatOptionsIndex = 0; + u32 WrapPositionsIndex = 0; + u32 X = XOffset, Y = YOffset; + + for (u32 TextIndex = 0; TextIndex < Len; TextIndex++) + { + if (MDFormat.Len && + TextIndex == MDFormat.Options[MDFormatOptionsIndex].Position) + { + fg ^= MDFormat.Options[MDFormatOptionsIndex].Color; + MDFormatOptionsIndex++; + } + if (WrapPositionsLen && + TextIndex == WrapPositions[WrapPositionsIndex]) + { + Y++; + if (Y == YLimit) return WrapPositionsIndex + 1; + WrapPositionsIndex++; + X = XOffset; + if (is_whitespace(Text[TextIndex])) continue; + } + tb_printf(X++, Y, fg, bg, "%lc", Text[TextIndex]); + } + assert(WrapPositionsIndex == WrapPositionsLen); + assert(MDFormat.Len == MDFormatOptionsIndex); + + return WrapPositionsLen + 1; +} + +// Return string without markdown markup characters using `is_markdown()` +// ScratchArena is used to allocate space for the raw text +// Len should be characters + null terminator +// Copies the null terminator as well +raw_result +markdown_to_raw(Arena* ScratchArena, u32* Text, u32 Len) +{ + raw_result Result = {0}; + Result.Text = ScratchArena->addr; + + for (u32 i = 0; i < Len; i++) + { + if (!is_markdown(Text[i])) + { + u32* ch = ArenaPush(ScratchArena, sizeof(*ch)); + *ch = Text[i]; + Result.Len++; + } + } + + return Result; +} + +// Get a string with markdown in it and fill array in makrdown_formtoptions with position and colors +// Use Scratcharena to make allocations on that buffer, The Maximimum space needed is Len, eg. when +// the string is only markup characters. +markdown_formatoptions +preprocess_markdown(Arena* ScratchArena, u32* Text, u32 Len) +{ + markdown_formatoptions Result = {0}; + Result.Options = (format_option*)((u8*)ScratchArena->addr + ScratchArena->pos); + + format_option* FormatOpt; + + // raw char iterator + u32 rawch = 0; + + for (u32 i = 0; i < Len; i++) + { + switch (Text[i]) + { + case L'_': + { + FormatOpt = ArenaPush(ScratchArena, sizeof(*FormatOpt)); + Result.Len++; + + FormatOpt->Position = rawch; + if (i < Len - 1 && Text[i + 1] == '_') + { + FormatOpt->Color = TB_UNDERLINE; + i++; + } + else + { + FormatOpt->Color = TB_ITALIC; + } + } break; + case L'*': + { + FormatOpt = ArenaPush(ScratchArena, sizeof(*FormatOpt)); + Result.Len++; + + FormatOpt->Position = rawch; + if (i < Len - 1 && Text[i + 1] == '*') + { + FormatOpt->Color = TB_BOLD; + i++; + } + else + { + FormatOpt->Color = TB_ITALIC; + } + } break; + default: + { + rawch++; + } break; + } + } + + return Result; +} |