diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-09-08 14:46:47 -0500 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-09-08 14:46:47 -0500 |
| commit | f34fb5042d40c186583f9e3009d8a1dd71d07d43 (patch) | |
| tree | 3f494f3723ebe9f173d0702a97509794f0ef85a5 /src | |
Initial commitmain
Diffstat (limited to 'src')
| -rw-r--r-- | src/args.c | 897 | ||||
| -rw-r--r-- | src/args.h | 18 | ||||
| -rw-r--r-- | src/audio.c | 126 | ||||
| -rw-r--r-- | src/audio.h | 18 | ||||
| -rw-r--r-- | src/audio_defs.h | 29 | ||||
| -rw-r--r-- | src/config.h | 69 | ||||
| -rw-r--r-- | src/core.c | 1272 | ||||
| -rw-r--r-- | src/core.h | 22 | ||||
| -rw-r--r-- | src/error.c | 44 | ||||
| -rw-r--r-- | src/error.h | 44 | ||||
| -rw-r--r-- | src/fetch.c | 209 | ||||
| -rw-r--r-- | src/fetch.h | 25 | ||||
| -rw-r--r-- | src/state.h | 69 | ||||
| -rw-r--r-- | src/utils.c | 583 | ||||
| -rw-r--r-- | src/utils.h | 81 | ||||
| -rw-r--r-- | src/video.c | 261 | ||||
| -rw-r--r-- | src/video.h | 31 | ||||
| -rw-r--r-- | src/video_defs.h | 82 |
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, ¤t_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, ¤t_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( + ¤t_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 */ |
