summaryrefslogtreecommitdiff
path: root/src/args.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/args.c')
-rw-r--r--src/args.c897
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