/* * neetdraw.c: terminal drawing application with multiplayer support * * Copyright (c) 2014 - 2023, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #include "config.h" #include "liberty/liberty.c" #include "termo.h" #include #include #ifndef TIOCGWINSZ #include #endif // ! TIOCGWINSZ #include #include #define PALETTE_WIDTH 9 ///< Width of the palette #define TOP_BAR_CUTOFF 3 ///< Height of the top bar #define BITMAP_BLOCK_SIZE 50 ///< Step for extending bitmap size #define PROTOCOL_VERSION 1 ///< Network protocol version enum { MESSAGE_HELLO, ///< Server/client hello MESSAGE_GET_BITMAP, ///< Request bitmap data MESSAGE_PUT_POINT, ///< Request to place a point MESSAGE_COUNT ///< Total number of messages }; enum network_mode { NETWORK_MODE_STANDALONE, ///< No networking taking place NETWORK_MODE_SERVER, ///< We're the server NETWORK_MODE_CLIENT ///< We're a client }; struct client { LIST_HEADER (struct client) int fd; ///< Client connection ev_io read_watcher; ///< Client readability watcher ev_io write_watcher; ///< Client writability watcher struct msg_reader msg_reader; ///< Client message reader struct write_queue write_queue; ///< Write queue }; #define BITMAP_PIXEL(app, x, y) (app)->bitmap[(y) * (app)->bitmap_w + (x)] struct app_context { termo_t *tk; ///< Termo instance ev_io tty_watcher; ///< TTY input watcher ev_timer tty_timer; ///< TTY timeout timer ev_signal winch_watcher; ///< SIGWINCH watcher enum network_mode mode; ///< Networking mode // Client: int server_fd; ///< Server connection ev_io server_read_watcher; ///< Server readability watcher ev_io server_write_watcher; ///< Server writability watcher struct msg_reader msg_reader; ///< Server message reader struct write_queue write_queue; ///< Server write queue bool no_wait; ///< Don't wait for server confirmations // Server: int listen_fd; ///< Listening FD ev_io listen_watcher; ///< Listening FD watcher struct client *clients; ///< Client connections chtype palette[2 * 9]; ///< Attribute palette uint8_t *bitmap; ///< Canvas data for drawing int bitmap_x; ///< X coord. of left top bitmap corner int bitmap_y; ///< Y coord. of left top bitmap corner size_t bitmap_w; ///< Canvas data width size_t bitmap_h; ///< Canvas data height int center_x; ///< X coordinate at center int center_y; ///< Y coordinate at center // These two are computed from `center_x' and `center_y': int corner_x; ///< X coordinate of LT screen corner int corner_y; ///< Y coordinate of LT screen corner int move_saved_x; ///< Saved X coord. for moving int move_saved_y; ///< Saved Y coord. for moving uint8_t current_color_left; ///< Left mouse button color uint8_t current_color_right; ///< Right mouse button color }; static void remove_client (struct app_context *app, struct client *client); static void on_server_disconnected (struct app_context *app); static void app_init (struct app_context *self) { memset (self, 0, sizeof *self); self->server_fd = -1; self->listen_fd = -1; self->msg_reader = msg_reader_make (); self->write_queue = write_queue_make (); } static void app_free (struct app_context *self) { if (self->tk) termo_destroy (self->tk); while (self->clients) // XXX: we probably shouldn't do this from here remove_client (self, self->clients); free (self->bitmap); msg_reader_free (&self->msg_reader); write_queue_free (&self->write_queue); } // --- Server-client messaging ------------------------------------------------- static bool read_loop (EV_P_ ev_io *watcher, bool (*cb) (EV_P_ ev_io *, const void *, ssize_t)) { char buf[8192]; while (true) { ssize_t n_read = recv (watcher->fd, buf, sizeof buf, 0); if (n_read < 0) { if (errno == EAGAIN) break; if (errno == EINTR) continue; } if (n_read <= 0 || !cb (EV_A_ watcher, buf, n_read)) return false; } return true; } static bool flush_queue (struct write_queue *queue, ev_io *watcher) { struct iovec vec[queue->len], *vec_iter = vec; for (struct write_req *iter = queue->head; iter; iter = iter->next) *vec_iter++ = iter->data; ssize_t written; again: written = writev (watcher->fd, vec, N_ELEMENTS (vec)); if (written < 0) { if (errno == EAGAIN) goto skip; if (errno == EINTR) goto again; return false; } write_queue_processed (queue, written); skip: if (write_queue_is_empty (queue)) ev_io_stop (EV_DEFAULT_ watcher); else ev_io_start (EV_DEFAULT_ watcher); return true; } static struct write_req * flush_writer (struct msg_writer *writer) { struct write_req *req = xcalloc (1, sizeof *req); req->data.iov_base = msg_writer_flush (writer, &req->data.iov_len); return req; } static void flush_writer_to_client (struct msg_writer *writer, struct client *client) { write_queue_add (&client->write_queue, flush_writer (writer)); ev_io_start (EV_DEFAULT_ &client->write_watcher); } static void flush_writer_to_server (struct msg_writer *writer, struct app_context *app) { write_queue_add (&app->write_queue, flush_writer (writer)); ev_io_start (EV_DEFAULT_ &app->server_write_watcher); } static void send_draw_point_response (struct client *client, int x, int y, uint8_t color) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_PUT_POINT); str_pack_i32 (&writer.buf, x); str_pack_i32 (&writer.buf, y); str_pack_u8 (&writer.buf, color); flush_writer_to_client (&writer, client); } static void send_draw_point_request (struct app_context *app, int x, int y, uint8_t color) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_PUT_POINT); str_pack_i32 (&writer.buf, x); str_pack_i32 (&writer.buf, y); str_pack_u8 (&writer.buf, color); flush_writer_to_server (&writer, app); } static void send_hello_request (struct app_context *app) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_HELLO); str_pack_u8 (&writer.buf, PROTOCOL_VERSION); flush_writer_to_server (&writer, app); } static void send_hello_response (struct client *client) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_HELLO); str_pack_u8 (&writer.buf, PROTOCOL_VERSION); flush_writer_to_client (&writer, client); } static void send_get_bitmap_request (struct app_context *app) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_GET_BITMAP); flush_writer_to_server (&writer, app); } static void send_get_bitmap_response (struct client *client, struct app_context *app) { struct msg_writer writer = msg_writer_make (); str_pack_u8 (&writer.buf, MESSAGE_GET_BITMAP); str_pack_i32 (&writer.buf, app->bitmap_x); str_pack_i32 (&writer.buf, app->bitmap_y); str_pack_u64 (&writer.buf, app->bitmap_w); str_pack_u64 (&writer.buf, app->bitmap_h); // Simple RLE compression size_t size = app->bitmap_w * app->bitmap_h; uint8_t last_value = 0, count = 0; for (size_t i = 0; i < size; i++) { uint8_t value = app->bitmap[i]; if ((count && value != last_value) || count == 0xFF) { str_pack_u8 (&writer.buf, count); str_pack_u8 (&writer.buf, last_value); count = 0; } count++; last_value = value; } if (count) { str_pack_u8 (&writer.buf, count); str_pack_u8 (&writer.buf, last_value); } flush_writer_to_client (&writer, client); } // --- Server-client messaging ------------------------------------------------- static void display (const char *format, ...) { va_list ap; mvwhline (stdscr, 0, 0, A_REVERSE, COLS); attron (A_REVERSE); va_start (ap, format); vw_printw (stdscr, format, ap); va_end (ap); attroff (A_REVERSE); refresh (); } static void init_palette (struct app_context *app) { start_color (); // Also does init_pair (0, -1, -1); use_default_colors (); // Duplicate it for convenience. init_pair (9, -1, -1); // Add the basic 8 colors to the default pair. Once normally, once // inverted to workaround VTE's inability to set a bright background. for (int i = 0; i < 8; i++) { init_pair (1 + i, COLOR_WHITE, COLOR_BLACK + i); init_pair (10 + i, COLOR_BLACK + i, COLOR_WHITE); } // Initialize the palette of characters with attributes for (int i = 0; i < PALETTE_WIDTH; i++) { app->palette[i] = ' ' | COLOR_PAIR (i); app->palette[i + 9] = ' ' | COLOR_PAIR (i + 9) | A_REVERSE | A_BOLD; } // This usually creates a solid black or white. app->current_color_left = app->current_color_right = 9; } static void update_canvas_for_screen (struct app_context *app) { app->corner_x = app->center_x - COLS / 2; app->corner_y = app->center_y - (LINES - TOP_BAR_CUTOFF) / 2; } static void redraw (struct app_context *app) { int i; mvwhline (stdscr, 1, 0, A_REVERSE, COLS); mvwhline (stdscr, 2, 0, A_REVERSE, COLS); for (i = 0; i < COLS; i++) { int pair = (float) i / COLS * PALETTE_WIDTH; mvaddch (1, i, app->palette[pair]); mvaddch (2, i, app->palette[pair + PALETTE_WIDTH]); } display ("Choose a color from the palette and draw. " "Press Escape or ^C to quit."); refresh (); } static bool is_in_bitmap_data (struct app_context *app, int x, int y) { return x >= app->bitmap_x && y >= app->bitmap_y && x < app->bitmap_x + (int) app->bitmap_w && y < app->bitmap_y + (int) app->bitmap_h; } static void redraw_canvas (struct app_context *app) { int y = app->corner_y; for (int screen_y = TOP_BAR_CUTOFF; screen_y < LINES; screen_y++, y++) { move (screen_y, 0); int x = app->corner_x; for (int screen_x = 0; screen_x < COLS; screen_x++, x++) { uint8_t color = 0; if (is_in_bitmap_data (app, x, y)) color = BITMAP_PIXEL (app, x - app->bitmap_x, y - app->bitmap_y); addch (app->palette[color]); } } refresh (); } static bool is_visible (struct app_context *app, int x, int y) { return x >= app->corner_x && y >= app->corner_y && x < app->corner_x + COLS && y < app->corner_y + LINES - TOP_BAR_CUTOFF; } static void make_place_for_point (struct app_context *app, int x, int y) { if (is_in_bitmap_data (app, x, y)) return; // Make sure the point has some place to go int new_bitmap_x = app->bitmap_x; int new_bitmap_y = app->bitmap_y; while (new_bitmap_x > x) new_bitmap_x -= BITMAP_BLOCK_SIZE; while (new_bitmap_y > y) new_bitmap_y -= BITMAP_BLOCK_SIZE; int new_bitmap_w = app->bitmap_w + (app->bitmap_x - new_bitmap_x); int new_bitmap_h = app->bitmap_h + (app->bitmap_y - new_bitmap_y); while (new_bitmap_x + new_bitmap_w <= x) new_bitmap_w += BITMAP_BLOCK_SIZE; while (new_bitmap_y + new_bitmap_h <= y) new_bitmap_h += BITMAP_BLOCK_SIZE; uint8_t *new_bitmap = xcalloc (new_bitmap_w * new_bitmap_h, sizeof *new_bitmap); if (app->bitmap) { // Copy data, assuming that the area can only get larger for (size_t data_y = 0; data_y < app->bitmap_h; data_y++) memcpy (new_bitmap + ((data_y + app->bitmap_y - new_bitmap_y) * new_bitmap_w) + (app->bitmap_x - new_bitmap_x), app->bitmap + (data_y * app->bitmap_w), app->bitmap_w * sizeof *new_bitmap); free (app->bitmap); } // Replace the bitmap with the reallocated version app->bitmap_x = new_bitmap_x; app->bitmap_y = new_bitmap_y; app->bitmap_w = new_bitmap_w; app->bitmap_h = new_bitmap_h; app->bitmap = new_bitmap; } static void draw_point_internal (struct app_context *app, int x, int y, uint8_t color) { make_place_for_point (app, x, y); BITMAP_PIXEL (app, x - app->bitmap_x, y - app->bitmap_y) = color; if (is_visible (app, x, y)) { int screen_x = x - app->corner_x; int screen_y = y - app->corner_y + TOP_BAR_CUTOFF; move (screen_y, screen_x); addch (app->palette[color]); refresh (); } } static void draw_point (struct app_context *app, int x, int y, uint8_t color) { if (app->mode == NETWORK_MODE_CLIENT) { send_draw_point_request (app, x, y, color); // We don't usually draw anything immediately in client mode, // instead we wait for confirmation from the server if (!app->no_wait) return; } draw_point_internal (app, x, y, color); // Broadcast clients about the event if (app->mode == NETWORK_MODE_SERVER) for (struct client *iter = app->clients; iter; iter = iter->next) send_draw_point_response (iter, x, y, color); } static void draw_line (struct app_context *app, int x0, int x1, int y0, int y1, uint8_t color) { // Integer version of Bresenham's line drawing algorithm, // loosely based on code from libcaca because screw math int dx = abs (x1 - x0); int dy = abs (y1 - y0); bool steep = dx < dy; if (steep) { // Flip the coordinate system on input int tmp; tmp = x0; x0 = y0; y0 = tmp; tmp = x1; x1 = y1; y1 = tmp; tmp = dx; dx = dy; dy = tmp; } int step_x = x0 > x1 ? -1 : 1; int step_y = y0 > y1 ? -1 : 1; int dpr = dy * 2; int delta = dpr - dx; int dpru = delta - dx; while (dx-- >= 0) { // Unflip the coordinate system on output if (steep) draw_point (app, y0, x0, color); else draw_point (app, x0, y0, color); x0 += step_x; if (delta > 0) { y0 += step_y; delta += dpru; } else delta += dpr; } } // --- Exports ----------------------------------------------------------------- static bool is_data_row_empty (struct app_context *app, int y) { for (size_t x = 0; x < app->bitmap_w; x++) if (app->bitmap[y * app->bitmap_w + x]) return false; return true; } static bool is_data_column_empty (struct app_context *app, int x) { for (size_t y = 0; y < app->bitmap_h; y++) if (app->bitmap[y * app->bitmap_w + x]) return false; return true; } static void find_data_bounding_rect (struct app_context *app, size_t *x, size_t *y, size_t *w, size_t *h) { size_t my_x = 0, my_y = 0; size_t my_w = app->bitmap_w, my_h = app->bitmap_h; size_t i; i = 0; while (i < app->bitmap_h && is_data_row_empty (app, i++)) my_y++; // Special case: the whole canvas is empty if (my_y == my_h) { my_x = my_w; goto end; } i = app->bitmap_h; while (i-- && is_data_row_empty (app, i)) my_h--; i = 0; while (i < app->bitmap_w && is_data_column_empty (app, i++)) my_x++; i = app->bitmap_w; while (i-- && is_data_column_empty (app, i)) my_w--; end: *x = my_x; *y = my_y; *w = my_w - my_x; *h = my_h - my_y; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static const char *g_ansi_table[2 * PALETTE_WIDTH] = { "\033[0m", "\033[0;40m", "\033[0;41m", "\033[0;42m", "\033[0;43m", "\033[0;44m", "\033[0;45m", "\033[0;46m", "\033[0;47m", "\033[0;1;7m", "\033[0;1;7;30m", "\033[0;1;7;31m", "\033[0;1;7;32m", "\033[0;1;7;33m", "\033[0;1;7;34m", "\033[0;1;7;35m", "\033[0;1;7;36m", "\033[0;1;7;37m", }; static const char * color_to_ansi (uint8_t color) { if (color < N_ELEMENTS (g_ansi_table)) return g_ansi_table[color]; return NULL; } static void export_ansi (struct app_context *app) { FILE *fp = fopen ("export-ansi.asc", "wb"); if (!fp) { display ("Error opening file for writing."); beep (); return; } size_t x, y, w, h; find_data_bounding_rect (app, &x, &y, &w, &h); for (size_t row = 0; row < h; row++) { const char *color = NULL; for (size_t column = 0; column < w; column++) { const char *new_color = color_to_ansi (BITMAP_PIXEL (app, x + column, y + row)); if (color != new_color) fputs (new_color, fp); color = new_color; fputc (' ', fp); } // We need to reset the attributes fputs (color_to_ansi (0), fp); fputc ('\n', fp); } fclose (fp); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum { MIRC_NONE = -1, MIRC_WHITE = 0, MIRC_BLACK = 1, MIRC_BLUE = 2, MIRC_GREEN = 3, MIRC_L_RED = 4, MIRC_RED = 5, MIRC_PURPLE = 6, MIRC_ORANGE = 7, MIRC_YELLOW = 8, MIRC_L_GREEN = 9, MIRC_CYAN = 10, MIRC_L_CYAN = 11, MIRC_L_BLUE = 12, MIRC_L_PURPLE = 13, MIRC_GRAY = 14, MIRC_L_GRAY = 15, MIRC_TRANSPARENT = 99 }; static int color_to_mirc (uint8_t color) { static const int table[2 * PALETTE_WIDTH] = { // XXX: not sure what to map the default color pair to; // the mIRC code for reverse colours seems to not be well supported MIRC_TRANSPARENT, MIRC_BLACK, MIRC_RED, MIRC_GREEN, MIRC_YELLOW, MIRC_BLUE, MIRC_PURPLE, MIRC_CYAN, MIRC_L_GRAY, MIRC_BLACK, MIRC_GRAY, MIRC_L_RED, MIRC_L_GREEN, MIRC_YELLOW, MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_L_CYAN, MIRC_WHITE }; if (color >= sizeof table / sizeof table[0]) return MIRC_NONE; return table[color]; } static void export_irc (struct app_context *app) { FILE *fp = fopen ("export-irc.asc", "wb"); if (!fp) { display ("Error opening file for writing."); beep (); return; } size_t x, y, w, h; find_data_bounding_rect (app, &x, &y, &w, &h); // This is tricky and needs to be tested with major IRC clients. Currently // works with: weechat 1.0, xchat 2.8.8, freenode's qwebirc. // We cannot use the same non-space character for transparent and opaque // pixels because many clients don't understand the transparent 99 colour // that is needed for the foreground of transparent pixels. Therefore it // is not possible to display correctly using non-monospace fonts. for (size_t row = 0; row < h; row++) { // qwebirc is retarded and in some cases it reduces spaces, misaligning // the picture. Appending two spaces after the attribute reset and // rendering opaque pixels as something different from a space seems // to prevent that behaviour. int color = MIRC_TRANSPARENT; fprintf (fp, "\x0f "); for (size_t column = 0; column < w; column++) { int new_color = color_to_mirc (BITMAP_PIXEL (app, x + column, y + row)); if (color != new_color) { color = new_color; if (color == MIRC_TRANSPARENT) fprintf (fp, "\x0f"); else fprintf (fp, "\x03%02d,%02d", color, color); } fputc ("# "[color == MIRC_TRANSPARENT], fp); } fputc ('\n', fp); } fclose (fp); } // --- Loading, saving --------------------------------------------------------- static void load (struct app_context *app, const char *filename) { // Client cannot load at all, the server would have send the new bitmap out if (app->mode != NETWORK_MODE_STANDALONE) { display ("Cannot load bitmaps in networked mode."); beep (); return; } FILE *fp = fopen (filename, "rb"); if (!fp) { display ("Error opening file for reading."); beep (); return; } // Some way of loading/saving is better than no way, let's just do our job. // The format neither standardised nor effective but it works for us. // We just eat everything and make sure to not crash. int x, y; size_t w, h; if (fscanf (fp, "%d %d %zu %zu", &x, &y, &w, &h) != 4) goto error; if (w && h > SIZE_MAX / w) goto error; size_t size = w * h; uint8_t *bitmap = calloc (size, sizeof *bitmap); if (!bitmap) goto error; int c; uint8_t pixel = 0; bool have_nibble = false; size_t loaded = 0; while (loaded < size && (c = fgetc (fp)) != EOF) { static const char digits[] = "0123456789abcdef"; const char *value = strchr (digits, c); if (value && c != '\0') { pixel = pixel << 4 | (value - digits); if (have_nibble) bitmap[loaded++] = pixel; have_nibble = !have_nibble; } } free (app->bitmap); app->bitmap = bitmap; app->bitmap_h = h; app->bitmap_x = x; app->bitmap_w = w; app->bitmap_y = y; redraw_canvas (app); error: fclose (fp); } static void save (struct app_context *app, const char *filename) { FILE *fp = fopen (filename, "wb"); if (!fp) { display ("Error opening file for writing."); return; } int x = app->bitmap_x, y = app->bitmap_y; size_t w = app->bitmap_w, h = app->bitmap_h; fprintf (fp, "%d %d %zu %zu\n", x, y, w, h); for (size_t row = 0; row < h; row++) { for (size_t column = 0; column < w; column++) fprintf (fp, "%02x", BITMAP_PIXEL (app, column, row)); fputc ('\n', fp); } fclose (fp); } // --- Event handlers ---------------------------------------------------------- static void move_canvas (struct app_context *app, int x, int y) { app->corner_x += x; app->corner_y += y; app->center_x += x; app->center_y += y; redraw_canvas (app); } static void on_mouse (struct app_context *app, termo_key_t *key) { int screen_y, screen_x, button; termo_mouse_event_t event; termo_interpret_mouse (app->tk, key, &event, &button, &screen_y, &screen_x); if (event != TERMO_MOUSE_PRESS && event != TERMO_MOUSE_DRAG) return; // Middle mouse button, or Ctrl + left mouse button, moves the canvas if (button == 2 || (button == 1 && key->modifiers == TERMO_KEYMOD_CTRL)) { if (event == TERMO_MOUSE_DRAG) move_canvas (app, app->move_saved_x - screen_x, app->move_saved_y - screen_y); app->move_saved_x = screen_x; app->move_saved_y = screen_y; return; } uint8_t *color; if (button == 1) color = &app->current_color_left; else if (button == 3) color = &app->current_color_right; else return; int canvas_x = app->corner_x + screen_x; int canvas_y = app->corner_y + screen_y - TOP_BAR_CUTOFF; if (screen_y >= TOP_BAR_CUTOFF) { if (event == TERMO_MOUSE_DRAG) draw_line (app, app->move_saved_x, canvas_x, app->move_saved_y, canvas_y, *color); else draw_point (app, canvas_x, canvas_y, *color); app->move_saved_x = canvas_x; app->move_saved_y = canvas_y; } else if (screen_y > 0 && event != TERMO_MOUSE_DRAG) { int pair = (float) screen_x / COLS * PALETTE_WIDTH; *color = pair + (screen_y - 1) * PALETTE_WIDTH; } } static bool on_key (struct app_context *app, termo_key_t *key) { if (key->type == TERMO_TYPE_KEYSYM) { if (key->code.sym == TERMO_SYM_ESCAPE) return false; if (key->modifiers) return true; switch (key->code.sym) { case TERMO_SYM_UP: move_canvas (app, 0, -1); break; case TERMO_SYM_DOWN: move_canvas (app, 0, 1); break; case TERMO_SYM_LEFT: move_canvas (app, -1, 0); break; case TERMO_SYM_RIGHT: move_canvas (app, 1, 0); break; default: break; } return true; } if (key->type == TERMO_TYPE_KEY) { if ((key->modifiers & TERMO_KEYMOD_CTRL) && (key->code.codepoint == 'C' || key->code.codepoint == 'c')) return false; if (key->modifiers) return true; if (key->code.codepoint == 'l') load (app, "drawing.bin"); if (key->code.codepoint == 's') save (app, "drawing.bin"); if (key->code.codepoint == 'e') export_ansi (app); if (key->code.codepoint == 'E') export_irc (app); return true; } if (key->type == TERMO_TYPE_MOUSE) on_mouse (app, key); return true; } static void on_winch (EV_P_ ev_signal *handle, int revents) { struct app_context *app = ev_userdata (loop); (void) handle; (void) revents; #if defined HAVE_RESIZETERM && defined TIOCGWINSZ struct winsize size; if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) { char *row = getenv ("LINES"); char *col = getenv ("COLUMNS"); unsigned long tmp; resizeterm ( (row && xstrtoul (&tmp, row, 10)) ? (int) tmp : size.ws_row, (col && xstrtoul (&tmp, col, 10)) ? (int) tmp : size.ws_col); } #else // ! HAVE_RESIZETERM || ! TIOCGWINSZ endwin (); refresh (); #endif // ! HAVE_RESIZETERM || ! TIOCGWINSZ update_canvas_for_screen (app); redraw (app); redraw_canvas (app); } static void on_key_timer (EV_P_ ev_timer *handle, int revents) { struct app_context *app = ev_userdata (loop); (void) handle; (void) revents; termo_key_t key; if (termo_getkey_force (app->tk, &key) == TERMO_RES_KEY) if (!on_key (app, &key)) ev_break (EV_A_ EVBREAK_ONE); } static void on_tty_readable (EV_P_ ev_io *handle, int revents) { // Ignoring and hoping for the best (void) handle; (void) revents; struct app_context *app = ev_userdata (loop); ev_timer_stop (EV_A_ &app->tty_timer); termo_advisereadable (app->tk); termo_key_t key; termo_result_t ret; while ((ret = termo_getkey (app->tk, &key)) == TERMO_RES_KEY) if (!on_key (app, &key)) ev_break (EV_A_ EVBREAK_ONE); if (ret == TERMO_RES_AGAIN) ev_timer_start (EV_A_ &app->tty_timer); } // --- Client-specific stuff --------------------------------------------------- typedef bool (*server_handler_fn) (struct app_context *, struct msg_unpacker *); static void on_server_disconnected (struct app_context *app) { write_queue_free (&app->write_queue); app->write_queue = write_queue_make (); ev_io_stop (EV_DEFAULT_ &app->server_read_watcher); ev_io_stop (EV_DEFAULT_ &app->server_write_watcher); xclose (app->server_fd); app->server_fd = -1; display ("Disconnected!"); beep (); // Beep beep! Made a boo-boo. // Let the user save the picture at least. // Also prevents us from trying to use the dead server handle. app->mode = NETWORK_MODE_STANDALONE; } static bool on_server_hello (struct app_context *app, struct msg_unpacker *unpacker) { (void) app; uint8_t version; if (!msg_unpacker_u8 (unpacker, &version)) return false; // Not enough data if (version != PROTOCOL_VERSION) return false; // Incompatible version return true; } static bool on_server_get_bitmap (struct app_context *app, struct msg_unpacker *unpacker) { int32_t x, y; uint64_t w, h; if (!msg_unpacker_i32 (unpacker, &x) || !msg_unpacker_i32 (unpacker, &y) || !msg_unpacker_u64 (unpacker, &w) || !msg_unpacker_u64 (unpacker, &h)) return false; // Not enough data size_t size = w * h; if ((h && w > SIZE_MAX / h) || w > SIZE_MAX || h > SIZE_MAX) return false; // The server is flooding us uint8_t *bitmap = xcalloc (size, sizeof *app->bitmap); // RLE decompression size_t i = 0; uint8_t len, value; while (msg_unpacker_u8 (unpacker, &len) && msg_unpacker_u8 (unpacker, &value)) { // Don't allow overflow if (i + len > size || i + len < i) break; for (size_t x = 0; x < len; x++) bitmap[i++] = value; } free (app->bitmap); app->bitmap = bitmap; app->bitmap_x = x; app->bitmap_y = y; app->bitmap_w = w; app->bitmap_h = h; redraw_canvas (app); return true; } static bool on_server_put_point (struct app_context *app, struct msg_unpacker *unpacker) { int32_t x, y; uint8_t color; if (!msg_unpacker_i32 (unpacker, &x) || !msg_unpacker_i32 (unpacker, &y) || !msg_unpacker_u8 (unpacker, &color)) return false; // Not enough data // Either a confirmation of our own request, or an event notification; // let's just put the pixel in place without further ado draw_point_internal (app, x, y, color); return true; } static bool on_server_data (EV_P_ ev_io *watcher, const void *buf, ssize_t n_read) { struct app_context *app = ev_userdata (loop); (void) watcher; msg_reader_feed (&app->msg_reader, buf, n_read); static const server_handler_fn handlers[MESSAGE_COUNT] = { [MESSAGE_HELLO] = on_server_hello, [MESSAGE_GET_BITMAP] = on_server_get_bitmap, [MESSAGE_PUT_POINT] = on_server_put_point, }; void *msg; size_t len; while ((msg = msg_reader_get (&app->msg_reader, &len))) { struct msg_unpacker unpacker = msg_unpacker_make (msg, len); uint8_t type; if (!msg_unpacker_u8 (&unpacker, &type) || type >= MESSAGE_COUNT) return false; // Unknown message server_handler_fn handler = handlers[type]; if (!handler) return false; // Unknown message if (!handler (app, &unpacker)) return false; // Invalid message if (msg_unpacker_get_available (&unpacker) > 0) return false; // Overlong message } return true; } static void on_server_ready (EV_P_ ev_io *watcher, int revents) { struct app_context *app = ev_userdata (loop); if (revents & EV_READ) if (!read_loop (EV_A_ watcher, on_server_data)) goto error; if (revents & EV_WRITE) if (!flush_queue (&app->write_queue, watcher)) goto error; return; error: on_server_disconnected (app); } // --- Server-specific stuff --------------------------------------------------- typedef bool (*client_handler_fn) (struct app_context *, struct client *, struct msg_unpacker *); static void remove_client (struct app_context *app, struct client *client) { ev_io_stop (EV_DEFAULT_ &client->read_watcher); ev_io_stop (EV_DEFAULT_ &client->write_watcher); xclose (client->fd); msg_reader_free (&client->msg_reader); write_queue_free (&client->write_queue); LIST_UNLINK (app->clients, client); free (client); } static bool on_client_hello (struct app_context *app, struct client *client, struct msg_unpacker *unpacker) { (void) app; uint8_t version; if (!msg_unpacker_u8 (unpacker, &version) || version != PROTOCOL_VERSION) // Nope, I don't like you return false; send_hello_response (client); return true; } static bool on_client_get_bitmap (struct app_context *app, struct client *client, struct msg_unpacker *unpacker) { (void) unpacker; send_get_bitmap_response (client, app); return true; } static bool on_client_put_point (struct app_context *app, struct client *client, struct msg_unpacker *unpacker) { (void) client; int32_t x, y; uint8_t color; if (!msg_unpacker_i32 (unpacker, &x) || !msg_unpacker_i32 (unpacker, &y) || !msg_unpacker_u8 (unpacker, &color)) return false; // The function takes care of broadcasting to all the other clients, // as well as back to the original sender draw_point (app, x, y, color); return true; } static bool on_client_data (EV_P_ ev_io *watcher, const void *buf, ssize_t n_read) { struct app_context *app = ev_userdata (loop); struct client *client = watcher->data; msg_reader_feed (&client->msg_reader, buf, n_read); static const client_handler_fn handlers[MESSAGE_COUNT] = { [MESSAGE_HELLO] = on_client_hello, [MESSAGE_GET_BITMAP] = on_client_get_bitmap, [MESSAGE_PUT_POINT] = on_client_put_point, }; void *msg; size_t len; while ((msg = msg_reader_get (&client->msg_reader, &len))) { struct msg_unpacker unpacker = msg_unpacker_make (msg, len); uint8_t type; if (!msg_unpacker_u8 (&unpacker, &type)) return false; // Invalid message if (type >= MESSAGE_COUNT) return false; // Unknown message client_handler_fn handler = handlers[type]; if (!handler) return false; // Unknown message if (!handler (app, client, &unpacker)) return false; // Invalid message if (msg_unpacker_get_available (&unpacker) > 0) return false; // Overlong message data } return true; } static void on_client_ready (EV_P_ ev_io *watcher, int revents) { struct app_context *app = ev_userdata (loop); struct client *client = watcher->data; if (revents & EV_READ) if (!read_loop (EV_A_ watcher, on_client_data)) goto error; if (revents & EV_WRITE) if (!flush_queue (&client->write_queue, watcher)) goto error; return; error: remove_client (app, client); } static void on_new_client (EV_P_ ev_io *watcher, int revents) { struct app_context *app = ev_userdata (loop); (void) revents; while (true) { int sock_fd = accept (watcher->fd, NULL, NULL); if (sock_fd == -1) { if (errno == EAGAIN) break; if (errno == EINTR || errno == ECONNABORTED) continue; // Stop accepting connections to prevent busy looping // TODO: indicate the error to the user ev_io_stop (EV_A_ watcher); break; } struct client *client = xcalloc (1, sizeof *client); client->fd = sock_fd; client->msg_reader = msg_reader_make (); client->write_queue = write_queue_make (); set_blocking (sock_fd, false); ev_io_init (&client->read_watcher, on_client_ready, sock_fd, EV_READ); ev_io_init (&client->write_watcher, on_client_ready, sock_fd, EV_WRITE); client->read_watcher.data = client; client->write_watcher.data = client; // We're only interested in reading as the write queue is empty now ev_io_start (EV_A_ &client->read_watcher); LIST_PREPEND (app->clients, client); } } // --- Program startup --------------------------------------------------------- struct app_options { struct addrinfo *client_address; ///< Address to connect to struct addrinfo *server_address; ///< Address to listen at bool no_wait; ///< Don't wait for server confirmations const char *filename; ///< A filename to preload }; static void app_options_init (struct app_options *self) { memset (self, 0, sizeof *self); } static void app_options_free (struct app_options *self) { if (self->client_address) freeaddrinfo (self->client_address); if (self->server_address) freeaddrinfo (self->server_address); } static struct addrinfo * parse_address (const char *address, int flags) { char address_copy[strlen (address) + 1]; strcpy (address_copy, address); char *colon = strrchr (address_copy, ':'); if (!colon) { print_error ("no port number specified in `%s'", address); return false; } char *host = address_copy, *service = colon + 1; if (host == colon) host = NULL; else if (host < colon && *host == '[' && colon[-1] == ']') { // Remove IPv6 RFC 2732-style [] brackets from the host, if present. // This also makes it possible to take the usage string literally. :)) host++; colon[-1] = '\0'; } else *colon = '\0'; struct addrinfo *result, hints = { .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, .ai_flags = flags, }; int err = getaddrinfo (host, service, &hints, &result); if (err) { print_error ("cannot resolve `%s', port `%s': %s", host, service, gai_strerror (err)); return false; } return result; } static void parse_program_arguments (struct app_options *options, int argc, char **argv) { static const struct opt opts[] = { { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 's', "server", "[ADDRESS]:PORT", 0, "start a server" }, { 'c', "client", "[ADDRESS]:PORT", 0, "connect to a server" }, { 'n', "no-wait", NULL, OPT_LONG_ONLY, "don't wait for server confirmations" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh = opt_handler_make (argc, argv, opts, "[drawing.bin]", "Terminal drawing application with multiplayer support"); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 's': if (options->server_address) exit_fatal ("cannot specify multiple listening addresses"); if (!(options->server_address = parse_address (optarg, AI_PASSIVE))) exit (EXIT_FAILURE); break; case 'c': if (options->client_address) exit_fatal ("cannot specify multiple addresses to connect to"); if (!(options->client_address = parse_address (optarg, 0))) exit (EXIT_FAILURE); break; case 'n': options->no_wait = true; break; default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } if (options->client_address && options->server_address) exit_fatal ("cannot be both a server and a client"); argc -= optind; argv += optind; options->filename = argv[0]; if (argc > 1) { opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } opt_handler_free (&oh); } static void initialize_client (struct app_context *app, struct addrinfo *address) { app->mode = NETWORK_MODE_CLIENT; int sock_fd, err; for (; address; address = address->ai_next) { sock_fd = socket (address->ai_family, address->ai_socktype, address->ai_protocol); if (sock_fd == -1) continue; char host_buf[NI_MAXHOST], serv_buf[NI_MAXSERV]; err = getnameinfo (address->ai_addr, address->ai_addrlen, host_buf, sizeof host_buf, serv_buf, sizeof serv_buf, NI_NUMERICHOST | NI_NUMERICSERV); if (err) { print_error ("%s: %s", "getnameinfo", gai_strerror (err)); print_status ("connecting..."); } else { char *x = format_host_port_pair (host_buf, serv_buf); print_status ("connecting to %s...", x); free (x); } if (!connect (sock_fd, address->ai_addr, address->ai_addrlen)) break; xclose (sock_fd); } if (!address) exit_fatal ("connection failed"); int yes = 1; (void) setsockopt (sock_fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes); set_blocking (sock_fd, false); app->server_fd = sock_fd; ev_io_init (&app->server_read_watcher, on_server_ready, sock_fd, EV_READ); ev_io_init (&app->server_write_watcher, on_server_ready, sock_fd, EV_WRITE); // We're only interested in reading as the write queue is empty now ev_io_start (EV_DEFAULT_ &app->server_read_watcher); send_hello_request (app); send_get_bitmap_request (app); } static void initialize_server (struct app_context *app, struct addrinfo *address) { app->mode = NETWORK_MODE_SERVER; int sock_fd = socket (address->ai_family, address->ai_socktype, address->ai_protocol); if (sock_fd == -1) goto fail_socket; if (bind (sock_fd, address->ai_addr, address->ai_addrlen) || listen (sock_fd, 10)) goto fail; int yes = 1; (void) setsockopt (sock_fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes); (void) setsockopt (sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); set_blocking (sock_fd, false); app->listen_fd = sock_fd; ev_io_init (&app->listen_watcher, on_new_client, sock_fd, EV_READ); ev_io_start (EV_DEFAULT_ &app->listen_watcher); return; fail: xclose (sock_fd); fail_socket: exit_fatal ("%s: %s", "initialization failed", strerror (errno)); } int main (int argc, char *argv[]) { TERMO_CHECK_VERSION; setlocale (LC_CTYPE, ""); struct app_context app; app_init (&app); struct ev_loop *loop = EV_DEFAULT; if (!loop) exit_fatal ("cannot initialize libev"); struct app_options options; app_options_init (&options); parse_program_arguments (&options, argc, argv); if (options.client_address) initialize_client (&app, options.client_address); else if (options.server_address) initialize_server (&app, options.server_address); else app.mode = NETWORK_MODE_STANDALONE; app.no_wait = options.no_wait; app_options_free (&options); termo_t *tk = termo_new (STDIN_FILENO, NULL, 0); if (!tk) exit_fatal ("cannot allocate termo instance"); app.tk = tk; termo_set_mouse_tracking_mode (tk, TERMO_MOUSE_TRACKING_DRAG); // Set up curses for our drawing needs if (!initscr () || nonl () == ERR || curs_set (0) == ERR) exit_fatal ("cannot initialize curses"); ev_set_userdata (loop, &app); ev_signal_init (&app.winch_watcher, on_winch, SIGWINCH); ev_signal_start (EV_DEFAULT_ &app.winch_watcher); ev_io_init (&app.tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ); ev_io_start (EV_DEFAULT_ &app.tty_watcher); ev_timer_init (&app.tty_timer, on_key_timer, termo_get_waittime (app.tk) / 1000., 0); init_palette (&app); update_canvas_for_screen (&app); redraw (&app); redraw_canvas (&app); if (options.filename) load (&app, options.filename); ev_run (loop, 0); endwin (); app_free (&app); ev_loop_destroy (loop); return 0; }