aboutsummaryrefslogtreecommitdiff
path: root/source/chatty.c
diff options
context:
space:
mode:
Diffstat (limited to 'source/chatty.c')
-rw-r--r--source/chatty.c834
1 files changed, 834 insertions, 0 deletions
diff --git a/source/chatty.c b/source/chatty.c
new file mode 100644
index 0000000..32a4431
--- /dev/null
+++ b/source/chatty.c
@@ -0,0 +1,834 @@
+#define TB_IMPL
+#include "termbox2.h"
+#undef TB_IMPL
+
+
+#include <arpa/inet.h>
+#include <locale.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#define TIMEOUT_POLL 60 * 1000
+// time to reconnect in seconds
+#define TIMEOUT_RECONNECT 1
+#define MAX_INPUT_LEN 512
+// Filepath where user ID is stored
+#define ID_FILE "_id"
+// Filepath where logged
+#define LOGFILE "chatty.log"
+// enable logging
+#define LOGGING
+// Number of spaces inserted when pressing Tab/Ctrl+I
+#define TAB_WIDTH 4
+
+#ifndef Assert
+#ifdef DEBUG
+#define Assert(expr) if (!(expr)) \
+ { \
+ tb_shutdown(); \
+ raise(SIGTRAP); \
+ }
+#else
+#define Assert(expr) ;
+#endif // DEBUG
+#endif // Assert
+
+
+#define ARENA_IMPL
+#include "arena.h"
+
+#define CHATTY_IMPL
+#include "chatty.h"
+
+#include "protocol.h"
+
+#define TEXTBOX_MAX_INPUT MAX_INPUT_LEN
+#define UI_IMPL
+#include "ui.h"
+
+enum { FDS_BI = 0, // for one-way communication with the server (eg. TextMessage)
+ FDS_UNI, // For two-way communication with the server (eg. IDMessage)
+ FDS_TTY,
+ FDS_RESIZE,
+ FDS_MAX };
+
+typedef struct {
+ u8 Author[AUTHOR_LEN];
+ ID ID;
+} User;
+#define USER_FMT "[%s](%lu)"
+#define USER_ARG(client) client.Author, client.ID
+
+typedef struct {
+ s32 NumRead;
+ u32 Error;
+} command_output;
+
+// User used by chatty
+global_variable User user = {0};
+// Address of chatty server
+global_variable struct sockaddr_in address;
+
+// fill str array with char
+void
+fillstr(u32* Str, u32 ch, u32 Len)
+{
+ for (u32 i = 0; i < Len; i++)
+ Str[i] = ch;
+}
+
+// Centered popup displaying message in the appropriate cololrs
+void
+popup(u32 fg, u32 bg, u8* text)
+{
+ u32 len = strlen((char*)text);
+ Assert(len > 0);
+ tb_print(global.width / 2 - len / 2, global.height / 2, fg, bg, (char*)text);
+}
+
+// Returns client in clientsArena matching id
+// Returns user if the id was the user's ID
+// Returns 0 if nothing was found
+User*
+get_user_by_id(Arena* clientsArena, ID id)
+{
+ // User is not in the clientsArena
+ 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)
+ return clients + i;
+ }
+ return 0;
+}
+
+// Request information of client from fd byd id and add it to clientsArena
+// Returns pointer to added client
+User*
+add_user_info(Arena* clientsArena, s32 fd, u64 id)
+{
+ // Request information about ID
+ HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID);
+ header.id = user.ID;
+ IDMessage message = {id};
+ s32 nsend = sendAnyMessage(fd, header, &message);
+ Assert(nsend != -1);
+
+ // Wait for response
+ IntroductionMessage introduction_message;
+ recvAnyMessageType(fd, &header, &introduction_message, HEADER_TYPE_INTRODUCTION);
+
+ // Add the information
+ User* client = ArenaPush(clientsArena, sizeof(*client));
+ memcpy(client->Author, introduction_message.author, AUTHOR_LEN);
+ client->ID = id;
+
+ LoggingF("Got " USER_FMT "\n", USER_ARG((*client)));
+ return client;
+}
+
+// Tries to connect to address and populates resulting file descriptors in ConnectionResult.
+s32
+get_connection(struct sockaddr_in* address)
+{
+ s32 fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd == -1) return -1;
+
+ s32 err = connect(fd, (struct sockaddr*)address, sizeof(*address));
+ if (err) return -1;
+
+ return fd;
+}
+
+// Authenticates a file descriptor with either the user's id if non-zero or
+// it's information if id is zero.
+// Returns 0 if an error occurred. Non-zero on success.
+u32
+authenticate(User* user, s32 fd)
+{
+ /* Scenario 1: Already have an ID */
+ if (user->ID)
+ {
+ HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID);
+ IDMessage message = {user->ID};
+ s32 nsend = sendAnyMessage(fd, header, &message);
+ Assert(nsend != -1);
+
+ ErrorMessage error_message;
+ s32 nrecv = recvAnyMessageType(fd, &header, &error_message, HEADER_TYPE_ERROR);
+ Assert(nrecv != -1);
+ // TODO: handle not found
+ if (nrecv == 0)
+ return 0;
+
+ if (error_message.type == ERROR_TYPE_SUCCESS)
+ return 1;
+ else
+ return 0;
+ }
+ /* Scenario 2: No ID, request one from server */
+ else
+ {
+ HeaderMessage header = HEADER_INIT(HEADER_TYPE_INTRODUCTION);
+ IntroductionMessage message;
+ 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;
+ return 1;
+ }
+}
+
+// Connect to *address_ptr of type `struct sockaddr_in*`. If it failed wait for TIMEOUT_RECONNECT
+// seconds.
+// This function is meant to be run by a thread.
+// An offline server means fds[FDS_SERVER] is set to -1. When online
+// it is set to with the appropriate file descriptor.
+// Returns 0.
+#define Miliseconds(s) (s*1000*1000)
+void*
+thread_reconnect(void* fds_ptr)
+{
+ s32 unifd, bifd;
+ struct pollfd* fds = fds_ptr;
+ struct timespec t = { 0, Miliseconds(300) }; // 300 miliseconds
+ LoggingF("Trying to reconnect\n");
+ while (1)
+ {
+ // timeout
+ nanosleep(&t, &t);
+
+ bifd = get_connection(&address);
+ if (bifd == -1)
+ {
+ LoggingF("errno: %d\n", errno);
+ continue;
+ }
+ unifd = get_connection(&address);
+ if (unifd == -1)
+ {
+ LoggingF("errno: %d\n", errno);
+ close(bifd);
+ continue;
+ }
+
+ LoggingF("Reconnect succeeded (%d, %d), authenticating\n", unifd, bifd);
+
+ if (authenticate(&user, bifd) &&
+ authenticate(&user, unifd))
+ {
+ break;
+ }
+
+ close(bifd);
+ close(unifd);
+
+ LoggingF("Failed, retrying...\n");
+ }
+
+ fds[FDS_BI].fd = bifd;
+ fds[FDS_UNI].fd = unifd;
+
+ // Redraw screen
+ raise(SIGWINCH);
+
+ return 0;
+}
+
+command_output
+run_command_get_output(char *Command, char *Argv[], u8 *OutputBuffer, int Len)
+{
+ command_output Result = {0};
+
+ int CommandPipe[2];
+ int Error = pipe(CommandPipe);
+ Assert(Error != -1);
+
+ int Pid = fork();
+ Assert(Pid != -1);
+
+ // Run command in child
+ if (!Pid)
+ {
+ dup2(CommandPipe[1], STDOUT_FILENO); //redirect stdout to Pipe
+ close(CommandPipe[0]);
+ close(CommandPipe[1]);
+
+ int fd = open("/dev/null", O_WRONLY);
+ dup2(fd, STDERR_FILENO);
+
+ execvp(Command, Argv);
+ }
+
+ // Wait for child
+ int statval;
+ waitpid(Pid, &statval, 0);
+
+ if(WIFEXITED(statval))
+ {
+ int ExitCode = WEXITSTATUS(statval);
+ if (ExitCode)
+ {
+ Result.Error = ExitCode;
+ }
+ }
+ else
+ {
+ Result.Error = 1;
+ return Result;
+ }
+
+ close(CommandPipe[1]);
+
+ Result.NumRead = read(CommandPipe[0], OutputBuffer, Len);
+ Assert(Result.NumRead != -1);
+
+ return Result;
+}
+
+// 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
+DisplayChat(Arena* ScratchArena,
+ Arena* MessagesArena, u32 MessagesNum,
+ Arena* ClientsArena, struct pollfd* fds,
+ wchar_t Input[], u32 InputLen)
+{
+ rect TextBox = {
+ 1, 0, global.width - 2, 3,
+ };
+ u32 FreeHeight = global.height - TextBox.H;
+ TextBox.Y = FreeHeight;
+
+#define MIN_TEXT_WIDTH_FOR_WRAPPING 20
+ s32 MinBoxWidth = TEXTBOX_MIN_WIDTH;
+ s32 InputBoxTextWidth = TextBox.W - MinBoxWidth + 2;
+ bool ShouldIncreaseSize = (
+ (s32)InputLen >= InputBoxTextWidth &&
+ InputBoxTextWidth > MIN_TEXT_WIDTH_FOR_WRAPPING
+ );
+ if (ShouldIncreaseSize)
+ {
+ TextBox.H++;
+ }
+#undef MIN_TEXT_WIDTH_FOR_WRAPPING
+
+ rect TextR = {
+ TextBox.X + 2, TextBox.Y + 1,
+ TextBox.W - 2*TEXTBOX_PADDING_X - 2*TEXTBOX_BORDER_WIDTH,
+ TextBox.H - 2*TEXTBOX_BORDER_WIDTH
+ };
+
+ if (global.height < TextBox.H || global.width < TextBox.W)
+ {
+ tb_hide_cursor();
+ return;
+ }
+
+ bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]);
+ global.cursor_x = TextR.X;
+ global.cursor_y = TextR.Y;
+ DrawBox(TextBox, 0);
+ DrawTextBox(TextR, Input, InputLen);
+
+ // Print vertical bar
+ s32 VerticalBarOffset = TIMESTAMP_LEN + AUTHOR_LEN + 2;
+ for (u32 Y = 0; Y < FreeHeight; Y++)
+ tb_print(VerticalBarOffset, Y, 0, 0, "│");
+
+ // show error popup if server disconnected
+ if (fds[FDS_UNI].fd == -1 || fds[FDS_BI].fd == -1)
+ {
+ popup(TB_RED, TB_BLACK, (u8*)"Server disconnected.");
+ }
+
+ // Print messages in msgsArena, if there are too many to display, start printing from an offset.
+ // Looks like this:
+ // 03:24:29 [1234567890ab] hello homes how are
+ // you doing?
+ // 03:24:33 [TlasT] │ I am fine
+ // 03:24:33 [Fin] │ I am too
+ {
+
+ // If there is not enough space to draw, do not draw
+ if (FreeHeight <= 0) return;
+
+ // Used to go to the next message in MessagesArena by incrementing with the messages' size.
+ u8* MessageAddress = MessagesArena->addr;
+ Assert(MessageAddress != 0);
+
+ // 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*)MessageAddress;
+ MessageAddress += sizeof(*header);
+
+ switch (header->type)
+ {
+ case HEADER_TYPE_TEXT:
+ {
+ TextMessage* message = (TextMessage*)MessageAddress;
+ MessageAddress += TEXTMESSAGE_SIZE;
+ MessageAddress += message->len * sizeof(*message->text);
+ break;
+ }
+ case HEADER_TYPE_PRESENCE:
+ MessageAddress += sizeof(PresenceMessage);
+ break;
+ case HEADER_TYPE_HISTORY:
+ MessageAddress += sizeof(HistoryMessage);
+ break;
+ default:
+ // unhandled message type
+ Assert(0);
+ }
+ }
+
+ u32 MessageY = 0;
+
+ for (u32 i = MessagesOffset;
+ i < MessagesNum;
+ i++)
+ {
+ if (MessageY >= FreeHeight) break;
+
+ HeaderMessage* header = (HeaderMessage*)MessageAddress;
+ MessageAddress += sizeof(*header);
+
+ User* client = get_user_by_id(ClientsArena, header->id);
+ if (!client)
+ {
+ 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*)MessageAddress;
+
+
+ // Color own messages
+ u32 fg = 0;
+ if (user.ID == header->id)
+ {
+ fg = TB_CYAN;
+ }
+ else
+ {
+ fg = TB_MAGENTA;
+ }
+
+ // prefix is of format "HH:MM:SS [<author>] ", create it
+ u8 timestamp[TIMESTAMP_LEN];
+ formatTimestamp(timestamp, message->timestamp);
+
+ 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, (wchar_t*)&message->text, message->len);
+ markdown_formatoptions MDFormat = preprocess_markdown(ScratchArena,
+ (wchar_t*)&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);
+ MessageAddress += message_size;
+ } break;
+ case HEADER_TYPE_PRESENCE:
+ {
+ 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*)MessageAddress;
+ MessageAddress += sizeof(*message);
+ // TODO: implement
+ } break;
+ default:
+ tb_printf(0, MessageY, 0, 0, "%s", headerTypeString(header->type));
+ MessageY++;
+ break;
+ }
+ }
+
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ if (argc < 2)
+ {
+ fprintf(stderr, "usage: chatty <username>\n");
+ return 1;
+ }
+
+ u32 arg_len = strlen(argv[1]);
+ Assert(arg_len <= AUTHOR_LEN - 1);
+ memcpy(user.Author, argv[1], arg_len);
+ user.Author[arg_len] = '\0';
+
+ s32 err = 0; // error code for functions
+
+ u32 MessagesNum = 0; // Number of messages in msgsArena
+ s32 nrecv = 0; // number of bytes received
+
+ wchar_t Input[MAX_INPUT_LEN] = {0}; // input buffer
+ u32 InputIndex = 0; // number of characters in input
+
+ 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
+ u8* quitmsg = 0; // this string will be printed before returning from main
+
+ pthread_t thr_rec; // thread for reconnecting to server when disconnected
+
+#ifdef LOGGING
+ LogFD = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600);
+ Assert(LogFD != -1);
+#else
+ logfd = 2; // stderr
+#endif
+
+ // poopoo C cannot infer type
+ struct pollfd fds[FDS_MAX] = {
+ {-1, POLLIN, 0}, // FDS_BI
+ {-1, POLLIN, 0}, // FDS_UNI
+ {-1, POLLIN, 0}, // FDS_TTY
+ {-1, POLLIN, 0}, // FDS_RESIZE
+ };
+
+ address = (struct sockaddr_in){
+ AF_INET,
+ htons(PORT),
+ {0},
+ {0},
+ };
+
+#ifdef IMPORT_ID
+ // File for storing the user's ID.
+ u32 idfile = open(ID_FILE, O_RDWR | O_CREAT, 0600);
+ s32 nread = read(idfile, &user.id, sizeof(user.id));
+ Assert(nread != -1);
+#endif
+ /* Authentication */
+ {
+ s32 unifd, bifd;
+ bifd = get_connection(&address);
+ if (bifd == -1)
+ {
+ LoggingF("errno: %d\n", errno);
+ return 1;
+ }
+ unifd = get_connection(&address);
+ if (unifd == -1)
+ {
+ LoggingF("errno: %d\n", errno);
+ return 1;
+ }
+ LoggingF("(%d,%d)\n", bifd, unifd);
+ if (!authenticate(&user, bifd) ||
+ !authenticate(&user, unifd))
+ {
+ LoggingF("errno: %d\n", errno);
+ return 1;
+ }
+ else
+ {
+ LoggingF("Authenticated (%d,%d)\n", bifd, unifd);
+ }
+ fds[FDS_BI].fd = bifd;
+ fds[FDS_UNI].fd = unifd;
+ }
+
+#ifdef IMPORT_ID
+ // Save id
+ write(idfile, &user.id, sizeof(user.id));
+#endif
+
+ LoggingF("Got ID: %lu\n", user.ID);
+
+ // for wide character printing
+ Assert(setlocale(LC_ALL, ""));
+
+ // init
+ tb_init();
+ tb_get_fds(&fds[FDS_TTY].fd, &fds[FDS_RESIZE].fd);
+
+ DisplayChat(&ScratchArena,
+ &MessagesArena, MessagesNum,
+ &ClientsArena, fds,
+ Input, InputIndex);
+ tb_present();
+
+ // main loop
+ while (!quit)
+ {
+ err = poll(fds, FDS_MAX, TIMEOUT_POLL);
+ // ignore resize events and use them to redraw the screen
+ Assert(err != -1 || errno == EINTR);
+
+ tb_clear();
+
+ if (fds[FDS_UNI].revents & POLLIN)
+ {
+ // got data from server
+ HeaderMessage header;
+ nrecv = recv(fds[FDS_UNI].fd, &header, sizeof(header), 0);
+ Assert(nrecv != -1);
+
+ // Server disconnects
+ if (nrecv == 0)
+ {
+ // close diconnected server's socket
+ err = close(fds[FDS_UNI].fd);
+ Assert(err == 0);
+ fds[FDS_UNI].fd = -1; // ignore
+ // start trying to reconnect in a thread
+ err = pthread_create(&thr_rec, 0, &thread_reconnect, (void*)fds);
+ Assert(err == 0);
+ }
+ else
+ {
+ if (header.version != PROTOCOL_VERSION)
+ {
+ LoggingF("Header received does not match version\n");
+ continue;
+ }
+
+ void* addr = ArenaPush(&MessagesArena, sizeof(header));
+ memcpy(addr, &header, sizeof(header));
+
+ // Messages handled from server
+ switch (header.type)
+ {
+ case HEADER_TYPE_TEXT:
+ recvTextMessage(&MessagesArena, fds[FDS_UNI].fd);
+ MessagesNum++;
+ break;
+ case HEADER_TYPE_PRESENCE:;
+ PresenceMessage* message = ArenaPush(&MessagesArena, sizeof(*message));
+ nrecv = recv(fds[FDS_UNI].fd, message, sizeof(*message), 0);
+ Assert(nrecv != -1);
+ Assert(nrecv == sizeof(*message));
+ MessagesNum++;
+ break;
+ default:
+ LoggingF("Got unhandled message: %s\n", headerTypeString(header.type));
+ break;
+ }
+ }
+ }
+
+ if (fds[FDS_TTY].revents & POLLIN)
+ {
+ // got a key event
+ tb_poll_event(&ev);
+
+ switch (ev.key)
+ {
+ case TB_KEY_CTRL_W:
+ // delete consecutive whitespace
+ while (InputIndex)
+ {
+ if (Input[InputIndex - 1] == L' ')
+ {
+ Input[InputIndex - 1] = 0;
+ InputIndex--;
+ continue;
+ }
+ break;
+ }
+ // delete until whitespace
+ while (InputIndex)
+ {
+ if (Input[InputIndex - 1] == L' ')
+ break;
+ // erase
+ Input[InputIndex - 1] = 0;
+ InputIndex--;
+ }
+ break;
+ case TB_KEY_CTRL_Z:
+ {
+ pid_t pid = getpid();
+ tb_shutdown();
+ kill(pid, SIGSTOP);
+ tb_init();
+ } break;
+ case TB_KEY_CTRL_Y: // Paste clipboard contents to input
+ {
+ u32 OutputBufferLen = MAX_INPUT_LEN - InputIndex;
+ if (OutputBufferLen <= 0) break;
+
+ u8 OutputBuffer[OutputBufferLen];
+
+ char *PathName = "xclip";
+ char *Argv[] = {PathName, "-o", "-sel", "c", 0};
+
+ command_output Output = run_command_get_output(PathName, Argv, OutputBuffer, OutputBufferLen - 1);
+ if (Output.Error) break;
+
+ // Remove trailing whitespace
+ int BufferIndex = Output.NumRead - 1;
+ while (BufferIndex > 0 &&
+ (OutputBuffer[BufferIndex] == '\n' ||
+ OutputBuffer[BufferIndex] == '\t'))
+ {
+ OutputBuffer[BufferIndex] = 0;
+ BufferIndex--;
+ }
+
+ // Append to output
+ for (s32 BufferIndex = 0; BufferIndex < Output.NumRead; BufferIndex++)
+ {
+ // convert u8 to u32
+ u32 ch = OutputBuffer[BufferIndex];
+ Input[InputIndex] = ch;
+ InputIndex++;
+ }
+
+ } break;
+ case TB_KEY_CTRL_I:
+ {
+ for (u32 i = 0;
+ i < TAB_WIDTH && InputIndex < MAX_INPUT_LEN - 1;
+ i++)
+ {
+ Input[InputIndex] = L' ';
+ InputIndex++;
+ }
+ } break;
+ case TB_KEY_BACKSPACE2:
+ if (InputIndex) InputIndex--;
+ Input[InputIndex] = 0;
+ break;
+ case TB_KEY_CTRL_D:
+ case TB_KEY_CTRL_C:
+ quit = 1;
+ break;
+ case TB_KEY_CTRL_M: // send message
+ {
+ raw_result RawText = markdown_to_raw(0, Input, InputIndex);
+
+ if (RawText.Len == 0)
+ // do not send empty message
+ break;
+ if (fds[FDS_UNI].fd == -1)
+ // do not send message to disconnected server
+ break;
+
+ // null terminate
+ Input[InputIndex] = 0;
+ InputIndex++;
+
+ // Save header
+ HeaderMessage* header = ArenaPush(&MessagesArena, sizeof(*header));
+ header->version = PROTOCOL_VERSION;
+ header->type = HEADER_TYPE_TEXT;
+ header->id = user.ID;
+
+ // Save message
+ TextMessage* sendmsg = ArenaPush(&MessagesArena, TEXTMESSAGE_SIZE);
+ sendmsg->timestamp = time(0);
+ sendmsg->len = InputIndex;
+
+ u32 text_size = InputIndex * sizeof(*Input);
+ ArenaPush(&MessagesArena, text_size);
+ memcpy(&sendmsg->text, Input, text_size);
+
+ sendAnyMessage(fds[FDS_UNI].fd, *header, sendmsg);
+
+ MessagesNum++;
+ // also clear input
+ } // fallthrough
+ case TB_KEY_CTRL_U: // clear input
+ bzero(Input, InputIndex * sizeof(*Input));
+ InputIndex = 0;
+ break;
+ default:
+ if (ev.ch == 0)
+ break;
+
+ // TODO: show error
+ if (InputIndex == MAX_INPUT_LEN - 1) // last byte reserved for \0
+ break;
+
+ // append key to input buffer
+ Input[InputIndex] = ev.ch;
+ InputIndex++;
+ }
+ if (quit)
+ break;
+ }
+
+ // These are used to redraw the screen from threads
+ if (fds[FDS_RESIZE].revents & POLLIN)
+ {
+ // ignore
+ tb_poll_event(&ev);
+ }
+
+ DisplayChat(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputIndex);
+
+ tb_present();
+ }
+
+ tb_shutdown();
+
+ if (quitmsg != 0)
+ printf("%s\n", quitmsg);
+
+ return 0;
+}