diff options
Diffstat (limited to 'src/args.c')
| -rw-r--r-- | src/args.c | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/src/args.c b/src/args.c new file mode 100644 index 0000000..ed5ddc8 --- /dev/null +++ b/src/args.c @@ -0,0 +1,897 @@ +/* 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 <chafa/chafa.h> +#include <glib.h> +#include <chafaconfig.h> +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#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 <video_path> <flags>\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 <path>\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 <W>x<H> or --size <W>\n", stdout); + (void)fputs(" Sets the dimensions of the animation output in character cells.\n", stdout); + (void)fputs(" Format:\n", stdout); + (void)fputs(" <W>: Automatically calculates the correct height to preserve the video's aspect ratio, based on the --cell-aspect.\n", stdout); + (void)fputs(" <W>x<H>: 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 <value>\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 <X>:<Y> or --pad <int>\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(" <X>:<Y>: Sets specific values (e.g., 10:5 shifts 10 spaces right and 5 lines down).\n", stdout); + (void)fputs(" <int>: 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 <value>\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 <value>\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 <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 <mode>\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 <mode>\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 <RRGGBB>\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 <RRGGBB>\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 <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 <list>\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 <symbol-string>\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 <W> or <WxH>.\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 <X> or <X>:<Y>.\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 " + "<path_to_video>.\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 |
