From f34fb5042d40c186583f9e3009d8a1dd71d07d43 Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Tue, 8 Sep 2026 14:46:47 -0500 Subject: Initial commit --- src/utils.c | 583 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 src/utils.c (limited to 'src/utils.c') 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 +#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); + } +} -- cgit v1.2.3