#include "../include/Config.hpp" #include "../include/Storage.hpp" #include #include #include #include #include // TODO: Show nice error message if config looks wrong (wrong types, has configs that do not exist (maybe spelling mistake?)) namespace QuickMedia { static bool config_initialized = false; static Config *config = nullptr; static float scale = 1.0f; static bool scale_set = false; // Whoever designed xdg-user-dirs is retarded. Why are some XDG variables environment variables // while others are in this pseudo shell config file ~/.config/user-dirs.dirs static std::map get_xdg_variables() { std::string user_dirs_filepath; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); if(xdg_config_home) { user_dirs_filepath = xdg_config_home; } else { user_dirs_filepath = get_home_dir().data + "/.config"; } user_dirs_filepath += "/user-dirs.dirs"; std::map result; FILE *f = fopen(user_dirs_filepath.c_str(), "rb"); if(!f) return result; char line[PATH_MAX]; while(fgets(line, sizeof(line), f)) { int len = strlen(line); if(len < 2) continue; if(line[0] == '#') continue; if(line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } if(line[len - 1] != '"') continue; line[len - 1] = '\0'; len--; const char *sep = strchr(line, '='); if(!sep) continue; if(sep[1] != '\"') continue; std::string value(sep + 2); if(strncmp(value.c_str(), "$HOME/", 6) == 0) value = get_home_dir().data + value.substr(5); std::string key(line, sep - line); result[std::move(key)] = std::move(value); } fclose(f); return result; } static const int XFT_DPI_DEFAULT = 96; // Returns XFT_DPI_DEFAULT on error static int xrdb_get_dpi() { int xft_dpi = XFT_DPI_DEFAULT; Display *display = XOpenDisplay(nullptr); if(!display) { fprintf(stderr, "Failed to open x display\n"); return xft_dpi; } char *dpi = XGetDefault(display, "Xft", "dpi"); if(dpi) { xft_dpi = strtol(dpi, nullptr, 10); if(xft_dpi == 0) xft_dpi = XFT_DPI_DEFAULT; } XCloseDisplay(display); return xft_dpi; } static float get_ui_scale() { if(scale_set) return scale; const char *gdk_scale = getenv("GDK_SCALE"); if(gdk_scale) { setlocale(LC_ALL, "C"); // Sigh... stupid C scale = atof(gdk_scale); } else { scale = (float)xrdb_get_dpi() / (float)XFT_DPI_DEFAULT; } if(scale < 0.0001f) scale = 1.0f; scale_set = true; return scale; } static std::string path_expanduser(const std::string &path) { std::string result; if(!path.empty() && path[0] == '~') result = get_home_dir().data + path.substr(1); else if(path.size() >= 5 && strncmp(path.c_str(), "$HOME", 5) == 0) result = get_home_dir().data + path.substr(5); else result = path; return result; } template static void get_json_value(const Json::Value &json_obj, const char *field_name, T &val) { const Json::Value &json_val = json_obj[field_name]; if(json_val.isNull()) return; if(!json_val.is()) { fprintf(stderr, "Warning: config variable \"%s\" is not a %s\n", field_name, typeid(T).name()); return; } val = json_val.as(); } static void get_json_value(const Json::Value &json_obj, const char *field_name, float &val) { const Json::Value &json_val = json_obj[field_name]; if(json_val.isNull()) return; if(!json_val.isDouble()) { fprintf(stderr, "Warning: config variable \"%s\" is not a float\n", field_name); return; } val = json_val.asDouble(); } static void get_json_value_path(const Json::Value &json_obj, const char *field_name, std::string &val) { get_json_value(json_obj, field_name, val); path_expanduser(val); while(!val.empty() && val.back() == '/') { val.pop_back(); } } static void matrix_known_homeservers_fallback() { config->matrix.known_homeservers.insert(config->matrix.known_homeservers.end(), { "midov.pl", "matrix.org", "kde.org", "librem.one", "maunium.net", "halogen.city", "gnome.org", "shivering-isles.com", "nerdsin.space", "glowers.club", "privacytools.io", "linuxdelta.com", "tchncs.de", "jupiterbroadcasting.com" }); } static void peertube_known_instances_fallback() { config->peertube.known_instances.insert(config->peertube.known_instances.end(), { "https://tube.midov.pl", "https://videos.lukesmith.xyz", "https://peertube.se", "https://bittube.video", "https://video.nobodyhasthe.biz", "https://libre.tube", "https://open.tube", "https://runtube.re", "https://tube.kenfm.de", "https://tcode.kenfm.de", "https://tube.querdenken-711.de", "https://peertube.rage.lol", "https://gegenstimme.tv", "https://tv.poa.st", "https://libre.video", "https://gorf.tube", "https://neogenesis.tv" }); } static void config_load_fail_fallback() { matrix_known_homeservers_fallback(); peertube_known_instances_fallback(); } // No-op if this has already been called before static void init_config() { if(config_initialized) return; setlocale(LC_ALL, "C"); // Sigh... stupid C config_initialized = true; // Wtf? can't use static non-pointer config because it causes a segfault when setting config.theme. // It looks like a libc bug??? crashes for both gcc and clang. config = new Config(); config->scale = get_ui_scale(); Path config_path = get_storage_dir().join("config.json"); if(get_file_type(config_path) != FileType::REGULAR) { config_load_fail_fallback(); return; } Json::Value json_root; if(!read_file_as_json(config_path, json_root) || !json_root.isObject()) { fprintf(stderr, "Warning: failed to parse config file: %s\n", config_path.data.c_str()); config_load_fail_fallback(); return; } const Json::Value &search_json = json_root["search"]; if(search_json.isObject()) get_json_value(search_json, "font_size", config->search.font_size); const Json::Value &tab_json = json_root["tab"]; if(tab_json.isObject()) get_json_value(tab_json, "font_size", config->tab.font_size); const Json::Value &body_json = json_root["body"]; if(body_json.isObject()) { get_json_value(body_json, "title_font_size", config->body.title_font_size); get_json_value(body_json, "author_font_size", config->body.author_font_size); get_json_value(body_json, "description_font_size", config->body.description_font_size); get_json_value(body_json, "timestamp_font_size", config->body.timestamp_font_size); get_json_value(body_json, "reaction_font_size", config->body.reaction_font_size); get_json_value(body_json, "embedded_load_font_size", config->body.embedded_load_font_size); get_json_value(body_json, "loading_text_font_size", config->body.loading_text_font_size); } const Json::Value &input_json = json_root["input"]; if(input_json.isObject()) get_json_value(input_json, "font_size", config->input.font_size); const Json::Value &animation_json = json_root["animation"]; if(animation_json.isObject()) { get_json_value(animation_json, "move_speed", config->animation.move_speed); get_json_value(animation_json, "loading_icon_speed", config->animation.loading_icon_speed); } const Json::Value &video_json = json_root["video"]; if(video_json.isObject()) get_json_value(video_json, "max_height", config->video.max_height); const Json::Value &local_manga_json = json_root["local_manga"]; if(local_manga_json.isObject()) { get_json_value_path(local_manga_json, "directory", config->local_manga.directory); get_json_value(local_manga_json, "load_progress", config->local_manga.sort_by_name); get_json_value(local_manga_json, "sort_chapters_by_name", config->local_manga.sort_chapters_by_name); } const Json::Value &local_anime_json = json_root["local_anime"]; if(local_anime_json.isObject()) { get_json_value_path(local_anime_json, "directory", config->local_anime.directory); get_json_value(local_anime_json, "sort_by_name", config->local_anime.sort_by_name); get_json_value(local_anime_json, "auto_group_episodes", config->local_anime.auto_group_episodes); } const Json::Value &youtube_json = json_root["youtube"]; if(youtube_json.isObject()) { get_json_value(youtube_json, "load_progress", config->youtube.load_progress); get_json_value(youtube_json, "invidious_instance", config->youtube.invidious_instance); const Json::Value &sponsorblock_json = youtube_json["sponsorblock"]; if(sponsorblock_json.isObject()) { get_json_value(sponsorblock_json, "enable", config->youtube.sponsorblock.enable); get_json_value(sponsorblock_json, "api_endpoint", config->youtube.sponsorblock.api_endpoint); get_json_value(sponsorblock_json, "min_votes", config->youtube.sponsorblock.min_votes); } } bool has_known_matrix_homeservers_config = false; const Json::Value &matrix_json = json_root["matrix"]; if(matrix_json.isObject()) { const Json::Value &known_homeservers_json = matrix_json["known_homeservers"]; if(known_homeservers_json.isArray()) { has_known_matrix_homeservers_config = true; for(const Json::Value &known_homeserver : known_homeservers_json) { if(!known_homeserver.isString()) { fprintf(stderr, "Warning: matrix.known_homeservers config contains non string value\n"); continue; } config->matrix.known_homeservers.push_back(known_homeserver.asString()); } } get_json_value(matrix_json, "gpg_user_id", config->matrix.gpg_user_id); get_json_value(matrix_json, "room_name_font_size", config->matrix.room_name_font_size); get_json_value(matrix_json, "room_description_font_size", config->matrix.room_description_font_size); get_json_value(matrix_json, "send_read_receipts", config->matrix.send_read_receipts); get_json_value(matrix_json, "send_typing_notifications", config->matrix.send_typing_notifications); get_json_value(matrix_json, "appear_online", config->matrix.appear_online); get_json_value(matrix_json, "clear_message_on_escape", config->matrix.clear_message_on_escape); } if(!has_known_matrix_homeservers_config) matrix_known_homeservers_fallback(); bool has_known_peertube_homeservers_config = false; const Json::Value &peertube_json = json_root["peertube"]; if(peertube_json.isObject()) { const Json::Value &known_instances_json = peertube_json["known_instances"]; if(known_instances_json.isArray()) { has_known_peertube_homeservers_config = true; for(const Json::Value &known_instance : known_instances_json) { if(!known_instance.isString()) { fprintf(stderr, "Warning: peertube.known_instances config contains non string value\n"); continue; } config->peertube.known_instances.push_back(known_instance.asString()); } } } if(!has_known_peertube_homeservers_config) peertube_known_instances_fallback(); const std::string home_dir = get_home_dir().data; auto xdg_vars = get_xdg_variables(); struct DownloadPaths { const char *json_field; std::string fallback_dir; const char *xdg_var_name; std::string *config_var; }; const int num_download_paths = 4; DownloadPaths download_paths_list[num_download_paths] = { { "video_directory", home_dir + "/Videos", "XDG_VIDEOS_DIR", &config->download.video_directory }, { "image_directory", home_dir + "/Pictures", "XDG_PICTURES_DIR", &config->download.image_directory }, { "music_directory", home_dir + "/Music", "XDG_MUSIC_DIR", &config->download.music_directory }, { "file_directory", home_dir + "/Downloads", "XDG_DOWNLOAD_DIR", &config->download.file_directory }, }; const Json::Value &download_json = json_root["download"]; if(download_json.isObject()) { for(const DownloadPaths &download_paths : download_paths_list) { const Json::Value &directory_json = download_json[download_paths.json_field]; if(!directory_json.isString()) { fprintf(stderr, "Warning: config variable config.download.%s is not a string, using path \"%s\" or \"%s\" instead\n", download_paths.json_field, download_paths.xdg_var_name, download_paths.fallback_dir.c_str()); continue; } *download_paths.config_var = path_expanduser(directory_json.asString()); } } for(const DownloadPaths &download_paths : download_paths_list) { if(download_paths.config_var->empty()) { std::string dir = download_paths.fallback_dir; const std::string &xdg_var = xdg_vars[download_paths.xdg_var_name]; if(!xdg_var.empty()) dir = xdg_var; *download_paths.config_var = std::move(dir); } } const Json::Value &font_json = json_root["font"]; if(font_json.isObject()) { get_json_value_path(font_json, "latin", config->font.latin); get_json_value_path(font_json, "latin_bold", config->font.latin_bold); get_json_value_path(font_json, "latin_monospace", config->font.latin_monospace); get_json_value_path(font_json, "cjk", config->font.cjk); get_json_value_path(font_json, "symbols", config->font.symbols); const Json::Value &font_scale_json = font_json["scale"]; if(font_scale_json.isObject()) { get_json_value(font_scale_json, "latin", config->font.scale.latin); get_json_value(font_scale_json, "latin_bold", config->font.scale.latin_bold); get_json_value(font_scale_json, "latin_monospace", config->font.scale.latin_monospace); get_json_value(font_scale_json, "cjk", config->font.scale.cjk); get_json_value(font_scale_json, "symbols", config->font.scale.symbols); } } const Json::Value &mangadex_json = json_root["mangadex"]; if(mangadex_json.isObject()) { get_json_value(mangadex_json, "allow_hentai", config->mangadex.allow_hentai); } const Json::Value &file_manager_json = json_root["file_manager"]; if(file_manager_json.isObject()) { get_json_value(file_manager_json, "grid_view", config->file_manager.grid_view); } get_json_value(json_root, "use_system_fonts", config->use_system_fonts); get_json_value(json_root, "use_system_mpv_config", config->use_system_mpv_config); get_json_value(json_root, "enable_shaders", config->enable_shaders); get_json_value(json_root, "theme", config->theme); get_json_value(json_root, "scale", config->scale); get_json_value(json_root, "font_scale", config->font_scale); get_json_value(json_root, "spacing_scale", config->spacing_scale); get_json_value(json_root, "low_latency_mode", config->low_latency_mode); } const Config& get_config() { init_config(); return *config; } }