neetdraw/neetdraw.c

1613 lines
39 KiB
C

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