diff options
| author | Trung Lê <8@tle.id.au> | 2026-02-21 23:40:05 +1100 |
|---|---|---|
| committer | Trung Lê <8@tle.id.au> | 2026-02-21 23:47:49 +1100 |
| commit | 3eed1b64af72dcb4bcb8ae28d45856767d4acff1 (patch) | |
| tree | 0aeef768169d69c4ed24c3cc03398c63aef3ff59 /src/gsr-config.c | |
Initial commit
Diffstat (limited to 'src/gsr-config.c')
| -rw-r--r-- | src/gsr-config.c | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/src/gsr-config.c b/src/gsr-config.c new file mode 100644 index 0000000..21405ea --- /dev/null +++ b/src/gsr-config.c @@ -0,0 +1,591 @@ +#include "gsr-config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <inttypes.h> +#include <unistd.h> +#include <limits.h> +#include <libgen.h> +#include <pwd.h> +#include <sys/stat.h> + +#include <gtk/gtk.h> + +/* + * X keysym constants for the custom modifier bitmask encoding. + * These are just integer values used for config file serialization — + * they do NOT require X11 headers or libraries at runtime. + */ +#define XK_Shift_L 0xFFE1 +#define XK_Shift_R 0xFFE2 +#define XK_Control_L 0xFFE3 +#define XK_Control_R 0xFFE4 +#define XK_Meta_L 0xFFE7 +#define XK_Meta_R 0xFFE8 +#define XK_Alt_L 0xFFE9 +#define XK_Alt_R 0xFFEA +#define XK_Super_L 0xFFEB +#define XK_Super_R 0xFFEC + +#ifdef HAVE_X11 +#include <X11/Xlib.h> /* ControlMask, ShiftMask, Mod1Mask, Mod4Mask */ +#endif + +/* ═══════════════════════════════════════════════════════════════════ + * gsr-config.c — Config file read/write + * + * File format: one key-value pair per line, space-separated. + * STRING_ARRAY keys appear multiple times (one per element). + * Backward-compatible with the C++ GTK3 version. + * ═══════════════════════════════════════════════════════════════════ */ + +/* ── Helpers ─────────────────────────────────────────────────────── */ + +static char * +get_home_dir(void) +{ + const char *home = g_get_home_dir(); + if (home) + return g_strdup(home); + return g_strdup("/tmp"); +} + +char * +gsr_config_get_dir(void) +{ + const char *xdg = g_getenv("XDG_CONFIG_HOME"); + if (xdg && xdg[0]) + return g_build_filename(xdg, "gpu-screen-recorder", NULL); + + char *home = get_home_dir(); + char *result = g_build_filename(home, ".config", "gpu-screen-recorder", NULL); + g_free(home); + return result; +} + +char * +gsr_config_get_videos_dir(void) +{ + const char *vdir = g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS); + if (vdir) + return g_strdup(vdir); + char *home = get_home_dir(); + char *result = g_build_filename(home, "Videos", NULL); + g_free(home); + return result; +} + +static int +create_directory_recursive(char *path) +{ + int path_len = (int)strlen(path); + char *p = path; + char *end = path + path_len; + + for (;;) { + char *slash_p = strchr(p, '/'); + + /* Skip leading '/' — don't try to mkdir root */ + if (slash_p == path) { + ++p; + continue; + } + + if (!slash_p) + slash_p = end; + + char prev = *slash_p; + *slash_p = '\0'; + int err = mkdir(path, S_IRWXU); + *slash_p = prev; + + if (err == -1 && errno != EEXIST) + return err; + + if (slash_p == end) + break; + else + p = slash_p + 1; + } + return 0; +} + +/* ── Config value type dispatch ──────────────────────────────────── */ + +typedef enum { + CFG_BOOL, + CFG_STRING, + CFG_I32, + CFG_HOTKEY, + CFG_STRING_ARRAY, +} CfgType; + +typedef struct { + const char *key; + CfgType type; + size_t offset; /* offset into GsrConfig */ + size_t count_offset; /* for STRING_ARRAY: offset of n_audio_input */ +} CfgEntry; + +/* Helper macro to compute offset from a nested config member */ +#define CFG_OFF(section, member) offsetof(GsrConfig, section.member) + +static const CfgEntry config_entries[] = { + /* ── main ── */ + { "main.record_area_option", CFG_STRING, CFG_OFF(main_config, record_area_option), 0 }, + { "main.record_area_width", CFG_I32, CFG_OFF(main_config, record_area_width), 0 }, + { "main.record_area_height", CFG_I32, CFG_OFF(main_config, record_area_height), 0 }, + { "main.video_width", CFG_I32, CFG_OFF(main_config, video_width), 0 }, + { "main.video_height", CFG_I32, CFG_OFF(main_config, video_height), 0 }, + { "main.fps", CFG_I32, CFG_OFF(main_config, fps), 0 }, + { "main.video_bitrate", CFG_I32, CFG_OFF(main_config, video_bitrate), 0 }, + { "main.merge_audio_tracks", CFG_BOOL, CFG_OFF(main_config, merge_audio_tracks), 0 }, + { "main.record_app_audio_inverted", CFG_BOOL, CFG_OFF(main_config, record_app_audio_inverted),0 }, + { "main.change_video_resolution", CFG_BOOL, CFG_OFF(main_config, change_video_resolution), 0 }, + { "main.audio_input", CFG_STRING_ARRAY, CFG_OFF(main_config, audio_input), + CFG_OFF(main_config, n_audio_input) }, + { "main.color_range", CFG_STRING, CFG_OFF(main_config, color_range), 0 }, + { "main.quality", CFG_STRING, CFG_OFF(main_config, quality), 0 }, + { "main.codec", CFG_STRING, CFG_OFF(main_config, codec), 0 }, + { "main.audio_codec", CFG_STRING, CFG_OFF(main_config, audio_codec), 0 }, + { "main.framerate_mode", CFG_STRING, CFG_OFF(main_config, framerate_mode), 0 }, + { "main.advanced_view", CFG_BOOL, CFG_OFF(main_config, advanced_view), 0 }, + { "main.overclock", CFG_BOOL, CFG_OFF(main_config, overclock), 0 }, + { "main.show_recording_started_notifications",CFG_BOOL, CFG_OFF(main_config, show_recording_started_notifications), 0 }, + { "main.show_recording_stopped_notifications",CFG_BOOL, CFG_OFF(main_config, show_recording_stopped_notifications), 0 }, + { "main.show_recording_saved_notifications", CFG_BOOL, CFG_OFF(main_config, show_recording_saved_notifications), 0 }, + { "main.record_cursor", CFG_BOOL, CFG_OFF(main_config, record_cursor), 0 }, + { "main.hide_window_when_recording", CFG_BOOL, CFG_OFF(main_config, hide_window_when_recording),0 }, + { "main.software_encoding_warning_shown", CFG_BOOL, CFG_OFF(main_config, software_encoding_warning_shown),0 }, + { "main.steam_deck_warning_shown", CFG_BOOL, CFG_OFF(main_config, steam_deck_warning_shown), 0 }, + { "main.hevc_amd_bug_warning_shown", CFG_BOOL, CFG_OFF(main_config, hevc_amd_bug_warning_shown),0 }, + { "main.av1_amd_bug_warning_shown", CFG_BOOL, CFG_OFF(main_config, av1_amd_bug_warning_shown),0 }, + { "main.restore_portal_session", CFG_BOOL, CFG_OFF(main_config, restore_portal_session), 0 }, + { "main.use_new_ui", CFG_BOOL, CFG_OFF(main_config, use_new_ui), 0 }, + { "main.installed_gsr_global_hotkeys_version",CFG_I32, CFG_OFF(main_config, installed_gsr_global_hotkeys_version),0 }, + + /* ── streaming ── */ + { "streaming.service", CFG_STRING, CFG_OFF(streaming_config, streaming_service), 0 }, + { "streaming.youtube.key", CFG_STRING, CFG_OFF(streaming_config, youtube_stream_key), 0 }, + { "streaming.twitch.key", CFG_STRING, CFG_OFF(streaming_config, twitch_stream_key), 0 }, + { "streaming.custom.url", CFG_STRING, CFG_OFF(streaming_config, custom_url), 0 }, + { "streaming.custom.container", CFG_STRING, CFG_OFF(streaming_config, custom_container), 0 }, + { "streaming.start_stop_recording_hotkey", CFG_HOTKEY, CFG_OFF(streaming_config, start_stop_hotkey), 0 }, + + /* ── record ── */ + { "record.save_directory", CFG_STRING, CFG_OFF(record_config, save_directory), 0 }, + { "record.container", CFG_STRING, CFG_OFF(record_config, container), 0 }, + { "record.start_stop_recording_hotkey", CFG_HOTKEY, CFG_OFF(record_config, start_stop_hotkey), 0 }, + { "record.pause_unpause_recording_hotkey", CFG_HOTKEY, CFG_OFF(record_config, pause_unpause_hotkey), 0 }, + + /* ── replay ── */ + { "replay.save_directory", CFG_STRING, CFG_OFF(replay_config, save_directory), 0 }, + { "replay.container", CFG_STRING, CFG_OFF(replay_config, container), 0 }, + { "replay.time", CFG_I32, CFG_OFF(replay_config, replay_time), 0 }, + { "replay.start_stop_recording_hotkey", CFG_HOTKEY, CFG_OFF(replay_config, start_stop_hotkey), 0 }, + { "replay.save_recording_hotkey", CFG_HOTKEY, CFG_OFF(replay_config, save_hotkey), 0 }, +}; + +#define N_CONFIG_ENTRIES ((int)(sizeof(config_entries) / sizeof(config_entries[0]))) + +static const CfgEntry * +find_entry(const char *key, int key_len) +{ + for (int i = 0; i < N_CONFIG_ENTRIES; i++) { + if ((int)strlen(config_entries[i].key) == key_len && + memcmp(config_entries[i].key, key, (size_t)key_len) == 0) + return &config_entries[i]; + } + return NULL; +} + +/* ── Default initialization ──────────────────────────────────────── */ + +void +gsr_config_init_defaults(GsrConfig *config) +{ + memset(config, 0, sizeof(*config)); + + GsrMainConfig *m = &config->main_config; + m->record_area_option = g_strdup(""); + m->record_area_width = 0; + m->record_area_height = 0; + m->video_width = 0; + m->video_height = 0; + m->fps = 60; + m->video_bitrate = 15000; + m->merge_audio_tracks = true; + m->record_app_audio_inverted = false; + m->change_video_resolution = false; + m->audio_input = NULL; + m->n_audio_input = 0; + m->color_range = g_strdup("limited"); + m->quality = g_strdup("very_high"); + m->codec = g_strdup("auto"); + m->audio_codec = g_strdup("opus"); + m->framerate_mode = g_strdup("auto"); + m->advanced_view = false; + m->overclock = false; + m->show_recording_started_notifications = false; + m->show_recording_stopped_notifications = false; + m->show_recording_saved_notifications = true; + m->record_cursor = true; + m->hide_window_when_recording = false; + m->restore_portal_session = true; + m->software_encoding_warning_shown = false; + m->steam_deck_warning_shown = false; + m->hevc_amd_bug_warning_shown = false; + m->av1_amd_bug_warning_shown = false; + m->use_new_ui = false; + m->installed_gsr_global_hotkeys_version = 0; + + /* Default hotkeys: Alt+1 = start/stop, Alt+2 = pause/save + * Custom bitmask: Alt_L = 1 << (XK_Alt_L - XK_Shift_L) = 1 << 8 = 256 + * XK_1 = 0x31 = 49, XK_2 = 0x32 = 50 */ + #define DEFAULT_HOTKEY_START_STOP ((GsrConfigHotkey){ .keysym = 49, .modifiers = 256 }) + #define DEFAULT_HOTKEY_SECONDARY ((GsrConfigHotkey){ .keysym = 50, .modifiers = 256 }) + + GsrStreamingConfig *s = &config->streaming_config; + s->streaming_service = g_strdup("twitch"); + s->youtube_stream_key = g_strdup(""); + s->twitch_stream_key = g_strdup(""); + s->custom_url = g_strdup(""); + s->custom_container = g_strdup("flv"); + s->start_stop_hotkey = DEFAULT_HOTKEY_START_STOP; + + GsrRecordConfig *r = &config->record_config; + r->save_directory = gsr_config_get_videos_dir(); + r->container = g_strdup("mp4"); + r->start_stop_hotkey = DEFAULT_HOTKEY_START_STOP; + r->pause_unpause_hotkey = DEFAULT_HOTKEY_SECONDARY; + + GsrReplayConfig *rp = &config->replay_config; + rp->save_directory = gsr_config_get_videos_dir(); + rp->container = g_strdup("mp4"); + rp->replay_time = 30; + rp->start_stop_hotkey = DEFAULT_HOTKEY_START_STOP; + rp->save_hotkey = DEFAULT_HOTKEY_SECONDARY; + + #undef DEFAULT_HOTKEY_START_STOP + #undef DEFAULT_HOTKEY_SECONDARY +} + +/* ── Read ────────────────────────────────────────────────────────── */ + +gboolean +gsr_config_read(GsrConfig *config) +{ + char *config_dir = gsr_config_get_dir(); + char *config_path = g_build_filename(config_dir, "config", NULL); + g_free(config_dir); + + gchar *contents = NULL; + gsize length = 0; + if (!g_file_get_contents(config_path, &contents, &length, NULL)) { + g_free(config_path); + return FALSE; + } + g_free(config_path); + + /* Parse line by line */ + char *p = contents; + char *end = contents + length; + + while (p < end) { + /* Find end of line */ + char *nl = memchr(p, '\n', (size_t)(end - p)); + if (!nl) + nl = end; + + int line_len = (int)(nl - p); + + /* Find space separator between key and value */ + char *sp = memchr(p, ' ', (size_t)line_len); + if (sp && sp > p) { + int key_len = (int)(sp - p); + const char *val = sp + 1; + int val_len = (int)(nl - val); + + if (key_len > 0 && val_len > 0) { + const CfgEntry *entry = find_entry(p, key_len); + if (entry) { + char *base = (char *)config; + switch (entry->type) { + case CFG_BOOL: { + bool *ptr = (bool *)(base + entry->offset); + *ptr = (val_len == 4 && memcmp(val, "true", 4) == 0); + break; + } + case CFG_STRING: { + char **ptr = (char **)(void *)(base + entry->offset); + g_free(*ptr); + *ptr = g_strndup(val, (gsize)val_len); + break; + } + case CFG_I32: { + int32_t *ptr = (int32_t *)(void *)(base + entry->offset); + char tmp[32]; + int copy_len = val_len < 31 ? val_len : 31; + memcpy(tmp, val, (size_t)copy_len); + tmp[copy_len] = '\0'; + if (sscanf(tmp, "%" PRIi32, ptr) != 1) { + *ptr = 0; + } + break; + } + case CFG_HOTKEY: { + GsrConfigHotkey *hk = (GsrConfigHotkey *)(void *)(base + entry->offset); + char tmp[64]; + int copy_len = val_len < 63 ? val_len : 63; + memcpy(tmp, val, (size_t)copy_len); + tmp[copy_len] = '\0'; + if (sscanf(tmp, "%" PRIi64 " %" PRIu32, + &hk->keysym, &hk->modifiers) != 2) { + hk->keysym = 0; + hk->modifiers = 0; + } + break; + } + case CFG_STRING_ARRAY: { + char ***arr_ptr = (char ***)(void *)(base + entry->offset); + int *count_ptr = (int *)(void *)(base + entry->count_offset); + int n = *count_ptr; + *arr_ptr = g_realloc(*arr_ptr, sizeof(char *) * (gsize)(n + 2)); + (*arr_ptr)[n] = g_strndup(val, (gsize)val_len); + (*arr_ptr)[n + 1] = NULL; + *count_ptr = n + 1; + break; + } + } + } + } + } + + p = nl + 1; + } + + g_free(contents); + return TRUE; +} + +/* ── Save ────────────────────────────────────────────────────────── */ + +void +gsr_config_save(const GsrConfig *config) +{ + char *config_dir = gsr_config_get_dir(); + char *config_path = g_build_filename(config_dir, "config", NULL); + + /* Ensure directory exists */ + if (create_directory_recursive(config_dir) != 0) { + g_warning("Failed to create config directory: %s", config_dir); + g_free(config_dir); + g_free(config_path); + return; + } + g_free(config_dir); + + FILE *file = fopen(config_path, "wb"); + if (!file) { + g_warning("Failed to create config file: %s", config_path); + g_free(config_path); + return; + } + g_free(config_path); + + const char *base = (const char *)config; + + for (int i = 0; i < N_CONFIG_ENTRIES; i++) { + const CfgEntry *e = &config_entries[i]; + switch (e->type) { + case CFG_BOOL: { + const bool *ptr = (const bool *)(base + e->offset); + fprintf(file, "%s %s\n", e->key, *ptr ? "true" : "false"); + break; + } + case CFG_STRING: { + const char *const *ptr = (const char *const *)(const void *)(base + e->offset); + fprintf(file, "%s %s\n", e->key, *ptr ? *ptr : ""); + break; + } + case CFG_I32: { + const int32_t *ptr = (const int32_t *)(const void *)(base + e->offset); + fprintf(file, "%s %" PRIi32 "\n", e->key, *ptr); + break; + } + case CFG_HOTKEY: { + const GsrConfigHotkey *hk = (const GsrConfigHotkey *)(const void *)(base + e->offset); + fprintf(file, "%s %" PRIi64 " %" PRIu32 "\n", e->key, + hk->keysym, hk->modifiers); + break; + } + case CFG_STRING_ARRAY: { + char *const *const *arr_ptr = (char *const *const *)(const void *)(base + e->offset); + const int *count_ptr = (const int *)(const void *)(base + e->count_offset); + int n = *count_ptr; + char *const *arr = *arr_ptr; + for (int j = 0; j < n && arr && arr[j]; j++) { + fprintf(file, "%s %s\n", e->key, arr[j]); + } + break; + } + } + } + + fclose(file); +} + +/* ── Clear ───────────────────────────────────────────────────────── */ + +void +gsr_config_clear(GsrConfig *config) +{ + GsrMainConfig *m = &config->main_config; + g_free(m->record_area_option); + g_free(m->color_range); + g_free(m->quality); + g_free(m->codec); + g_free(m->audio_codec); + g_free(m->framerate_mode); + + if (m->audio_input) { + for (int i = 0; i < m->n_audio_input; i++) + g_free(m->audio_input[i]); + g_free(m->audio_input); + } + + GsrStreamingConfig *s = &config->streaming_config; + g_free(s->streaming_service); + g_free(s->youtube_stream_key); + g_free(s->twitch_stream_key); + g_free(s->custom_url); + g_free(s->custom_container); + + g_free(config->record_config.save_directory); + g_free(config->record_config.container); + + g_free(config->replay_config.save_directory); + g_free(config->replay_config.container); + + memset(config, 0, sizeof(*config)); +} + +/* ── Hotkey conversion utilities ─────────────────────────────────── */ + +/* + * The GTK3 code uses a custom bitmask for modifier keys: + * modkey_to_mask(keysym) = 1 << (keysym - XK_Shift_L) + * + * The relevant X keysyms (from <X11/keysym.h>): + * XK_Shift_L = 0xFFE1 → bit 0 + * XK_Shift_R = 0xFFE2 → bit 1 + * XK_Control_L = 0xFFE3 → bit 2 + * XK_Control_R = 0xFFE4 → bit 3 + * XK_Caps_Lock = 0xFFE5 → bit 4 (unused) + * XK_Shift_Lock= 0xFFE6 → bit 5 (unused) + * XK_Meta_L = 0xFFE7 → bit 6 + * XK_Meta_R = 0xFFE8 → bit 7 + * XK_Alt_L = 0xFFE9 → bit 8 + * XK_Alt_R = 0xFFEA → bit 9 + * XK_Super_L = 0xFFEB → bit 10 + * XK_Super_R = 0xFFEC → bit 11 + */ + +#define CUSTOM_MASK(ks) (1u << ((ks) - XK_Shift_L)) + +#define MASK_SHIFT (CUSTOM_MASK(XK_Shift_L) | CUSTOM_MASK(XK_Shift_R)) +#define MASK_CONTROL (CUSTOM_MASK(XK_Control_L) | CUSTOM_MASK(XK_Control_R)) +#define MASK_ALT (CUSTOM_MASK(XK_Alt_L) | CUSTOM_MASK(XK_Alt_R)) +#define MASK_META (CUSTOM_MASK(XK_Meta_L) | CUSTOM_MASK(XK_Meta_R)) +#define MASK_SUPER (CUSTOM_MASK(XK_Super_L) | CUSTOM_MASK(XK_Super_R)) + +char * +gsr_config_hotkey_to_accel(const GsrConfigHotkey *hk) +{ + if (!hk || hk->keysym == 0) + return NULL; + + /* Convert custom modifier bitmask to GdkModifierType */ + GdkModifierType mods = 0; + uint32_t m = hk->modifiers; + + if (m & MASK_SHIFT) mods |= GDK_SHIFT_MASK; + if (m & MASK_CONTROL) mods |= GDK_CONTROL_MASK; + if (m & MASK_ALT) mods |= GDK_ALT_MASK; + if (m & (MASK_META | MASK_SUPER)) + mods |= GDK_SUPER_MASK; + + return gtk_accelerator_name((guint)hk->keysym, mods); +} + +gboolean +gsr_config_hotkey_from_accel(GsrConfigHotkey *hk, const char *accel) +{ + if (!hk) + return FALSE; + + if (!accel || !accel[0]) { + hk->keysym = 0; + hk->modifiers = 0; + return TRUE; + } + + guint keyval = 0; + GdkModifierType mods = 0; + + if (!gtk_accelerator_parse(accel, &keyval, &mods)) + return FALSE; + + if (keyval == 0) { + hk->keysym = 0; + hk->modifiers = 0; + return TRUE; + } + + hk->keysym = (int64_t)keyval; + + /* Convert GdkModifierType back to custom bitmask. + * We use the _L variant for each modifier pair. */ + uint32_t m = 0; + if (mods & GDK_SHIFT_MASK) m |= CUSTOM_MASK(XK_Shift_L); + if (mods & GDK_CONTROL_MASK) m |= CUSTOM_MASK(XK_Control_L); + if (mods & GDK_ALT_MASK) m |= CUSTOM_MASK(XK_Alt_L); + if (mods & GDK_SUPER_MASK) m |= CUSTOM_MASK(XK_Super_L); + hk->modifiers = m; + + return TRUE; +} + +#ifdef HAVE_X11 +void +gsr_config_hotkey_to_x11(const GsrConfigHotkey *hk, + unsigned int *out_x11_modifiers, + uint64_t *out_keysym) +{ + if (!hk) { + if (out_x11_modifiers) *out_x11_modifiers = 0; + if (out_keysym) *out_keysym = 0; + return; + } + + if (out_keysym) + *out_keysym = (uint64_t)hk->keysym; + + if (!out_x11_modifiers) + return; + + /* Convert custom modifier bitmask to X11 mask. + * This matches key_mod_mask_to_x11_mask() from the GTK3 code. */ + uint32_t m = hk->modifiers; + unsigned int x11 = 0; + + if (m & MASK_CONTROL) x11 |= ControlMask; + if (m & MASK_ALT) x11 |= Mod1Mask; + if (m & MASK_SHIFT) x11 |= ShiftMask; + if (m & (MASK_META | MASK_SUPER)) x11 |= Mod4Mask; + + *out_x11_modifiers = x11; +} +#endif /* HAVE_X11 */ |
