Files
chat/chatclient-cli.c
2026-05-27 23:16:48 +03:00

461 lines
13 KiB
C
Executable File

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <locale.h>
#include <ncurses.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
#include <pthread.h>
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;
}