41 if (!std::isfinite(scale)) {
48 auto parent = path.parent_path();
50 return absl::OkStatus();
74 std::unordered_map<std::string, bool>* panel_state) {
78 for (
auto it = panel_state->begin(); it != panel_state->end();) {
80 it = panel_state->erase(it);
88 return panel_id ==
"dungeon.object_editor" ||
89 panel_id ==
"dungeon.settings" || panel_id ==
"dungeon.dungeon_map";
93 std::unordered_map<std::string, bool>* panel_state) {
97 for (
auto it = panel_state->begin(); it != panel_state->end();) {
99 it = panel_state->erase(it);
107 "dungeon.room_selector",
"dungeon.room_matrix",
108 "dungeon.object_selector",
"dungeon.sprite_editor",
109 "dungeon.item_editor",
"dungeon.room_graphics",
110 "dungeon.door_editor",
"dungeon.palette_editor",
111 "dungeon.entrance_list",
"dungeon.entrance_properties",
115 "dungeon.custom_collision",
"dungeon.water_fill",
116 "dungeon.room_tags",
"dungeon.object_coverage",
117 "dungeon.object_tile_editor",
"dungeon.overlay_manager",
118 "dungeon.minecart_tracks",
122 std::unordered_map<std::string, bool>* panel_state) {
126 (*panel_state)[
"dungeon.workbench"] =
true;
128 (*panel_state)[panel_id] =
false;
133 std::unordered_map<std::string, bool>* panel_state) {
139 (*panel_state)[panel_id] =
false;
155 {
"Agent",
"agent.mesen_debug"},
156 {
"Agent",
"agent.mesen_screenshot"},
157 {
"Agent",
"agent.oracle_states"},
158 {
"Agent",
"agent.feature_flags"},
159 {
"Agent",
"agent.manifest"},
160 {
"Agent",
"agent.sram_viewer"},
163 {
"Agent",
"oracle.validation"},
164 {
"Agent",
"oracle.menu_inspector"},
166 {
"Graphics",
"graphics.prototype_viewer"},
168 {
"Screen",
"screen.naming_screen"},
170 {
"Emulator",
"emulator.save_states"},
174 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
176 if (!visibility_state) {
180 auto category_it = visibility_state->find(window.category);
181 if (category_it == visibility_state->end()) {
184 auto window_it = category_it->second.find(window.window_id);
185 if (window_it != category_it->second.end()) {
186 window_it->second =
false;
192 if (!json_body || json_body->empty()) {
195 auto parsed = nlohmann::json::parse(*json_body,
nullptr,
false);
196 if (parsed.is_discarded() || !parsed.is_object() ||
197 !parsed.contains(
"root")) {
201 bool changed =
false;
202 auto prune_node = [&changed](
auto&& self, nlohmann::json& node) ->
void {
203 if (!node.is_object()) {
206 const std::string type = node.contains(
"type") && node[
"type"].is_string()
207 ? node[
"type"].get<std::string>()
209 if (type ==
"leaf" && node.contains(
"panels") &&
210 node[
"panels"].is_array()) {
211 auto& panels = node[
"panels"];
212 for (
auto it = panels.begin(); it != panels.end();) {
213 const bool erase = it->is_object() && it->contains(
"panel_id") &&
214 (*it)[
"panel_id"].is_string() &&
216 (*it)[
"panel_id"].get<std::string>());
218 it = panels.erase(it);
224 const int max_active =
225 panels.empty() ? 0 :
static_cast<int>(panels.size()) - 1;
226 const int active_tab_index =
227 node.contains(
"active_tab_index") &&
228 node[
"active_tab_index"].is_number_integer()
229 ? node[
"active_tab_index"].get<
int>()
231 const int clamped_active = std::clamp(active_tab_index, 0, max_active);
232 if (active_tab_index != clamped_active) {
233 node[
"active_tab_index"] = clamped_active;
237 if (node.contains(
"child_a")) {
238 self(self, node[
"child_a"]);
240 if (node.contains(
"child_b")) {
241 self(self, node[
"child_b"]);
244 prune_node(prune_node, parsed[
"root"]);
247 *json_body = parsed.dump();
255 return absl::InvalidArgumentError(
"prefs is null");
260 return absl::OkStatus();
263 std::istringstream ss(data);
268 auto to_float = [](
const std::string& s,
float fallback) {
271 }
catch (
const std::exception&) {
275 auto to_int = [](
const std::string& s,
int fallback) {
278 }
catch (
const std::exception&) {
282 while (std::getline(ss, line)) {
283 size_t eq_pos = line.find(
'=');
284 if (eq_pos == std::string::npos) {
288 std::string key = line.substr(0, eq_pos);
289 std::string val = line.substr(eq_pos + 1);
292 if (key ==
"font_global_scale") {
295 }
else if (key ==
"backup_rom") {
297 }
else if (key ==
"save_new_auto") {
299 }
else if (key ==
"autosave_enabled") {
301 }
else if (key ==
"autosave_interval") {
303 }
else if (key ==
"recent_files_limit") {
305 }
else if (key ==
"last_rom_path") {
307 }
else if (key ==
"last_project_path") {
309 }
else if (key ==
"show_welcome_on_startup") {
311 }
else if (key ==
"test_mode") {
313 }
else if (key ==
"restore_last_session") {
315 }
else if (key ==
"prefer_hmagic_sprite_names") {
317 }
else if (key ==
"feature_hints_enabled") {
319 }
else if (
constexpr std::string_view kDismissedHintPrefix =
320 "dismissed_feature_hint.";
321 key.rfind(kDismissedHintPrefix, 0) == 0) {
322 const std::string hint_id = key.substr(kDismissedHintPrefix.size());
323 if (!hint_id.empty() && val ==
"1") {
326 }
else if (key ==
"welcome_triforce_alpha") {
329 }
else if (key ==
"welcome_triforce_speed") {
332 }
else if (key ==
"welcome_triforce_size") {
335 }
else if (key ==
"welcome_particles_enabled") {
337 }
else if (key ==
"welcome_mouse_repel_enabled") {
339 }
else if (key ==
"reduced_motion") {
341 }
else if (key ==
"switch_motion_profile") {
343 }
else if (key ==
"last_theme_name") {
345 }
else if (key ==
"language_locale") {
347 }
else if (key ==
"font_family_index") {
351 else if (key ==
"backup_before_save") {
353 }
else if (key ==
"default_editor") {
357 else if (key ==
"vsync") {
358 prefs->
vsync = (val ==
"1");
359 }
else if (key ==
"target_fps") {
361 }
else if (key ==
"cache_size_mb") {
363 }
else if (key ==
"undo_history_size") {
367 else if (key ==
"ai_provider") {
369 }
else if (key ==
"ai_model") {
371 }
else if (key ==
"ollama_url") {
373 }
else if (key ==
"gemini_api_key") {
375 }
else if (key ==
"openai_api_key") {
377 }
else if (key ==
"anthropic_api_key") {
379 }
else if (key ==
"ai_temperature") {
381 }
else if (key ==
"ai_max_tokens") {
383 }
else if (key ==
"ai_proactive") {
385 }
else if (key ==
"ai_auto_learn") {
387 }
else if (key ==
"ai_multimodal") {
391 else if (key ==
"log_level") {
393 }
else if (key ==
"log_to_file") {
395 }
else if (key ==
"log_file_path") {
397 }
else if (key ==
"log_ai_requests") {
399 }
else if (key ==
"log_rom_operations") {
401 }
else if (key ==
"log_gui_automation") {
403 }
else if (key ==
"log_proposals") {
407 else if (key.substr(0, 15) ==
"panel_shortcut.") {
408 std::string panel_id = key.substr(15);
412 else if (key.substr(0, 14) ==
"card_shortcut.") {
413 std::string panel_id = key.substr(14);
417 else if (key ==
"sidebar_visible") {
419 }
else if (key ==
"sidebar_panel_expanded") {
421 }
else if (key ==
"sidebar_panel_width") {
423 }
else if (key ==
"panel_browser_category_width") {
426 }
else if (key ==
"panel_layout_defaults_revision") {
429 }
else if (key ==
"sidebar_active_category") {
431 }
else if (key.rfind(
"sidebar_context_collapsed.", 0) == 0) {
432 const std::string category =
433 key.substr(std::string(
"sidebar_context_collapsed.").size());
434 if (!category.empty()) {
441 }
else if (key ==
"dungeon_inspector_side") {
443 (val ==
"left") ? std::string(
"left") : std::string(
"right");
446 else if (key ==
"show_status_bar") {
448 }
else if (key ==
"emulator_keep_running_in_background") {
450 }
else if (key ==
"show_experimental_editors") {
454 else if (key.substr(0, 17) ==
"panel_visibility.") {
455 std::string rest = key.substr(17);
456 size_t dot_pos = rest.find(
'.');
457 if (dot_pos != std::string::npos) {
458 std::string editor_type = rest.substr(0, dot_pos);
459 std::string panel_id = rest.substr(dot_pos + 1);
464 else if (key.substr(0, 13) ==
"pinned_panel.") {
465 std::string panel_id = key.substr(13);
469 else if (key.substr(0, 18) ==
"right_panel_width.") {
470 std::string panel_key = key.substr(18);
474 else if (key.substr(0, 13) ==
"saved_layout.") {
475 std::string rest = key.substr(13);
476 size_t dot_pos = rest.find(
'.');
477 if (dot_pos != std::string::npos) {
478 std::string layout_name = rest.substr(0, dot_pos);
479 std::string panel_id = rest.substr(dot_pos + 1);
486 else if (key.substr(0, 13) ==
"named_layout.") {
487 std::string layout_name = key.substr(13);
489 }
else if (key ==
"last_applied_layout_name") {
495 else if (key ==
"rom_load_choice") {
496 const size_t tab = val.find(
'\t');
497 if (tab != std::string::npos && tab + 1 < val.size()) {
503 return absl::OkStatus();
509 if (!ensure_status.ok()) {
510 return ensure_status;
513 std::ostringstream ss;
516 ss <<
"backup_rom=" << (prefs.
backup_rom ? 1 : 0) <<
"\n";
517 ss <<
"save_new_auto=" << (prefs.
save_new_auto ? 1 : 0) <<
"\n";
525 ss <<
"test_mode=" << (prefs.
test_mode ? 1 : 0) <<
"\n";
527 ss <<
"prefer_hmagic_sprite_names="
531 std::vector<std::string> dismissed_hints(
534 std::sort(dismissed_hints.begin(), dismissed_hints.end());
535 for (
const auto& hint_id : dismissed_hints) {
536 ss <<
"dismissed_feature_hint." << hint_id <<
"=1\n";
541 ss <<
"welcome_particles_enabled="
543 ss <<
"welcome_mouse_repel_enabled="
545 ss <<
"reduced_motion=" << (prefs.
reduced_motion ? 1 : 0) <<
"\n";
556 ss <<
"vsync=" << (prefs.
vsync ? 1 : 0) <<
"\n";
557 ss <<
"target_fps=" << prefs.
target_fps <<
"\n";
563 ss <<
"ai_model=" << prefs.
ai_model <<
"\n";
564 ss <<
"ollama_url=" << prefs.
ollama_url <<
"\n";
570 ss <<
"ai_proactive=" << (prefs.
ai_proactive ? 1 : 0) <<
"\n";
571 ss <<
"ai_auto_learn=" << (prefs.
ai_auto_learn ? 1 : 0) <<
"\n";
572 ss <<
"ai_multimodal=" << (prefs.
ai_multimodal ? 1 : 0) <<
"\n";
575 ss <<
"log_level=" << prefs.
log_level <<
"\n";
576 ss <<
"log_to_file=" << (prefs.
log_to_file ? 1 : 0) <<
"\n";
581 ss <<
"log_proposals=" << (prefs.
log_proposals ? 1 : 0) <<
"\n";
585 ss <<
"panel_shortcut." << panel_id <<
"=" << shortcut <<
"\n";
595 ss <<
"panel_layout_defaults_revision="
599 ss <<
"sidebar_context_collapsed." << category <<
"=1\n";
605 ss <<
"emulator_keep_running_in_background="
607 ss <<
"show_experimental_editors="
612 for (
const auto& [panel_id, visible] : panel_state) {
613 ss <<
"panel_visibility." << editor_type <<
"." << panel_id <<
"="
614 << (visible ? 1 : 0) <<
"\n";
620 ss <<
"pinned_panel." << panel_id <<
"=" << (pinned ? 1 : 0) <<
"\n";
624 ss <<
"right_panel_width." << panel_key <<
"=" << width <<
"\n";
628 for (
const auto& [layout_name, panel_state] : prefs.
saved_layouts) {
629 for (
const auto& [panel_id, visible] : panel_state) {
630 ss <<
"saved_layout." << layout_name <<
"." << panel_id <<
"="
631 << (visible ? 1 : 0) <<
"\n";
639 for (
const auto& [layout_name, json_body] : prefs.
named_layouts) {
642 std::string single_line;
643 single_line.reserve(json_body.size());
644 for (
char c : json_body) {
645 if (c ==
'\n' || c ==
'\r')
647 single_line.push_back(c);
649 ss <<
"named_layout." << layout_name <<
"=" << single_line <<
"\n";
655 if (rom_path.find_first_of(
"\r\n") != std::string::npos ||
656 choice.find_first_of(
"\t\r\n") != std::string::npos) {
659 ss <<
"rom_load_choice=" << choice <<
"\t" << rom_path <<
"\n";
662 std::ofstream file(path);
663 if (!file.is_open()) {
664 return absl::InternalError(
665 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
668 return absl::OkStatus();
685 UserSettings::Preferences::AiHost host;
686 host.id =
"ollama-local";
687 host.label =
"Ollama (local)";
689 host.api_type =
"ollama";
690 host.supports_tools =
true;
691 host.supports_streaming =
true;
696 UserSettings::Preferences::AiHost lmstudio;
697 lmstudio.id =
"lmstudio-local";
698 lmstudio.label =
"LM Studio (local)";
699 lmstudio.base_url =
"http://localhost:1234";
700 lmstudio.api_type =
"lmstudio";
701 lmstudio.supports_tools =
true;
702 lmstudio.supports_streaming =
true;
703 prefs->
ai_hosts.push_back(lmstudio);
710void EnsureDefaultAiProfiles(UserSettings::Preferences* prefs) {
714 if (!prefs->ai_profiles.empty()) {
715 if (prefs->active_ai_profile.empty()) {
716 prefs->active_ai_profile = prefs->ai_profiles.front().name;
720 if (!prefs->ai_model.empty()) {
721 UserSettings::Preferences::AiModelProfile profile;
722 profile.name =
"default";
723 profile.model = prefs->ai_model;
724 profile.temperature = prefs->ai_temperature;
725 profile.top_p = 0.95f;
726 profile.max_output_tokens = prefs->ai_max_tokens;
727 profile.supports_tools =
true;
728 prefs->ai_profiles.push_back(profile);
729 prefs->active_ai_profile = profile.name;
733void EnsureDefaultFilesystemRoots(UserSettings::Preferences* prefs) {
738 auto add_unique_root = [&](
const std::filesystem::path& path) {
742 const std::string path_str = path.string();
743 auto it = std::find(prefs->project_root_paths.begin(),
744 prefs->project_root_paths.end(), path_str);
745 if (it == prefs->project_root_paths.end()) {
746 prefs->project_root_paths.push_back(path_str);
752 add_unique_root(*docs_dir);
755 if (prefs->use_icloud_sync) {
758 if (icloud_dir.ok()) {
759 add_unique_root(*icloud_dir);
760 if (prefs->default_project_root.empty()) {
761 prefs->default_project_root = icloud_dir->string();
766 if (prefs->default_project_root.empty() &&
767 !prefs->project_root_paths.empty()) {
768 prefs->default_project_root = prefs->project_root_paths.front();
772void EnsureDefaultModelPaths(UserSettings::Preferences* prefs) {
776 if (!prefs->ai_model_paths.empty()) {
780 auto add_unique_path = [&](
const std::filesystem::path& path) {
784 const std::string path_str = path.string();
785 auto it = std::find(prefs->ai_model_paths.begin(),
786 prefs->ai_model_paths.end(), path_str);
787 if (it == prefs->ai_model_paths.end()) {
788 prefs->ai_model_paths.push_back(path_str);
793 if (!home_dir.empty() && home_dir !=
".") {
794 add_unique_path(home_dir /
"models");
795 add_unique_path(home_dir /
".lmstudio" /
"models");
796 add_unique_path(home_dir /
".ollama" /
"models");
800void LoadStringMap(
const json& src,
801 std::unordered_map<std::string, std::string>* target) {
802 if (!target || !src.is_object()) {
806 for (
const auto& [key, value] : src.items()) {
807 if (value.is_string()) {
808 (*target)[key] = value.get<std::string>();
813void LoadBoolMap(
const json& src,
814 std::unordered_map<std::string, bool>* target) {
815 if (!target || !src.is_object()) {
819 for (
const auto& [key, value] : src.items()) {
820 if (value.is_boolean()) {
821 (*target)[key] = value.get<
bool>();
826void LoadFloatMap(
const json& src,
827 std::unordered_map<std::string, float>* target) {
828 if (!target || !src.is_object()) {
832 for (
const auto& [key, value] : src.items()) {
833 if (value.is_number()) {
834 (*target)[key] = value.get<
float>();
839void LoadNestedBoolMap(
841 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
843 if (!target || !src.is_object()) {
847 for (
const auto& [outer_key, outer_val] : src.items()) {
848 if (!outer_val.is_object()) {
851 auto& inner = (*target)[outer_key];
853 for (
const auto& [inner_key, inner_val] : outer_val.items()) {
854 if (inner_val.is_boolean()) {
855 inner[inner_key] = inner_val.get<
bool>();
861json ToStringMap(
const std::unordered_map<std::string, std::string>& map) {
862 json obj = json::object();
863 for (
const auto& [key, value] : map) {
869json ToBoolMap(
const std::unordered_map<std::string, bool>& map) {
870 json obj = json::object();
871 for (
const auto& [key, value] : map) {
877json ToFloatMap(
const std::unordered_map<std::string, float>& map) {
878 json obj = json::object();
879 for (
const auto& [key, value] : map) {
885json ToNestedBoolMap(
const std::unordered_map<
886 std::string, std::unordered_map<std::string, bool>>& map) {
887 json obj = json::object();
888 for (
const auto& [outer_key, inner] : map) {
889 obj[outer_key] = ToBoolMap(inner);
894absl::Status LoadPreferencesFromJson(
const std::filesystem::path& path,
895 UserSettings::Preferences* prefs) {
897 return absl::InvalidArgumentError(
"prefs is null");
900 std::ifstream file(path);
901 if (!file.is_open()) {
902 return absl::NotFoundError(
903 absl::StrFormat(
"Settings file not found: %s", path.string()));
909 }
catch (
const std::exception& e) {
910 return absl::InternalError(
911 absl::StrFormat(
"Failed to parse settings.json: %s", e.what()));
914 if (root.contains(
"general")) {
915 const auto& g = root[
"general"];
917 g.value(
"font_global_scale", prefs->font_global_scale));
918 prefs->backup_rom = g.value(
"backup_rom", prefs->backup_rom);
919 prefs->save_new_auto = g.value(
"save_new_auto", prefs->save_new_auto);
920 prefs->autosave_enabled =
921 g.value(
"autosave_enabled", prefs->autosave_enabled);
922 prefs->autosave_interval =
923 g.value(
"autosave_interval", prefs->autosave_interval);
924 prefs->recent_files_limit =
925 g.value(
"recent_files_limit", prefs->recent_files_limit);
926 prefs->last_rom_path = g.value(
"last_rom_path", prefs->last_rom_path);
927 prefs->last_project_path =
928 g.value(
"last_project_path", prefs->last_project_path);
929 prefs->show_welcome_on_startup =
930 g.value(
"show_welcome_on_startup", prefs->show_welcome_on_startup);
931 prefs->test_mode = g.value(
"test_mode", prefs->test_mode);
932 prefs->restore_last_session =
933 g.value(
"restore_last_session", prefs->restore_last_session);
934 prefs->prefer_hmagic_sprite_names = g.value(
935 "prefer_hmagic_sprite_names", prefs->prefer_hmagic_sprite_names);
936 prefs->welcome_triforce_alpha =
937 g.value(
"welcome_triforce_alpha", prefs->welcome_triforce_alpha);
938 prefs->welcome_triforce_speed =
939 g.value(
"welcome_triforce_speed", prefs->welcome_triforce_speed);
940 prefs->welcome_triforce_size =
941 g.value(
"welcome_triforce_size", prefs->welcome_triforce_size);
942 prefs->welcome_particles_enabled =
943 g.value(
"welcome_particles_enabled", prefs->welcome_particles_enabled);
944 prefs->welcome_mouse_repel_enabled = g.value(
945 "welcome_mouse_repel_enabled", prefs->welcome_mouse_repel_enabled);
948 if (root.contains(
"onboarding")) {
949 const auto& onboarding = root[
"onboarding"];
950 prefs->feature_hints_enabled =
951 onboarding.value(
"feature_hints_enabled", prefs->feature_hints_enabled);
952 if (onboarding.contains(
"dismissed_feature_hints") &&
953 onboarding[
"dismissed_feature_hints"].is_array()) {
954 prefs->dismissed_feature_hints.clear();
955 for (
const auto& item : onboarding[
"dismissed_feature_hints"]) {
956 if (item.is_string()) {
957 prefs->dismissed_feature_hints.insert(item.get<std::string>());
963 if (root.contains(
"appearance")) {
964 const auto& appearance = root[
"appearance"];
965 prefs->reduced_motion =
966 appearance.value(
"reduced_motion", prefs->reduced_motion);
967 prefs->switch_motion_profile =
968 appearance.value(
"switch_motion_profile", prefs->switch_motion_profile);
969 prefs->last_theme_name =
970 appearance.value(
"last_theme_name", prefs->last_theme_name);
971 prefs->language_locale =
972 appearance.value(
"language_locale", prefs->language_locale);
973 prefs->font_family_index =
974 appearance.value(
"font_family_index", prefs->font_family_index);
977 if (root.contains(
"editor")) {
978 const auto& e = root[
"editor"];
979 prefs->backup_before_save =
980 e.value(
"backup_before_save", prefs->backup_before_save);
981 prefs->default_editor = e.value(
"default_editor", prefs->default_editor);
984 e.value(
"dungeon_inspector_side", prefs->dungeon_inspector_side);
985 prefs->dungeon_inspector_side = (side ==
"left") ?
"left" :
"right";
989 if (root.contains(
"performance")) {
990 const auto& p = root[
"performance"];
991 prefs->vsync = p.value(
"vsync", prefs->vsync);
992 prefs->target_fps = p.value(
"target_fps", prefs->target_fps);
993 prefs->cache_size_mb = p.value(
"cache_size_mb", prefs->cache_size_mb);
994 prefs->undo_history_size =
995 p.value(
"undo_history_size", prefs->undo_history_size);
998 if (root.contains(
"ai")) {
999 const auto& ai = root[
"ai"];
1000 prefs->ai_provider = ai.value(
"provider", prefs->ai_provider);
1001 prefs->ai_model = ai.value(
"model", prefs->ai_model);
1002 prefs->ollama_url = ai.value(
"ollama_url", prefs->ollama_url);
1003 prefs->gemini_api_key = ai.value(
"gemini_api_key", prefs->gemini_api_key);
1004 prefs->openai_api_key = ai.value(
"openai_api_key", prefs->openai_api_key);
1005 prefs->anthropic_api_key =
1006 ai.value(
"anthropic_api_key", prefs->anthropic_api_key);
1007 std::string google_key = ai.value(
"google_api_key", std::string());
1008 if (prefs->gemini_api_key.empty() && !google_key.empty()) {
1009 prefs->gemini_api_key = google_key;
1011 prefs->ai_temperature = ai.value(
"temperature", prefs->ai_temperature);
1012 prefs->ai_max_tokens = ai.value(
"max_tokens", prefs->ai_max_tokens);
1013 prefs->ai_proactive = ai.value(
"proactive", prefs->ai_proactive);
1014 prefs->ai_auto_learn = ai.value(
"auto_learn", prefs->ai_auto_learn);
1015 prefs->ai_multimodal = ai.value(
"multimodal", prefs->ai_multimodal);
1016 prefs->active_ai_host_id =
1017 ai.value(
"active_host_id", prefs->active_ai_host_id);
1018 prefs->active_ai_profile =
1019 ai.value(
"active_profile", prefs->active_ai_profile);
1020 prefs->remote_build_host_id =
1021 ai.value(
"remote_build_host_id", prefs->remote_build_host_id);
1022 if (ai.contains(
"model_paths") && ai[
"model_paths"].is_array()) {
1023 prefs->ai_model_paths.clear();
1024 for (
const auto& item : ai[
"model_paths"]) {
1025 if (item.is_string()) {
1026 prefs->ai_model_paths.push_back(item.get<std::string>());
1031 if (ai.contains(
"hosts") && ai[
"hosts"].is_array()) {
1032 prefs->ai_hosts.clear();
1033 for (
const auto& host : ai[
"hosts"]) {
1034 if (!host.is_object()) {
1037 UserSettings::Preferences::AiHost entry;
1038 entry.id = host.value(
"id",
"");
1039 entry.label = host.value(
"label",
"");
1040 entry.base_url = host.value(
"base_url",
"");
1041 entry.api_type = host.value(
"api_type",
"");
1042 entry.supports_vision =
1043 host.value(
"supports_vision", entry.supports_vision);
1044 entry.supports_tools =
1045 host.value(
"supports_tools", entry.supports_tools);
1046 entry.supports_streaming =
1047 host.value(
"supports_streaming", entry.supports_streaming);
1048 entry.allow_insecure =
1049 host.value(
"allow_insecure", entry.allow_insecure);
1050 entry.api_key = host.value(
"api_key",
"");
1051 entry.credential_id = host.value(
"credential_id",
"");
1052 prefs->ai_hosts.push_back(entry);
1056 if (ai.contains(
"profiles") && ai[
"profiles"].is_array()) {
1057 prefs->ai_profiles.clear();
1058 for (
const auto& profile : ai[
"profiles"]) {
1059 if (!profile.is_object()) {
1062 UserSettings::Preferences::AiModelProfile entry;
1063 entry.name = profile.value(
"name",
"");
1064 entry.model = profile.value(
"model",
"");
1065 entry.temperature = profile.value(
"temperature", entry.temperature);
1066 entry.top_p = profile.value(
"top_p", entry.top_p);
1067 entry.max_output_tokens =
1068 profile.value(
"max_output_tokens", entry.max_output_tokens);
1069 entry.supports_vision =
1070 profile.value(
"supports_vision", entry.supports_vision);
1071 entry.supports_tools =
1072 profile.value(
"supports_tools", entry.supports_tools);
1073 prefs->ai_profiles.push_back(entry);
1078 if (root.contains(
"logging")) {
1079 const auto& log = root[
"logging"];
1080 prefs->log_level = log.value(
"level", prefs->log_level);
1081 prefs->log_to_file = log.value(
"to_file", prefs->log_to_file);
1082 prefs->log_file_path = log.value(
"file_path", prefs->log_file_path);
1083 prefs->log_ai_requests = log.value(
"ai_requests", prefs->log_ai_requests);
1084 prefs->log_rom_operations =
1085 log.value(
"rom_operations", prefs->log_rom_operations);
1086 prefs->log_gui_automation =
1087 log.value(
"gui_automation", prefs->log_gui_automation);
1088 prefs->log_proposals = log.value(
"proposals", prefs->log_proposals);
1091 if (root.contains(
"shortcuts")) {
1092 const auto& shortcuts = root[
"shortcuts"];
1093 if (shortcuts.contains(
"panel")) {
1094 LoadStringMap(shortcuts[
"panel"], &prefs->panel_shortcuts);
1096 if (shortcuts.contains(
"global")) {
1097 LoadStringMap(shortcuts[
"global"], &prefs->global_shortcuts);
1099 if (shortcuts.contains(
"editor")) {
1100 LoadStringMap(shortcuts[
"editor"], &prefs->editor_shortcuts);
1104 if (root.contains(
"sidebar")) {
1105 const auto& sidebar = root[
"sidebar"];
1106 prefs->sidebar_visible = sidebar.value(
"visible", prefs->sidebar_visible);
1107 prefs->sidebar_panel_expanded =
1108 sidebar.value(
"panel_expanded", prefs->sidebar_panel_expanded);
1109 prefs->sidebar_panel_width =
1110 sidebar.value(
"panel_width", prefs->sidebar_panel_width);
1111 prefs->panel_browser_category_width = sidebar.value(
1112 "panel_browser_category_width", prefs->panel_browser_category_width);
1113 prefs->sidebar_active_category =
1114 sidebar.value(
"active_category", prefs->sidebar_active_category);
1116 if (sidebar.contains(
"order") && sidebar[
"order"].is_array()) {
1117 prefs->sidebar_order.clear();
1118 for (
const auto& item : sidebar[
"order"]) {
1119 if (item.is_string()) {
1120 prefs->sidebar_order.push_back(item.get<std::string>());
1124 if (sidebar.contains(
"hidden") && sidebar[
"hidden"].is_array()) {
1125 prefs->sidebar_hidden.clear();
1126 for (
const auto& item : sidebar[
"hidden"]) {
1127 if (item.is_string()) {
1128 prefs->sidebar_hidden.insert(item.get<std::string>());
1132 if (sidebar.contains(
"pinned") && sidebar[
"pinned"].is_array()) {
1133 prefs->sidebar_pinned.clear();
1134 for (
const auto& item : sidebar[
"pinned"]) {
1135 if (item.is_string()) {
1136 prefs->sidebar_pinned.insert(item.get<std::string>());
1140 if (sidebar.contains(
"context_collapsed") &&
1141 sidebar[
"context_collapsed"].is_array()) {
1142 prefs->sidebar_context_collapsed.clear();
1143 for (
const auto& item : sidebar[
"context_collapsed"]) {
1144 if (item.is_string()) {
1145 prefs->sidebar_context_collapsed.insert(item.get<std::string>());
1151 if (root.contains(
"status_bar")) {
1152 const auto& status_bar = root[
"status_bar"];
1153 prefs->show_status_bar =
1154 status_bar.value(
"visible", prefs->show_status_bar);
1157 if (root.contains(
"emulator")) {
1158 const auto& emulator = root[
"emulator"];
1159 prefs->emulator_keep_running_in_background =
1160 emulator.value(
"keep_running_in_background",
1161 prefs->emulator_keep_running_in_background);
1164 if (root.contains(
"editors")) {
1165 const auto& editors = root[
"editors"];
1166 prefs->show_experimental_editors =
1167 editors.value(
"show_experimental", prefs->show_experimental_editors);
1170 if (root.contains(
"layouts")) {
1171 const auto& layouts = root[
"layouts"];
1172 prefs->panel_layout_defaults_revision = layouts.value(
1173 "defaults_revision", prefs->panel_layout_defaults_revision);
1174 if (layouts.contains(
"panel_visibility")) {
1175 LoadNestedBoolMap(layouts[
"panel_visibility"],
1176 &prefs->panel_visibility_state);
1178 if (layouts.contains(
"pinned_panels")) {
1179 LoadBoolMap(layouts[
"pinned_panels"], &prefs->pinned_panels);
1181 if (layouts.contains(
"right_panel_widths")) {
1182 LoadFloatMap(layouts[
"right_panel_widths"], &prefs->right_panel_widths);
1184 if (layouts.contains(
"saved_layouts")) {
1185 LoadNestedBoolMap(layouts[
"saved_layouts"], &prefs->saved_layouts);
1187 if (layouts.contains(
"named_layouts") &&
1188 layouts[
"named_layouts"].is_object()) {
1189 prefs->named_layouts.clear();
1190 for (
auto it = layouts[
"named_layouts"].begin();
1191 it != layouts[
"named_layouts"].end(); ++it) {
1192 if (it.value().is_string()) {
1193 prefs->named_layouts[it.key()] = it.value().get<std::string>();
1194 }
else if (it.value().is_object() || it.value().is_array()) {
1197 prefs->named_layouts[it.key()] = it.value().dump();
1201 prefs->last_applied_layout_name = layouts.value(
1202 "last_applied_layout_name", prefs->last_applied_layout_name);
1205 if (root.contains(
"rom_load_choices")) {
1206 LoadStringMap(root[
"rom_load_choices"], &prefs->rom_load_choices);
1209 if (root.contains(
"filesystem")) {
1210 const auto& fs = root[
"filesystem"];
1211 if (fs.contains(
"project_root_paths") &&
1212 fs[
"project_root_paths"].is_array()) {
1213 prefs->project_root_paths.clear();
1214 for (
const auto& item : fs[
"project_root_paths"]) {
1215 if (item.is_string()) {
1216 prefs->project_root_paths.push_back(item.get<std::string>());
1220 prefs->default_project_root =
1221 fs.value(
"default_project_root", prefs->default_project_root);
1222 prefs->use_files_app = fs.value(
"use_files_app", prefs->use_files_app);
1223 prefs->use_icloud_sync =
1224 fs.value(
"use_icloud_sync", prefs->use_icloud_sync);
1227 EnsureDefaultAiHosts(prefs);
1228 EnsureDefaultAiProfiles(prefs);
1229 EnsureDefaultFilesystemRoots(prefs);
1231 return absl::OkStatus();
1234absl::Status SavePreferencesToJson(
const std::filesystem::path& path,
1235 const UserSettings::Preferences& prefs) {
1237 if (!ensure_status.ok()) {
1238 return ensure_status;
1242 root[
"version"] = 1;
1244 {
"font_global_scale", prefs.font_global_scale},
1245 {
"backup_rom", prefs.backup_rom},
1246 {
"save_new_auto", prefs.save_new_auto},
1247 {
"autosave_enabled", prefs.autosave_enabled},
1248 {
"autosave_interval", prefs.autosave_interval},
1249 {
"recent_files_limit", prefs.recent_files_limit},
1250 {
"last_rom_path", prefs.last_rom_path},
1251 {
"last_project_path", prefs.last_project_path},
1252 {
"show_welcome_on_startup", prefs.show_welcome_on_startup},
1253 {
"test_mode", prefs.test_mode},
1254 {
"restore_last_session", prefs.restore_last_session},
1255 {
"prefer_hmagic_sprite_names", prefs.prefer_hmagic_sprite_names},
1256 {
"welcome_triforce_alpha", prefs.welcome_triforce_alpha},
1257 {
"welcome_triforce_speed", prefs.welcome_triforce_speed},
1258 {
"welcome_triforce_size", prefs.welcome_triforce_size},
1259 {
"welcome_particles_enabled", prefs.welcome_particles_enabled},
1260 {
"welcome_mouse_repel_enabled", prefs.welcome_mouse_repel_enabled},
1264 auto set_to_sorted_vec = [](
const std::unordered_set<std::string>& s)
1265 -> std::vector<std::string> {
1266 std::vector<std::string> v(s.begin(), s.end());
1267 std::sort(v.begin(), v.end());
1270 root[
"onboarding"] = {
1271 {
"feature_hints_enabled", prefs.feature_hints_enabled},
1272 {
"dismissed_feature_hints",
1273 set_to_sorted_vec(prefs.dismissed_feature_hints)},
1277 root[
"appearance"] = {
1278 {
"reduced_motion", prefs.reduced_motion},
1279 {
"switch_motion_profile", prefs.switch_motion_profile},
1280 {
"last_theme_name", prefs.last_theme_name},
1281 {
"language_locale", prefs.language_locale},
1282 {
"font_family_index", prefs.font_family_index},
1286 {
"backup_before_save", prefs.backup_before_save},
1287 {
"default_editor", prefs.default_editor},
1288 {
"dungeon_inspector_side", prefs.dungeon_inspector_side},
1291 root[
"performance"] = {
1292 {
"vsync", prefs.vsync},
1293 {
"target_fps", prefs.target_fps},
1294 {
"cache_size_mb", prefs.cache_size_mb},
1295 {
"undo_history_size", prefs.undo_history_size},
1298 json ai_hosts = json::array();
1299 for (
const auto& host : prefs.ai_hosts) {
1300 ai_hosts.push_back({
1302 {
"label", host.label},
1303 {
"base_url", host.base_url},
1304 {
"api_type", host.api_type},
1305 {
"supports_vision", host.supports_vision},
1306 {
"supports_tools", host.supports_tools},
1307 {
"supports_streaming", host.supports_streaming},
1308 {
"allow_insecure", host.allow_insecure},
1309 {
"api_key", host.api_key},
1310 {
"credential_id", host.credential_id},
1314 json ai_profiles = json::array();
1315 for (
const auto& profile : prefs.ai_profiles) {
1316 ai_profiles.push_back({
1317 {
"name", profile.name},
1318 {
"model", profile.model},
1319 {
"temperature", profile.temperature},
1320 {
"top_p", profile.top_p},
1321 {
"max_output_tokens", profile.max_output_tokens},
1322 {
"supports_vision", profile.supports_vision},
1323 {
"supports_tools", profile.supports_tools},
1328 {
"provider", prefs.ai_provider},
1329 {
"model", prefs.ai_model},
1330 {
"ollama_url", prefs.ollama_url},
1331 {
"gemini_api_key", prefs.gemini_api_key},
1332 {
"google_api_key", prefs.gemini_api_key},
1333 {
"openai_api_key", prefs.openai_api_key},
1334 {
"anthropic_api_key", prefs.anthropic_api_key},
1335 {
"temperature", prefs.ai_temperature},
1336 {
"max_tokens", prefs.ai_max_tokens},
1337 {
"proactive", prefs.ai_proactive},
1338 {
"auto_learn", prefs.ai_auto_learn},
1339 {
"multimodal", prefs.ai_multimodal},
1340 {
"hosts", ai_hosts},
1341 {
"active_host_id", prefs.active_ai_host_id},
1342 {
"profiles", ai_profiles},
1343 {
"active_profile", prefs.active_ai_profile},
1344 {
"remote_build_host_id", prefs.remote_build_host_id},
1345 {
"model_paths", prefs.ai_model_paths},
1349 {
"level", prefs.log_level},
1350 {
"to_file", prefs.log_to_file},
1351 {
"file_path", prefs.log_file_path},
1352 {
"ai_requests", prefs.log_ai_requests},
1353 {
"rom_operations", prefs.log_rom_operations},
1354 {
"gui_automation", prefs.log_gui_automation},
1355 {
"proposals", prefs.log_proposals},
1358 root[
"shortcuts"] = {
1359 {
"panel", ToStringMap(prefs.panel_shortcuts)},
1360 {
"global", ToStringMap(prefs.global_shortcuts)},
1361 {
"editor", ToStringMap(prefs.editor_shortcuts)},
1364 auto set_to_sorted_vec =
1365 [](
const std::unordered_set<std::string>& s) -> std::vector<std::string> {
1366 std::vector<std::string> v(s.begin(), s.end());
1367 std::sort(v.begin(), v.end());
1372 {
"visible", prefs.sidebar_visible},
1373 {
"panel_expanded", prefs.sidebar_panel_expanded},
1374 {
"panel_width", prefs.sidebar_panel_width},
1375 {
"panel_browser_category_width", prefs.panel_browser_category_width},
1376 {
"active_category", prefs.sidebar_active_category},
1377 {
"order", prefs.sidebar_order},
1378 {
"hidden", set_to_sorted_vec(prefs.sidebar_hidden)},
1379 {
"pinned", set_to_sorted_vec(prefs.sidebar_pinned)},
1380 {
"context_collapsed", set_to_sorted_vec(prefs.sidebar_context_collapsed)},
1383 root[
"status_bar"] = {
1384 {
"visible", prefs.show_status_bar},
1387 root[
"emulator"] = {
1388 {
"keep_running_in_background", prefs.emulator_keep_running_in_background},
1392 {
"show_experimental", prefs.show_experimental_editors},
1398 nlohmann::json named_layouts_json = nlohmann::json::object();
1399 for (
const auto& [layout_name, json_body] : prefs.named_layouts) {
1400 nlohmann::json parsed = nlohmann::json::parse(json_body,
nullptr,
false);
1401 if (parsed.is_discarded() || !parsed.is_object()) {
1402 named_layouts_json[layout_name] = json_body;
1404 named_layouts_json[layout_name] = std::move(parsed);
1409 {
"defaults_revision", prefs.panel_layout_defaults_revision},
1410 {
"panel_visibility", ToNestedBoolMap(prefs.panel_visibility_state)},
1411 {
"pinned_panels", ToBoolMap(prefs.pinned_panels)},
1412 {
"right_panel_widths", ToFloatMap(prefs.right_panel_widths)},
1413 {
"saved_layouts", ToNestedBoolMap(prefs.saved_layouts)},
1414 {
"named_layouts", std::move(named_layouts_json)},
1415 {
"last_applied_layout_name", prefs.last_applied_layout_name},
1418 root[
"rom_load_choices"] = ToStringMap(prefs.rom_load_choices);
1420 root[
"filesystem"] = {
1421 {
"project_root_paths", prefs.project_root_paths},
1422 {
"default_project_root", prefs.default_project_root},
1423 {
"use_files_app", prefs.use_files_app},
1424 {
"use_icloud_sync", prefs.use_icloud_sync},
1427 std::ofstream file(path);
1428 if (!file.is_open()) {
1429 return absl::InternalError(
1430 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
1433 file << root.dump(2) <<
"\n";
1434 return absl::OkStatus();