#include "shared.h" #include #include #include #include #include #include #include #include struct { struct { bool title_header; bool message_field; bool input_field; } needs_redraw; struct { int x, y; } cursor; int scroll_offset; int unread; } render_state = { .needs_redraw = {1, 1, 1}, .cursor = {0, 0}, .scroll_offset = 0, .unread = 0 }; struct input_state_t { char buffer[MAX_MESSAGE_LENGTH]; int buffer_size; int content_length; int cursor_pos; int view_start; }; int server_fd; FILE *reader; FILE *writer; struct input_state_t input_state = (struct input_state_t){{0}, MAX_MESSAGE_LENGTH, 0, 0, 0}; static int input_state_insert_char(const char ch) { if (input_state.content_length >= input_state.buffer_size - 1) return 1; for (int i = input_state.content_length; i > input_state.cursor_pos; i--) input_state.buffer[i] = input_state.buffer[i - 1]; input_state.buffer[input_state.cursor_pos] = ch; input_state.content_length++; input_state.cursor_pos++; input_state.buffer[input_state.content_length] = '\0'; return 0; } static void input_state_delete_char() { if (input_state.cursor_pos < input_state.content_length) { for (int i = input_state.cursor_pos; i < input_state.content_length - 1; i++) { input_state.buffer[i] = input_state.buffer[i + 1]; } input_state.content_length--; input_state.buffer[input_state.content_length] = '\0'; } } static void input_state_backspace_char() { if (input_state.cursor_pos > 0) { input_state.cursor_pos--; input_state_delete_char(); } } static int input_state_handle_input(const int ch) { render_state.needs_redraw.input_field = 1; if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return 1; if (ch == KEY_BACKSPACE || ch == 127) { input_state_backspace_char(); return 0; } if (ch == KEY_DC) { input_state_delete_char(); return 0; } if (ch == KEY_LEFT) { if (input_state.cursor_pos > 0) input_state.cursor_pos--; return 0; } if (ch == KEY_RIGHT) { if (input_state.cursor_pos < input_state.content_length) input_state.cursor_pos++; return 0; } if (ch == KEY_HOME) { input_state.cursor_pos = 0; return 0; } if (ch == KEY_END) { input_state.cursor_pos = input_state.content_length; return 0; } if (ch == KEY_PPAGE) { render_state.scroll_offset += LINES - 3 - 1 -2; render_state.needs_redraw.message_field = 1; render_state.unread = 0; render_state.needs_redraw.title_header = 1; return 0; } if (ch == KEY_NPAGE) { render_state.scroll_offset -= LINES - 3 - 1 -2; if (render_state.scroll_offset < 0) render_state.scroll_offset = 0; render_state.needs_redraw.message_field = 1; render_state.needs_redraw.title_header = 1; return 0; } if (ch == KEY_UP) { render_state.scroll_offset++; render_state.needs_redraw.message_field = 1; render_state.needs_redraw.title_header = 1; return 0; } if (ch == KEY_DOWN) { if (render_state.scroll_offset > 0) { render_state.scroll_offset--; render_state.needs_redraw.message_field = 1; if (render_state.scroll_offset == 0) render_state.unread = 0; render_state.needs_redraw.title_header = 1; } return 0; } if (ch >= 32 && ch <= 126) input_state_insert_char((char)ch); return 0; } struct { char** data; size_t size; size_t capacity; pthread_mutex_t lock; } messages; static void init_messages() { messages.size = 0; messages.capacity = 16; messages.data = malloc(sizeof(*messages.data) * messages.capacity); pthread_mutex_init(&messages.lock, NULL); } static void add_new_message(const char* str) { pthread_mutex_lock(&messages.lock); if (messages.size == messages.capacity) { messages.capacity *= 2; messages.data = realloc(messages.data, sizeof(*messages.data) * messages.capacity); } messages.data[messages.size++] = strdup(str); if (render_state.scroll_offset > 0) { render_state.scroll_offset++; render_state.unread++; render_state.needs_redraw.title_header = 1; } render_state.needs_redraw.message_field = 1; pthread_mutex_unlock(&messages.lock); } #define PRINT_SPACES(n) printw("%*s", (n), "") #define PROGRAM_NAME "chatclient-cli" #define CC_COLOR_PAIR_TITLE_HEADER 1 #define CC_COLOR_PAIR_TITLE_HEADER2 3 #define CC_COLOR_PAIR_INPUT 2 char server_str[CC_ADDRLEN]; int server_str_len; static void cc_render_title_header( const int x, const int y, const int w ) { char system_time[21]; const time_t now = time(NULL); int time_length = strftime(system_time, sizeof(system_time), "%Y-%m-%d %H:%M:%S", localtime(&now)); char indicator[32] = {0}; int indicator_length = 0; if (render_state.unread > 0) { indicator_length = snprintf(indicator, sizeof(indicator), " %c%d unread%c", now % 2 ? ' ' : '[', render_state.unread, now % 2 ? ' ' : ']' ); } const int name_length = strlen(PROGRAM_NAME); const int total_length = name_length + indicator_length + server_str_len + time_length; char header_str[1024]; int n; int left_right_padding = w / 16; if (left_right_padding > 2) left_right_padding = 2; if (total_length + left_right_padding * 2 + 2 * 2 <= w) { const int total_padding = w - left_right_padding * 2 - total_length; const int padding2 = total_padding / 2; const int padding1 = total_padding - padding2; n = snprintf(header_str, sizeof(header_str), "%*c" "%s" "%s" "%*c" "%s" "%*c" "%s" "%*c", left_right_padding, ' ', PROGRAM_NAME, indicator, padding1, ' ', server_str, padding2, ' ', system_time, left_right_padding, ' ' ); } else if (name_length + indicator_length + server_str_len + left_right_padding * 2 + 2 <= w) { const int padding = w - left_right_padding * 2 - name_length - indicator_length - server_str_len; n = snprintf(header_str, sizeof(header_str), "%*c" "%s" "%s" "%*c" "%s" "%*c", left_right_padding, ' ', PROGRAM_NAME, indicator, padding, ' ', server_str, left_right_padding, ' ' ); } else if (name_length + indicator_length <= w) { const int total_left_right_padding = w - name_length - indicator_length; int right_padding = total_left_right_padding / 2; int left_padding = total_left_right_padding - right_padding; n = snprintf(header_str, sizeof(header_str), "%*c" "%s" "%s" "%*c", left_padding, ' ', PROGRAM_NAME, indicator, right_padding, ' ' ); } else if (name_length <= w) { const int total_left_right_padding = w - name_length; int right_padding = total_left_right_padding / 2; int left_padding = total_left_right_padding - right_padding; n = snprintf(header_str, sizeof(header_str), "%*c" "%s" "%*c", left_padding, ' ', PROGRAM_NAME, right_padding, ' ' ); } else { n = snprintf(header_str, sizeof(header_str), "%*c", w, ' ' ); } int begin_color = render_state.scroll_offset % (w * 2) > w ? CC_COLOR_PAIR_TITLE_HEADER : CC_COLOR_PAIR_TITLE_HEADER2; int end_color = render_state.scroll_offset % (w * 2) < w ? CC_COLOR_PAIR_TITLE_HEADER : CC_COLOR_PAIR_TITLE_HEADER2; int current_color = begin_color; move(y, x); attron(COLOR_PAIR(current_color)); for (int i = 0; i < n; i++) { if (i == render_state.scroll_offset % w) { attroff(COLOR_PAIR(current_color)); current_color = end_color; attron(COLOR_PAIR(current_color)); } addch(header_str[i]); } attroff(COLOR_PAIR(current_color)); } static void cc_render_message_field( const int x, const int y, const int w, const int h ) { for (int j = y; j < h; j++) { move(j, x); for (int i = x; i < w; i++) { if (j == y || j == h - 1) { addch(i == x ? '+' : i == w-1 ? '+' : '-'); } else if (i == x || i == w - 1) { addch('|'); } else { addch(' '); } } } const unsigned int inner_w = w - x - 2; const unsigned int inner_h = h - y - 2; int max_offset = messages.size > inner_h ? messages.size - inner_h : 0; if (render_state.scroll_offset > max_offset) render_state.scroll_offset = max_offset; if (render_state.scroll_offset < 0) render_state.scroll_offset = 0; int last = messages.size - render_state.scroll_offset; if (last < 0) last = 0; int first = last - inner_h; if (first < 0) first = 0; for (int i = first; i < last; i++) { mvprintw(y + 1 + (i - first), x + 1, "%-*.*s", inner_w, inner_w, messages.data[i]); } } static void cc_render_input_field( const int x, const int y, const int w ) { if (input_state.cursor_pos < input_state.view_start) input_state.view_start = input_state.cursor_pos; else if (input_state.cursor_pos >= input_state.view_start + w) input_state.view_start = input_state.cursor_pos - w + 1; move(y, x); addch('+'); for (int i = 0; i < w - 2; i++) addch('-'); addch('+'); move(y + 1, x); addch('|'); for (int i = 0; i < w - 2; i++) addch(' '); addch('|'); move(y + 1, x + 1); int visible_length = input_state.content_length - input_state.view_start; if (visible_length > w - 2) visible_length = w - 2; attron(COLOR_PAIR(CC_COLOR_PAIR_INPUT) | A_BOLD); for (int i = 0; i < visible_length; i++) addch(input_state.buffer[input_state.view_start + i]); attroff(COLOR_PAIR(CC_COLOR_PAIR_INPUT) | A_BOLD); move(y + 2, x); addch('+'); for (int i = 0; i < w - 2; i++) addch('-'); addch('+'); const int cursor_x = input_state.cursor_pos - input_state.view_start; render_state.cursor.y = y + 1; if (cursor_x > w - 3) render_state.cursor.x = x + w - 2; render_state.cursor.x = x + 1 + cursor_x; } static void cc_render_ui( const int x, const int y, const int w, const int h ) { pthread_mutex_lock(&messages.lock); bool do_refresh = false; if (render_state.needs_redraw.title_header) { render_state.needs_redraw.title_header = 0; cc_render_title_header(x, y, w); do_refresh = 1; } if (render_state.needs_redraw.message_field) { render_state.needs_redraw.message_field = 0; cc_render_message_field(x, y + 1, w, h - 3); do_refresh = 1; } if (render_state.needs_redraw.input_field) { render_state.needs_redraw.input_field = 0; cc_render_input_field(x, y + h - 3, w); do_refresh = 1; } if (do_refresh) { move(render_state.cursor.y, render_state.cursor.x); refresh(); } pthread_mutex_unlock(&messages.lock); } static void update_terminal_size() { endwin(); refresh(); clear(); initscr(); render_state.needs_redraw.title_header = 1; render_state.needs_redraw.message_field = 1; render_state.needs_redraw.input_field = 1; } static int handle_input() { timeout(500); const int ch = getch(); if (ch == ERR) { return 0; } if (ch == KEY_RESIZE) { update_terminal_size(); return 0; } return ch; } void *reader_thread(void *data) { char buffer[1078]; while (fgets(buffer, sizeof(buffer), reader) != NULL) { const size_t len = strlen(buffer); if (buffer[len - 1] == '\n') buffer[len - 1] = '\0'; add_new_message(buffer); } return NULL; } int main(int argc, char* argv[]) { setlocale(LC_ALL, ""); initscr(); start_color(); use_default_colors(); init_pair(CC_COLOR_PAIR_TITLE_HEADER, COLOR_BLACK, COLOR_CYAN); init_pair(CC_COLOR_PAIR_TITLE_HEADER2, COLOR_BLACK, COLOR_YELLOW); init_pair(CC_COLOR_PAIR_INPUT, COLOR_GREEN, -1); cbreak(); noecho(); curs_set(1); keypad(stdscr, TRUE); struct sockaddr_in6 server_addr; cc_client_parse_args_and_connect(argc, argv, &server_addr, &server_fd, &reader, &writer); server_str_len = sockaddr2str((struct sockaddr*)&server_addr, server_str, sizeof(server_str)); init_messages(); pthread_t reader_thread_id; pthread_create(&reader_thread_id, NULL, reader_thread, NULL); time_t last_second = 0; while (true) { cc_render_ui(0, 0, COLS, LINES); const int input = handle_input(); if (input_state_handle_input(input)) { if (input_state.content_length > 0) { fprintf(writer, "%s\n", input_state.buffer); fflush(writer); input_state = (struct input_state_t){{0}, MAX_MESSAGE_LENGTH, 0, 0, 0}; render_state.needs_redraw.message_field = 1; render_state.unread = -1; } } const time_t now = time(NULL); if (now > last_second) { last_second = now; render_state.needs_redraw.title_header = 1; } } endwin(); return 0; }