/* SPDX-License-Identifier: GPL-3.0-or-later */ /* * vfetch - neofetch but the ascii part is a video * * Copyright (C) 2026 Doan Luu Duc Tai * */ #include "args.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" /* clang-format off */ static void print_help(void) { (void)fputs("USAGE\n", stdout); (void)fputs("=====\n", stdout); (void)fputc('\n', stdout); (void)fputs("After building, you can run the program:\n", stdout); (void)fputc('\n', stdout); (void)fputs(" vfetch --file \n", stdout); (void)fputc('\n', stdout); (void)fputc('\n', stdout); (void)fputs("VIDEO FORMAT REQUIREMENTS\n", stdout); (void)fputs("-------------------------\n", stdout); (void)fputc('\n', stdout); (void)fputs("- Video Codec: MPEG-1\n", stdout); (void)fputs("- Audio Codec: MPEG-1 Audio Layer II (MP2)\n", stdout); (void)fputs("- Container: MPEG-PS (.mpg, .mpeg)\n", stdout); (void)fputc('\n', stdout); (void)fputs("Standard MP4, MKV, or MOV files will NOT work directly. You must convert them\n", stdout); (void)fputs("accordingly (e.g. FFmpeg).\n", stdout); (void)fputc('\n', stdout); (void)fputc('\n', stdout); (void)fputs("COMMAND REFERENCE\n", stdout); (void)fputs("=================\n", stdout); (void)fputc('\n', stdout); (void)fputs("--file \n", stdout); (void)fputs(" (Required) Specifies the path to the input video file to be animated.\n", stdout); (void)fputs(" MUST be an MPEG-1 encoded file (see VIDEO FORMAT REQUIREMENTS section).\n", stdout); (void)fputc('\n', stdout); (void)fputs("--help\n", stdout); (void)fputs(" Provides help. All other commands will be ignored if --help is passed.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--size x or --size \n", stdout); (void)fputs(" Sets the dimensions of the animation output in character cells.\n", stdout); (void)fputs(" Format:\n", stdout); (void)fputs(" : Automatically calculates the correct height to preserve the video's aspect ratio, based on the --cell-aspect.\n", stdout); (void)fputs(" x: Forces the animation to specific dimensions, ignoring aspect ratio.\n", stdout); (void)fputs(" Default: 80\n", stdout); (void)fputc('\n', stdout); (void)fputs("--cell-aspect \n", stdout); (void)fputs(" Specifies the aspect ratio (Width / Height) of a single character cell in\n", stdout); (void)fputs(" your terminal. This is used to calculate the correct image height when only\n", stdout); (void)fputs(" the width is provided in --size.\n", stdout); (void)fputs(" Most terminal fonts are twice as tall as they are wide. Therefore:\n", stdout); (void)fputs(" Default: 0.50\n", stdout); (void)fputc('\n', stdout); (void)fputs("--pad : or --pad \n", stdout); (void)fputs(" Sets the positioning offset (padding) of the animation relative to the\n", stdout); (void)fputs(" terminal's top-left corner.\n", stdout); (void)fputs(" X (Horizontal): The number of spaces to indent from the left.\n", stdout); (void)fputs(" Y (Vertical): The number of lines to push down from the top.\n", stdout); (void)fputs(" Format:\n", stdout); (void)fputs(" :: Sets specific values (e.g., 10:5 shifts 10 spaces right and 5 lines down).\n", stdout); (void)fputs(" : Applies the same number to both X and Y (e.g., 5 becomes 5:5).\n", stdout); (void)fputs(" Default: 4:2\n", stdout); (void)fputc('\n', stdout); (void)fputs("--fps \n", stdout); (void)fputs(" Sets the target FPS for the animation.\n", stdout); (void)fputs(" Accepts floating-point numbers with up to two decimal places (e.g., 23.97, 29.98).\n", stdout); (void)fputs(" Cannot be higher than video's native FPS.\n", stdout); (void)fputs(" Default: Source video's native FPS.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--refresh-rate \n", stdout); (void)fputs(" Sets the target refresh rate for the terminal animation in Hz.\n", stdout); (void)fputs(" Default: 60\n", stdout); (void)fputs(" Values: A positive integer from 1 to 65535\n", stdout); (void)fputs(" NOTE: This controls the screen redraw rate, which is independent of the\n", stdout); (void)fputs(" video's frame rate (--fps). A high refresh rate will not generate new video\n", stdout); (void)fputs(" frames but can make the animation appear more fluid.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--audio\n", stdout); (void)fputs(" Enables audio playback. The program extracts the audio track directly\n", stdout); (void)fputs(" from the input video file.\n", stdout); (void)fputs(" The audio codec MUST be MP2 (see VIDEO FORMAT REQUIREMENTS section).\n", stdout); (void)fputc('\n', stdout); (void)fputs("--loop\n", stdout); (void)fputs(" Enables infinite looping of the animation. If this flag is not provided,\n", stdout); (void)fputs(" the program will play the video once and then exit automatically.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--fetch-tool \n", stdout); (void)fputs(" Controls which system information utility is displayed on the side of the animation.\n", stdout); (void)fputs(" Default Behavior: If not raised, the program will first try to use\n", stdout); (void)fputs(" 'fastfetch', falling back to 'neofetch' if it's not available.\n", stdout); (void)fputs(" Values:\n", stdout); (void)fputs(" fastfetch: Explicitly use fastfetch. The program will exit with an error\n", stdout); (void)fputs(" if it's not installed.\n", stdout); (void)fputs(" neofetch: Explicitly use neofetch. The program will exit with an error\n", stdout); (void)fputs(" if it's not installed.\n", stdout); (void)fputs(" none: Completely disables the system information display.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--verbose\n", stdout); (void)fputs(" Enables verbose console output, showing high-level information about\n", stdout); (void)fputs(" caching, command execution, and pipeline steps.\n", stdout); (void)fputs(" By default, this output goes to stdout. If used with --debug, all verbose\n", stdout); (void)fputs(" output is redirected to stderr.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--debug\n", stdout); (void)fputs(" Enables highly detailed developer output to stderr for troubleshooting.\n", stdout); (void)fputs(" This includes low-level details like worker thread activity and raw command\n", stdout); (void)fputs(" outputs on failure.\n", stdout); (void)fputs(" This flag also enables --verbose and forces all diagnostic output to stderr.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--format \n", stdout); (void)fputs(" Sets the output format for rendering in the terminal. The fidelity and\n", stdout); (void)fputs(" compatibility vary significantly.\n", stdout); (void)fputs(" Default: symbols.\n", stdout); (void)fputs(" Values:\n", stdout); (void)fputs(" symbols: (Default) Renders using Unicode characters.\n", stdout); (void)fputs(" sixels: Renders a high-quality bitmap image.\n", stdout); (void)fputs(" Requires a Sixel-compatible terminal (e.g., Xterm, WezTerm).\n", stdout); #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 8) (void)fputs(" kitty: Renders a pixel-perfect image.\n", stdout); (void)fputs(" Requires a Kitty-compatible terminal.\n", stdout); (void)fputs(" iterm2: Renders a pixel-perfect image.\n", stdout); (void)fputs(" Requires an iTerm2-compatible terminal.\n", stdout); # endif #endif (void)fputc('\n', stdout); (void)fputs("--colors \n", stdout); (void)fputs(" Sets the color mode for the terminal output.\n", stdout); (void)fputs(" Values:\n", stdout); #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 8) (void)fputs(" 8: Use the 8 standard ANSI colors.\n", stdout); # endif #endif #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 12) (void)fputs(" 16/8: A 16-color compatibility mode that maps to the 8 base colors.\n", stdout); # endif #endif (void)fputs(" 16: Use the 16 standard ANSI colors.\n", stdout); (void)fputs(" 240: Use a 240-color palette.\n", stdout); (void)fputs(" 256: Use the full 256-color xterm palette.\n", stdout); (void)fputs(" full: Use 24-bit truecolor for the highest color fidelity.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--fg \n", stdout); (void)fputs(" Sets a specific foreground color to be used for all characters. This is\n", stdout); (void)fputs(" most effective in 2-color modes but can influence other modes. Color is\n", stdout); (void)fputs(" specified as a 6-digit hex string.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--bg \n", stdout); (void)fputs(" Sets a specific background color. This color will be blended with the\n", stdout); (void)fputs(" video's background. Color is specified as a 6-digit hex string.\n", stdout); (void)fputc('\n', stdout); #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 8) (void)fputs("--fg-only\n", stdout); (void)fputs(" Renders only the foreground characters, leaving the background of each\n", stdout); (void)fputs(" character cell transparent. This allows the terminal's background color to\n", stdout); (void)fputs(" show through.\n", stdout); (void)fputc('\n', stdout); # endif #endif (void)fputs("--color-space \n", stdout); (void)fputs(" Specifies the color space to use for calculating color similarity, which\n", stdout); (void)fputs(" affects color quantization.\n", stdout); (void)fputs(" Values:\n", stdout); (void)fputs(" rgb: RGB color space.\n", stdout); (void)fputs(" din99d: A perceptually uniform color space for better visual results.\n", stdout); (void)fputc('\n', stdout); (void)fputs("--symbols \n", stdout); (void)fputs(" Controls the set of characters used to render the image.\n", stdout); (void)fputs(" You can specify a preset or combine multiple classes with commas\n", stdout); (void)fputs(" (e.g., --symbols \"block,braille,stipple\").\n", stdout); (void)fputc('\n', stdout); (void)fputs(" Common Values:\n", stdout); (void)fputs(" all: Use all available symbols.\n", stdout); (void)fputs(" ascii: Restrict output to only the 7-bit ASCII character set.\n", stdout); (void)fputs(" block: Use all block-drawing characters (half, quad, sextant, etc.).\n", stdout); (void)fputs(" braille: Use all 256 braille patterns.\n", stdout); (void)fputs(" solid: Use only solid block characters.\n", stdout); (void)fputs(" stipple: Use stippled (shaded/dotted) characters.\n", stdout); (void)fputs(" none: Use no symbols.\n", stdout); (void)fputc('\n', stdout); (void)fputs(" List of supported values:\n", stdout); (void)fputs(" all, none, ascii, braille, space, solid, stipple, inverted,\n", stdout); (void)fputs(" block, border, diagonal, dot, half, hhalf, vhalf, quad, sextant, wedge,\n", stdout); (void)fputs(" alnum, alpha, digit, latin, wide,\n", stdout); (void)fputs(" geometric, technical,\n", stdout); (void)fputs(" ambiguous, bad, extra, imported, legacy, narrow, ugly\n", stdout); (void)fputc('\n', stdout); (void)fputs("--fill \n", stdout); (void)fputs(" Fills transparent background areas with repeating characters from the\n", stdout); (void)fputs(" provided string. This is not a class selector like --symbols.\n", stdout); (void)fputs(" Any valid UTF-8 characters can be used.\n", stdout); (void)fputs(" Example: --fill \"ยท.\" will fill transparent areas with an alternating pattern\n", stdout); (void)fputs(" of dots.\n", stdout); (void)fputc('\n', stdout); (void)fputc('\n', stdout); (void)fputs("INTERACTIVE CONTROLS\n", stdout); (void)fputs("====================\n", stdout); (void)fputc('\n', stdout); (void)fputs("During video playback, these keybindings are available:\n", stdout); (void)fputc('\n', stdout); (void)fputs("* 'q' or 'Q': Quit the program immediately.\n", stdout); (void)fputs("* 'p' or 'P': Toggle Pause/Resume.\n", stdout); } /* clang-format on */ static VfetchError validate_decimal_places(const char *value, const char *arg_name, int max_decimals) { const char *decimal_point = strchr(value, '.'); if (decimal_point && strlen(decimal_point + 1) > (size_t)max_decimals) { (void)fprintf( stderr, "\nERROR: Invalid value for %s '%s'.\n", arg_name, value); (void)fprintf(stderr, " The value must have at most %d decimal places.\n", max_decimals); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } static VfetchError parse_double_value(int *idx, int argc, const char *argv[], const char *arg_name, int max_decimals, double *out_val) { char *endptr = NULL; if (*idx + 1 >= argc) { (void)fprintf(stderr, "\nERROR: Argument %s requires a value.\n", arg_name); return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (validate_decimal_places(val, arg_name, max_decimals) != VFETCH_SUCCESS) { return VFETCH_ERROR_INVALID_ARGUMENT; } *out_val = strtod(val, &endptr); if (endptr == val || *endptr != '\0') { (void)fprintf(stderr, "\nERROR: Invalid value for %s '%s'. Must be a number.\n", arg_name, val); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } static VfetchError parse_file_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 < argc) { free(args->filename); args->filename = strdup(argv[++(*idx)]); if (!args->filename) { return VFETCH_ERROR_MEMORY_ALLOCATION; } return VFETCH_SUCCESS; } return VFETCH_ERROR_INVALID_ARGUMENT; } #define K_RADIX 10 static VfetchError parse_size_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { char *endptr = NULL; if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; errno = 0; long width_val = strtol(val, &endptr, K_RADIX); if (errno != 0 || endptr == val || width_val <= 0) { (void)fprintf(stderr, "\nERROR: Invalid width in --size '%s'.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } args->width = (uint32_t)width_val; if (*endptr == 'x') { long height_val = strtol(endptr + 1, &endptr, K_RADIX); if (*endptr != '\0' || height_val <= 0) { (void)fprintf(stderr, "\nERROR: Invalid height in --size '%s'.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } args->height = (uint32_t)height_val; } else if (*endptr == '\0') { args->height = 0; } else { (void)fprintf( stderr, "\nERROR: Invalid format for --size '%s'. Use or .\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } #undef K_RADIX static VfetchError parse_cell_aspect_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { VfetchError err = parse_double_value( idx, argc, argv, "--cell-aspect", 2, &args->cell_aspect_ratio); if (err) { return err; } if (args->cell_aspect_ratio <= 0.0) { (void)fprintf(stderr, "\nERROR: --cell-aspect must be positive.\n"); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } static VfetchError parse_fps_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { VfetchError err = parse_double_value(idx, argc, argv, "--fps", 2, &args->fps); if (err) { return err; } if (args->fps <= 0.0) { (void)fprintf( stderr, "\nERROR: Invalid value for --fps '%.2f'.\n", args->fps); (void)fputs(" The FPS must be a positive number.\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } #define K_DECIMAL_BASE 10 static VfetchError parse_padding_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; char *endptr = NULL; long p_x = strtol(val, &endptr, K_DECIMAL_BASE); if (endptr == val || p_x < 0) { (void)fprintf(stderr, "\nERROR: Invalid padding '%s'.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } args->pad_x = (uint32_t)p_x; if (*endptr != '\0') { if (*endptr == ':') { const char *y_part = endptr + 1; long p_y = strtol(y_part, &endptr, K_DECIMAL_BASE); if (endptr == y_part || *endptr != '\0' || p_y < 0) { (void)fprintf(stderr, "\nERROR: Invalid Y padding '%s'.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } args->pad_y = (uint32_t)p_y; } else { (void)fprintf( stderr, "\nERROR: Invalid padding format '%s'. Use or :.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } } else { args->pad_y = (uint32_t)p_x; } return VFETCH_SUCCESS; } #undef K_DECIMAL_BASE #define K_DECIMAL_BASE 10 static VfetchError parse_refresh_rate_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { char *endptr = NULL; if (*idx + 1 >= argc) { (void)fputs("\nERROR: Argument --refresh-rate requires a value.\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; long l_val = strtol(val, &endptr, K_DECIMAL_BASE); if (*endptr != '\0' || endptr == val || l_val <= 0 || l_val > USHRT_MAX) { (void)fprintf(stderr, "\nERROR: Invalid value for --refresh-rate '%s'. Must be a " "positive integer between 1 and %d.\n", val, USHRT_MAX); return VFETCH_ERROR_INVALID_ARGUMENT; } args->refresh_rate = (uint16_t)l_val; return VFETCH_SUCCESS; } #undef K_DECIMAL_BASE static VfetchError parse_chafa_map_arg( int *idx, int argc, const char *argv[], VfetchArgs *args, const char *key) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } g_hash_table_insert(args->chafa_options, strdup(key), strdup(argv[++(*idx)])); return VFETCH_SUCCESS; } static VfetchError parse_bounded_float_arg(int *idx, int argc, const char *argv[], float *target, const char *arg_name, float min, float max) { double temp_val = 0.0; VfetchError err = parse_double_value(idx, argc, argv, arg_name, 2, &temp_val); if (err) { return err; } *target = (float)temp_val; if (*target < min || *target > max) { (void)fprintf( stderr, "\nERROR: Value for %s '%.2f' is out of range [%.2f, %.2f].\n", arg_name, (double)*target, (double)min, (double)max); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } static VfetchError parse_fetch_tool_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (strcmp(val, "fastfetch") != 0 && strcmp(val, "neofetch") != 0 && strcmp(val, "none") != 0) { (void)fprintf( stderr, "\nERROR: Invalid value for --fetch-tool '%s'.\n", val); (void)fputs( " The value must be 'fastfetch', 'neofetch', or 'none'.\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } if (args->fetch_tool_arg) { free(args->fetch_tool_arg); } args->fetch_tool_arg = strdup(val); return args->fetch_tool_arg ? VFETCH_SUCCESS : VFETCH_ERROR_MEMORY_ALLOCATION; } static VfetchError parse_format_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (strcmp(val, "symbols") != 0 && strcmp(val, "sixels") != 0 && strcmp(val, "kitty") != 0 && strcmp(val, "iterm2") != 0) { (void)fprintf(stderr, "\nERROR: Invalid value for --format '%s'. Must be " "'symbols', 'sixels', 'kitty', or 'iterm2'.\n", val); return VFETCH_ERROR_INVALID_ARGUMENT; } if (args->format_arg) { free(args->format_arg); } args->format_arg = strdup(val); return args->format_arg ? VFETCH_SUCCESS : VFETCH_ERROR_MEMORY_ALLOCATION; } static VfetchError parse_symbols_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; GError *error = NULL; ChafaSymbolMap *test_map = chafa_symbol_map_new(); bool valid = chafa_symbol_map_apply_selectors(test_map, val, &error) != FALSE; chafa_symbol_map_unref(test_map); if (!valid) { (void)fprintf(stderr, "\nERROR: Invalid value for --symbols '%s'.\n", val); if (error && error->message) { (void)fprintf(stderr, " %s\n", error->message); } (void)fputs( " Please refer to `man chafa` for valid symbol classes.\n", stderr); if (error) { g_error_free(error); } return VFETCH_ERROR_INVALID_ARGUMENT; } g_hash_table_insert(args->chafa_options, strdup("--symbols"), strdup(val)); return VFETCH_SUCCESS; } static VfetchError parse_colors_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; bool valid = false; if (strcmp(val, "16") == 0 || strcmp(val, "240") == 0 || strcmp(val, "256") == 0 || strcmp(val, "full") == 0 #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 8) || strcmp(val, "8") == 0 # endif # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 12) || strcmp(val, "16/8") == 0 # endif #endif ) { valid = true; } if (!valid) { (void)fprintf(stderr, "\nERROR: Invalid value for --colors '%s'.\n", val); (void)fputs(" Supported: 16, 240, 256, full" #if defined(CHAFA_MAJOR_VERSION) && defined(CHAFA_MINOR_VERSION) # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 8) ", 8" # endif # if (CHAFA_MAJOR_VERSION > 1) || \ (CHAFA_MAJOR_VERSION == 1 && CHAFA_MINOR_VERSION >= 12) ", 16/8" # endif #endif ".\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } if (args->colors_arg) { free(args->colors_arg); } args->colors_arg = strdup(val); return args->colors_arg ? VFETCH_SUCCESS : VFETCH_ERROR_MEMORY_ALLOCATION; } #define K_HEX_COLOR_LEN 6 static bool is_valid_hex_color(const char *hex_str) { if (!hex_str) { return false; } const char *ptr = hex_str; if (ptr[0] == '#') { ptr++; } if (strlen(ptr) != K_HEX_COLOR_LEN) { return false; } for (int i = 0; i < K_HEX_COLOR_LEN; i++) { if (!isxdigit((unsigned char)ptr[i])) { return false; } } return true; } #undef K_HEX_COLOR_LEN static VfetchError parse_fg_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (!is_valid_hex_color(val)) { (void)fprintf(stderr, "\nERROR: Invalid hex color for --fg '%s'.\n", val); (void)fputs(" Use RRGGBB format (e.g., FF0000).\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } if (args->fg_color_arg) { free(args->fg_color_arg); } args->fg_color_arg = strdup(val); return args->fg_color_arg ? VFETCH_SUCCESS : VFETCH_ERROR_MEMORY_ALLOCATION; } static VfetchError parse_bg_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (!is_valid_hex_color(val)) { (void)fprintf(stderr, "\nERROR: Invalid hex color for --bg '%s'.\n", val); (void)fputs(" Use RRGGBB format (e.g., FF0000).\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } if (args->bg_color_arg) { free(args->bg_color_arg); } args->bg_color_arg = strdup(val); return args->bg_color_arg ? VFETCH_SUCCESS : VFETCH_ERROR_MEMORY_ALLOCATION; } static VfetchError parse_color_space_arg(int *idx, int argc, const char *argv[], VfetchArgs *args) { if (*idx + 1 >= argc) { return VFETCH_ERROR_INVALID_ARGUMENT; } const char *val = argv[++(*idx)]; if (strcmp(val, "rgb") != 0 && strcmp(val, "din99d") != 0) { (void)fprintf( stderr, "\nERROR: Invalid value for --color-space '%s'.\n", val); (void)fputs(" Supported: rgb, din99d.\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } g_hash_table_insert( args->chafa_options, strdup("--color-space"), strdup(val)); return VFETCH_SUCCESS; } static const ArgumentHandler *initialize_argument_handlers(void) { static const ArgumentHandler argument_handlers[] = { {.name = "--file", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_file_arg}, {.name = "--size", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_size_arg}, {.name = "--cell-aspect", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_cell_aspect_arg}, {.name = "--pad", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_padding_arg}, {.name = "--fps", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_fps_arg}, {.name = "--refresh-rate", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_refresh_rate_arg}, {.name = "--audio", .type = ARG_HANDLER_BOOLEAN, .data.boolean_offset = offsetof(VfetchArgs, audio)}, {.name = "--loop", .type = ARG_HANDLER_BOOLEAN, .data.boolean_offset = offsetof(VfetchArgs, loop)}, {.name = "--fetch-tool", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_fetch_tool_arg}, {.name = "--format", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_format_arg}, {.name = "--verbose", .type = ARG_HANDLER_BOOLEAN, .data.boolean_offset = offsetof(VfetchArgs, verbose)}, {.name = "--debug", .type = ARG_HANDLER_BOOLEAN, .data.boolean_offset = offsetof(VfetchArgs, debug)}, {.name = "--colors", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_colors_arg}, {.name = "--fg", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_fg_arg}, {.name = "--bg", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_bg_arg}, {.name = "--symbols", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_symbols_arg}, {.name = "--fill", .type = ARG_HANDLER_CHAFA_MAP, .data.chafa_key = "--fill"}, {.name = "--color-space", .type = ARG_HANDLER_CUSTOM, .data.custom_parser = parse_color_space_arg}, {.name = "--fg-only", .type = ARG_HANDLER_CHAFA_FLAG, .data.chafa_key = "--fg-only"}, {.name = NULL}}; return argument_handlers; } static const ArgumentHandler *get_argument_handler(const char *arg_string) { for (const ArgumentHandler *handler = initialize_argument_handlers(); handler->name != NULL; ++handler) { if (strcmp(arg_string, handler->name) == 0) { return handler; } } return NULL; } static VfetchError process_single_argument(int *idx, int argc, const char *argv[], VfetchArgs *args) { const char *arg = argv[*idx]; const ArgumentHandler *handler = get_argument_handler(arg); VfetchError err = VFETCH_SUCCESS; if (handler) { switch (handler->type) { case ARG_HANDLER_BOOLEAN: { bool temp_bool = true; memcpy((char *)args + handler->data.boolean_offset, &temp_bool, sizeof(bool)); break; } case ARG_HANDLER_CHAFA_MAP: err = parse_chafa_map_arg(idx, argc, argv, args, handler->data.chafa_key); break; case ARG_HANDLER_CHAFA_FLAG: g_hash_table_insert( args->chafa_options, strdup(handler->data.chafa_key), strdup("")); break; case ARG_HANDLER_BOUNDED_FLOAT: { float temp_val = 0.0F; err = parse_bounded_float_arg(idx, argc, argv, &temp_val, handler->name, handler->data.bounded_float.min, handler->data.bounded_float.max); if (err == VFETCH_SUCCESS) { memcpy((char *)args + handler->data.bounded_float.offset, &temp_val, sizeof(float)); } break; } case ARG_HANDLER_CUSTOM: err = handler->data.custom_parser(idx, argc, argv, args); break; default: break; } } else if (strncmp(arg, "--", 2) == 0) { (void)fprintf(stderr, "\nERROR: Unrecognized argument '%s'\n", arg); return VFETCH_ERROR_INVALID_ARGUMENT; } return err; } static VfetchError validate_final_arguments(const VfetchArgs *args) { if (!args->filename) { (void)fputs("\nERROR: Filename required.\n", stderr); (void)fputs(" Please specify the input video with --file " ".\n\n", stderr); return VFETCH_ERROR_INVALID_ARGUMENT; } return VFETCH_SUCCESS; } VfetchError parse_arguments(int argc, const char *argv[], VfetchArgs *args) { VfetchError err = VFETCH_SUCCESS; for (int idx = 1; idx < argc; idx++) { const char *arg = argv[idx]; if (strcmp(arg, "--help") == 0) { print_help(); return VFETCH_SUCCESS_EXIT_EARLY; } err = process_single_argument(&idx, argc, argv, args); if (err) { return err; } } if (!args->format_arg) { args->format_arg = strdup("symbols"); } err = validate_final_arguments(args); if (err) { return err; } return VFETCH_SUCCESS; } #define K_DEFAULT_ANIMATION_WIDTH 80 #define K_DEFAULT_CELL_ASPECT 0.50 #define K_DEFAULT_PAD_X 4 #define K_DEFAULT_PAD_Y 2 #define K_DEFAULT_REFRESH_RATE_HZ 60 VfetchError initialize_arguments(VfetchArgs *args) { if (!args) { return VFETCH_ERROR_INVALID_ARGUMENT; } args->width = K_DEFAULT_ANIMATION_WIDTH; args->height = 0; args->cell_aspect_ratio = K_DEFAULT_CELL_ASPECT; args->pad_x = K_DEFAULT_PAD_X; args->pad_y = K_DEFAULT_PAD_Y; args->fps = 0.0; args->refresh_rate = K_DEFAULT_REFRESH_RATE_HZ; args->audio = false; args->loop = false; args->verbose = false; args->debug = false; args->filename = NULL; args->colors_arg = NULL; args->fetch_tool_arg = NULL; args->fg_color_arg = NULL; args->bg_color_arg = NULL; args->format_arg = NULL; args->chafa_options = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); if (!args->chafa_options) { return VFETCH_ERROR_MEMORY_ALLOCATION; } return VFETCH_SUCCESS; } #undef K_DEFAULT_ANIMATION_WIDTH #undef K_DEFAULT_CELL_ASPECT #undef K_DEFAULT_PAD_X #undef K_DEFAULT_PAD_Y #undef K_DEFAULT_REFRESH_RATE_HZ