summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
commitf34fb5042d40c186583f9e3009d8a1dd71d07d43 (patch)
tree3f494f3723ebe9f173d0702a97509794f0ef85a5 /src
Initial commitmain
Diffstat (limited to 'src')
-rw-r--r--src/args.c897
-rw-r--r--src/args.h18
-rw-r--r--src/audio.c126
-rw-r--r--src/audio.h18
-rw-r--r--src/audio_defs.h29
-rw-r--r--src/config.h69
-rw-r--r--src/core.c1272
-rw-r--r--src/core.h22
-rw-r--r--src/error.c44
-rw-r--r--src/error.h44
-rw-r--r--src/fetch.c209
-rw-r--r--src/fetch.h25
-rw-r--r--src/state.h69
-rw-r--r--src/utils.c583
-rw-r--r--src/utils.h81
-rw-r--r--src/video.c261
-rw-r--r--src/video.h31
-rw-r--r--src/video_defs.h82
18 files changed, 3880 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
diff --git a/src/args.h b/src/args.h
new file mode 100644
index 0000000..52789f0
--- /dev/null
+++ b/src/args.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__ARGS_H
+#define SRC__ARGS_H
+
+#include "config.h"
+#include "error.h"
+
+VfetchError initialize_arguments(VfetchArgs *args);
+VfetchError parse_arguments(int argc, const char *argv[], VfetchArgs *args);
+
+#endif /* SRC__ARGS_H */
diff --git a/src/audio.c b/src/audio.c
new file mode 100644
index 0000000..3db0d70
--- /dev/null
+++ b/src/audio.c
@@ -0,0 +1,126 @@
+/* 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 "audio.h"
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <string.h>
+#include "audio_defs.h"
+#include "miniaudio.h"
+#include "pl_mpeg.h"
+
+/* This warning is suppressed because this function is a callback that
+ * implements a specific signature required by the miniaudio library
+ * (ma_data_callback). The library itself dictates the order of parameters
+ * (pOutput, pInput), and we cannot change it. Since this function is not called
+ * directly by our own code but by the audio library, the risk of a developer
+ * accidentally swapping the parameters is nonexistent. */
+static void
+data_callback(ma_device *pDevice,
+ /* NOLINTNEXTLINE(bugprone-easily-swappable-parameters) */
+ void *pOutput,
+ const void *pInput,
+ ma_uint32 frame_count) {
+ (void)pInput;
+ AudioState *audio = (AudioState *)pDevice->pUserData;
+ float *out = (float *)pOutput;
+ ma_uint32 total_frames_written = 0;
+
+ while (total_frames_written < frame_count) {
+ if (audio->buffer_index >= audio->buffer_count) {
+ plm_samples_t *samples = plm_decode_audio(audio->plm);
+
+ if (!samples) {
+ (void)memset(out + ((size_t)total_frames_written * 2),
+ 0,
+ (frame_count - total_frames_written) * sizeof(float) * 2);
+ return;
+ }
+
+ (void)memcpy(audio->sample_buffer,
+ samples->interleaved,
+ sizeof(float) * (size_t)samples->count * 2);
+ audio->buffer_index = 0;
+ audio->buffer_count = samples->count;
+ }
+
+ ma_uint32 frames_to_copy = frame_count - total_frames_written;
+ ma_uint32 frames_available = audio->buffer_count - audio->buffer_index;
+ if (frames_to_copy > frames_available) {
+ frames_to_copy = frames_available;
+ }
+
+ (void)memcpy(out + ((size_t)total_frames_written * 2),
+ audio->sample_buffer + ((size_t)audio->buffer_index * 2),
+ sizeof(float) * (size_t)frames_to_copy * 2);
+
+ audio->buffer_index += frames_to_copy;
+ total_frames_written += frames_to_copy;
+ }
+
+ atomic_fetch_add(&audio->frames_consumed, frame_count);
+}
+
+#define K_MPG_SAMPLES_PER_FRAME 1152
+VfetchError initialize_audio(AppState *state, const char *audio_source_file) {
+ AudioState *audio = &state->audio_state;
+ audio->state = state;
+ audio->buffer_index = 0;
+ audio->buffer_count = 0;
+ audio->sample_buffer = NULL;
+
+ audio->plm = plm_create_with_filename(audio_source_file);
+ if (!audio->plm || !plm_get_num_audio_streams(audio->plm)) {
+ return VFETCH_ERROR_MA_INIT;
+ }
+
+ ma_device_config config = ma_device_config_init(ma_device_type_playback);
+ config.playback.format = ma_format_f32;
+ config.playback.channels = 2;
+ config.sampleRate = (ma_uint32)plm_get_samplerate(audio->plm);
+ config.dataCallback = data_callback;
+ config.pUserData = audio;
+
+ if (ma_device_init(NULL, &config, &audio->device) != MA_SUCCESS) {
+ plm_destroy(audio->plm);
+ return VFETCH_ERROR_MA_INIT;
+ }
+
+ audio->sample_buffer = malloc(sizeof(float) * K_MPG_SAMPLES_PER_FRAME * 2);
+ if (!audio->sample_buffer) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ ma_uint32 buffered_frames =
+ audio->device.playback.internalPeriodSizeInFrames *
+ audio->device.playback.internalPeriods;
+ if (audio->device.sampleRate > 0) {
+ audio->output_latency_seconds =
+ (double)buffered_frames / (double)audio->device.sampleRate;
+ } else {
+ audio->output_latency_seconds = 0.0;
+ }
+
+ if (ma_device_start(&audio->device) != MA_SUCCESS) {
+ shutdown_audio(state);
+ return VFETCH_ERROR_MA_INIT;
+ }
+
+ return VFETCH_SUCCESS;
+}
+#undef K_MPG_SAMPLES_PER_FRAME
+
+void shutdown_audio(AppState *state) {
+ ma_device_uninit(&state->audio_state.device);
+ if (state->audio_state.plm) {
+ plm_destroy(state->audio_state.plm);
+ }
+ if (state->audio_state.sample_buffer) {
+ free(state->audio_state.sample_buffer);
+ }
+}
diff --git a/src/audio.h b/src/audio.h
new file mode 100644
index 0000000..1aacb1f
--- /dev/null
+++ b/src/audio.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__AUDIO_H
+#define SRC__AUDIO_H
+
+#include "error.h"
+#include "state.h"
+
+VfetchError initialize_audio(AppState *state, const char *audio_source_file);
+void shutdown_audio(AppState *state);
+
+#endif /* SRC__AUDIO_H */
diff --git a/src/audio_defs.h b/src/audio_defs.h
new file mode 100644
index 0000000..b1d6a85
--- /dev/null
+++ b/src/audio_defs.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__AUDIO_DEFS_H
+#define SRC__AUDIO_DEFS_H
+
+#include <stdint.h>
+#include "miniaudio.h"
+#include "pl_mpeg.h"
+
+typedef struct AppState AppState;
+
+typedef struct AudioState {
+ plm_t *plm;
+ ma_device device;
+ double output_latency_seconds;
+ AppState *state;
+ float *sample_buffer;
+ uint32_t buffer_index;
+ uint32_t buffer_count;
+ _Atomic uint64_t frames_consumed;
+} AudioState;
+
+#endif /* SRC__AUDIO_DEFS_H */
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..f8824ab
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__CONFIG_H
+#define SRC__CONFIG_H
+
+#include <glib.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include "error.h"
+
+typedef enum {
+ ARG_HANDLER_BOOLEAN,
+ ARG_HANDLER_CHAFA_MAP,
+ ARG_HANDLER_CHAFA_FLAG,
+ ARG_HANDLER_BOUNDED_FLOAT,
+ ARG_HANDLER_CUSTOM
+} ArgHandlerType;
+
+typedef struct VfetchArgs {
+ GHashTable *chafa_options;
+ gchar *filename;
+ gchar *colors_arg;
+ gchar *fetch_tool_arg;
+ gchar *fg_color_arg;
+ gchar *bg_color_arg;
+ gchar *format_arg;
+ double cell_aspect_ratio;
+ double fps;
+ uint32_t width;
+ uint32_t height;
+ uint32_t pad_x;
+ uint32_t pad_y;
+ uint32_t num_frames;
+ uint16_t refresh_rate;
+ bool audio;
+ bool loop;
+ bool debug;
+ bool verbose;
+} VfetchArgs;
+
+typedef struct BoundedFloatData {
+ size_t offset;
+ float min;
+ float max;
+} BoundedFloatData;
+
+typedef VfetchError (*CustomArgParserFunc)(int *idx,
+ int argc,
+ const char *argv[],
+ VfetchArgs *args);
+typedef struct ArgumentHandler {
+ union {
+ BoundedFloatData bounded_float;
+ CustomArgParserFunc custom_parser;
+ const char *chafa_key;
+ size_t boolean_offset;
+ } data;
+ const char *name;
+ ArgHandlerType type;
+} ArgumentHandler;
+
+#endif /* SRC__CONFIG_H */
diff --git a/src/core.c b/src/core.c
new file mode 100644
index 0000000..f4a53e7
--- /dev/null
+++ b/src/core.c
@@ -0,0 +1,1272 @@
+/* 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 "core.h"
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <miniaudio.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "args.h"
+#include "audio.h"
+#include "fetch.h"
+#include "pl_mpeg.h"
+#include "state.h"
+#include "utils.h"
+#include "video.h"
+#include "video_defs.h"
+
+static void free_cache_paths(CacheFilePaths *paths) {
+ g_free(paths->data_file_path);
+ g_free(paths->meta_file_path);
+}
+
+static VfetchError build_cache_paths(const char *cache_dir,
+ CacheFilePaths *paths) {
+ paths->data_file_path = g_build_filename(cache_dir, "frames.dat", NULL);
+ paths->meta_file_path = g_build_filename(cache_dir, "frames.meta", NULL);
+
+ if (!paths->data_file_path || !paths->meta_file_path) {
+ log_vfetch_error(VFETCH_ERROR_MEMORY_ALLOCATION,
+ "Failed to construct cache file paths.");
+ free_cache_paths(paths);
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError read_metadata_files(AnimationResources *res,
+ const CacheFilePaths *paths) {
+ res->meta_stream = fopen(paths->meta_file_path, "rb");
+ if (!res->meta_stream) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ if (fseek(res->meta_stream, 0, SEEK_END) != 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ long file_size = ftell(res->meta_stream);
+ if (file_size < 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ if (fseek(res->meta_stream, 0, SEEK_SET) != 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ if ((size_t)file_size % sizeof(FrameMetadata) != 0) {
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ res->anim_args.num_frames = (size_t)file_size / sizeof(FrameMetadata);
+
+ if (res->anim_args.num_frames == 0) {
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ res->anim_args.frames =
+ malloc(res->anim_args.num_frames * sizeof(FrameMetadata));
+ if (!res->anim_args.frames) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ if (fread(res->anim_args.frames,
+ sizeof(FrameMetadata),
+ res->anim_args.num_frames,
+ res->meta_stream) != res->anim_args.num_frames) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError map_frame_data_file(AnimationResources *res,
+ const char *data_file_path) {
+ GError *gerr = NULL;
+
+ res->mapped_file = g_mapped_file_new(data_file_path, FALSE, &gerr);
+
+ if (!res->mapped_file) {
+ if (gerr) {
+ (void)fprintf(stderr, "ERROR: Memory map failed: %s\n", gerr->message);
+ g_error_free(gerr);
+ }
+ return VFETCH_ERROR_MMAP_FAILED;
+ }
+
+ res->anim_args.data_map = g_mapped_file_get_contents(res->mapped_file);
+ res->data_file_size = (off_t)g_mapped_file_get_length(res->mapped_file);
+
+ if (res->data_file_size <= 0) {
+ g_mapped_file_unref(res->mapped_file);
+ res->mapped_file = NULL;
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static bool
+validate_cache_file(const gchar *path, GStatBuf *st_out, bool check_non_empty) {
+ if (g_stat(path, st_out) != 0) {
+ return false;
+ }
+
+ if (check_non_empty && st_out->st_size == 0) {
+ return false;
+ }
+
+ return true;
+}
+
+static gboolean validate_all_cache_files(const gchar *cache_dir_path,
+ const CacheValidationStats *stats) {
+ struct {
+ const gchar *filename;
+ GStatBuf *st_out;
+ gboolean check_non_empty;
+ } validation_rules[] = {
+ {.filename = "_SUCCESS",
+ .st_out = stats->st_success,
+ .check_non_empty = FALSE},
+ {.filename = "frames.dat",
+ .st_out = stats->st_data,
+ .check_non_empty = TRUE},
+ {.filename = "frames.meta",
+ .st_out = stats->st_meta,
+ .check_non_empty = TRUE},
+ };
+
+ gchar *file_path = NULL;
+
+ for (size_t idx = 0;
+ idx < sizeof(validation_rules) / sizeof(validation_rules[0]);
+ ++idx) {
+ file_path =
+ g_build_filename(cache_dir_path, validation_rules[idx].filename, NULL);
+
+ if (!validate_cache_file(file_path,
+ validation_rules[idx].st_out,
+ validation_rules[idx].check_non_empty != FALSE)) {
+ g_free(file_path);
+ return FALSE;
+ }
+
+ g_free(file_path);
+ }
+
+ return TRUE;
+}
+
+static size_t verify_cache_file_integrity(const char *meta_path,
+ const GStatBuf *st_data,
+ const VfetchArgs *args) {
+ FILE *meta_fp = fopen(meta_path, "rb");
+ if (!meta_fp) {
+ return 0;
+ }
+
+ if (fseek(meta_fp, 0, SEEK_END) != 0) {
+ (void)fclose(meta_fp);
+ return 0;
+ }
+
+ long meta_size = ftell(meta_fp);
+ if (meta_size <= 0 || (size_t)meta_size % sizeof(FrameMetadata) != 0) {
+ log_debug(
+ args->debug,
+ "Cache invalid: Meta file size is not a multiple of a frame entry.");
+ (void)fclose(meta_fp);
+ return 0;
+ }
+
+ long last_entry_pos = meta_size - (long)sizeof(FrameMetadata);
+ if (fseek(meta_fp, last_entry_pos, SEEK_SET) != 0) {
+ (void)fclose(meta_fp);
+ return 0;
+ }
+
+ FrameMetadata last_frame = {0};
+ if (fread(&last_frame, sizeof(FrameMetadata), 1, meta_fp) != 1) {
+ (void)fclose(meta_fp);
+ return 0;
+ }
+ (void)fclose(meta_fp);
+
+ uint64_t expected_data_size = last_frame.offset + last_frame.length;
+
+ if ((uint64_t)st_data->st_size != expected_data_size) {
+ log_debug(args->debug,
+ "Cache corrupted: Data file size mismatch. Expected %" PRIu64
+ ", got %" PRIu64,
+ expected_data_size,
+ (uint64_t)st_data->st_size);
+ return 0;
+ }
+
+ return (size_t)meta_size / sizeof(FrameMetadata);
+}
+
+static VfetchError perform_cache_validation(AppState *state,
+ const gchar *cache_dir_path,
+ const VfetchArgs *args,
+ const CacheValidationStats *stats,
+ uint32_t *num_frames_out) {
+ if (!validate_all_cache_files(cache_dir_path, stats)) {
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ char *meta_path = g_build_filename(cache_dir_path, "frames.meta", NULL);
+ if (!meta_path) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ size_t frame_count =
+ verify_cache_file_integrity(meta_path, stats->st_data, args);
+ free(meta_path);
+
+ if (frame_count == 0) {
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ if (frame_count > INT_MAX) {
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "Error: Frame count (%zu) exceeds INT_MAX.",
+ frame_count);
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "Cache is valid with %zu frames.",
+ frame_count);
+ *num_frames_out = (uint32_t)frame_count;
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError is_cache_valid(AppState *state,
+ const gchar *cache_dir_path,
+ const VfetchArgs *args,
+ uint32_t *num_frames) {
+ VfetchError err = VFETCH_ERROR_CACHE_INVALID;
+
+ *num_frames = 0;
+
+ CacheValidationStats *stats = malloc(sizeof(CacheValidationStats));
+ if (!stats) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ stats->st_success = g_new0(GStatBuf, 1);
+ stats->st_data = g_new0(GStatBuf, 1);
+ stats->st_meta = g_new0(GStatBuf, 1);
+ if (!stats->st_success || !stats->st_data || !stats->st_meta) {
+ err = VFETCH_ERROR_MEMORY_ALLOCATION;
+ } else {
+ err = perform_cache_validation(
+ state, cache_dir_path, args, stats, num_frames);
+ }
+
+ free(stats->st_success);
+ free(stats->st_data);
+ free(stats->st_meta);
+ free(stats);
+
+ return err;
+}
+
+static VfetchError setup_cache_directory(AppState *state,
+ const VfetchArgs *args) {
+ GStatBuf file_stat = {0};
+ gchar *base_cache_dir = NULL;
+ gchar *video_filename = NULL;
+ gchar *video_cache_dirname = NULL;
+
+ base_cache_dir = g_build_filename(g_get_user_cache_dir(), "vfetch", NULL);
+
+ if (g_mkdir_with_parents(base_cache_dir, S_IRWXU) != 0) {
+ log_vfetch_error(VFETCH_ERROR_MKDIR_FAILED, base_cache_dir);
+ g_free(base_cache_dir);
+ return VFETCH_ERROR_MKDIR_FAILED;
+ }
+
+ if (g_stat(args->filename, &file_stat) != 0) {
+ log_vfetch_error(VFETCH_ERROR_STAT_FAILED, args->filename);
+ g_free(base_cache_dir);
+ return VFETCH_ERROR_STAT_FAILED;
+ }
+
+ video_filename = g_path_get_basename(args->filename);
+
+ video_cache_dirname =
+ g_strdup_printf("%s_dev%" G_GINT64_FORMAT "_ino%" G_GINT64_FORMAT,
+ video_filename,
+ (gint64)file_stat.st_dev,
+ (gint64)file_stat.st_ino);
+
+ state->video_specific_cache_root =
+ g_build_filename(base_cache_dir, video_cache_dirname, NULL);
+
+ g_free(base_cache_dir);
+ g_free(video_filename);
+ g_free(video_cache_dirname);
+
+ if (!state->video_specific_cache_root) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+#define VFETCH_CACHE_VERSION 1
+static VfetchError build_cache_key(const VfetchArgs *args, CacheKey *key) {
+ (void)memset(key, 0, sizeof(CacheKey));
+
+ GStatBuf file_stat = {0};
+ if (g_stat(args->filename, &file_stat) != 0) {
+ return VFETCH_ERROR_STAT_FAILED;
+ }
+
+ key->file_size = file_stat.st_size;
+ key->file_mtime = file_stat.st_mtime;
+ key->width = args->width;
+ key->height = args->height;
+ key->fps = args->fps;
+ key->cell_aspect_ratio = args->cell_aspect_ratio;
+ key->version_tag = VFETCH_CACHE_VERSION;
+
+ hash_string_to_bytes(args->format_arg, key->format_md5);
+ hash_string_to_bytes(args->colors_arg, key->colors_md5);
+
+ char *symbols = g_hash_table_lookup(args->chafa_options, "--symbols");
+ hash_string_to_bytes(symbols ? symbols : "all", key->symbols_md5);
+
+ char *serialized_opts = serialize_map_for_hashing(args->chafa_options);
+ hash_string_to_bytes(serialized_opts, key->chafa_opts_md5);
+ g_free(serialized_opts);
+
+ return VFETCH_SUCCESS;
+}
+#undef VFETCH_CACHE_VERSION
+
+static VfetchError
+generate_cache_hash(AppState *state, VfetchArgs *args, gchar **out_hash_str) {
+ CacheKey key = {0};
+ VfetchError err = build_cache_key(args, &key);
+ if (err != VFETCH_SUCCESS) {
+ return err;
+ }
+
+ *out_hash_str = hash_cache_key(&key);
+
+ state->current_args_cache_dir =
+ g_build_filename(state->video_specific_cache_root, *out_hash_str, NULL);
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError initialize_pipeline_resources(const VfetchArgs *args,
+ size_t *total_frames_out) {
+ VfetchError err =
+ get_video_frame_count(args->filename, args->fps, total_frames_out);
+ if (err) {
+ log_vfetch_error(err, args->filename);
+ return err;
+ }
+
+ if (*total_frames_out == 0) {
+ log_vfetch_error(VFETCH_ERROR_AV_STREAM_INFO,
+ "Video has zero or negative duration");
+ return VFETCH_ERROR_AV_STREAM_INFO;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError open_cache_files_for_writing(CacheFiles *files,
+ const gchar *cache_dir,
+ gchar **data_path,
+ gchar **meta_path) {
+ *data_path = g_build_filename(cache_dir, "frames.dat", NULL);
+ *meta_path = g_build_filename(cache_dir, "frames.meta", NULL);
+
+ if (!*data_path || !*meta_path) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ files->data_file = fopen(*data_path, "wb");
+ files->meta_file = fopen(*meta_path, "wb");
+
+ if (!files->data_file || !files->meta_file) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError generate_ascii_cache(AppState *state,
+ const VfetchArgs *args) {
+ gchar *data_file_path = NULL;
+ gchar *meta_file_path = NULL;
+ WorkerResources res = {0};
+ CacheFiles files = {0};
+ unsigned long current_offset = 0;
+ VfetchError err = VFETCH_SUCCESS;
+
+ err = initialize_worker_resources(&res, args);
+ if (err != VFETCH_SUCCESS) {
+ cleanup_worker_resources(&res);
+ return err;
+ }
+
+ err = open_cache_files_for_writing(
+ &files, state->current_args_cache_dir, &data_file_path, &meta_file_path);
+
+ if (err == VFETCH_SUCCESS) {
+ err = populate_frame_cache_loop(&res, state, &files, &current_offset);
+ }
+
+ if (files.data_file) {
+ (void)fclose(files.data_file);
+ }
+ if (files.meta_file) {
+ (void)fclose(files.meta_file);
+ }
+ g_free(data_file_path);
+ g_free(meta_file_path);
+ cleanup_worker_resources(&res);
+ return err;
+}
+
+static VfetchError run_processing_pipeline(AppState *state,
+ const VfetchArgs *args,
+ size_t total_frames) {
+ ProgressThreadArgs *progress_args = NULL;
+ pthread_t progress_thread_id = 0;
+ VfetchError err = VFETCH_SUCCESS;
+
+ progress_args = malloc(sizeof(ProgressThreadArgs));
+ if (!progress_args) {
+ log_vfetch_error(VFETCH_ERROR_MEMORY_ALLOCATION, "progress thread args");
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+ progress_args->state = state;
+ progress_args->total_frames = total_frames;
+ if (pthread_create(&progress_thread_id,
+ NULL,
+ progress_display_thread_func,
+ progress_args) != 0) {
+ log_vfetch_error(VFETCH_ERROR_THREAD_CREATION, "progress display thread");
+ free(progress_args);
+ return VFETCH_ERROR_THREAD_CREATION;
+ }
+
+ err = generate_ascii_cache(state, args);
+
+ atomic_store_explicit(
+ &state->processing_finished, true, memory_order_release);
+
+ if (progress_thread_id != 0) {
+ (void)pthread_join(progress_thread_id, NULL);
+ }
+
+ return err;
+}
+
+static VfetchError finalize_cache(const AppState *state) {
+ GStatBuf st_dat = {0};
+ VfetchError err = VFETCH_SUCCESS;
+
+ gchar *data_file_path =
+ g_build_filename(state->current_args_cache_dir, "frames.dat", NULL);
+ if (g_stat(data_file_path, &st_dat) != 0 &&
+ state->ascii_frames_completed > 0) {
+ log_vfetch_error(VFETCH_ERROR_CACHE_INVALID,
+ "Could not stat data file, but frames were written");
+ g_free(data_file_path);
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+ g_free(data_file_path);
+
+ char *success_file_path =
+ g_build_filename(state->current_args_cache_dir, "_SUCCESS", NULL);
+ FILE *success_file = fopen(success_file_path, "w");
+ if (success_file) {
+ (void)fclose(success_file);
+ } else {
+ err = VFETCH_ERROR_FILE_IO;
+ log_vfetch_error(err, success_file_path);
+ }
+ g_free(success_file_path);
+
+ return err;
+}
+
+static VfetchError handle_cache_regeneration(AppState *state,
+ VfetchArgs *args) {
+ size_t total_frames = 0;
+
+ if (!rm_rf(state->current_args_cache_dir)) {
+ die(VFETCH_ERROR_FILE_IO, "Failed to clear cache directory");
+ }
+ if (g_mkdir_with_parents(state->current_args_cache_dir, S_IRWXU) != 0) {
+ log_vfetch_error(VFETCH_ERROR_MKDIR_FAILED, state->current_args_cache_dir);
+ return VFETCH_ERROR_MKDIR_FAILED;
+ }
+
+ VfetchError err = initialize_pipeline_resources(args, &total_frames);
+ if (err) {
+ return err;
+ }
+
+ err = run_processing_pipeline(state, args, total_frames);
+
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ (void)fputs("\nCaching cancelled. Cleaning up partial cache.\n", stderr);
+ (void)rm_rf(state->current_args_cache_dir);
+ return VFETCH_SUCCESS_EXIT_EARLY;
+ }
+
+ if (err != VFETCH_SUCCESS) {
+ (void)fputs("\nPipeline encountered a technical error. Cleaning up partial "
+ "cache.\n",
+ stderr);
+ (void)rm_rf(state->current_args_cache_dir);
+ return err;
+ }
+
+ args->num_frames = (uint32_t)atomic_load_explicit(
+ &state->ascii_frames_completed, memory_order_relaxed);
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "Cache finalized with %u frames.",
+ args->num_frames);
+
+ return finalize_cache(state);
+}
+
+static VfetchError prepare_animation_assets(AppState *state,
+ VfetchArgs *args,
+ bool *cache_was_newly_created) {
+ gchar *current_args_hash_str = NULL;
+ uint32_t num_frames_from_cache = 0;
+
+ VfetchError err = validate_fps(args);
+ if (err != VFETCH_SUCCESS) {
+ return err;
+ }
+ err = setup_cache_directory(state, args);
+ if (err != VFETCH_SUCCESS) {
+ return err;
+ }
+ err = generate_cache_hash(state, args, &current_args_hash_str);
+ if (err != VFETCH_SUCCESS) {
+ return err;
+ }
+
+ *cache_was_newly_created = false;
+ if (is_cache_valid(
+ state, state->current_args_cache_dir, args, &num_frames_from_cache) ==
+ VFETCH_SUCCESS) {
+ args->num_frames = num_frames_from_cache;
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "Loaded settings from a valid and integral cache.");
+ g_free(current_args_hash_str);
+ return VFETCH_SUCCESS;
+ }
+
+ *cache_was_newly_created = true;
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "Cache invalid or not found, re-rendering.");
+
+ err = handle_cache_regeneration(state, args);
+
+ g_free(current_args_hash_str);
+
+ return err;
+}
+
+static void cleanup_animation_resources(AnimationResources *res) {
+ if (res->mapped_file) {
+ g_mapped_file_unref(res->mapped_file);
+ res->mapped_file = NULL;
+ res->anim_args.data_map = NULL;
+ }
+
+ if (res->meta_stream) {
+ (void)fclose(res->meta_stream);
+ res->meta_stream = NULL;
+ }
+
+ free(res->anim_args.frames);
+ res->anim_args.frames = NULL;
+
+ free(res->anim_args.screen_buffer);
+ res->anim_args.screen_buffer = NULL;
+}
+
+static VfetchError load_animation_data(const gchar *cache_dir,
+ AnimationResources *res) {
+ CacheFilePaths paths = {0};
+ VfetchError err = VFETCH_SUCCESS;
+
+ if (build_cache_paths(cache_dir, &paths) != VFETCH_SUCCESS) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ err = read_metadata_files(res, &paths);
+ if (err == VFETCH_SUCCESS) {
+ err = map_frame_data_file(res, paths.data_file_path);
+ }
+
+ free_cache_paths(&paths);
+ return err;
+}
+
+static VfetchError handle_animation_events(AppState *state,
+ size_t *last_displayed_idx_ptr) {
+ const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J";
+
+ if (atomic_load_explicit(&state->terminal_resized, memory_order_acquire)) {
+ if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) <
+ 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ atomic_store_explicit(
+ &state->terminal_resized, false, memory_order_release);
+ *last_displayed_idx_ptr = (size_t)-1;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static void handle_input_char(AppState *state, char char_typed) {
+ if (char_typed == 'q' || char_typed == 'Q') {
+ atomic_store_explicit(&state->exit_requested, true, memory_order_release);
+ } else if (char_typed == 'p' || char_typed == 'P') {
+ bool current = atomic_load_explicit(&state->paused, memory_order_acquire);
+ atomic_store_explicit(&state->paused, (bool)!current, memory_order_release);
+ }
+}
+
+static long perform_blocking_pause(AppState *state, struct pollfd *pfd) {
+ gint64 p_start = 0;
+ gint64 p_end = 0;
+
+ (void)pthread_mutex_lock(&state->audio_state_mutex);
+ if (state->audio_state.device.pContext) {
+ ma_device_stop(&state->audio_state.device);
+ }
+ (void)pthread_mutex_unlock(&state->audio_state_mutex);
+
+ p_start = g_get_monotonic_time();
+
+ while (atomic_load_explicit(&state->paused, memory_order_acquire) &&
+ !atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ if (poll(pfd, 1, -1) > 0) {
+ char char_typed = 0;
+ if (pfd->revents & POLLIN && read(STDIN_FILENO, &char_typed, 1) > 0) {
+ handle_input_char(state, char_typed);
+ }
+ }
+ }
+
+ p_end = g_get_monotonic_time();
+
+ (void)pthread_mutex_lock(&state->audio_state_mutex);
+ if (state->audio_state.device.pContext &&
+ !atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ ma_device_start(&state->audio_state.device);
+ }
+ (void)pthread_mutex_unlock(&state->audio_state_mutex);
+
+ return p_end - p_start;
+}
+
+#define K_MICROSECONDS_IN_MILLISECOND 1000L
+static long wait_for_input(AppState *state, long delay_us) {
+ struct pollfd pfd = {0};
+ pfd.fd = STDIN_FILENO;
+ pfd.events = POLLIN;
+
+ int timeout_ms = (int)(delay_us / K_MICROSECONDS_IN_MILLISECOND);
+ if (timeout_ms < 0) {
+ timeout_ms = 0;
+ }
+
+ if (poll(&pfd, 1, timeout_ms) > 0) {
+ char char_typed = 0;
+ if (pfd.revents & POLLIN && read(STDIN_FILENO, &char_typed, 1) > 0) {
+ handle_input_char(state, char_typed);
+ }
+ }
+
+ if (atomic_load_explicit(&state->paused, memory_order_acquire)) {
+ return perform_blocking_pause(state, &pfd);
+ }
+
+ return 0;
+}
+#undef K_MICROSECONDS_IN_MILLISECOND
+
+static VfetchError render_frame(size_t frame_idx,
+ size_t *last_displayed_idx_ptr,
+ VideoAnimationArgs *anim_args,
+ AppState *state,
+ const VfetchArgs *args) {
+ const char ansi_hide_cursor[] = "\x1b[?25l";
+ size_t final_len = 0;
+
+ if (anim_args->num_frames == 0 || frame_idx >= anim_args->num_frames) {
+ log_vfetch_error(VFETCH_ERROR_INVALID_ARGUMENT,
+ "Attempted to render an out-of-bounds frame index.");
+ return VFETCH_ERROR_INVALID_ARGUMENT;
+ }
+
+ if (frame_idx == *last_displayed_idx_ptr) {
+ return VFETCH_SUCCESS;
+ }
+
+ int written = snprintf(
+ anim_args->screen_buffer, anim_args->screen_buffer_size, "\x1b[H");
+ if (written > 0) {
+ final_len = (size_t)written;
+ }
+
+ FrameMetadata current_frame = anim_args->frames[frame_idx];
+ if (current_frame.offset + current_frame.length >
+ (uint64_t)anim_args->data_file_size) {
+ log_vfetch_error(VFETCH_ERROR_CACHE_INVALID,
+ "Frame index data is out of bounds.");
+ return VFETCH_ERROR_CACHE_INVALID;
+ }
+
+ const char *frame_data_ptr = anim_args->data_map + current_frame.offset;
+ append_block_to_buffer(anim_args->screen_buffer,
+ anim_args->screen_buffer_size,
+ &final_len,
+ args->pad_x,
+ args->pad_y,
+ frame_data_ptr,
+ current_frame.length);
+
+ render_fetch_panel(anim_args->screen_buffer,
+ anim_args->screen_buffer_size,
+ &final_len,
+ state,
+ args);
+
+ if (final_len + sizeof(ansi_hide_cursor) < anim_args->screen_buffer_size) {
+ (void)snprintf(anim_args->screen_buffer + final_len,
+ anim_args->screen_buffer_size - final_len,
+ "%s",
+ ansi_hide_cursor);
+ }
+
+ (void)pthread_mutex_lock(&state->stdout_mutex);
+ ssize_t bytes_written = write(STDOUT_FILENO,
+ anim_args->screen_buffer,
+ strlen(anim_args->screen_buffer));
+ (void)pthread_mutex_unlock(&state->stdout_mutex);
+
+ if (bytes_written < 0 && errno != EINTR) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ *last_displayed_idx_ptr = frame_idx;
+ return VFETCH_SUCCESS;
+}
+
+#define K_MICROSECONDS_IN_SECOND 1000000L
+static VfetchError run_continuous_animation_loop(VideoAnimationArgs *anim_args,
+ AppState *state,
+ const VfetchArgs *args) {
+ const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J";
+ VfetchError err = VFETCH_SUCCESS;
+ gint64 start_time = 0;
+ size_t last_displayed_idx = (size_t)-1;
+
+ if (anim_args->num_frames == 0) {
+ return VFETCH_SUCCESS;
+ }
+
+ double total_duration = anim_args->frames[anim_args->num_frames - 1].pts;
+ start_time = g_get_monotonic_time();
+
+ size_t target_frame_idx = 0;
+ if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) < 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ for (;;) {
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ break;
+ }
+
+ err = handle_animation_events(state, &last_displayed_idx);
+ if (err) {
+ break;
+ }
+
+ gint64 current_time = g_get_monotonic_time();
+ double master_media_clock =
+ (double)(current_time - start_time) / K_MICROSECONDS_IN_SECOND;
+
+ if (master_media_clock >= total_duration) {
+ if (args->loop) {
+ start_time = current_time;
+ target_frame_idx = 0;
+ last_displayed_idx = (size_t)-1;
+ master_media_clock = 0.0;
+ } else {
+ break;
+ }
+ }
+
+ while (target_frame_idx < anim_args->num_frames - 1 &&
+ anim_args->frames[target_frame_idx + 1].pts < master_media_clock) {
+ target_frame_idx++;
+ }
+
+ err = render_frame(
+ target_frame_idx, &last_displayed_idx, anim_args, state, args);
+ if (err) {
+ atomic_store_explicit(&state->exit_requested, true, memory_order_release);
+ break;
+ }
+
+ long refresh_delay_us = K_MICROSECONDS_IN_SECOND / args->refresh_rate;
+ long paused_duration = wait_for_input(state, refresh_delay_us);
+
+ if (paused_duration > 0) {
+ start_time += paused_duration;
+ }
+ }
+
+ return err;
+}
+#undef K_MICROSECONDS_IN_SECOND
+
+static void update_frame_index_from_clock(size_t *current_frame_idx,
+ double master_media_clock,
+ const VideoAnimationArgs *anim_args,
+ AppState *state) {
+ if (anim_args->num_frames == 0) {
+ return;
+ }
+ double total_duration = anim_args->frames[anim_args->num_frames - 1].pts;
+
+ double current_loop_clock = fmod(master_media_clock, total_duration);
+
+ if (current_loop_clock < anim_args->frames[*current_frame_idx].pts) {
+ *current_frame_idx = 0;
+ }
+
+ while (*current_frame_idx < anim_args->num_frames - 1 &&
+ anim_args->frames[*current_frame_idx + 1].pts <= current_loop_clock) {
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ break;
+ }
+ (*current_frame_idx)++;
+ }
+}
+
+#define K_MICROSECONDS_IN_SECOND 1000000.0
+#define K_MAX_SYNC_DRIFT_SECONDS 2.0
+static double calculate_media_clock_for_sync(AppState *state) {
+ const double sample_rate =
+ (double)state->audio_state.device.playback.internalSampleRate;
+ const double audio_latency = state->audio_state.output_latency_seconds;
+
+ uint64_t frames_consumed = atomic_load_explicit(
+ &state->audio_state.frames_consumed, memory_order_acquire);
+ double consumption_clock =
+ (sample_rate > 0) ? (double)frames_consumed / sample_rate : 0.0;
+
+ gint64 current_wall_time = g_get_monotonic_time();
+
+ if (state->media_sync_start_time == 0) {
+ state->media_sync_start_time =
+ current_wall_time - (gint64)((consumption_clock - audio_latency) *
+ K_MICROSECONDS_IN_SECOND);
+ }
+
+ double wall_clock =
+ (double)(current_wall_time - state->media_sync_start_time) /
+ K_MICROSECONDS_IN_SECOND;
+
+ double max_allowed = consumption_clock;
+ double min_allowed = consumption_clock - K_MAX_SYNC_DRIFT_SECONDS;
+ if (min_allowed < 0.0) {
+ min_allowed = 0.0;
+ }
+
+ if (wall_clock > max_allowed) {
+ wall_clock = max_allowed;
+ state->media_sync_start_time =
+ current_wall_time - (gint64)(wall_clock * K_MICROSECONDS_IN_SECOND);
+ } else if (wall_clock < min_allowed) {
+ wall_clock = min_allowed;
+ state->media_sync_start_time =
+ current_wall_time - (gint64)(wall_clock * K_MICROSECONDS_IN_SECOND);
+ }
+
+ return wall_clock;
+}
+#undef K_MICROSECONDS_IN_SECOND
+#undef K_MAX_SYNC_DRIFT_SECONDS
+
+#define K_MICROSECONDS_IN_SECOND 1000000L
+static VfetchError run_animation_with_sync(VideoAnimationArgs *anim_args,
+ AppState *state,
+ const VfetchArgs *args) {
+ size_t last_displayed_idx = (size_t)-1;
+ const char k_clear_command[] = "\x1b[H\x1b[2J\x1b[3J";
+ VfetchError err = VFETCH_SUCCESS;
+
+ if (anim_args->num_frames == 0) {
+ return VFETCH_SUCCESS;
+ }
+
+ double total_duration = anim_args->frames[anim_args->num_frames - 1].pts;
+
+ size_t current_frame_idx = 0;
+ if (write(STDOUT_FILENO, k_clear_command, sizeof(k_clear_command) - 1) < 0) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ for (;;) {
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ break;
+ }
+
+ err = handle_animation_events(state, &last_displayed_idx);
+ if (err) {
+ break;
+ }
+
+ double master_media_clock = calculate_media_clock_for_sync(state);
+
+ if (master_media_clock >= total_duration) {
+ if (args->loop) {
+ (void)pthread_mutex_lock(&state->audio_state_mutex);
+ plm_rewind(state->audio_state.plm);
+ atomic_store_explicit(
+ &state->audio_state.frames_consumed, 0, memory_order_release);
+ (void)pthread_mutex_unlock(&state->audio_state_mutex);
+
+ current_frame_idx = 0;
+ last_displayed_idx = (size_t)-1;
+ continue;
+ }
+ break;
+ }
+
+ update_frame_index_from_clock(
+ &current_frame_idx, master_media_clock, anim_args, state);
+
+ if (current_frame_idx != last_displayed_idx) {
+ err = render_frame(
+ current_frame_idx, &last_displayed_idx, anim_args, state, args);
+ if (err) {
+ break;
+ }
+ }
+
+ long sleep_us = K_MICROSECONDS_IN_SECOND / args->refresh_rate;
+ wait_for_input(state, sleep_us);
+ }
+ return err;
+}
+#undef K_MICROSECONDS_IN_SECOND
+
+static VfetchError run_animation_extract_audio(AppState *state,
+ const VfetchArgs *args,
+ VideoAnimationArgs *anim_args) {
+ VfetchError err = VFETCH_SUCCESS;
+
+ const char *audio_source = args->filename;
+ err = initialize_audio(state, audio_source);
+
+ if (err) {
+ bool was_interrupted =
+ atomic_load_explicit(&state->exit_requested, memory_order_acquire);
+
+ if (was_interrupted) {
+ shutdown_audio(state);
+ return VFETCH_SUCCESS;
+ }
+
+ log_vfetch_error(err, "Could not initialize audio from video file.");
+ return err;
+ }
+ err = run_animation_with_sync(anim_args, state, args);
+
+ shutdown_audio(state);
+ return err;
+}
+
+static VfetchError select_and_run_animation(AppState *state,
+ const VfetchArgs *args,
+ VideoAnimationArgs *anim_args) {
+ if (args->audio) {
+ return run_animation_extract_audio(state, args, anim_args);
+ }
+ return run_continuous_animation_loop(anim_args, state, args);
+}
+
+#define K_SCREEN_BUFFER_SIZE (2 * 1024 * 1024)
+static VfetchError run_animation_loop(AppState *state, const VfetchArgs *args) {
+ atomic_store_explicit(
+ &state->animation_has_started, true, memory_order_release);
+
+ AnimationResources res = {0};
+ res.mapped_file = NULL;
+
+ VfetchError err = load_animation_data(state->current_args_cache_dir, &res);
+ if (err) {
+ cleanup_animation_resources(&res);
+ log_vfetch_error(err, "Failed to load animation data from cache");
+ return err;
+ }
+
+ res.anim_args.data_file_size = res.data_file_size;
+ res.anim_args.screen_buffer_size = (long)K_SCREEN_BUFFER_SIZE;
+ res.anim_args.screen_buffer = malloc(res.anim_args.screen_buffer_size);
+ if (!res.anim_args.screen_buffer) {
+ err = VFETCH_ERROR_MEMORY_ALLOCATION;
+ log_vfetch_error(err, "Failed to allocate screen buffer.");
+ cleanup_animation_resources(&res);
+ return err;
+ }
+
+ err = select_and_run_animation(state, args, &res.anim_args);
+
+ cleanup_animation_resources(&res);
+
+ return err;
+}
+#undef K_SCREEN_BUFFER_SIZE
+
+static VfetchError initialize_paths_and_geometry(VfetchArgs *args) {
+
+ gchar *abs_path = g_canonicalize_filename(args->filename, NULL);
+ if (abs_path == NULL) {
+ return VFETCH_ERROR_PATH_FAILED;
+ }
+ g_free(args->filename);
+ args->filename = abs_path;
+
+ VfetchError err = resolve_geometry(args);
+ if (err) {
+ return err;
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError initialize_app_state(AppState *state) {
+ global_signal_handler_state_ptr = state;
+
+ if (pthread_mutex_init(&state->fetch_mutex, NULL) != 0 ||
+ pthread_mutex_init(&state->audio_state_mutex, NULL) != 0 ||
+ pthread_mutex_init(&state->stdout_mutex, NULL) != 0) {
+ return VFETCH_ERROR_MUTEX_INIT;
+ }
+
+ atomic_store_explicit(
+ &state->processing_finished, false, memory_order_release);
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError setup_environment(AppState *state) {
+ struct sigaction sig_action = {0};
+ VfetchError err = VFETCH_SUCCESS;
+
+ setup_terminal(state);
+ set_cursor_visible(state, false);
+
+ if (pipe(global_signal_pipe) == -1) {
+ err = VFETCH_ERROR_PIPE_CREATION;
+ log_vfetch_error(err, "Failed to create signal pipe.");
+ return err;
+ }
+
+ (void)memset(&sig_action, 0, sizeof(sig_action));
+
+ sig_action.sa_flags = SA_SIGINFO;
+ sig_action.sa_sigaction = app_signal_handler;
+ (void)sigaction(SIGINT, &sig_action, NULL);
+ (void)sigaction(SIGTERM, &sig_action, NULL);
+
+ sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
+ sig_action.sa_sigaction = app_resize_handler;
+ (void)sigaction(SIGWINCH, &sig_action, NULL);
+
+ return err;
+}
+
+static VfetchError run_core_logic(AppState *state, VfetchArgs *args) {
+ bool cache_was_newly_created = false;
+
+ VfetchError err = initialize_paths_and_geometry(args);
+ if (err) {
+ log_vfetch_error(err, "Failed to initialize paths and geometry");
+ return err;
+ }
+
+ if (!args->fetch_tool_arg || strcmp(args->fetch_tool_arg, "none") != 0) {
+ err = initialize_fetch_utility(state, args);
+ if (err) {
+ return err;
+ }
+ }
+
+ err = prepare_animation_assets(state, args, &cache_was_newly_created);
+ if (err == VFETCH_SUCCESS_EXIT_EARLY) {
+ return VFETCH_SUCCESS;
+ }
+ if (err) {
+ return err;
+ }
+
+ bool should_exit =
+ atomic_load_explicit(&state->exit_requested, memory_order_acquire);
+ if (should_exit) {
+ (void)fputs("\nOperation cancelled during asset preparation.\n", stderr);
+ err = VFETCH_SUCCESS;
+ return err;
+ }
+
+ err = start_fetch_thread(state, args);
+ if (err) {
+ return err;
+ }
+
+ err = run_animation_loop(state, args);
+
+ return err;
+}
+
+static void teardown_threads_and_pipes(AppState *state) {
+ atomic_store_explicit(&state->exit_requested, true, memory_order_release);
+
+ if (global_signal_pipe[1] != -1) {
+ (void)close(global_signal_pipe[1]);
+ global_signal_pipe[1] = -1;
+ }
+ if (state->signal_monitor_thread_id != 0) {
+ (void)pthread_join(state->signal_monitor_thread_id, NULL);
+ }
+ if (global_signal_pipe[0] != -1) {
+ (void)close(global_signal_pipe[0]);
+ global_signal_pipe[0] = -1;
+ }
+}
+
+static void cleanup_args_resources(VfetchArgs *args) {
+ if (!args) {
+ return;
+ }
+
+ g_free(args->filename);
+ g_free(args->colors_arg);
+ g_free(args->fg_color_arg);
+ g_free(args->bg_color_arg);
+ g_free(args->format_arg);
+ g_free(args->fetch_tool_arg);
+
+ if (args->chafa_options) {
+ g_hash_table_destroy(args->chafa_options);
+ }
+}
+
+VfetchError execute_program(int argc,
+ const char *argv[],
+ AppState *state,
+ VfetchArgs *args) {
+ VfetchError err = initialize_arguments(args);
+ if (err) {
+ return err;
+ }
+
+ err = parse_arguments(argc, argv, args);
+ if (err) {
+ return err;
+ }
+
+ err = initialize_app_state(state);
+ if (err) {
+ return err;
+ }
+
+ state->args = args;
+
+ err = setup_environment(state);
+ if (err) {
+ return err;
+ }
+
+ if (pthread_create(&state->signal_monitor_thread_id,
+ NULL,
+ signal_monitor_thread_func,
+ state) != 0) {
+ err = VFETCH_ERROR_THREAD_CREATION;
+ log_vfetch_error(err, "signal monitor thread");
+ return err;
+ }
+
+ err = run_core_logic(state, args);
+
+ return err;
+}
+
+void cleanup_application(AppState *state, VfetchArgs *args) {
+ if (state) {
+ teardown_threads_and_pipes(state);
+ restore_terminal();
+
+ if (state->audio_state.device.pContext != NULL) {
+ shutdown_audio(state);
+ }
+
+ pthread_mutex_destroy(&state->fetch_mutex);
+ pthread_mutex_destroy(&state->audio_state_mutex);
+
+ g_free(state->video_specific_cache_root);
+ g_free(state->current_args_cache_dir);
+ g_free(state->live_fetch_output);
+ g_free(state->fetch_command);
+ }
+
+ global_signal_handler_state_ptr = NULL;
+ cleanup_args_resources(args);
+}
diff --git a/src/core.h b/src/core.h
new file mode 100644
index 0000000..7510e1c
--- /dev/null
+++ b/src/core.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__CORE_H
+#define SRC__CORE_H
+
+#include "config.h"
+#include "error.h"
+#include "state.h"
+
+VfetchError execute_program(int argc,
+ const char *argv[],
+ AppState *state,
+ VfetchArgs *args);
+void cleanup_application(AppState *state, VfetchArgs *args);
+
+#endif /* SRC__CORE_H */
diff --git a/src/error.c b/src/error.c
new file mode 100644
index 0000000..af84cba
--- /dev/null
+++ b/src/error.c
@@ -0,0 +1,44 @@
+/* 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 "error.h"
+#include <sysexits.h>
+
+int map_vfetch_error_to_exit_code(VfetchError err) {
+ switch (err) {
+ case VFETCH_SUCCESS:
+ case VFETCH_SUCCESS_EXIT_EARLY:
+ return EX_OK;
+ case VFETCH_ERROR_INVALID_ARGUMENT:
+ return EX_USAGE;
+ case VFETCH_ERROR_FILE_IO:
+ case VFETCH_ERROR_MKDIR_FAILED:
+ case VFETCH_ERROR_STAT_FAILED:
+ case VFETCH_ERROR_PATH_FAILED:
+ return EX_IOERR;
+ case VFETCH_ERROR_VIDEO_OPEN_FAILED:
+ case VFETCH_ERROR_AV_STREAM_INFO:
+ case VFETCH_ERROR_CACHE_INVALID:
+ return EX_DATAERR;
+ case VFETCH_ERROR_EXEC_FAILED:
+ case VFETCH_ERROR_COMMAND_FAILED:
+ case VFETCH_ERROR_MA_INIT:
+ return EX_UNAVAILABLE;
+ case VFETCH_ERROR_MEMORY_ALLOCATION:
+ case VFETCH_ERROR_PIPE_CREATION:
+ case VFETCH_ERROR_FORK_FAILED:
+ case VFETCH_ERROR_WAITPID_FAILED:
+ case VFETCH_ERROR_THREAD_CREATION:
+ case VFETCH_ERROR_MUTEX_INIT:
+ case VFETCH_ERROR_MMAP_FAILED:
+ return EX_OSERR;
+ case VFETCH_ERROR_CHAFA_CONFIG:
+ default:
+ return EX_SOFTWARE;
+ }
+}
diff --git a/src/error.h b/src/error.h
new file mode 100644
index 0000000..fe7c309
--- /dev/null
+++ b/src/error.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__ERROR_H
+#define SRC__ERROR_H
+
+typedef enum {
+ VFETCH_SUCCESS = 0,
+ VFETCH_SUCCESS_EXIT_EARLY,
+ VFETCH_ERROR_MEMORY_ALLOCATION,
+ VFETCH_ERROR_INVALID_ARGUMENT,
+ VFETCH_ERROR_FILE_IO,
+ VFETCH_ERROR_PIPE_CREATION,
+ VFETCH_ERROR_FORK_FAILED,
+ VFETCH_ERROR_EXEC_FAILED,
+ VFETCH_ERROR_WAITPID_FAILED,
+ VFETCH_ERROR_COMMAND_FAILED,
+ VFETCH_ERROR_MKDIR_FAILED,
+ VFETCH_ERROR_STAT_FAILED,
+ VFETCH_ERROR_PATH_FAILED,
+ VFETCH_ERROR_THREAD_CREATION,
+ VFETCH_ERROR_MUTEX_INIT,
+ VFETCH_ERROR_MMAP_FAILED,
+ VFETCH_ERROR_VIDEO_OPEN_FAILED,
+ VFETCH_ERROR_AV_STREAM_INFO,
+ VFETCH_ERROR_MA_INIT,
+ VFETCH_ERROR_CACHE_INVALID,
+ VFETCH_ERROR_CHAFA_CONFIG
+} VfetchError;
+
+#ifdef __GNUC__
+# define ATTR_CONST __attribute__((const))
+#else
+# define ATTR_CONST
+#endif
+int map_vfetch_error_to_exit_code(VfetchError err) ATTR_CONST;
+#undef ATTR_CONST
+
+#endif /* SRC__ERROR_H */
diff --git a/src/fetch.c b/src/fetch.c
new file mode 100644
index 0000000..270089e
--- /dev/null
+++ b/src/fetch.c
@@ -0,0 +1,209 @@
+/* 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 "fetch.h"
+#include <glib.h>
+#include <sys/ioctl.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include "utils.h"
+
+static VfetchError try_fastfetch(AppState *state, const VfetchArgs *args) {
+ VfetchError err =
+ run_command_fork_output("fastfetch --logo none --pipe false",
+ &state->live_fetch_output,
+ args->debug);
+ if (err == VFETCH_SUCCESS) {
+ state->fetch_command = g_strdup("fastfetch --logo none --pipe false");
+ if (!state->fetch_command) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+ }
+
+ return err;
+}
+
+static VfetchError try_neofetch(AppState *state, const VfetchArgs *args) {
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "fastfetch failed, trying neofetch...");
+
+ VfetchError err = run_command_fork_output(
+ "neofetch --off", &state->live_fetch_output, args->debug);
+ if (err == VFETCH_SUCCESS) {
+ state->fetch_command = g_strdup("neofetch --off");
+ if (!state->fetch_command) {
+ err = VFETCH_ERROR_MEMORY_ALLOCATION;
+ return err;
+ }
+ }
+
+ return err;
+}
+
+static VfetchError set_fetch_unavailable(void) {
+ (void)fputs("\nERROR: Neither 'fastfetch' nor 'neofetch' could be found in "
+ "your system's PATH.\n\n",
+ stderr);
+ (void)fputs(" Action Required:\n", stderr);
+ (void)fputs(" - Please install either 'fastfetch' or 'neofetch'.\n",
+ stderr);
+ (void)fputs(" - Alternatively, to disable the system information display, "
+ "use the '--fetch-tool none' flag.\n\n",
+ stderr);
+
+ return VFETCH_ERROR_COMMAND_FAILED;
+}
+
+static VfetchError fail_fetch_unavailable_override(const char *tool_name) {
+ (void)fprintf(stderr,
+ "\nERROR: The specified fetch tool ('%s') is not available in "
+ "your system's PATH.\n\n",
+ tool_name);
+ (void)fputs(" Action Required:\n", stderr);
+ (void)fputs(
+ " - Please install the tool or correct the --fetch-tool argument.\n",
+ stderr);
+ (void)fputs(" - To disable the system information display, use "
+ "'--fetch-tool none'.\n\n",
+ stderr);
+
+ return VFETCH_ERROR_COMMAND_FAILED;
+}
+
+static VfetchError handle_fetch_override(AppState *state,
+ const VfetchArgs *args) {
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "User specified fetch tool: '%s'",
+ args->fetch_tool_arg);
+
+ if (strcmp(args->fetch_tool_arg, "fastfetch") == 0) {
+ if (try_fastfetch(state, args) != VFETCH_SUCCESS) {
+ return fail_fetch_unavailable_override("fastfetch");
+ }
+ } else if (strcmp(args->fetch_tool_arg, "neofetch") == 0) {
+ if (try_neofetch(state, args) != VFETCH_SUCCESS) {
+ return fail_fetch_unavailable_override("neofetch");
+ }
+ }
+
+ return VFETCH_SUCCESS;
+}
+
+static VfetchError handle_fetch_fallback(AppState *state,
+ const VfetchArgs *args) {
+ log_verbose(state,
+ args->verbose,
+ args->debug,
+ "No fetch tool specified, using fallback logic.");
+
+ if (try_fastfetch(state, args) == VFETCH_SUCCESS) {
+ return VFETCH_SUCCESS;
+ }
+ if (try_neofetch(state, args) == VFETCH_SUCCESS) {
+ return VFETCH_SUCCESS;
+ }
+
+ return set_fetch_unavailable();
+}
+
+VfetchError initialize_fetch_utility(AppState *state, const VfetchArgs *args) {
+ log_verbose(
+ state, args->verbose, args->debug, "Initializing fetch utility...");
+
+ if (args->fetch_tool_arg) {
+ return handle_fetch_override(state, args);
+ }
+
+ return handle_fetch_fallback(state, args);
+}
+
+VfetchError start_fetch_thread(AppState *state, VfetchArgs *args) {
+ if (args->fetch_tool_arg && strcmp(args->fetch_tool_arg, "none") == 0) {
+ state->fetch_command = g_strdup("echo");
+ state->live_fetch_output = g_strdup("");
+ if (!state->fetch_command || !state->live_fetch_output) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+ return VFETCH_SUCCESS;
+ }
+
+ if (!state->fetch_command) {
+ VfetchError err = initialize_fetch_utility(state, args);
+ if (err) {
+ return err;
+ }
+ }
+
+ atomic_store_explicit(&state->last_fetch_lines,
+ count_lines(state->live_fetch_output),
+ memory_order_relaxed);
+
+ return VFETCH_SUCCESS;
+}
+
+#define K_DEFAULT_TERMINAL_WIDTH 80
+void render_fetch_panel(char *screen_buffer,
+ size_t screen_buffer_size,
+ size_t *current_len,
+ AppState *state,
+ const VfetchArgs *args) {
+ struct winsize window_size = {0};
+ char *fetch_output_copy = NULL;
+
+ size_t terminal_width = K_DEFAULT_TERMINAL_WIDTH;
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window_size) == 0 &&
+ window_size.ws_col > 0) {
+ terminal_width = window_size.ws_col;
+ }
+
+ (void)pthread_mutex_lock(&state->fetch_mutex);
+ if (state->live_fetch_output) {
+ fetch_output_copy = g_strdup(state->live_fetch_output);
+ }
+ size_t current_fetch_lines =
+ atomic_load_explicit(&state->last_fetch_lines, memory_order_relaxed);
+ size_t prev_fetch_lines = state->previous_fetch_lines;
+ state->previous_fetch_lines = current_fetch_lines;
+ (void)pthread_mutex_unlock(&state->fetch_mutex);
+
+ uint32_t fetch_x = args->pad_x + args->width + 4;
+ size_t clear_width = terminal_width > fetch_x ? terminal_width - fetch_x : 0;
+ size_t lines_to_clear = (prev_fetch_lines > current_fetch_lines)
+ ? prev_fetch_lines
+ : current_fetch_lines;
+ if (lines_to_clear > 0 && clear_width > 0) {
+ Rect rect_to_clear = {.height = lines_to_clear,
+ .width = clear_width,
+ .start_x = fetch_x,
+ .start_y = args->pad_y};
+ clear_rect_in_buffer(
+ screen_buffer, screen_buffer_size, current_len, &rect_to_clear);
+ }
+
+ const char *fetch_output_to_draw =
+ fetch_output_copy ? fetch_output_copy : "...";
+ append_block_to_buffer(screen_buffer,
+ screen_buffer_size,
+ current_len,
+ fetch_x,
+ args->pad_y,
+ fetch_output_to_draw,
+ strlen(fetch_output_to_draw));
+
+ g_free(fetch_output_copy);
+}
+#undef K_DEFAULT_TERMINAL_WIDTH
diff --git a/src/fetch.h b/src/fetch.h
new file mode 100644
index 0000000..4f77abb
--- /dev/null
+++ b/src/fetch.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__FETCH_H
+#define SRC__FETCH_H
+
+#include <stddef.h>
+#include "config.h"
+#include "error.h"
+#include "state.h"
+
+VfetchError initialize_fetch_utility(AppState *state, const VfetchArgs *args);
+VfetchError start_fetch_thread(AppState *state, VfetchArgs *args);
+void render_fetch_panel(char *screen_buffer,
+ size_t screen_buffer_size,
+ size_t *current_len,
+ AppState *state,
+ const VfetchArgs *args);
+
+#endif /* SRC__FETCH_H */
diff --git a/src/state.h b/src/state.h
new file mode 100644
index 0000000..20e1a7b
--- /dev/null
+++ b/src/state.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__STATE_H
+#define SRC__STATE_H
+
+#include <glib.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <termios.h>
+#include "audio_defs.h"
+#include "config.h"
+
+typedef struct AppState {
+ VfetchArgs *args;
+ gchar *video_specific_cache_root;
+ gchar *current_args_cache_dir;
+ gchar *live_fetch_output;
+ gchar *fetch_command;
+ AudioState audio_state;
+ struct termios original_termios;
+ gint64 media_sync_start_time;
+ pthread_mutex_t audio_state_mutex;
+ pthread_mutex_t fetch_mutex;
+ pthread_mutex_t stdout_mutex;
+ pthread_t signal_monitor_thread_id;
+ pthread_t fetch_thread_id;
+ _Atomic size_t ascii_frames_completed;
+ _Atomic size_t last_fetch_lines;
+ _Atomic size_t previous_fetch_lines;
+ _Atomic bool terminal_resized;
+ _Atomic bool exit_requested;
+ _Atomic bool animation_has_started;
+ _Atomic bool termios_saved;
+ _Atomic bool cursor_visible;
+ _Atomic bool processing_finished;
+ _Atomic bool paused;
+} AppState;
+
+typedef struct ProgressThreadArgs {
+ AppState *state;
+ size_t total_frames;
+} ProgressThreadArgs;
+
+typedef struct FetchUpdaterArgs {
+ AppState *state;
+ VfetchArgs *args;
+ bool verbose_flag;
+ bool debug_flag;
+} FetchUpdaterArgs;
+
+/* Required for the POSIX signal handler, which has a fixed signature and cannot
+ * be passed user context. This global is the standard mechanism for the handler
+ * to access application state.
+ * NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) */
+extern AppState *global_signal_handler_state_ptr;
+
+/* Required for the self-pipe mechanism, which allows the signal handler to
+ * safely communicate with the main application thread.
+ * NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) */
+extern int global_signal_pipe[2];
+
+#endif /* SRC__STATE_H */
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..9a4487f
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,583 @@
+/* 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 "utils.h"
+#include <sys/types.h>
+#include <errno.h>
+#include <ftw.h>
+#include <miniaudio.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+#include "config.h"
+#include "state.h"
+
+void log_debug(bool debug_flag, const char *format, ...) {
+ if (!debug_flag) {
+ return;
+ }
+
+ va_list args;
+ (void)fputs("[DEBUG] ", stderr);
+ va_start(args, format);
+ (void)vfprintf(stderr, format, args);
+ va_end(args);
+ (void)fputc('\n', stderr);
+ (void)fflush(stderr);
+}
+
+void log_verbose(AppState *state,
+ bool verbose_flag,
+ bool debug_flag,
+ const char *format,
+ ...) {
+ if (!verbose_flag && !debug_flag) {
+ return;
+ }
+
+ FILE *const stream = (int)debug_flag ? stderr : stdout;
+ if (stream == stdout) {
+ (void)pthread_mutex_lock(&state->stdout_mutex);
+ }
+
+ va_list args;
+ (void)fputs("[VERBOSE] ", stream);
+ va_start(args, format);
+ (void)vfprintf(stream, format, args);
+ va_end(args);
+ (void)fputc('\n', stream);
+ (void)fflush(stream);
+
+ if (stream == stdout) {
+ (void)pthread_mutex_unlock(&state->stdout_mutex);
+ }
+}
+
+#define K_ERROR_BUFFER_SIZE 256
+void log_vfetch_error(VfetchError err, const char *context_msg) {
+ const char *err_str = NULL;
+
+ switch (err) {
+ case VFETCH_SUCCESS:
+ case VFETCH_SUCCESS_EXIT_EARLY:
+ return;
+ case VFETCH_ERROR_MEMORY_ALLOCATION:
+ err_str = "Memory allocation failed";
+ break;
+ case VFETCH_ERROR_INVALID_ARGUMENT:
+ err_str = "Invalid argument provided";
+ break;
+ case VFETCH_ERROR_FILE_IO:
+ err_str = "File I/O error";
+ break;
+ case VFETCH_ERROR_PIPE_CREATION:
+ err_str = "Pipe creation failed";
+ break;
+ case VFETCH_ERROR_FORK_FAILED:
+ err_str = "Fork failed";
+ break;
+ case VFETCH_ERROR_EXEC_FAILED:
+ err_str = "Exec failed";
+ break;
+ case VFETCH_ERROR_WAITPID_FAILED:
+ err_str = "Waitpid failed";
+ break;
+ case VFETCH_ERROR_COMMAND_FAILED:
+ err_str = "Command failed";
+ break;
+ case VFETCH_ERROR_MKDIR_FAILED:
+ err_str = "Failed to create directory";
+ break;
+ case VFETCH_ERROR_STAT_FAILED:
+ err_str = "Failed to stat file";
+ break;
+ case VFETCH_ERROR_PATH_FAILED:
+ err_str = "Failed to resolve path";
+ break;
+ case VFETCH_ERROR_THREAD_CREATION:
+ err_str = "Thread creation failed";
+ break;
+ case VFETCH_ERROR_MUTEX_INIT:
+ err_str = "Mutex initialization failed";
+ break;
+ case VFETCH_ERROR_MMAP_FAILED:
+ err_str = "Mmap failed";
+ break;
+ case VFETCH_ERROR_VIDEO_OPEN_FAILED:
+ err_str = "Could not open video file (MPEG-1 required)";
+ break;
+ case VFETCH_ERROR_AV_STREAM_INFO:
+ err_str = "Could not read video stream info";
+ break;
+ case VFETCH_ERROR_MA_INIT:
+ err_str = "Miniaudio initialization failed";
+ break;
+ case VFETCH_ERROR_CACHE_INVALID:
+ err_str = "Cache is invalid or corrupt";
+ break;
+ case VFETCH_ERROR_CHAFA_CONFIG:
+ err_str = "Chafa configuration failed";
+ break;
+ default:
+ err_str = "Unknown Error";
+ break;
+ }
+
+ if (context_msg && context_msg[0] != '\0') {
+ (void)fprintf(stderr, "ERROR: %s: %s\n", err_str, context_msg);
+ } else {
+ (void)fprintf(stderr, "ERROR: %s\n", err_str);
+ }
+
+ bool is_sys_err =
+ (err == VFETCH_ERROR_FILE_IO || err == VFETCH_ERROR_STAT_FAILED ||
+ err == VFETCH_ERROR_MKDIR_FAILED || err == VFETCH_ERROR_FORK_FAILED ||
+ err == VFETCH_ERROR_MMAP_FAILED) != 0;
+
+ if (is_sys_err && errno != 0) {
+ char err_buf[K_ERROR_BUFFER_SIZE] = {0};
+ if (strerror_r(errno, err_buf, sizeof(err_buf)) == 0) {
+ (void)fprintf(stderr, " System error: %s\n", err_buf);
+ }
+ }
+
+ errno = 0;
+}
+#undef K_ERROR_BUFFER_SIZE
+
+_Noreturn void die(VfetchError err, const char *context_msg) {
+ log_vfetch_error(err, context_msg);
+ restore_terminal();
+ exit(map_vfetch_error_to_exit_code(err));
+}
+
+void print_progress(AppState *state, size_t current, size_t total) {
+ if (total == 0) {
+ return;
+ }
+
+ char *status_line =
+ g_strdup_printf("\rCaching: %zu / %zu \x1b[K", current, total);
+
+ if (status_line != NULL) {
+ (void)pthread_mutex_lock(&state->stdout_mutex);
+ (void)fputs(status_line, stdout);
+ (void)fflush(stdout);
+ (void)pthread_mutex_unlock(&state->stdout_mutex);
+
+ g_free(status_line);
+ }
+}
+
+#define K_PROGRESS_UPDATE_INTERVAL_NS 100000000L
+void *progress_display_thread_func(void *arg) {
+ struct timespec req = {0};
+
+ ProgressThreadArgs *progress_args = (ProgressThreadArgs *)arg;
+ AppState *state = progress_args->state;
+ size_t total_frames = progress_args->total_frames;
+ size_t completed_count = 0;
+
+ while (completed_count < total_frames) {
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire) ||
+ atomic_load_explicit(&state->processing_finished,
+ memory_order_acquire)) {
+ break;
+ }
+
+ completed_count = atomic_load_explicit(&state->ascii_frames_completed,
+ memory_order_relaxed);
+ print_progress(state, completed_count, total_frames);
+
+ req.tv_sec = 0;
+ req.tv_nsec = K_PROGRESS_UPDATE_INTERVAL_NS;
+ (void)nanosleep(&req, NULL);
+ }
+
+ if (!atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ print_progress(state, total_frames, total_frames);
+ }
+
+ (void)pthread_mutex_lock(&state->stdout_mutex);
+ (void)fputc('\n', stdout);
+ (void)fflush(stdout);
+ (void)pthread_mutex_unlock(&state->stdout_mutex);
+
+ free(progress_args);
+ return NULL;
+}
+#undef K_PROGRESS_UPDATE_INTERVAL_NS
+
+void restore_terminal(void) {
+ static bool has_been_restored = false;
+
+ if (has_been_restored || !global_signal_handler_state_ptr) {
+ return;
+ }
+ has_been_restored = true;
+
+ AppState *state = global_signal_handler_state_ptr;
+ VfetchArgs *args = state->args;
+
+ (void)pthread_mutex_lock(&state->stdout_mutex);
+
+ (void)fputs("\x1b[0m\x1b[?25h", stdout);
+
+ if (atomic_load_explicit(&state->animation_has_started,
+ memory_order_acquire)) {
+ size_t anim_h = (size_t)args->height;
+ size_t fetch_h =
+ atomic_load_explicit(&state->last_fetch_lines, memory_order_relaxed);
+
+ size_t content_height = (anim_h > fetch_h) ? anim_h : fetch_h;
+ size_t final_row = (size_t)args->pad_y + content_height + 1;
+
+ (void)fprintf(stdout, "\x1b[%zu;1H\n", final_row);
+ }
+
+ if (atomic_load_explicit(&state->termios_saved, memory_order_acquire)) {
+ (void)tcsetattr(STDIN_FILENO, TCSANOW, &state->original_termios);
+ }
+
+ (void)fflush(stdout);
+ (void)pthread_mutex_unlock(&state->stdout_mutex);
+}
+
+void setup_terminal(AppState *state) {
+ if (isatty(STDIN_FILENO)) {
+ if (tcgetattr(STDIN_FILENO, &state->original_termios) == 0) {
+ struct termios raw_termios = {0};
+
+ atomic_store_explicit(&state->termios_saved, true, memory_order_release);
+
+ raw_termios = state->original_termios;
+ raw_termios.c_lflag &= (tcflag_t) ~(ECHO | ICANON);
+ (void)tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios);
+ }
+ }
+}
+
+void set_cursor_visible(AppState *state, bool visible) {
+ atomic_store_explicit(&state->cursor_visible, visible, memory_order_release);
+ bool is_visible =
+ atomic_load_explicit(&state->cursor_visible, memory_order_acquire);
+ if (is_visible) {
+ const char *cmd = "\x1b[?25h";
+ if (write(STDOUT_FILENO, cmd, strlen(cmd)) < 0) {
+ perror("write failed when showing cursor");
+ }
+ } else {
+ const char *cmd = "\x1b[?25l";
+ if (write(STDOUT_FILENO, cmd, strlen(cmd)) < 0) {
+ perror("write failed when hiding cursor");
+ }
+ }
+}
+
+#define K_ANSI_ESCAPE_BUFFER_PADDING 32
+void append_block_to_buffer(char *buffer,
+ size_t buffer_size,
+ size_t *current_len,
+ uint32_t start_x,
+ uint32_t start_y,
+ const char *text_block,
+ size_t text_block_len) {
+ const char *block_end = text_block + text_block_len;
+ const char *line_start = text_block;
+ int line_offset_y = 0;
+
+ while (line_start < block_end) {
+ const char *line_end = (const char *)memchr(
+ line_start, '\n', (size_t)(block_end - line_start));
+ if (line_end == NULL) {
+ line_end = block_end;
+ }
+
+ size_t line_len = (size_t)(line_end - line_start);
+
+ if (line_len > 0 && line_start[line_len - 1] == '\r') {
+ line_len--;
+ }
+
+ if (line_len > 0) {
+ if (*current_len + line_len + K_ANSI_ESCAPE_BUFFER_PADDING <
+ buffer_size) {
+ int written = snprintf(buffer + *current_len,
+ buffer_size - *current_len,
+ "\x1b[%u;%uH%.*s",
+ start_y + (uint32_t)line_offset_y,
+ start_x + 1,
+ (int)line_len,
+ line_start);
+ if (written > 0) {
+ *current_len += (size_t)written;
+ }
+ }
+ }
+
+ line_offset_y++;
+ line_start = line_end + 1;
+ }
+}
+#undef K_ANSI_ESCAPE_BUFFER_PADDING
+
+#define K_ANSI_ESCAPE_BUFFER_PADDING 32
+void clear_rect_in_buffer(char *buffer,
+ size_t buffer_size,
+ size_t *current_len,
+ const Rect *rect) {
+ if (rect->height == 0 || rect->width == 0) {
+ return;
+ }
+
+ char *clear_line = malloc(rect->width + 1);
+
+ if (!clear_line) {
+ (void)fputs("ERROR: Memory allocation failed in clear_rect_in_buffer\n",
+ stderr);
+ return;
+ }
+
+ (void)memset(clear_line, ' ', rect->width);
+ clear_line[rect->width] = '\0';
+
+ for (size_t idx = 0; idx < rect->height; idx++) {
+ if (*current_len + rect->width + K_ANSI_ESCAPE_BUFFER_PADDING <
+ buffer_size) {
+ int written = snprintf(buffer + *current_len,
+ buffer_size - *current_len,
+ "\x1b[%u;%uH%s",
+ rect->start_y + (uint32_t)idx,
+ rect->start_x + 1,
+ clear_line);
+ if (written > 0) {
+ *current_len += (size_t)written;
+ }
+ }
+ }
+
+ free(clear_line);
+}
+#undef K_ANSI_ESCAPE_BUFFER_PADDING
+
+VfetchError
+run_command_fork_output(const gchar *command, gchar **output, bool debug_flag) {
+ gchar *stdout_buf = NULL;
+ gchar *stderr_buf = NULL;
+ gint wait_status = 0;
+ GError *gerr = NULL;
+
+ log_debug(debug_flag, "Executing: %s", command);
+
+ if (!g_spawn_command_line_sync(
+ command, &stdout_buf, &stderr_buf, &wait_status, &gerr)) {
+ log_debug(debug_flag, "GLib spawn failed: %s", gerr->message);
+ g_error_free(gerr);
+ return VFETCH_ERROR_EXEC_FAILED;
+ }
+
+ if (WIFEXITED(wait_status) && WEXITSTATUS(wait_status) != 0) {
+ log_debug(
+ debug_flag, "Command exited with status %d", WEXITSTATUS(wait_status));
+ if (stderr_buf && *stderr_buf) {
+ log_debug(debug_flag, "Command Stderr: %s", stderr_buf);
+ }
+ g_free(stdout_buf);
+ g_free(stderr_buf);
+ return VFETCH_ERROR_COMMAND_FAILED;
+ }
+
+ if (output) {
+ if (stderr_buf && *stderr_buf) {
+ *output = g_strconcat(stdout_buf, stderr_buf, NULL);
+ g_free(stdout_buf);
+ } else {
+ *output = stdout_buf;
+ }
+ } else {
+ g_free(stdout_buf);
+ }
+
+ g_free(stderr_buf);
+ return VFETCH_SUCCESS;
+}
+
+int unlink_cb(const char *fpath,
+ const struct stat *stat_buf,
+ int typeflag,
+ struct FTW *ftwbuf) {
+ (void)stat_buf;
+ (void)ftwbuf;
+ int result = 0;
+
+ if (typeflag == FTW_DP || typeflag == FTW_D) {
+ result = rmdir(fpath);
+ } else {
+ result = unlink(fpath);
+ }
+
+ if (result != 0 && errno != ENOENT) {
+ (void)fprintf(stderr,
+ "Warning: Delete failed for '%s': %s\n",
+ fpath,
+ strerror(errno));
+ }
+
+ return 0;
+}
+
+#define MAX_DFT_OPEN_FDS 64
+bool rm_rf(const char *path) {
+ if (path == NULL || *path == '\0') {
+ return true;
+ }
+
+ if (nftw(path, unlink_cb, MAX_DFT_OPEN_FDS, FTW_DEPTH | FTW_PHYS) != 0) {
+ if (errno == ENOENT) {
+ return true;
+ }
+ return false;
+ }
+
+ return true;
+}
+#undef MAX_DFT_OPEN_FDS
+
+#define K_MD5_DIGEST_LENGTH 16
+void hash_string_to_bytes(const char *str, uint8_t *out_bytes) {
+ if (!str) {
+ (void)memset(out_bytes, 0, K_MD5_DIGEST_LENGTH);
+ return;
+ }
+ GChecksum *checksum = g_checksum_new(G_CHECKSUM_MD5);
+ g_checksum_update(checksum, (const guchar *)str, -1);
+
+ gsize digest_len = K_MD5_DIGEST_LENGTH;
+ g_checksum_get_digest(checksum, out_bytes, &digest_len);
+ g_checksum_free(checksum);
+}
+#undef K_MD5_DIGEST_LENGTH
+
+gchar *hash_cache_key(const CacheKey *key) {
+ GChecksum *checksum = g_checksum_new(G_CHECKSUM_MD5);
+ g_checksum_update(checksum, (const guchar *)key, sizeof(CacheKey));
+ gchar *hash_str = g_strdup(g_checksum_get_string(checksum));
+ g_checksum_free(checksum);
+ return hash_str;
+}
+
+int compare_strings_wrapper(gconstpointer first_pointer,
+ gconstpointer second_pointer) {
+ return strcmp((const char *)first_pointer, (const char *)second_pointer);
+}
+
+gchar *serialize_map_for_hashing(GHashTable *map) {
+ if (map == NULL || g_hash_table_size(map) == 0) {
+ return g_strdup("");
+ }
+
+ GString *builder_object = g_string_new("");
+
+ GList *keys = g_hash_table_get_keys(map);
+ keys = g_list_sort(keys, (GCompareFunc)compare_strings_wrapper);
+
+ for (const GList *list_node = keys; list_node != NULL;
+ list_node = list_node->next) {
+ const char *current_key = (const char *)list_node->data;
+ const char *current_val =
+ (const char *)g_hash_table_lookup(map, current_key);
+
+ if (current_val != NULL) {
+ g_string_append_printf(
+ builder_object, "%s=%s;", current_key, current_val);
+ }
+ }
+
+ g_list_free(keys);
+ return g_string_free(builder_object, FALSE);
+}
+
+#define K_ALPHA_OPAQUE 0xFF
+#define K_HEXADECIMAL_BASE 16
+#define K_ALPHA_CHANNEL_SHIFT 8
+guint32 parse_hex_color(const char *hex_str) {
+ if (hex_str[0] == '#') {
+ hex_str++;
+ }
+
+ uint32_t color_val = (uint32_t)strtoul(hex_str, NULL, K_HEXADECIMAL_BASE);
+
+ return (color_val << K_ALPHA_CHANNEL_SHIFT) | K_ALPHA_OPAQUE;
+}
+#undef K_ALPHA_OPAQUE
+#undef K_HEXADECIMAL_BASE
+#undef K_ALPHA_CHANNEL_SHIFT
+
+size_t count_lines(const gchar *str) {
+ if (!str) {
+ return 0;
+ }
+
+ size_t count = 1;
+ for (const gchar *cursor = str; *cursor; cursor++) {
+ if (*cursor == '\n') {
+ count++;
+ }
+ }
+ return count;
+}
+
+void *signal_monitor_thread_func(void *arg) {
+ char dummy = '\0';
+
+ AppState *state = (AppState *)arg;
+ while (read(global_signal_pipe[0], &dummy, 1) > 0) {
+ if (atomic_load_explicit(&state->exit_requested, memory_order_acquire)) {
+ continue;
+ }
+ atomic_store_explicit(&state->exit_requested, true, memory_order_release);
+
+ (void)pthread_mutex_lock(&state->audio_state_mutex);
+ bool has_audio = (state->audio_state.device.pContext != NULL);
+ (void)pthread_mutex_unlock(&state->audio_state_mutex);
+
+ if (has_audio) {
+ ma_device_stop(&state->audio_state.device);
+ }
+ }
+
+ return NULL;
+}
+
+void app_signal_handler(int signum, siginfo_t *info, void *context) {
+ (void)signum;
+ (void)info;
+ (void)context;
+ if (global_signal_pipe[1] != -1) {
+ char dummy = 's';
+ ssize_t bytes_written = write(global_signal_pipe[1], &dummy, 1);
+ (void)bytes_written;
+ }
+}
+
+void app_resize_handler(int signum, siginfo_t *info, void *context) {
+ (void)signum;
+ (void)info;
+ (void)context;
+ if (global_signal_handler_state_ptr) {
+ atomic_store_explicit(&global_signal_handler_state_ptr->terminal_resized,
+ true,
+ memory_order_release);
+ }
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..b0fd464
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__UTILS_H
+#define SRC__UTILS_H
+
+#include <glib.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "error.h"
+#include "state.h"
+#include "video_defs.h"
+
+struct FTW;
+struct stat;
+
+typedef struct Rect {
+ size_t width;
+ size_t height;
+ uint32_t start_x;
+ uint32_t start_y;
+} Rect;
+
+#ifdef __GNUC__
+# define ATTR_PRINTF_FORMAT(FMT_POS, ARG_POS) \
+ __attribute__((format(printf, FMT_POS, ARG_POS)))
+#else
+# define ATTR_PRINTF_FORMAT(FMT_POS, ARG_POS)
+#endif
+void log_debug(bool debug_flag, const char *format, ...)
+ ATTR_PRINTF_FORMAT(2, 3);
+void log_verbose(AppState *state,
+ bool verbose_flag,
+ bool debug_flag,
+ const char *format,
+ ...) ATTR_PRINTF_FORMAT(4, 5);
+#undef ATTR_PRINTF_FORMAT
+
+void log_vfetch_error(VfetchError err, const char *context_msg);
+_Noreturn void die(VfetchError err, const char *context_msg);
+void print_progress(AppState *state, size_t current, size_t total);
+void *progress_display_thread_func(void *arg);
+void restore_terminal(void);
+void setup_terminal(AppState *state);
+void set_cursor_visible(AppState *state, bool visible);
+void append_block_to_buffer(char *buffer,
+ size_t buffer_size,
+ size_t *current_len,
+ uint32_t start_x,
+ uint32_t start_y,
+ const char *text_block,
+ size_t text_block_len);
+void clear_rect_in_buffer(char *buffer,
+ size_t buffer_size,
+ size_t *current_len,
+ const Rect *rect);
+VfetchError
+run_command_fork_output(const gchar *command, gchar **output, bool debug_flag);
+int unlink_cb(const char *fpath,
+ const struct stat *stat_buf,
+ int typeflag,
+ struct FTW *ftwbuf);
+bool rm_rf(const char *path);
+void hash_string_to_bytes(const char *str, uint8_t *out_bytes);
+gchar *hash_cache_key(const CacheKey *key);
+int compare_strings_wrapper(gconstpointer first_pointer,
+ gconstpointer second_pointer);
+gchar *serialize_map_for_hashing(GHashTable *map);
+guint32 parse_hex_color(const char *hex_str);
+size_t count_lines(const gchar *str);
+void *signal_monitor_thread_func(void *arg);
+void app_signal_handler(int signum, siginfo_t *info, void *context);
+void app_resize_handler(int signum, siginfo_t *info, void *context);
+
+#endif /* SRC__UTILS_H */
diff --git a/src/video.c b/src/video.c
new file mode 100644
index 0000000..6bfcff0
--- /dev/null
+++ b/src/video.c
@@ -0,0 +1,261 @@
+/* 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 "video.h"
+#include <chafa/chafa.h>
+#include <glib.h>
+#include <sys/ioctl.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include "pl_mpeg.h"
+#include "utils.h"
+
+static void apply_pixel_mode(ChafaCanvasConfig *config,
+ const VfetchArgs *args) {
+ if (strcmp(args->format_arg, "symbols") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SYMBOLS);
+ } else if (strcmp(args->format_arg, "sixels") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_SIXELS);
+ } else if (strcmp(args->format_arg, "kitty") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_KITTY);
+ } else if (strcmp(args->format_arg, "iterm2") == 0) {
+ chafa_canvas_config_set_pixel_mode(config, CHAFA_PIXEL_MODE_ITERM2);
+ }
+}
+
+static void apply_color_settings(ChafaCanvasConfig *config,
+ const VfetchArgs *args) {
+ if (args->colors_arg) {
+ if (strcmp(args->colors_arg, "16") == 0) {
+ chafa_canvas_config_set_canvas_mode(config, CHAFA_CANVAS_MODE_INDEXED_16);
+ } else if (strcmp(args->colors_arg, "240") == 0) {
+ chafa_canvas_config_set_canvas_mode(config,
+ CHAFA_CANVAS_MODE_INDEXED_240);
+ } else if (strcmp(args->colors_arg, "256") == 0) {
+ chafa_canvas_config_set_canvas_mode(config,
+ CHAFA_CANVAS_MODE_INDEXED_256);
+ } else if (strcmp(args->colors_arg, "full") == 0) {
+ chafa_canvas_config_set_canvas_mode(config, CHAFA_CANVAS_MODE_TRUECOLOR);
+ }
+ }
+ if (args->fg_color_arg) {
+ chafa_canvas_config_set_fg_color(config,
+ parse_hex_color(args->fg_color_arg));
+ }
+ if (args->bg_color_arg) {
+ chafa_canvas_config_set_bg_color(config,
+ parse_hex_color(args->bg_color_arg));
+ }
+}
+
+#define K_FALLBACK_CELL_WIDTH 10.0
+static VfetchError configure_chafa_canvas(uint32_t width,
+ uint32_t height,
+ ChafaCanvas **canvas_out,
+ const VfetchArgs *args) {
+ ChafaCanvasConfig *config = chafa_canvas_config_new();
+ chafa_canvas_config_set_geometry(config, (int)width, (int)height);
+
+ struct winsize window_size = {0};
+ int cell_px_w = 0;
+ int cell_px_h = 0;
+
+ if (isatty(STDOUT_FILENO) &&
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &window_size) == 0) {
+ if (window_size.ws_xpixel > 0 && window_size.ws_ypixel > 0 &&
+ window_size.ws_col > 0 && window_size.ws_row > 0) {
+ cell_px_w = window_size.ws_xpixel / window_size.ws_col;
+ cell_px_h = window_size.ws_ypixel / window_size.ws_row;
+ }
+ }
+
+ if (cell_px_w <= 0 || cell_px_h <= 0) {
+ cell_px_w = K_FALLBACK_CELL_WIDTH;
+ double h_val = round(K_FALLBACK_CELL_WIDTH / args->cell_aspect_ratio);
+ cell_px_h = (int)h_val;
+ }
+
+ chafa_canvas_config_set_cell_geometry(config, cell_px_w, cell_px_h);
+
+ apply_pixel_mode(config, args);
+ apply_color_settings(config, args);
+
+ ChafaSymbolMap *symbol_map = chafa_symbol_map_new();
+ char *symbols = (char *)g_hash_table_lookup(args->chafa_options, "--symbols");
+ chafa_symbol_map_apply_selectors(symbol_map, symbols ? symbols : "all", NULL);
+ chafa_canvas_config_set_symbol_map(config, symbol_map);
+ chafa_symbol_map_unref(symbol_map);
+
+ if (g_hash_table_lookup(args->chafa_options, "--fg-only")) {
+ chafa_canvas_config_set_fg_only_enabled(config, TRUE);
+ }
+
+ char *colorspace =
+ (char *)g_hash_table_lookup(args->chafa_options, "--color-space");
+ if (colorspace) {
+ if (strcmp(colorspace, "rgb") == 0) {
+ chafa_canvas_config_set_color_space(config, CHAFA_COLOR_SPACE_RGB);
+ } else if (strcmp(colorspace, "din99d") == 0) {
+ chafa_canvas_config_set_color_space(config, CHAFA_COLOR_SPACE_DIN99D);
+ }
+ }
+
+ char *fill = (char *)g_hash_table_lookup(args->chafa_options, "--fill");
+ if (fill && *fill) {
+ ChafaSymbolMap *fill_map = chafa_symbol_map_new();
+ for (guint i = 0; i < strlen(fill); i++) {
+ chafa_symbol_map_add_by_range(
+ fill_map, (gunichar)fill[i], (gunichar)fill[i]);
+ }
+ chafa_canvas_config_set_fill_symbol_map(config, fill_map);
+ chafa_symbol_map_unref(fill_map);
+ }
+
+ *canvas_out = chafa_canvas_new(config);
+ chafa_canvas_config_unref(config);
+
+ return (*canvas_out) ? VFETCH_SUCCESS : VFETCH_ERROR_CHAFA_CONFIG;
+}
+#undef K_FALLBACK_CELL_WIDTH
+
+static plm_t *probe_video(const char *filename) {
+ plm_t *plm = plm_create_with_filename(filename);
+ if (!plm) {
+ log_vfetch_error(VFETCH_ERROR_VIDEO_OPEN_FAILED, filename);
+ }
+ return plm;
+}
+
+VfetchError resolve_geometry(VfetchArgs *args) {
+ if (args->height > 0) {
+ return VFETCH_SUCCESS;
+ }
+
+ plm_t *plm = probe_video(args->filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+
+ int v_w = plm_get_width(plm);
+ int v_h = plm_get_height(plm);
+ plm_destroy(plm);
+
+ double video_ratio = (double)v_h / (double)v_w;
+ double target_h = (double)args->width * args->cell_aspect_ratio * video_ratio;
+
+ double final_h = fmax(1.0, round(target_h));
+ args->height = (uint32_t)final_h;
+
+ return VFETCH_SUCCESS;
+}
+
+VfetchError get_video_frame_count(const char *filename,
+ double fps,
+ size_t *frame_count_out) {
+ plm_t *plm = probe_video(filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+ double total_f = ceil(plm_get_duration(plm) * fps);
+ *frame_count_out = (size_t)total_f;
+ plm_destroy(plm);
+ return VFETCH_SUCCESS;
+}
+
+VfetchError validate_fps(VfetchArgs *args) {
+ plm_t *plm = probe_video(args->filename);
+ if (!plm) {
+ return VFETCH_ERROR_VIDEO_OPEN_FAILED;
+ }
+ if (args->fps <= 0.0) {
+ args->fps = plm_get_framerate(plm);
+ }
+ plm_destroy(plm);
+ return VFETCH_SUCCESS;
+}
+
+VfetchError initialize_worker_resources(WorkerResources *res,
+ const VfetchArgs *args) {
+ res->plm = plm_create_with_filename(args->filename);
+ if (!res->plm) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+
+ res->video_width = plm_get_width(res->plm);
+ res->video_height = plm_get_height(res->plm);
+ res->rgb_buffer =
+ malloc((size_t)res->video_width * (size_t)res->video_height * 3);
+
+ return configure_chafa_canvas(args->width, args->height, &res->canvas, args);
+}
+
+void cleanup_worker_resources(WorkerResources *res) {
+ if (res->plm) {
+ plm_destroy(res->plm);
+ }
+ if (res->canvas) {
+ chafa_canvas_unref(res->canvas);
+ }
+ free(res->rgb_buffer);
+}
+
+static VfetchError cache_single_frame(WorkerResources *res,
+ plm_frame_t *frame,
+ CacheFiles *files,
+ unsigned long *current_offset) {
+ plm_frame_to_rgb(frame, res->rgb_buffer, res->video_width * 3);
+ chafa_canvas_draw_all_pixels(res->canvas,
+ CHAFA_PIXEL_RGB8,
+ res->rgb_buffer,
+ res->video_width,
+ res->video_height,
+ res->video_width * 3);
+
+ GString *gstr = chafa_canvas_print(res->canvas, NULL);
+ if (!gstr) {
+ return VFETCH_ERROR_MEMORY_ALLOCATION;
+ }
+
+ FrameMetadata meta = {
+ .offset = *current_offset, .length = gstr->len, .pts = frame->time};
+ VfetchError err = VFETCH_SUCCESS;
+
+ if (fwrite(gstr->str, 1, gstr->len, files->data_file) != gstr->len ||
+ fwrite(&meta, sizeof(FrameMetadata), 1, files->meta_file) != 1) {
+ err = VFETCH_ERROR_FILE_IO;
+ } else {
+ *current_offset += gstr->len;
+ }
+
+ g_string_free(gstr, TRUE);
+ return err;
+}
+
+VfetchError populate_frame_cache_loop(WorkerResources *res,
+ AppState *state,
+ CacheFiles *files,
+ unsigned long *current_offset) {
+ while (!atomic_load(&state->exit_requested)) {
+ plm_frame_t *frame = plm_decode_video(res->plm);
+ if (!frame) {
+ break;
+ }
+ if (cache_single_frame(res, frame, files, current_offset) !=
+ VFETCH_SUCCESS) {
+ return VFETCH_ERROR_FILE_IO;
+ }
+ atomic_fetch_add(&state->ascii_frames_completed, 1);
+ }
+ return VFETCH_SUCCESS;
+}
diff --git a/src/video.h b/src/video.h
new file mode 100644
index 0000000..df21662
--- /dev/null
+++ b/src/video.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__VIDEO_H
+#define SRC__VIDEO_H
+
+#include <stddef.h>
+#include "config.h"
+#include "error.h"
+#include "state.h"
+#include "video_defs.h"
+
+VfetchError resolve_geometry(VfetchArgs *args);
+VfetchError get_video_frame_count(const char *filename,
+ double fps,
+ size_t *frame_count_out);
+VfetchError validate_fps(VfetchArgs *args);
+VfetchError initialize_worker_resources(WorkerResources *res,
+ const VfetchArgs *args);
+void cleanup_worker_resources(WorkerResources *res);
+VfetchError populate_frame_cache_loop(WorkerResources *res,
+ AppState *state,
+ CacheFiles *files,
+ unsigned long *current_offset);
+
+#endif /* SRC__VIDEO_H */
diff --git a/src/video_defs.h b/src/video_defs.h
new file mode 100644
index 0000000..df3fa7d
--- /dev/null
+++ b/src/video_defs.h
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later */
+/*
+ * vfetch - neofetch but the ascii part is a video
+ *
+ * Copyright (C) 2026 Doan Luu Duc Tai
+ *
+ */
+
+#ifndef SRC__VIDEO_DEFS_H
+#define SRC__VIDEO_DEFS_H
+
+#include <chafa/chafa.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <sys/types.h>
+#include <stdint.h>
+#include <stdio.h>
+#include "pl_mpeg.h"
+
+typedef struct FrameMetadata {
+ uint64_t offset;
+ uint64_t length;
+ double pts;
+} FrameMetadata;
+
+typedef struct WorkerResources {
+ plm_t *plm;
+ ChafaCanvas *canvas;
+ uint8_t *rgb_buffer;
+ int video_width;
+ int video_height;
+} WorkerResources;
+
+typedef struct CacheFiles {
+ FILE *data_file;
+ FILE *meta_file;
+} CacheFiles;
+
+typedef struct CacheFilePaths {
+ gchar *data_file_path;
+ gchar *meta_file_path;
+} CacheFilePaths;
+
+typedef struct CacheValidationStats {
+ GStatBuf *st_success;
+ GStatBuf *st_data;
+ GStatBuf *st_meta;
+} CacheValidationStats;
+
+#define K_MD5_DIGEST_LENGTH 16
+typedef struct {
+ int64_t file_size;
+ int64_t file_mtime;
+ uint32_t width;
+ uint32_t height;
+ double fps;
+ double cell_aspect_ratio;
+ uint8_t symbols_md5[K_MD5_DIGEST_LENGTH];
+ uint8_t colors_md5[K_MD5_DIGEST_LENGTH];
+ uint8_t format_md5[K_MD5_DIGEST_LENGTH];
+ uint8_t chafa_opts_md5[K_MD5_DIGEST_LENGTH];
+ uint32_t version_tag;
+} CacheKey;
+#undef K_MD5_DIGEST_LENGTH
+
+typedef struct VideoAnimationArgs {
+ FrameMetadata *frames;
+ char *data_map;
+ char *screen_buffer;
+ off_t data_file_size;
+ size_t num_frames;
+ size_t screen_buffer_size;
+} VideoAnimationArgs;
+
+typedef struct AnimationResources {
+ FILE *meta_stream;
+ GMappedFile *mapped_file;
+ VideoAnimationArgs anim_args;
+ off_t data_file_size;
+} AnimationResources;
+
+#endif /* SRC__VIDEO_DEFS_H */