From 023d69bdbfa41bdfbd624b58fd2f30dd3cf1a625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Tue, 16 Nov 2021 11:50:48 +0100 Subject: Rename the project The old name had its context, but now it's mostly just confusing. Make it netdraw with an extra 'e'. --- .gitignore | 12 +- CMakeLists.txt | 2 +- README.adoc | 24 +- autistdraw.c | 1607 -------------------------------------------------------- autistdraw.png | Bin 1249 -> 0 bytes config.h.in | 2 +- neetdraw.c | 1607 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ neetdraw.png | Bin 0 -> 1249 bytes 8 files changed, 1627 insertions(+), 1627 deletions(-) delete mode 100644 autistdraw.c delete mode 100644 autistdraw.png create mode 100644 neetdraw.c create mode 100644 neetdraw.png diff --git a/.gitignore b/.gitignore index 109ecfd..b819ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ # Qt Creator files /CMakeLists.txt.user* -/autistdraw.config -/autistdraw.files -/autistdraw.creator* -/autistdraw.includes -/autistdraw.cflags -/autistdraw.cxxflags +/neetdraw.config +/neetdraw.files +/neetdraw.creator* +/neetdraw.includes +/neetdraw.cflags +/neetdraw.cxxflags diff --git a/CMakeLists.txt b/CMakeLists.txt index bcdf70f..c502b4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required (VERSION 3.0) -project (autistdraw VERSION 0.1.0 LANGUAGES C) +project (neetdraw VERSION 0.1.0 LANGUAGES C) # Moar warnings if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) diff --git a/README.adoc b/README.adoc index f438ab3..a30ffcf 100644 --- a/README.adoc +++ b/README.adoc @@ -1,9 +1,9 @@ -autistdraw -========== +neetdraw +======== -'autistdraw' is a terminal drawing application with multiplayer support. +'neetdraw' is a terminal drawing application with multiplayer support. -image::autistdraw.png[align="center"] +image::neetdraw.png[align="center"] Packages -------- @@ -15,9 +15,9 @@ Building Build dependencies: CMake, pkg-config, liberty (included), termo (included) + Runtime dependencies: ncursesw, libev - $ git clone --recursive https://git.janouch.name/p/autistdraw.git - $ mkdir autistdraw/build - $ cd autistdraw/build + $ git clone --recursive https://git.janouch.name/p/neetdraw.git + $ mkdir neetdraw/build + $ cd neetdraw/build $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug $ make @@ -28,21 +28,21 @@ To install the application, you can do either the usual: Or you can try telling CMake to make a package for you. For Debian it is: $ cpack -G DEB - # dpkg -i autistdraw-*.deb + # dpkg -i neetdraw-*.deb Usage ----- For standalone mode you can run the program without arguments: - $ autistdraw + $ neetdraw To run as a server for other clients to connect to and draw simultaneously: - $ autistdraw -s :1234 + $ neetdraw -s :1234 To connect to a running server, run: - $ autistdraw -c localhost:1234 + $ neetdraw -c localhost:1234 Once you have the program running, simply select a colour you like from the palette and draw by pressing and dragging the mouse. Use the middle mouse @@ -66,7 +66,7 @@ you flood the communication channel with the terminal. Contributing and Support ------------------------ -Use https://git.janouch.name/p/autistdraw to report any bugs, request features, +Use https://git.janouch.name/p/neetdraw to report any bugs, request features, or submit pull requests. `git send-email` is tolerated. If you want to discuss the project, feel free to join me at ircs://irc.janouch.name, channel #dev. diff --git a/autistdraw.c b/autistdraw.c deleted file mode 100644 index 472f630..0000000 --- a/autistdraw.c +++ /dev/null @@ -1,1607 +0,0 @@ -/* - * autistdraw.c: terminal drawing for NEET autists^Wartists - * - * Copyright (c) 2014, 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) -{ - // 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 ("drawing.bin", "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) -{ - FILE *fp = fopen ("drawing.bin", "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); - if (key->code.codepoint == 's') save (app); - 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 -}; - -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, - NULL, "Terminal drawing for NEET autists^Wartists"); - - 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; - - if (argc) - { - 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); - - ev_run (loop, 0); - endwin (); - - app_free (&app); - ev_loop_destroy (loop); - return 0; -} diff --git a/autistdraw.png b/autistdraw.png deleted file mode 100644 index cdc7cbf..0000000 Binary files a/autistdraw.png and /dev/null differ diff --git a/config.h.in b/config.h.in index 0fd8a30..e8af66f 100644 --- a/config.h.in +++ b/config.h.in @@ -1,7 +1,7 @@ #ifndef CONFIG_H #define CONFIG_H -#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}" +#define PROGRAM_NAME "${PROJECT_NAME}" #define PROGRAM_VERSION "${PROJECT_VERSION}" #cmakedefine HAVE_RESIZETERM diff --git a/neetdraw.c b/neetdraw.c new file mode 100644 index 0000000..ab80477 --- /dev/null +++ b/neetdraw.c @@ -0,0 +1,1607 @@ +/* + * neetdraw.c: terminal drawing application with multiplayer support + * + * Copyright (c) 2014, 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) +{ + // 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 ("drawing.bin", "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) +{ + FILE *fp = fopen ("drawing.bin", "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); + if (key->code.codepoint == 's') save (app); + 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 +}; + +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, + NULL, "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; + + if (argc) + { + 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); + + ev_run (loop, 0); + endwin (); + + app_free (&app); + ev_loop_destroy (loop); + return 0; +} diff --git a/neetdraw.png b/neetdraw.png new file mode 100644 index 0000000..cdc7cbf Binary files /dev/null and b/neetdraw.png differ -- cgit v1.2.3