From 023d69bdbfa41bdfbd624b58fd2f30dd3cf1a625 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
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-70-g09d2