summaryrefslogtreecommitdiff
path: root/src/utils.c
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-09-08 14:46:47 -0500
commitf34fb5042d40c186583f9e3009d8a1dd71d07d43 (patch)
tree3f494f3723ebe9f173d0702a97509794f0ef85a5 /src/utils.c
Initial commitmain
Diffstat (limited to 'src/utils.c')
-rw-r--r--src/utils.c583
1 files changed, 583 insertions, 0 deletions
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);
+ }
+}