#include #include #include #include #include #include #include #include #include #include #include #include #include struct { bool title_header; bool message_field; bool input_field; struct { int x, y; } cursor; int scroll_offset; int unread; } render_state = {0}; struct input_state_t { char buffer[1024]; 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}, sizeof(input_state.buffer) - 1, 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_line(const int ch) { render_state.input_field = 0; 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.message_field = false; render_state.title_header = false; 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.message_field = false; render_state.title_header = false; return 0; } if (ch == KEY_UP) { render_state.scroll_offset++; render_state.message_field = false; render_state.title_header = false; return 0; } if (ch == KEY_DOWN) { if (render_state.scroll_offset > 0) { render_state.scroll_offset--; render_state.message_field = false; render_state.title_header = false; } 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); pthread_mutex_unlock(&messages.lock); if (render_state.scroll_offset > 0) { render_state.scroll_offset++; render_state.unread++; render_state.title_header = 0; } render_state.message_field = 0; } #define PRINT_SPACES(n) printw("%*s", (n), "") #define PROGRAM_NAME "chatclient-cli" #define CC_COLOR_PAIR_TITLE_HEADER 1 #define CC_COLOR_PAIR_INPUT 2 static void cc_render_title_header( const int x, const int y, const int w ) { char system_time[20]; const int time_length = strftime(system_time, 20, "%Y-%m-%d %H:%M:%S", localtime(&(time_t){time(NULL)})); char indicator[32] = ""; int indicator_length = 0; if (render_state.unread > 0) { indicator_length = snprintf(indicator, sizeof(indicator), " [%d unread]", render_state.unread); } move(y, x); attron(COLOR_PAIR(CC_COLOR_PAIR_TITLE_HEADER)); const int name_length = sizeof(PROGRAM_NAME) - 1; const int total_length = name_length + time_length + indicator_length; if (total_length + 2 > w) { const int padding = (w - name_length - indicator_length) / 2; PRINT_SPACES(padding); printw("%-*s", w - padding, PROGRAM_NAME); printw("%s", indicator); } else { const int left_right_padding = 2; const int padding = w - left_right_padding * 2 - total_length; PRINT_SPACES(left_right_padding); printw("%s", PROGRAM_NAME); printw("%s", indicator); PRINT_SPACES(padding); printw("%s", system_time); PRINT_SPACES(left_right_padding); } attroff(COLOR_PAIR(CC_COLOR_PAIR_TITLE_HEADER)); } 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; pthread_mutex_lock(&messages.lock); const 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; const int last = messages.size - render_state.scroll_offset; const int first = last - inner_h > 0 ? last - inner_h : 0; for (int i = first; i < last; i++) { mvprintw(y + 1 + (i - first), x + 1, "%-*.*s", inner_w, inner_w, messages.data[i]); } pthread_mutex_unlock(&messages.lock); } 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 ) { bool do_refresh = false; if (!render_state.title_header) { render_state.title_header = true; cc_render_title_header(x, y, w); do_refresh = true; } if (!render_state.message_field) { render_state.message_field = true; cc_render_message_field(x, y + 1, w, h - 3); do_refresh = true; } if (!render_state.input_field) { render_state.input_field = true; cc_render_input_field(x, y + h - 3, w); do_refresh = true; } if (do_refresh) { move(render_state.cursor.y, render_state.cursor.x); refresh(); } } static void update_terminal_size() { endwin(); refresh(); clear(); initscr(); render_state.title_header = false; render_state.message_field = false; render_state.input_field = false; } 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; } #define exit_error(...) do { \ fprintf(stderr, __VA_ARGS__); \ endwin(); exit(EXIT_FAILURE); \ } while (0) #define exit_perror(s) do { \ perror(s); \ endwin(); exit(EXIT_FAILURE); \ } while (0) static int connect_to_server(struct in6_addr addr, int port) { struct sockaddr_in6 server_addr; server_fd = socket(AF_INET6, SOCK_STREAM, 0); if (server_fd < 0) exit_perror("Socket creation failed"); const int server_fd2 = dup(server_fd); if (server_fd2 < 0) exit_perror("dup"); server_addr.sin6_family = AF_INET6; server_addr.sin6_addr = addr; server_addr.sin6_port = htons(port); int opt = 0; if (setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)) < 0) { // i guess so bro } if (connect(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) exit_perror("connect"); if ((reader = fdopen(server_fd, "r")) == NULL) exit_perror("fdopen reader"); if ((writer = fdopen(server_fd2, "w")) == NULL) exit_perror("fdopen writer"); return 0; } 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; } static int resolve_domain_to_ipv6(const char *hostname, struct in6_addr *server_addr) { struct addrinfo hints, *res; int found = 0; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(hostname, NULL, &hints, &res) != 0) return 0; for (struct addrinfo *p = res; p != NULL; p = p->ai_next) { if (p->ai_family == AF_INET6) { struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; memcpy(server_addr, &ipv6->sin6_addr, sizeof(struct in6_addr)); found = 1; break; } if (p->ai_family == AF_INET && !found) { struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; memset(server_addr, 0, sizeof(struct in6_addr)); server_addr->s6_addr[10] = 0xff; server_addr->s6_addr[11] = 0xff; memcpy(&server_addr->s6_addr[12], &(ipv4->sin_addr), 4); found = 1; } } freeaddrinfo(res); return found; } int main(int argc, char* argv[]) { struct in6_addr server_addr = in6addr_any; int port = 5000; if (argc >= 2) { if (inet_pton(AF_INET6, argv[1], &server_addr) != 1) { struct in_addr v4; if (inet_pton(AF_INET, argv[1], &v4) != 1) { if (!resolve_domain_to_ipv6(argv[1], &server_addr)) { exit_error("invalid address or domain: %s\n", argv[1]); } } else { memset(&server_addr, 0, sizeof(server_addr)); server_addr.s6_addr[10] = 0xff; server_addr.s6_addr[11] = 0xff; memcpy(&server_addr.s6_addr[12], &v4, 4); } } } if (argc >= 3) { port = atoi(argv[2]); if (port <= 0) exit_error("unknown argument: %s\n", argv[2]); } 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_INPUT, COLOR_GREEN, -1); cbreak(); noecho(); curs_set(1); keypad(stdscr, TRUE); init_messages(); connect_to_server(server_addr, port); 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_line(input)) { if (input_state.content_length > 0) { fprintf(writer, "%s\n", input_state.buffer); fflush(writer); input_state = (struct input_state_t){{0}, sizeof(input_state.buffer) - 1, 0, 0, 0}; render_state.message_field = false; render_state.unread = 0; } } // Redraw title every second const time_t now = time(NULL); if (now > last_second) { last_second = now; render_state.title_header = false; } } endwin(); return 0; }