diff options
Diffstat (limited to 'video/out/vo_sixel.c')
-rw-r--r-- | video/out/vo_sixel.c | 422 |
1 files changed, 271 insertions, 151 deletions
diff --git a/video/out/vo_sixel.c b/video/out/vo_sixel.c index 8318c5190c..c7df67f17f 100644 --- a/video/out/vo_sixel.c +++ b/video/out/vo_sixel.c @@ -35,49 +35,49 @@ #include "video/sws_utils.h" #include "video/mp_image.h" +#if HAVE_POSIX +#include <unistd.h> +#endif + #define IMGFMT IMGFMT_RGB24 +#define TERM_ESC_USE_GLOBAL_COLOR_REG "\033[?1070l" + #define TERMINAL_FALLBACK_COLS 80 #define TERMINAL_FALLBACK_ROWS 25 #define TERMINAL_FALLBACK_PX_WIDTH 320 #define TERMINAL_FALLBACK_PX_HEIGHT 240 -#define ESC_HIDE_CURSOR "\033[?25l" -#define ESC_RESTORE_CURSOR "\033[?25h" -#define ESC_CLEAR_SCREEN "\033[2J" -#define ESC_GOTOXY "\033[%d;%df" -#define ESC_USE_GLOBAL_COLOR_REG "\033[?1070l" +struct vo_sixel_opts { + int diffuse; + int reqcolors; + bool fixedpal; + int threshold; + int width, height, top, left; + int pad_y, pad_x; + int rows, cols; + bool config_clear, alt_screen; + bool buffered; +}; struct priv { - // User specified options - int opt_diffuse; - int opt_width; - int opt_height; - int opt_reqcolors; - int opt_fixedpal; - int opt_threshold; - int opt_top; - int opt_left; - int opt_pad_y; - int opt_pad_x; - int opt_rows; - int opt_cols; + struct vo_sixel_opts opts; // Internal data sixel_output_t *output; sixel_dither_t *dither; sixel_dither_t *testdither; uint8_t *buffer; + char *sixel_output_buf; + bool skip_frame_draw; - // The dimensions that will be actually - // be used after processing user inputs - int top; - int left; - int width; - int height; + int left, top; // image origin cell (1 based) + int width, height; // actual image px size - always reflects dst_rect. + int num_cols, num_rows; // terminal size in cells + int canvas_ok; // whether canvas vo->dwidth and vo->dheight are positive - int previous_histgram_colors; + int previous_histogram_colors; struct mp_rect src_rect; struct mp_rect dst_rect; @@ -91,23 +91,23 @@ static const unsigned int depth = 3; static int detect_scene_change(struct vo* vo) { struct priv* priv = vo->priv; - int previous_histgram_colors = priv->previous_histgram_colors; - int histgram_colors = 0; + int previous_histogram_colors = priv->previous_histogram_colors; + int histogram_colors = 0; // If threshold is set negative, then every frame must be a scene change - if (priv->dither == NULL || priv->opt_threshold < 0) + if (priv->dither == NULL || priv->opts.threshold < 0) return 1; - histgram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither); + histogram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither); - int color_difference_count = previous_histgram_colors - histgram_colors; + int color_difference_count = previous_histogram_colors - histogram_colors; color_difference_count = (color_difference_count > 0) ? // abs value color_difference_count : -color_difference_count; if (100 * color_difference_count > - priv->opt_threshold * previous_histgram_colors) + priv->opts.threshold * previous_histogram_colors) { - priv->previous_histgram_colors = histgram_colors; // update history + priv->previous_histogram_colors = histogram_colors; // update history return 1; } else { return 0; @@ -115,7 +115,7 @@ static int detect_scene_change(struct vo* vo) } -static void dealloc_dithers_and_buffer(struct vo* vo) +static void dealloc_dithers_and_buffers(struct vo* vo) { struct priv* priv = vo->priv; @@ -124,6 +124,11 @@ static void dealloc_dithers_and_buffer(struct vo* vo) priv->buffer = NULL; } + if (priv->frame) { + talloc_free(priv->frame); + priv->frame = NULL; + } + if (priv->dither) { sixel_dither_unref(priv->dither); priv->dither = NULL; @@ -139,15 +144,15 @@ static SIXELSTATUS prepare_static_palette(struct vo* vo) { struct priv* priv = vo->priv; - if (priv->dither) { - sixel_dither_set_body_only(priv->dither, 1); - } else { + if (!priv->dither) { priv->dither = sixel_dither_get(BUILTIN_XTERM256); if (priv->dither == NULL) return SIXEL_FALSE; - sixel_dither_set_diffusion_type(priv->dither, priv->opt_diffuse); + sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse); } + + sixel_dither_set_body_only(priv->dither, 0); return SIXEL_OK; } @@ -156,10 +161,11 @@ static SIXELSTATUS prepare_dynamic_palette(struct vo *vo) SIXELSTATUS status = SIXEL_FALSE; struct priv *priv = vo->priv; - /* create histgram and construct color palette + /* create histogram and construct color palette * with median cut algorithm. */ status = sixel_dither_initialize(priv->testdither, priv->buffer, - priv->width, priv->height, 3, + priv->width, priv->height, + SIXEL_PIXELFORMAT_RGB888, LARGE_NORM, REP_CENTER_BOX, QUALITY_LOW); if (SIXEL_FAILED(status)) @@ -172,29 +178,25 @@ static SIXELSTATUS prepare_dynamic_palette(struct vo *vo) } priv->dither = priv->testdither; - status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL); + status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL); if (SIXEL_FAILED(status)) return status; - sixel_dither_set_diffusion_type(priv->dither, priv->opt_diffuse); + sixel_dither_set_diffusion_type(priv->dither, priv->opts.diffuse); } else { - if (priv->dither == NULL) { + if (priv->dither == NULL) return SIXEL_FALSE; - } - sixel_dither_set_body_only(priv->dither, 1); } + sixel_dither_set_body_only(priv->dither, 0); return status; } -static void resize(struct vo *vo) +static void update_canvas_dimensions(struct vo *vo) { // this function sets the vo canvas size in pixels vo->dwidth, vo->dheight, - // and the output scaled size in priv->width, priv->height - // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect - // as well as image positioning in cells priv->top, priv->left. - // no other scaling/rendering size values are required past this point. + // and the number of rows and columns available in priv->num_rows/cols struct priv *priv = vo->priv; int num_rows = TERMINAL_FALLBACK_ROWS; int num_cols = TERMINAL_FALLBACK_COLS; @@ -204,8 +206,8 @@ static void resize(struct vo *vo) terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height); // If the user has specified rows/cols use them for further calculations - num_rows = (priv->opt_rows > 0) ? priv->opt_rows : num_rows; - num_cols = (priv->opt_cols > 0) ? priv->opt_cols : num_cols; + num_rows = (priv->opts.rows > 0) ? priv->opts.rows : num_rows; + num_cols = (priv->opts.cols > 0) ? priv->opts.cols : num_cols; // If the pad value is set in between 0 and width/2 - 1, then we // subtract from the detected width. Otherwise, we assume that the width @@ -213,17 +215,17 @@ static void resize(struct vo *vo) // total_width to be an integer multiple of num_cols. So in case the padding // added by terminal is less than the number of cells in that axis, then rounding // down will take care of correcting the detected width and remove padding. - if (priv->opt_width > 0) { + if (priv->opts.width > 0) { // option - set by the user, hard truth - total_px_width = priv->opt_width; + total_px_width = priv->opts.width; } else { if (total_px_width <= 0) { // ioctl failed to read terminal width total_px_width = TERMINAL_FALLBACK_PX_WIDTH; } else { - if (priv->opt_pad_x >= 0 && priv->opt_pad_x < total_px_width / 2) { + if (priv->opts.pad_x >= 0 && priv->opts.pad_x < total_px_width / 2) { // explicit padding set by the user - total_px_width -= (2 * priv->opt_pad_x); + total_px_width -= (2 * priv->opts.pad_x); } else { // rounded "auto padding" total_px_width = total_px_width / num_cols * num_cols; @@ -231,14 +233,14 @@ static void resize(struct vo *vo) } } - if (priv->opt_height > 0) { - total_px_height = priv->opt_height; + if (priv->opts.height > 0) { + total_px_height = priv->opts.height; } else { if (total_px_height <= 0) { total_px_height = TERMINAL_FALLBACK_PX_HEIGHT; } else { - if (priv->opt_pad_y >= 0 && priv->opt_pad_y < total_px_height / 2) { - total_px_height -= (2 * priv->opt_pad_y); + if (priv->opts.pad_y >= 0 && priv->opts.pad_y < total_px_height / 2) { + total_px_height -= (2 * priv->opts.pad_y); } else { total_px_height = total_px_height / num_rows * num_rows; } @@ -254,6 +256,19 @@ static void resize(struct vo *vo) vo->dheight = total_px_height * (num_rows - 1) / num_rows / 6 * 6; vo->dwidth = total_px_width; + priv->num_rows = num_rows; + priv->num_cols = num_cols; + + priv->canvas_ok = vo->dwidth > 0 && vo->dheight > 0; +} + +static void set_sixel_output_parameters(struct vo *vo) +{ + // This function sets output scaled size in priv->width, priv->height + // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect + // as well as image positioning in cells priv->top, priv->left. + struct priv *priv = vo->priv; + vo_get_src_dst_rects(vo, &priv->src_rect, &priv->dst_rect, &priv->osd); // priv->width and priv->height are the width and height of dst_rect @@ -264,16 +279,15 @@ static void resize(struct vo *vo) // top/left values must be greater than 1. If it is set, then // the image will be rendered from there and no further centering is done. - priv->top = (priv->opt_top > 0) ? priv->opt_top : - num_rows * priv->dst_rect.y0 / vo->dheight + 1; - priv->left = (priv->opt_left > 0) ? priv->opt_left : - num_cols * priv->dst_rect.x0 / vo->dwidth + 1; + priv->top = (priv->opts.top > 0) ? priv->opts.top : + priv->num_rows * priv->dst_rect.y0 / vo->dheight + 1; + priv->left = (priv->opts.left > 0) ? priv->opts.left : + priv->num_cols * priv->dst_rect.x0 / vo->dwidth + 1; } -static int reconfig(struct vo *vo, struct mp_image_params *params) +static int update_sixel_swscaler(struct vo *vo, struct mp_image_params *params) { struct priv *priv = vo->priv; - resize(vo); priv->sws->src = *params; priv->sws->src.w = mp_rect_w(priv->src_rect); @@ -286,6 +300,8 @@ static int reconfig(struct vo *vo, struct mp_image_params *params) .p_h = 1, }; + dealloc_dithers_and_buffers(vo); + priv->frame = mp_image_alloc(IMGFMT, priv->width, priv->height); if (!priv->frame) return -1; @@ -293,17 +309,15 @@ static int reconfig(struct vo *vo, struct mp_image_params *params) if (mp_sws_reinit(priv->sws) < 0) return -1; - printf(ESC_HIDE_CURSOR); - printf(ESC_CLEAR_SCREEN); - vo->want_redraw = true; - - dealloc_dithers_and_buffer(vo); - SIXELSTATUS status = sixel_dither_new(&priv->testdither, - priv->opt_reqcolors, NULL); - if (SIXEL_FAILED(status)) { - MP_ERR(vo, "reconfig: Failed to create new dither: %s\n", - sixel_helper_format_error(status)); - return -1; + // create testdither only if dynamic palette mode is set + if (!priv->opts.fixedpal) { + SIXELSTATUS status = sixel_dither_new(&priv->testdither, + priv->opts.reqcolors, NULL); + if (SIXEL_FAILED(status)) { + MP_ERR(vo, "update_sixel_swscaler: Failed to create new dither: %s\n", + sixel_helper_format_error(status)); + return -1; + } } priv->buffer = @@ -312,80 +326,188 @@ static int reconfig(struct vo *vo, struct mp_image_params *params) return 0; } -static void draw_image(struct vo *vo, mp_image_t *mpi) +static inline int sixel_buffer(char *data, int size, void *priv) { + char **out = (char **)priv; + *out = talloc_strndup_append_buffer(*out, data, size); + return size; +} + +static inline int sixel_write(char *data, int size, void *priv) +{ + FILE *p = (FILE *)priv; + // On POSIX platforms, write() is the fastest method. It also is the only + // one that allows atomic writes so mpv’s output will not be interrupted + // by other processes or threads that write to stdout, which would cause + // screen corruption. POSIX does not guarantee atomicity for writes + // exceeding PIPE_BUF, but at least Linux does seem to implement it that + // way. +#if HAVE_POSIX + int remain = size; + + while (remain > 0) { + ssize_t written = write(fileno(p), data, remain); + if (written < 0) + return written; + remain -= written; + data += written; + } + + return size; +#else + int ret = fwrite(data, 1, size, p); + fflush(p); + return ret; +#endif +} + +static inline void sixel_strwrite(char *s) +{ + sixel_write(s, strlen(s), stdout); +} + +static int reconfig(struct vo *vo, struct mp_image_params *params) +{ + struct priv *priv = vo->priv; + int ret = 0; + update_canvas_dimensions(vo); + if (priv->canvas_ok) { // if too small - succeed but skip the rendering + set_sixel_output_parameters(vo); + ret = update_sixel_swscaler(vo, params); + } + + if (priv->opts.config_clear) + sixel_strwrite(TERM_ESC_CLEAR_SCREEN); + vo->want_redraw = true; + + return ret; +} + +static void draw_frame(struct vo *vo, struct vo_frame *frame) { struct priv *priv = vo->priv; - struct mp_image src = *mpi; SIXELSTATUS status; + struct mp_image *mpi = NULL; + + int prev_rows = priv->num_rows; + int prev_cols = priv->num_cols; + int prev_height = vo->dheight; + int prev_width = vo->dwidth; + bool resized = false; + update_canvas_dimensions(vo); + if (!priv->canvas_ok) + return; + + if (prev_rows != priv->num_rows || prev_cols != priv->num_cols || + prev_width != vo->dwidth || prev_height != vo->dheight) + { + set_sixel_output_parameters(vo); + // Not checking for vo->config_ok because draw_frame is never called + // with a failed reconfig. + update_sixel_swscaler(vo, vo->params); + + if (priv->opts.config_clear) + sixel_strwrite(TERM_ESC_CLEAR_SCREEN); + resized = true; + } + + if (frame->repeat && !frame->redraw && !resized) { + // Frame is repeated, and no need to update OSD either + priv->skip_frame_draw = true; + return; + } else { + // Either frame is new, or OSD has to be redrawn + priv->skip_frame_draw = false; + } - struct mp_rect src_rc = priv->src_rect; - src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x); - src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y); - mp_image_crop_rc(&src, src_rc); + // Normal case where we have to draw the frame and the image is not NULL + if (frame->current) { + mpi = mp_image_new_ref(frame->current); + struct mp_rect src_rc = priv->src_rect; + src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x); + src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y); + mp_image_crop_rc(mpi, src_rc); - // Downscale the image - mp_sws_scale(priv->sws, priv->frame, &src); + // scale/pan to our dest rect + mp_sws_scale(priv->sws, priv->frame, mpi); + } else { + // Image is NULL, so need to clear image and draw OSD + mp_image_clear(priv->frame, 0, 0, priv->width, priv->height); + } struct mp_osd_res dim = { .w = priv->width, .h = priv->height }; osd_draw_on_image(vo->osd, dim, mpi ? mpi->pts : 0, 0, priv->frame); + // Copy from mpv to RGB format as required by libsixel - memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth, priv->height, - priv->width * depth, priv->frame->stride[0]); + memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth, + priv->height, priv->width * depth, priv->frame->stride[0]); // Even if either of these prepare palette functions fail, on re-running them // they should try to re-initialize the dithers, so it shouldn't dereference // any NULL pointers. flip_page also has a check to make sure dither is not // NULL before drawing, so failure in these functions should still be okay. - if (priv->opt_fixedpal) { + if (priv->opts.fixedpal) { status = prepare_static_palette(vo); } else { status = prepare_dynamic_palette(vo); } if (SIXEL_FAILED(status)) { - MP_WARN(vo, "draw_image: prepare_palette returned error: %s\n", + MP_WARN(vo, "draw_frame: prepare_palette returned error: %s\n", sixel_helper_format_error(status)); } - talloc_free(mpi); -} - -static int sixel_write(char *data, int size, void *priv) -{ - return fwrite(data, 1, size, (FILE *)priv); + if (mpi) + talloc_free(mpi); } static void flip_page(struct vo *vo) { struct priv* priv = vo->priv; + if (!priv->canvas_ok) + return; + + // If frame is repeated and no update required, then we skip encoding + if (priv->skip_frame_draw) + return; // Make sure that image and dither are valid before drawing if (priv->buffer == NULL || priv->dither == NULL) return; // Go to the offset row and column, then display the image - printf(ESC_GOTOXY, priv->top, priv->left); + priv->sixel_output_buf = talloc_asprintf(NULL, TERM_ESC_GOTO_YX, + priv->top, priv->left); + if (!priv->opts.buffered) + sixel_strwrite(priv->sixel_output_buf); + sixel_encode(priv->buffer, priv->width, priv->height, - PIXELFORMAT_RGB888, - priv->dither, priv->output); - fflush(stdout); + depth, priv->dither, priv->output); + + if (priv->opts.buffered) + sixel_write(priv->sixel_output_buf, + ta_get_size(priv->sixel_output_buf), stdout); + + talloc_free(priv->sixel_output_buf); } static int preinit(struct vo *vo) { struct priv *priv = vo->priv; SIXELSTATUS status = SIXEL_FALSE; - FILE* sixel_output_file = stdout; // Parse opts set by CLI or conf priv->sws = mp_sws_alloc(vo); priv->sws->log = vo->log; mp_sws_enable_cmdline_opts(priv->sws, vo->global); - status = sixel_output_new(&priv->output, sixel_write, sixel_output_file, NULL); + if (priv->opts.buffered) + status = sixel_output_new(&priv->output, sixel_buffer, + &priv->sixel_output_buf, NULL); + else + status = sixel_output_new(&priv->output, sixel_write, stdout, NULL); if (SIXEL_FAILED(status)) { MP_ERR(vo, "preinit: Failed to create output file: %s\n", sixel_helper_format_error(status)); @@ -394,25 +516,28 @@ static int preinit(struct vo *vo) sixel_output_set_encode_policy(priv->output, SIXEL_ENCODEPOLICY_FAST); - printf(ESC_HIDE_CURSOR); + if (priv->opts.alt_screen) + sixel_strwrite(TERM_ESC_ALT_SCREEN); + + sixel_strwrite(TERM_ESC_HIDE_CURSOR); + terminal_set_mouse_input(true); /* don't use private color registers for each frame. */ - printf(ESC_USE_GLOBAL_COLOR_REG); + sixel_strwrite(TERM_ESC_USE_GLOBAL_COLOR_REG); priv->dither = NULL; - status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL); - if (SIXEL_FAILED(status)) { - MP_ERR(vo, "preinit: Failed to create new dither: %s\n", - sixel_helper_format_error(status)); - return -1; + // create testdither only if dynamic palette mode is set + if (!priv->opts.fixedpal) { + status = sixel_dither_new(&priv->testdither, priv->opts.reqcolors, NULL); + if (SIXEL_FAILED(status)) { + MP_ERR(vo, "preinit: Failed to create new dither: %s\n", + sixel_helper_format_error(status)); + return -1; + } } - resize(vo); - priv->buffer = - talloc_array(NULL, uint8_t, depth * priv->width * priv->height); - - priv->previous_histgram_colors = 0; + priv->previous_histogram_colors = 0; return 0; } @@ -424,15 +549,9 @@ static int query_format(struct vo *vo, int format) static int control(struct vo *vo, uint32_t request, void *data) { - if (request == VOCTRL_SET_PANSCAN) { - if (!reconfig(vo, vo->params)) { - return VO_TRUE; - } else { - return VO_FALSE; - } - } else { - return VO_NOTIMPL; - } + if (request == VOCTRL_SET_PANSCAN) + return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE; + return VO_NOTIMPL; } @@ -440,10 +559,11 @@ static void uninit(struct vo *vo) { struct priv *priv = vo->priv; - printf(ESC_RESTORE_CURSOR); + sixel_strwrite(TERM_ESC_RESTORE_CURSOR); + terminal_set_mouse_input(false); - printf(ESC_CLEAR_SCREEN); - printf(ESC_GOTOXY, 1, 1); + if (priv->opts.alt_screen) + sixel_strwrite(TERM_ESC_NORMAL_SCREEN); fflush(stdout); if (priv->output) { @@ -451,38 +571,34 @@ static void uninit(struct vo *vo) priv->output = NULL; } - dealloc_dithers_and_buffer(vo); + dealloc_dithers_and_buffers(vo); } #define OPT_BASE_STRUCT struct priv const struct vo_driver video_out_sixel = { .name = "sixel", - .description = "libsixel", + .description = "terminal graphics using sixels", .preinit = preinit, .query_format = query_format, .reconfig = reconfig, .control = control, - .draw_image = draw_image, + .draw_frame = draw_frame, .flip_page = flip_page, .uninit = uninit, .priv_size = sizeof(struct priv), .priv_defaults = &(const struct priv) { - .opt_diffuse = DIFFUSE_ATKINSON, - .opt_width = 0, - .opt_height = 0, - .opt_reqcolors = 256, - .opt_threshold = -1, - .opt_fixedpal = 1, - .opt_top = 0, - .opt_left = 0, - .opt_pad_y = -1, - .opt_pad_x = -1, - .opt_rows = 0, - .opt_cols = 0, + .opts.diffuse = DIFFUSE_AUTO, + .opts.reqcolors = 256, + .opts.threshold = -1, + .opts.fixedpal = true, + .opts.pad_y = -1, + .opts.pad_x = -1, + .opts.config_clear = true, + .opts.alt_screen = true, }, .options = (const m_option_t[]) { - {"dither", OPT_CHOICE(opt_diffuse, + {"dither", OPT_CHOICE(opts.diffuse, {"auto", DIFFUSE_AUTO}, {"none", DIFFUSE_NONE}, {"atkinson", DIFFUSE_ATKINSON}, @@ -492,17 +608,21 @@ const struct vo_driver video_out_sixel = { {"burkes", DIFFUSE_BURKES}, {"arithmetic", DIFFUSE_A_DITHER}, {"xor", DIFFUSE_X_DITHER})}, - {"width", OPT_INT(opt_width)}, - {"height", OPT_INT(opt_height)}, - {"reqcolors", OPT_INT(opt_reqcolors)}, - {"fixedpalette", OPT_FLAG(opt_fixedpal)}, - {"threshold", OPT_INT(opt_threshold)}, - {"top", OPT_INT(opt_top)}, - {"left", OPT_INT(opt_left)}, - {"pad-y", OPT_INT(opt_pad_y)}, - {"pad-x", OPT_INT(opt_pad_x)}, - {"rows", OPT_INT(opt_rows)}, - {"cols", OPT_INT(opt_cols)}, + {"width", OPT_INT(opts.width)}, + {"height", OPT_INT(opts.height)}, + {"reqcolors", OPT_INT(opts.reqcolors)}, + {"fixedpalette", OPT_BOOL(opts.fixedpal)}, + {"threshold", OPT_INT(opts.threshold)}, + {"top", OPT_INT(opts.top)}, + {"left", OPT_INT(opts.left)}, + {"pad-y", OPT_INT(opts.pad_y)}, + {"pad-x", OPT_INT(opts.pad_x)}, + {"rows", OPT_INT(opts.rows)}, + {"cols", OPT_INT(opts.cols)}, + {"config-clear", OPT_BOOL(opts.config_clear), }, + {"alt-screen", OPT_BOOL(opts.alt_screen), }, + {"buffered", OPT_BOOL(opts.buffered), }, + {"exit-clear", OPT_REPLACED("vo-sixel-alt-screen")}, {0} }, .options_prefix = "vo-sixel", |