From 61c5747ac01bd85eb6b061941926e0c1adb938ce Mon Sep 17 00:00:00 2001 From: mekb Date: Sat, 7 Dec 2024 16:02:54 +1100 Subject: [PATCH] :sparkles: Add terminal support --- meson.build | 3 +- src/arg.c | 4 +- src/color.c | 69 +++++++++++++++++++ src/color.h | 21 ++++++ src/image.c | 4 +- src/main.c | 188 +++++++++++++++++++++++++++++----------------------- src/term.c | 106 +++++++++++++++++++++++++++++ src/term.h | 30 +++++++++ src/util.c | 2 +- 9 files changed, 338 insertions(+), 89 deletions(-) create mode 100644 src/color.c create mode 100644 src/color.h create mode 100644 src/term.c create mode 100644 src/term.h diff --git a/meson.build b/meson.build index 5acb082..b43b0cb 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('Foto', 'c', default_options: ['warning_level=3']) # define source files -src = files('src/main.c', 'src/arg.c', 'src/arg.h', 'src/image.c', 'src/image.h', 'src/util.c', 'src/util.h') +src = files('src/main.c', 'src/arg.c', 'src/arg.h', 'src/image.c', 'src/image.h', 'src/util.c', 'src/util.h', 'src/term.c', 'src/term.h', 'src/color.c', 'src/color.h') # define project metadata url = 'https://github.com/mekb-turtle/Foto' @@ -20,4 +20,5 @@ add_project_arguments( exe = executable('foto', sources: src, install: true, dependencies: [ dependency('SDL2'), dependency('SDL2_image'), + dependency('ncurses') ]) diff --git a/src/arg.c b/src/arg.c index 3fd46bb..7cc6065 100644 --- a/src/arg.c +++ b/src/arg.c @@ -6,8 +6,8 @@ #include -#include "./arg.h" -#include "./util.h" +#include "arg.h" +#include "util.h" bool is_num(char *str) { // locale-aware diff --git a/src/color.c b/src/color.c new file mode 100644 index 0000000..1a0e283 --- /dev/null +++ b/src/color.c @@ -0,0 +1,69 @@ +#include "color.h" + +#define SQ(x) ((x) * (x)) + +size_t closest_color(struct color color, struct color *color_table, size_t color_len) { + size_t closest; + uint32_t closest_dist = -1; // max value + for (size_t i = 0; i < color_len; ++i) { + uint32_t dist = SQ(color.r - color_table[i].r) + SQ(color.g - color_table[i].g) + SQ(color.b - color_table[i].b); + if (dist < closest_dist) { + closest = i; + closest_dist = dist; + } + } + return closest; +} + +#define VAL(n) ((n) == 0 ? 0 : 40 * (n) + 55) // 0, 95, 135, 175, 215, 255 + +uint8_t rgb_to_8bit(struct color color) { + static struct color color_table[240]; + static bool color_table_init = false; + if (!color_table_init) { + color_table_init = true; + uint8_t i = 0; + struct color color; + color.a = 0xff; + + // fill colors (6^3) + for (color.r = 0; color.r < 6; ++color.r) + for (color.g = 0; color.g < 6; ++color.g) + for (color.b = 0; color.b < 6; ++color.b, ++i) { + color_table[i] = (struct color){ + .r = VAL(color.r), + .g = VAL(color.g), + .b = VAL(color.b), + }; + } + // fill grayscale (24) + for (color.r = 8; color.r < 238; color.r += 10, ++i) { + color.b = color.g = color.r; + color_table[i] = color; + } + } + + // find closest color (16-255) + return closest_color(color, color_table, 240) + 16; +} + +uint8_t rgb_to_4bit(struct color color) { + static struct color color_table[16]; + static bool color_table_init = false; + if (!color_table_init) { + color_table_init = true; + for (uint8_t i = 0; i < 16; ++i) { + color_table[i] = (struct color){.a = 0xff}; + uint8_t n = (i & 0x8) ? 0xff : 0x80; + // dynamically generate color table + color_table[i].r = (i & 0x1) ? n : 0x00; + color_table[i].g = (i & 0x2) ? n : 0x00; + color_table[i].b = (i & 0x4) ? n : 0x00; + }; + color_table[8] = color_table[7]; + color_table[7] = (struct color){{{0xc0, 0xc0, 0xc0, 0xff}}}; + } + + // find closest color (0-15) + return closest_color(color, color_table, 16); +} diff --git a/src/color.h b/src/color.h new file mode 100644 index 0000000..91b21b2 --- /dev/null +++ b/src/color.h @@ -0,0 +1,21 @@ +#ifndef COLOR_H +#define COLOR_H +#include +#include +#include +#include + +struct color { + union { + struct { + uint8_t r, g, b, a; + }; + uint32_t u32; + }; +}; + +size_t closest_color(struct color color, struct color *color_table, size_t color_len); +uint8_t rgb_to_8bit(struct color color); +uint8_t rgb_to_4bit(struct color color); + +#endif diff --git a/src/image.c b/src/image.c index a76dfee..c03805e 100644 --- a/src/image.c +++ b/src/image.c @@ -5,8 +5,8 @@ #include #include -#include "./util.h" -#include "./image.h" +#include "util.h" +#include "image.h" // get file pointer from filename FILE *open_file(char *filename) { diff --git a/src/main.c b/src/main.c index add1241..114e211 100644 --- a/src/main.c +++ b/src/main.c @@ -6,13 +6,17 @@ #include #include #include +#include +#include +#undef buttons // conflicts with SDL #include #include -#include "./util.h" -#include "./arg.h" -#include "./image.h" +#include "util.h" +#include "arg.h" +#include "image.h" +#include "term.h" // long options with getopt static struct option options_getopt[] = { @@ -39,7 +43,7 @@ static struct option options_getopt[] = { {0, 0, 0, 0 } }; -bool sigusr1 = false, sigusr2 = false, sigwinch = false; +bool sigusr1 = false, sigusr2 = false; void sigusr1_handler() { sigusr1 = true; @@ -49,37 +53,47 @@ void sigusr2_handler() { sigusr2 = true; } -void sigwinch_handler() { - sigwinch = true; -} - SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; SDL_Surface *term_surface = NULL; SDL_Surface *surface = NULL; SDL_Texture *texture = NULL; char *title_default = NULL; -bool sdl_init = false, sdl_image_init = false; - -struct term_cursor { - unsigned int x, y, width, height, scaled_width, scaled_height; -}; -bool fetch_term_size(struct term_cursor *cursor) { +bool fetch_term_size(struct position *size) { struct winsize w; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) { warn("ioctl"); return false; } - if (cursor) { - cursor->width = w.ws_col; - cursor->height = w.ws_row; + if (size) { + size->x = w.ws_col; + size->y = w.ws_row; } return true; } +bool sdl_init = false, sdl_image_init = false, term_init = false; + +// arguments +struct { + char *title; + bool stretch, hot_reload, sigusr1, sigusr2, position_set, size_set, background_set, terminal; + SDL_Point position, size; + SDL_Color background; + enum bit_depth bit_depth; + enum toggle_mode { + TOGGLE_AUTO = 0, + TOGGLE_OFF, + TOGGLE_ON + } unicode; +} options = {0}; // all false/NULL/0 + void cleanup() { - printf("Cleaning up\n"); + if (term_init) { + term_init = false; + } + printf("\n"); if (texture) SDL_DestroyTexture(texture); if (renderer) SDL_DestroyRenderer(renderer); if (window) SDL_DestroyWindow(window); @@ -98,25 +112,13 @@ void cleanup() { title_default = NULL; } +bool should_reload = true; + +static bool should_continue() { + return !(sigusr1 || sigusr2 || should_reload); +} + int main(int argc, char *argv[]) { - // arguments - struct { - char *title; - bool stretch, hot_reload, sigusr1, sigusr2, position_set, size_set, background_set, terminal; - SDL_Point position, size; - SDL_Color background; - enum { - BIT_AUTO = 0, - BIT_4, - BIT_8, - BIT_24 - } bit_depth; - enum toggle_mode { - TOGGLE_AUTO = 0, - TOGGLE_OFF, - TOGGLE_ON - } unicode; - } options = {0}; // all false/NULL/0 bool invalid = false; // don't immediately exit when invalid argument, so we can still use --help int opt; @@ -144,7 +146,7 @@ int main(int argc, char *argv[]) { -T --term --terminal: Shows the image in the terminal instead of on screen\n\ Highly experimental! Not functional yet\n\ Uses software renderer, which may be slower\n\ - -p and -s will instead specify the image bounds on the terminal, these cannot be set separately\n\ + -p and -s will instead specify the image bounds on the terminal, -p cannot be set without -s\n\ \n\ -u --unicode: Force enable unicode support for -T\n\ -U --no-unicode: Disables unicode support for -T\n\ @@ -246,17 +248,16 @@ int main(int argc, char *argv[]) { eprintf("Cannot use SIGUSR1 in terminal mode, ignoring...\n"); options.sigusr1 = false; } - if (options.position_set != options.size_set) { - eprintf("Cannot set position or size seperately in terminal mode, ignoring...\n"); + if (options.position_set && !options.size_set) { + eprintf("Cannot set position without size being set in terminal mode, ignoring...\n"); options.position_set = options.size_set = false; } - if ((options.position_set || options.size_set) && options.stretch) { - eprintf("Cannot set position or size, while stretch is enabled in terminal mode, ignoring stretch...\n"); - options.stretch = false; - } } else if (options.unicode != TOGGLE_AUTO) { eprintf("Cannot specify unicode support without terminal mode, ignoring...\n"); options.unicode = TOGGLE_AUTO; + } else if (options.bit_depth != BIT_AUTO) { + eprintf("Cannot specify bit depth without terminal mode, ignoring...\n"); + options.bit_depth = BIT_AUTO; } char *filename = argv[optind]; @@ -331,9 +332,6 @@ int main(int argc, char *argv[]) { eprintf("Failed to create renderer: %s\n", SDL_GetError()); return 1; } - } else if (options.title) { - // print title - printf("\033]0;%s\007", options.title); } struct sigaction sa; @@ -345,8 +343,6 @@ int main(int argc, char *argv[]) { if (sigaction(SIGUSR1, &sa, NULL) == -1) err(1, "sigaction"); sa.sa_handler = sigusr2_handler; if (sigaction(SIGUSR2, &sa, NULL) == -1) err(1, "sigaction"); - sa.sa_handler = sigwinch_handler; - if (sigaction(SIGWINCH, &sa, NULL) == -1) err(1, "sigaction"); // variables for hot-reload struct stat st; // stat @@ -354,11 +350,11 @@ int main(int argc, char *argv[]) { unsigned long long last_checked = 0; // the time at when we have last checked // terminal mode - struct term_cursor cursor; + struct position term_size; bool term_should_render = true; // main loop - bool running = true, should_reload; + bool running = true; while (running) { // from signal handler if (window && sigusr1 && options.sigusr1) { @@ -413,27 +409,50 @@ int main(int argc, char *argv[]) { } } - if ((sigwinch || !renderer) && options.terminal) { - sigwinch = false; - struct term_cursor old_cursor = cursor; - if (!fetch_term_size(&cursor)) { + if (options.terminal) { + if (!term_init) { + if (setupterm(NULL, STDOUT_FILENO, NULL) != OK) { + eprintf("Failed to set up terminal\n"); + return 1; + } + + if (options.title) printf("\x1b]0;%s\007", options.title); // print title + + int colors = tigetnum("colors"); + if (colors < 256) { + if (options.bit_depth == BIT_AUTO) options.bit_depth = BIT_4; + if (options.unicode == TOGGLE_AUTO) options.unicode = TOGGLE_OFF; // unicode is probably not supported, and it won't look good with 4-bit color + } else if (options.bit_depth == BIT_AUTO) { + // https://github.com/dankamongmen/notcurses/blob/master/src/lib/termdesc.c + bool rgb = (tigetflag("RGB") > 0 || tigetflag("Tc") > 0); + if (!rgb) { + const char *cterm = getenv("COLORTERM"); + rgb = cterm && (strcmp(cterm, "truecolor") == 0 || strcmp(cterm, "24bit") == 0); + } + options.bit_depth = rgb ? BIT_24 : BIT_8; + } + if (options.unicode == TOGGLE_AUTO) options.unicode = TOGGLE_ON; + term_init = true; + } + + struct position old_size = term_size; + if (!fetch_term_size(&term_size)) { eprintf("Failed to get terminal size\n"); return 1; } + // if size has changed - if (!renderer || old_cursor.width != cursor.width || old_cursor.height != cursor.height) { + if (!renderer || old_size.x != term_size.x || old_size.y != term_size.y) { term_should_render = true; - options.unicode = TOGGLE_ON; // TODO: check if terminal supports unicode and set if auto - // dsetroy the renderer and surface + // dsetroy the texture, renderer and surface + if (texture) SDL_DestroyTexture(texture); + texture = NULL; if (renderer) SDL_DestroyRenderer(renderer); renderer = NULL; if (term_surface) SDL_FreeSurface(term_surface); - cursor.scaled_width = cursor.width; - cursor.scaled_height = cursor.height * (options.unicode == TOGGLE_ON ? 2 : 1); - - term_surface = SDL_CreateRGBSurface(0, cursor.scaled_width, cursor.scaled_height, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); + term_surface = SDL_CreateRGBSurface(0, term_size.x, term_size.y * (options.unicode == TOGGLE_ON ? 2 : 1), TERM_DEPTH, TERM_R_MASK, TERM_G_MASK, TERM_B_MASK, TERM_A_MASK); if (!term_surface) { eprintf("Failed to create terminal surface: %s\n", SDL_GetError()); return 1; @@ -448,31 +467,44 @@ int main(int argc, char *argv[]) { } // set background color - if (!options.terminal || options.background_set) - SDL_SetRenderDrawColor(renderer, options.background.r, options.background.g, options.background.b, 255); + SDL_SetRenderDrawColor(renderer, options.background.r, options.background.g, options.background.b, 255); SDL_RenderClear(renderer); // get the size of the window SDL_Point window_size; - if (window) + if (window) { SDL_GetWindowSize(window, &window_size.x, &window_size.y); - else if (options.terminal) - window_size = (SDL_Point){cursor.scaled_width, cursor.scaled_height}; - else { + } else if (options.terminal) { + if (options.size_set) + window_size = (SDL_Point){options.size.x, options.size.y}; // specified size + else + window_size = (SDL_Point){term_surface->w, term_surface->h}; // whole terminal size + } else { eprintf("Window is NULL\n"); return 1; } // find the right scaling mode to fit the image in the window SDL_Rect rect; - if (options.terminal && options.position_set) { - // set exact bounds of image in terminal - } else if (options.stretch) { + if (options.stretch) { // stretch image to window/terminal size rect = (SDL_Rect){.x = 0, .y = 0, .w = window_size.x, .h = window_size.y}; } else { // fit image to window/terminal size - rect = get_fit_mode((SDL_Point){surface->w, surface->h}, window_size); + unsigned int x_mul = options.terminal && options.unicode != TOGGLE_ON ? 2u : 1u; + rect = get_fit_mode((SDL_Point){surface->w * (x_mul), surface->h}, window_size); + } + + if (options.terminal) { + if (options.position_set) { + // offset by the correct position + rect.x += options.position.x; + rect.y += options.position.y; + } else if (options.size_set) { + // center the image in the terminal + rect.x = ((int) term_size.x - rect.w) / 2; + rect.y = ((int) term_size.y - rect.h) / 2; + } } if (!texture) { @@ -490,20 +522,10 @@ int main(int argc, char *argv[]) { if (term_should_render && options.terminal) { term_should_render = false; - // TODO: draw the surface to the terminal - printf("render\n"); - // testing - /* - if (IMG_SavePNG(term_surface, "test.png") != 0) { - eprintf("Failed to save image: %s\n", IMG_GetError()); + if (render_image_to_terminal(term_surface, options.unicode == TOGGLE_ON, options.bit_depth, stdout, should_continue) == FAIL) { + eprintf("Failed to render image to terminal\n"); return 1; } - */ - /* - 4-bit: 16 colors - 8-bit: 16-231: R=(n-16)/36, G=((n-16)%36)/6, B=(n-16)%6 - 24-bit: 232-255: RGB=(n-232)*10+8 - */ } SDL_Event event; diff --git a/src/term.c b/src/term.c new file mode 100644 index 0000000..4114260 --- /dev/null +++ b/src/term.c @@ -0,0 +1,106 @@ +#include "term.h" +#include "util.h" +#include +#include "color.h" + +static bool get_pixel(SDL_Surface *surface, struct position position, struct color *color) { + Uint8 *p = &((Uint8 *) surface->pixels)[position.y * surface->pitch + position.x * surface->format->BytesPerPixel]; + Uint32 c = *(Uint32 *) p; + + // Extract RGBA values + SDL_GetRGBA(c, surface->format, &color->r, &color->g, &color->b, &color->a); + + return true; +} + +static void print_color(struct color color, bool fg, enum bit_depth bit_depth, FILE *fp) { + fprintf(fp, "\x1b[%c", fg ? '3' : '4'); + switch (bit_depth) { + case BIT_4: + fprintf(fp, "8;5;%d", rgb_to_4bit(color)); + break; + case BIT_8: + fprintf(fp, "8;5;%d", rgb_to_8bit(color)); + break; + case BIT_24: + fprintf(fp, "8;2;%d;%d;%d", color.r, color.g, color.b); + break; + default: + fprintf(fp, "0"); + break; + } + fprintf(fp, "m"); +} + +static void move_cursor(struct position position) { + fprintf(stdout, "\x1b[%d;%dH", position.y + 1, position.x + 1); +} + +enum render_callback render_image_to_terminal(SDL_Surface *surface, bool unicode, enum bit_depth bit_depth, FILE *fp, bool (*callback)()) { + enum render_callback ret = FAIL; + // lock surface + if (SDL_MUSTLOCK(surface) && SDL_LockSurface(surface) < 0) return FAIL; + + if (surface->format->BitsPerPixel != TERM_DEPTH) goto end; + if (surface->h <= 0 || surface->w <= 0) goto end; + + // print each pixel + struct position c; + unsigned int y_mul = unicode ? 2u : 1u; + + for (c.y = 0; c.y < (unsigned int) surface->h; c.y += y_mul) { + struct color col_bg_old, col_fg_old; + bool bg_set = false; + for (c.x = 0; c.x < (unsigned int) surface->w; ++c.x) { + // check if we should stop + if (callback && !callback()) { + ret = ABORT; + goto end; + } + + struct position term_pos = {.x = c.x, .y = c.y / y_mul}; + + // get pixel color(s) + struct color col_bg, col_fg; + if (!get_pixel(surface, c, &col_bg)) goto end; + if (unicode) { + if (c.y + 1 >= (unsigned int) surface->h) col_fg = col_bg; // out of bounds + if (!get_pixel(surface, (struct position){.x = c.x, .y = c.y + 1}, &col_fg)) goto end; + } + + // set cursor for first column, the cursor is moved automatically by the terminal for the next columns + if (c.x == 0) { + move_cursor(term_pos); + } + + // update bg color if last color was different + if (c.x == 0 || col_bg.u32 != col_bg_old.u32) { + col_bg_old = col_bg; + print_color(col_bg, false, bit_depth, fp); + } + + // print unicode block + if (unicode && col_bg.u32 != col_fg.u32) { // save bandwidth + // update fg color if last color was different + if (!bg_set || col_fg.u32 != col_fg_old.u32) { + bg_set = true; + col_fg_old = col_fg; + print_color(col_fg, true, bit_depth, fp); + } + fprintf(fp, "\u2584"); // lower half block + } else { + fprintf(fp, " "); + } + } + fprintf(fp, "\x1b[0m"); + } + + ret = SUCCESS; +end: + fprintf(fp, "\x1b[0m"); + fflush(fp); + if (SDL_MUSTLOCK(surface)) { + SDL_UnlockSurface(surface); + } + return ret; +} diff --git a/src/term.h b/src/term.h new file mode 100644 index 0000000..881da8f --- /dev/null +++ b/src/term.h @@ -0,0 +1,30 @@ +#ifndef TERM_H +#define TERM_H +#include +#include +#include "color.h" + +#define TERM_DEPTH (32) +#define TERM_R_MASK (0xff000000) +#define TERM_G_MASK (0x00ff0000) +#define TERM_B_MASK (0x0000ff00) +#define TERM_A_MASK (0x000000ff) + +enum bit_depth { + BIT_AUTO = 0, + BIT_4, + BIT_8, + BIT_24 +}; + +struct position { + unsigned int x, y; +}; + +enum render_callback { + FAIL, + SUCCESS, + ABORT +}; +enum render_callback render_image_to_terminal(SDL_Surface *surface, bool unicode, enum bit_depth bit_depth, FILE *fp, bool (*callback)()); +#endif // TERM_H diff --git a/src/util.c b/src/util.c index c9b4ddf..31bd650 100644 --- a/src/util.c +++ b/src/util.c @@ -1,6 +1,6 @@ #include -#include "./util.h" +#include "util.h" unsigned long long get_time() { // get current time in ms