// might need this for sigset_t #define _XOPEN_SOURCE 600 #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 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 int g_winch_pipe[2]; 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); } 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 void winch_handler (int signum) { (void) signum; write (g_winch_pipe[1], "x", 1); } int main (int argc, char *argv[]) { (void) argc; (void) argv; TERMO_CHECK_VERSION; setlocale (LC_CTYPE, ""); struct sigaction act; act.sa_handler = winch_handler; act.sa_flags = SA_RESTART; sigemptyset (&act.sa_mask); // Set up a self-pipe so that we can actually poll on SIGWINCH if (sigaction (SIGWINCH, &act, NULL) || pipe (g_winch_pipe)) { fprintf (stderr, "Cannot set up signal handler\n"); exit (EXIT_FAILURE); } 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; init_palette (&app); update_canvas_for_screen (&app); redraw (&app); redraw_canvas (&app); termo_result_t ret; termo_key_t key; // We listen for mouse/key input and terminal resize events struct pollfd fds[2] = { { .fd = STDIN_FILENO, .events = POLLIN }, { .fd = g_winch_pipe[0], .events = POLLIN }, }; // Run a simple event loop with poll() int nextwait = -1; bool running = true; while (running) { if (!poll (fds, 2, nextwait)) if (termo_getkey_force (tk, &key) == TERMO_RES_KEY) running &= on_key (&app, &key); if (fds[1].revents & (POLLIN | POLLHUP | POLLERR)) { char x; read (fds[1].fd, &x, 1); // The "official" simple and flicker-prone method of resizing // the internal buffers of curses endwin (); refresh (); update_canvas_for_screen (&app); redraw (&app); redraw_canvas (&app); } if (fds[0].revents & (POLLIN | POLLHUP | POLLERR)) termo_advisereadable (tk); while ((ret = termo_getkey (tk, &key)) == TERMO_RES_KEY) running &= on_key (&app, &key); nextwait = -1; if (ret == TERMO_RES_AGAIN) nextwait = termo_get_waittime (tk); } endwin (); termo_destroy (tk); }