470 lines
14 KiB
C
Executable File
470 lines
14 KiB
C
Executable File
#include "shared.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
#include <locale.h>
|
|
|
|
#include <ncurses.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
|
|
#include <pthread.h>
|
|
|
|
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;
|
|
}
|