/*
* autistdraw.c: terminal drawing for NEET autists
*
* Copyright (c) 2014, Přemysl Janouch
* All rights reserved.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* 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
#include
#include
#include
#include
#include
#include
#include
#include
#include "termo.h"
#include "config.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
typedef struct app_context app_context_t;
struct app_context
{
termo_t *tk; ///< Termo instance
uv_tty_t tty; ///< TTY
uv_poll_t tty_watcher; ///< TTY input watcher
uv_timer_t tty_timer; ///< TTY timeout timer
uv_signal_t winch_watcher; ///< SIGWINCH watcher
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
app_init (app_context_t *self)
{
memset (self, 0, sizeof *self);
}
static void
app_free (app_context_t *self)
{
if (self->tk)
termo_destroy (self->tk);
free (self->bitmap);
}
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 (app_context_t *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 (app_context_t *app)
{
app->corner_x = app->center_x - COLS / 2;
app->corner_y = app->center_y - (LINES - TOP_BAR_CUTOFF) / 2;
}
static void
redraw (app_context_t *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 (app_context_t *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 (app_context_t *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;
if (!is_in_bitmap_data (app, x, y))
color = 0;
else
{
int data_x = x - app->bitmap_x;
int data_y = y - app->bitmap_y;
color = app->bitmap[data_y * app->bitmap_w + data_x];
}
addch (app->palette[color]);
}
}
refresh ();
}
static bool
is_visible (app_context_t *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 (app_context_t *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 = calloc (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 (app_context_t *app, int x, int y, uint8_t color)
{
make_place_for_point (app, x, y);
int data_x = x - app->bitmap_x;
int data_y = y - app->bitmap_y;
app->bitmap[data_y * app->bitmap_w + data_x] = 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 ();
}
}
// --- Exports -----------------------------------------------------------------
static bool
is_data_row_empty (app_context_t *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 (app_context_t *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 (app_context_t *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 *
color_to_ansi (uint8_t color)
{
static const char *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",
};
if (color > sizeof table / sizeof table[0])
return NULL;
return table[color];
}
static void
export_ansi (app_context_t *app)
{
FILE *fp = fopen ("export-ansi.asc", "wb");
if (!fp)
{
display ("Error opening file for writing.");
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 (app->bitmap[
(y + row) * app->bitmap_w + (x + column)]);
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 (app_context_t *app)
{
FILE *fp = fopen ("export-irc.asc", "wb");
if (!fp)
{
display ("Error opening file for writing.");
return;
}
size_t x, y, w, h;
find_data_bounding_rect (app, &x, &y, &w, &h);
for (size_t row = 0; row < h; row++)
{
int color = MIRC_NONE;
for (size_t column = 0; column < w; column++)
{
int new_color = color_to_mirc (app->bitmap[
(y + row) * app->bitmap_w + (x + column)]);
if (color != new_color)
fprintf (fp, "\x03%02d,%02d", new_color, new_color);
color = new_color;
fputc ('_', fp);
}
fputc ('\n', fp);
}
fclose (fp);
}
// -----------------------------------------------------------------------------
static bool
on_key (app_context_t *app, termo_key_t *key)
{
if (key->type == TERMO_TYPE_KEYSYM && key->code.sym == TERMO_SYM_ESCAPE)
return false;
if (key->type == TERMO_TYPE_KEY
&& (key->modifiers & TERMO_KEYMOD_CTRL)
&& (key->code.codepoint == 'C' || key->code.codepoint == 'c'))
return false;
if (key->type == TERMO_TYPE_KEY && key->code.codepoint == 'e')
{
export_ansi (app);
return true;
}
if (key->type == TERMO_TYPE_KEY && key->code.codepoint == 'E')
{
export_irc (app);
return true;
}
if (key->type != TERMO_TYPE_MOUSE)
return true;
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 true;
if (button == 2)
{
if (event == TERMO_MOUSE_DRAG)
{
app->corner_x += app->move_saved_x - screen_x;
app->corner_y += app->move_saved_y - screen_y;
app->center_x += app->move_saved_x - screen_x;
app->center_y += app->move_saved_y - screen_y;
redraw_canvas (app);
}
app->move_saved_x = screen_x;
app->move_saved_y = screen_y;
return true;
}
uint8_t *color;
if (button == 1)
color = &app->current_color_left;
else if (button == 3)
color = &app->current_color_right;
else
return true;
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)
draw_point (app, canvas_x, canvas_y, *color);
else if (screen_y > 0 && event != TERMO_MOUSE_DRAG)
{
int pair = (float) screen_x / COLS * PALETTE_WIDTH;
*color = pair + (screen_y - 1) * PALETTE_WIDTH;
}
return true;
}
static bool
xstrtoul (unsigned long *out, const char *s, int base)
{
char *end;
errno = 0;
*out = strtoul (s, &end, base);
return errno == 0 && !*end && end != s;
}
static void
on_winch (uv_signal_t *handle, int signum)
{
app_context_t *app = handle->loop->data;
(void) signum;
#ifdef HAVE_RESIZETERM
int w, h;
if (!uv_tty_get_winsize (&app->tty, &w, &h))
{
char *row = getenv ("LINES");
char *col = getenv ("COLUMNS");
unsigned long tmp;
resizeterm (
(row && xstrtoul (&tmp, row, 10)) ? (int) tmp : h,
(col && xstrtoul (&tmp, col, 10)) ? (int) tmp : w);
}
#else // ! HAVE_RESIZETERM
endwin ();
refresh ();
#endif // ! HAVE_RESIZETERM
update_canvas_for_screen (app);
redraw (app);
redraw_canvas (app);
}
static void
on_key_timer (uv_timer_t *handle)
{
app_context_t *app = handle->loop->data;
termo_key_t key;
if (termo_getkey_force (app->tk, &key) == TERMO_RES_KEY)
if (!on_key (app, &key))
uv_stop (handle->loop);
}
static void
on_tty_readable (uv_poll_t *handle, int status, int events)
{
// Ignoring and hoping for the best
(void) status;
(void) events;
app_context_t *app = handle->loop->data;
uv_timer_stop (&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))
uv_stop (handle->loop);
if (ret == TERMO_RES_AGAIN)
uv_timer_start (&app->tty_timer,
on_key_timer, termo_get_waittime (app->tk), 0);
}
int
main (int argc, char *argv[])
{
(void) argc;
(void) argv;
TERMO_CHECK_VERSION;
setlocale (LC_CTYPE, "");
termo_t *tk = termo_new (STDIN_FILENO, NULL, 0);
if (!tk)
{
fprintf (stderr, "Cannot allocate termo instance\n");
exit (EXIT_FAILURE);
}
termo_set_mouse_proto (tk, termo_guess_mouse_proto (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)
{
fprintf (stderr, "Cannot initialize curses\n");
exit (EXIT_FAILURE);
}
app_context_t app;
app_init (&app);
app.tk = tk;
uv_loop_t *loop = uv_default_loop ();
loop->data = &app;
uv_signal_init (loop, &app.winch_watcher);
uv_signal_start (&app.winch_watcher, on_winch, SIGWINCH);
uv_tty_init (loop, &app.tty, STDOUT_FILENO, false);
uv_poll_init (loop, &app.tty_watcher, STDIN_FILENO);
uv_poll_start (&app.tty_watcher, UV_READABLE, on_tty_readable);
uv_timer_init (loop, &app.tty_timer);
init_palette (&app);
update_canvas_for_screen (&app);
redraw (&app);
redraw_canvas (&app);
uv_run (loop, UV_RUN_DEFAULT);
endwin ();
uv_loop_close (loop);
app_free (&app);
return 0;
}