/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #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); } }