aboutsummaryrefslogtreecommitdiff
path: root/chatty.c
diff options
context:
space:
mode:
Diffstat (limited to 'chatty.c')
-rw-r--r--chatty.c490
1 files changed, 490 insertions, 0 deletions
diff --git a/chatty.c b/chatty.c
new file mode 100644
index 0000000..817d421
--- /dev/null
+++ b/chatty.c
@@ -0,0 +1,490 @@
+#define TB_IMPL
+#include "termbox2.h"
+
+#include "arena.h"
+#include "common.h"
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <locale.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sys/socket.h>
+
+#define TIMEOUT_POLL 60 * 1000
+// time to reconnect in seconds
+#define TIMEOUT_RECONNECT 1
+
+// must be of AUTHOR_LEN -1
+static u8 username[AUTHOR_LEN] = "(null)";
+// file descriptros for polling
+static struct pollfd *fds = NULL;
+// mutex for locking fds when in thread_reconnect()
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+
+enum { FDS_SERVER = 0,
+ FDS_TTY,
+ FDS_RESIZE,
+ FDS_MAX };
+
+void *thread_reconnect(void *address_ptr);
+void fillstr(wchar_t *str, wchar_t ch, u32 len);
+void popup(u32 fg, u32 bg, char *text);
+u32 tb_printf_wrap(u32 x, u32 y, u32 fg, u32 bg, wchar_t *text, u32 fg_pfx, u32 bg_pfx, char *pfx, s32 limit);
+void screen_home(Arena *msgsArena, wchar_t input[]);
+
+int main(int argc, char **argv)
+{
+ // Use first argument as username
+ if (argc > 1) {
+ u32 arg_len = strlen(argv[1]);
+ assert(arg_len <= AUTHOR_LEN - 1);
+ memcpy(username, argv[1], arg_len);
+ username[arg_len] = '\0';
+ }
+
+ s32 err, serverfd, ttyfd, resizefd, nsend;
+ setlocale(LC_ALL, ""); /* Fix unicode handling */
+
+ const struct sockaddr_in address = {
+ AF_INET,
+ htons(PORT),
+ {0},
+ };
+
+ serverfd = socket(AF_INET, SOCK_STREAM, 0);
+ assert(serverfd > 2); // greater than STDERR
+
+ err = connect(serverfd, (struct sockaddr *)&address, sizeof(address));
+ if (err != 0) {
+ perror("Server");
+ return 1;
+ }
+
+ tb_init();
+ tb_get_fds(&ttyfd, &resizefd);
+
+ // poopoo C cannot infer type
+ fds = (struct pollfd[FDS_MAX]){
+ {serverfd, POLLIN, 0},
+ { ttyfd, POLLIN, 0},
+ {resizefd, POLLIN, 0},
+ };
+
+ Arena *msgsArena = ArenaAlloc();
+ // Message *messages = msgsArena->memory; // helper pointer, for indexing memory
+ Arena *msgTextArena = ArenaAlloc();
+ u32 nrecv = 0;
+ // buffer used for receiving and sending messages
+ u8 buf[STREAM_BUF] = {0};
+ Message *mbuf = (Message *)buf;
+
+ wchar_t input[256] = {0};
+ u32 input_len = 0;
+ struct tb_event ev;
+ char *errmsg = NULL;
+
+ // Display loop
+ screen_home(msgsArena, input);
+ tb_present();
+
+ u8 quit = 0;
+ 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_SERVER].revents & POLLIN) {
+ // got data from server
+ u8 timestamp[TIMESTAMP_LEN];
+ message_timestamp(timestamp);
+
+ nrecv = recv(fds[FDS_SERVER].fd, buf, STREAM_LIMIT, 0);
+ assert(nrecv != -1);
+
+ // TODO: Handle this in a thread, the best way would be
+ // -> server disconnect info (somewhere, for now popup)
+ // -> user can still view messages, exit & type but not send
+ // -> try to reconnect in background
+ if (nrecv == 0) {
+ // close diconnected server's socket
+ err = close(fds[FDS_SERVER].fd);
+ assert(err == 0);
+ fds[FDS_SERVER].fd = -1; // ignore
+ // start trying to reconnect in a thread
+ pthread_t t;
+ err = pthread_create(&t, NULL, &thread_reconnect, (void*)&address);
+ assert(err == 0);
+
+ } else {
+ Message *buf_msg = (Message *)buf; // helper for indexing memory
+ Message *recvmsg = ArenaPush(msgsArena, sizeof(Message));
+ // copy everything but the text
+ memcpy(recvmsg, buf, AUTHOR_LEN + TIMESTAMP_LEN + sizeof(buf_msg->text_len));
+ // allocate memeory for text
+ recvmsg->text = ArenaPush(msgTextArena, recvmsg->text_len * sizeof(wchar_t));
+ // copy the text to the allocated space
+ memcpy(recvmsg->text, buf + TIMESTAMP_LEN + AUTHOR_LEN + sizeof(recvmsg->text_len), recvmsg->text_len * sizeof(wchar_t));
+ }
+ }
+
+ 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 (input_len) {
+ if (input[input_len - 1] == L' ') {
+ input[input_len - 1] = 0;
+ input_len--;
+ continue;
+ }
+ break;
+ }
+ // delete until whitespace
+ while (input_len) {
+ if (input[input_len - 1] == L' ')
+ break;
+ // erase
+ input[input_len - 1] = 0;
+ input_len--;
+ }
+ break;
+ case TB_KEY_CTRL_D:
+ case TB_KEY_CTRL_C:
+ quit = 1;
+ break;
+ case TB_KEY_CTRL_M: // send message
+ if (input_len == 0)
+ // do not send empty message
+ break;
+ if (fds[FDS_SERVER].fd == -1)
+ // do not send message to disconnected server
+ break;
+
+ // null terminate
+ input[input_len] = 0;
+ input_len++;
+ // TODO: check size does not exceed buffer
+
+ // add to msgsArena
+ Message *sendmsg = ArenaPush(msgsArena, sizeof(Message));
+ memcpy(sendmsg->author, username, AUTHOR_LEN);
+ message_timestamp(sendmsg->timestamp);
+ sendmsg->text_len = input_len;
+ sendmsg->text = ArenaPush(msgTextArena, input_len * sizeof(wchar_t));
+ // copy the text to the allocated space
+ memcpy(sendmsg->text, input, input_len * sizeof(wchar_t));
+
+ // Send the message
+ // copy everything but the text
+ memcpy(buf, sendmsg, AUTHOR_LEN + TIMESTAMP_LEN + sizeof(wchar_t));
+ memcpy(&mbuf->text, input, input_len * sizeof(wchar_t));
+ nsend = send(fds[FDS_SERVER].fd, buf, AUTHOR_LEN + TIMESTAMP_LEN + input_len * sizeof(wchar_t), 0);
+ assert(nsend > 0);
+
+ case TB_KEY_CTRL_U: // clear input
+ bzero(input, input_len * sizeof(wchar_t));
+ input_len = 0;
+ break;
+ default:
+ assert(ev.ch >= 0);
+ if (ev.ch == 0)
+ break;
+ // append key to input buffer
+ // TODO: check size does not exceed buffer
+ input[input_len] = ev.ch;
+ input_len++;
+
+ break;
+ }
+ if (quit)
+ break;
+ }
+
+ // These are used to redraw the screen from threads
+ if (fds[FDS_RESIZE].revents & POLLIN) {
+ // ignore
+ tb_poll_event(&ev);
+ }
+
+ screen_home(msgsArena, input);
+
+ tb_present();
+ }
+
+ tb_shutdown();
+
+ if (errmsg != NULL)
+ printf("%s\n", errmsg);
+
+ ArenaRelease(msgTextArena);
+ ArenaRelease(msgsArena);
+
+ return 0;
+}
+
+// Takes as paramter `struct sockaddr_in*` and uses it to connect to the server.
+// When the server sends a disconnect message this function must be called with the fds struct as
+// paramter. To indicate that the server is offline the fds[FDS_SERVER] is set to -1. When online
+// it is set to a non-zero value.
+// Returns NULL.
+void *thread_reconnect(void *address_ptr)
+{
+ u32 serverfd, err;
+ struct sockaddr_in *address = address_ptr;
+
+ while (1) {
+ serverfd = socket(AF_INET, SOCK_STREAM, 0);
+ assert(serverfd > 2); // greater than STDERR
+ err = connect(serverfd, (struct sockaddr *)address, sizeof(*address));
+ if (err == 0)
+ break;
+ assert(errno == ECONNREFUSED);
+ sleep(TIMEOUT_RECONNECT);
+ }
+
+ // if the server would send a disconnect again and the polling catches up there could be two
+ // threads accessing fds.
+ pthread_mutex_lock(&mutex);
+ fds[FDS_SERVER].fd = serverfd;
+ pthread_mutex_unlock(&mutex);
+
+ // ask to redraw screen
+ raise(SIGWINCH);
+
+ return NULL;
+}
+
+// fill str array with char
+void fillstr(wchar_t *str, wchar_t 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, char *text)
+{
+ u32 len = strlen(text);
+ assert(len > 0);
+ tb_print(global.width / 2 - len / 2, global.height / 2, fg, bg, text);
+}
+
+// Print `text` of text_len` wide characters wrapped to limit. 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: remove text_len and calculate it in the function
+// TODO: add y limit
+// TODO:(bug) text after pfx is wrapped one too soon
+// TODO: text == NULL to know how many lines *would* be printed
+// TODO: check if text[i] goes out of bounds
+u32 tb_printf_wrap(u32 x, u32 y, u32 fg, u32 bg, wchar_t *text, u32 fg_pfx, u32 bg_pfx, char *pfx, s32 limit)
+{
+ assert(limit > 0);
+
+ /// Algorithm
+ // 1. Advance by limit
+ // 2. Look backwards for whitespace
+ // 3. split the string at the whitespace
+ // 4. print the string
+ // 5. restore the string (optional)
+ // 6. set the offset
+ // 7. repeat step 1. until i > len
+ // 8. print remaining part of the string
+
+ // lines y, incremented after each wrap
+ s32 ly = y;
+ // character the text is split on
+ wchar_t t = 0;
+ // index used for searching in string
+ s32 i = limit;
+ // previous i for windowing through the text
+ s32 offset = 0;
+ // used when retrying to get a longer limit
+ u32 failed = 0;
+
+ u32 text_len = 0;
+ while (text[text_len] != 0)
+ text_len++;
+
+ // NOTE: We can assume that we need to wrap, therefore print a newline after the prefix string
+ if (pfx != NULL) {
+ tb_printf(x, ly, fg_pfx, bg_pfx, "%s", pfx);
+
+ s32 pfx_len = strlen(pfx);
+ if (limit > pfx_len + text_len) {
+ // everything fits on one line
+ tb_printf(pfx_len, y, fg, bg, "%ls", text);
+ return 1;
+ } else {
+ ly++;
+ }
+ }
+
+ while (i < text_len) {
+ // search backwards for whitespace
+ while (i > offset && text[i] != L' ')
+ i--;
+
+ // retry with bigger limit
+ if (i == offset) {
+ offset = i;
+ failed++;
+ i += limit + failed * limit;
+ 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;
+ }
+ 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 for user input and the received messages from msgsArena
+void screen_home(Arena *msgsArena, wchar_t input[])
+{
+ // config options
+ const u32 box_max_len = 80;
+ const u32 box_min_len = 3;
+ const u32 box_x = 0, box_y = global.height - 3, box_pad_x = 1, box_mar_x = 1, box_bwith = 1, box_height = 3;
+ u32 input_len = 0;
+ while (input[input_len] != 0)
+ input_len++;
+ const u32 prompt_x = box_x + box_pad_x + box_mar_x + box_bwith + input_len;
+
+ // 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
+ if (global.height < box_height ||
+ global.width < (box_x + box_mar_x * 2 + box_pad_x * 2 + box_bwith * 2 + 1)) {
+ // + 1 for cursor
+ tb_hide_cursor();
+ return;
+ } else {
+ // show cursor
+ // TODO: show cursor as block character instead of using the real cursor
+ bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]);
+ }
+
+ // 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
+ {
+ u32 freesp = global.height - box_height;
+ if (freesp <= 0)
+ goto draw_prompt;
+
+ Message *messages = msgsArena->memory;
+ assert(messages != NULL);
+ // on what line to print the current message, used for scrolling
+ u32 msg_y = 0;
+
+ u32 nmessages = (msgsArena->pos / sizeof(Message));
+ u32 offs = (nmessages > freesp) ? nmessages - freesp : 0;
+
+ for (u32 i = offs; i < nmessages; i++) {
+ // Color user's own messages
+ u32 fg = 0;
+ if (strncmp((char *)username, (char *)messages[i].author, AUTHOR_LEN) == 0) {
+ fg = TB_CYAN;
+ } else {
+ fg = TB_MAGENTA;
+ }
+
+ u32 ty = 0;
+ char pfx[AUTHOR_LEN + TIMESTAMP_LEN - 2 + 5] = {0};
+ sprintf(pfx, "%s [%s] ", messages[i].timestamp, messages[i].author);
+ ty = tb_printf_wrap(0, msg_y, TB_WHITE, 0, messages[i].text, fg, 0, pfx, global.width);
+ msg_y += ty;
+ }
+
+ draw_prompt:
+ // Draw prompt box which is a box made out of
+ // should look like this: ╭───────╮
+ // │ text█ │
+ // ╰───────╯
+ // the text is padded to the left and right by box_pad_x
+ // the middle/inner part is opaque
+ // TODO: wrapping when the text is bigger & alternated with scrolling when there is not
+ // enough space.
+ {
+ u32 box_len = 0;
+ if (global.width >= box_max_len + 2 * box_mar_x)
+ box_len = box_max_len;
+ else
+ box_len = global.width - box_mar_x * 2;
+
+ // +2 for corners and null terminator
+ wchar_t box_up[box_len + 1];
+ wchar_t box_in[box_len + 1];
+ wchar_t box_down[box_len + 1];
+ wchar_t lr = L'─', ur = L'╭', rd = L'╮', dr = L'╰', ru = L'╯', ud = L'│';
+
+ // top bar
+ box_up[0] = ur;
+ fillstr(box_up + 1, lr, box_len - 1);
+ box_up[box_len - 1] = rd;
+ box_up[box_len] = 0;
+ // inner part
+ fillstr(box_in + 1, L' ', box_len - 1);
+ box_in[0] = ud;
+ box_in[box_len - 1] = ud;
+ box_in[box_len] = 0;
+ // bottom bar
+ box_down[0] = dr;
+ fillstr(box_down + 1, lr, box_len - 1);
+ box_down[box_len - 1] = ru;
+ box_down[box_len] = 0;
+
+ tb_printf(box_x + box_mar_x, box_y, 0, 0, "%ls", box_up);
+ tb_printf(box_x + box_mar_x, box_y + 1, 0, 0, "%ls", box_in);
+ tb_printf(box_x + box_mar_x, box_y + 2, 0, 0, "%ls", box_down);
+
+ global.cursor_y = box_y + 1;
+
+ // NOTE: wrapping would be better.
+ // Scroll the text when it exceeds the prompt's box length
+ u32 freesp = box_len - box_pad_x * 2 - box_bwith * 2;
+ if (freesp <= 0)
+ return;
+
+ if (input_len > freesp) {
+ wchar_t *text_offs = input + (input_len - 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);
+ }
+ }
+
+ if (fds[FDS_SERVER].fd == -1) {
+ // show error popup
+ popup(TB_RED, TB_BLACK, "Server disconnected.");
+ }
+ }
+}
+