35 auto parent = path.parent_path();
37 return absl::OkStatus();
43 std::unordered_map<std::string, bool>* panel_state) {
47 for (
auto it = panel_state->begin(); it != panel_state->end();) {
49 it = panel_state->erase(it);
57 return panel_id ==
"dungeon.object_editor" ||
58 panel_id ==
"dungeon.settings" || panel_id ==
"dungeon.dungeon_map";
62 std::unordered_map<std::string, bool>* panel_state) {
66 for (
auto it = panel_state->begin(); it != panel_state->end();) {
68 it = panel_state->erase(it);
76 "dungeon.room_selector",
"dungeon.room_matrix",
77 "dungeon.object_selector",
"dungeon.sprite_editor",
78 "dungeon.item_editor",
"dungeon.room_graphics",
79 "dungeon.door_editor",
"dungeon.palette_editor",
80 "dungeon.entrance_list",
"dungeon.entrance_properties",
84 "dungeon.custom_collision",
"dungeon.water_fill",
85 "dungeon.room_tags",
"dungeon.object_coverage",
86 "dungeon.object_tile_editor",
"dungeon.overlay_manager",
87 "dungeon.minecart_tracks",
91 std::unordered_map<std::string, bool>* panel_state) {
95 (*panel_state)[
"dungeon.workbench"] =
true;
97 (*panel_state)[panel_id] =
false;
102 std::unordered_map<std::string, bool>* panel_state) {
108 (*panel_state)[panel_id] =
false;
124 {
"Agent",
"agent.mesen_debug"},
125 {
"Agent",
"agent.mesen_screenshot"},
126 {
"Agent",
"agent.oracle_states"},
127 {
"Agent",
"agent.feature_flags"},
128 {
"Agent",
"agent.manifest"},
129 {
"Agent",
"agent.sram_viewer"},
132 {
"Agent",
"oracle.validation"},
133 {
"Agent",
"oracle.menu_inspector"},
135 {
"Graphics",
"graphics.prototype_viewer"},
137 {
"Screen",
"screen.naming_screen"},
139 {
"Emulator",
"emulator.save_states"},
143 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
145 if (!visibility_state) {
149 auto category_it = visibility_state->find(window.category);
150 if (category_it == visibility_state->end()) {
153 auto window_it = category_it->second.find(window.window_id);
154 if (window_it != category_it->second.end()) {
155 window_it->second =
false;
161 if (!json_body || json_body->empty()) {
164 auto parsed = nlohmann::json::parse(*json_body,
nullptr,
false);
165 if (parsed.is_discarded() || !parsed.is_object() ||
166 !parsed.contains(
"root")) {
170 bool changed =
false;
171 auto prune_node = [&changed](
auto&& self, nlohmann::json& node) ->
void {
172 if (!node.is_object()) {
175 const std::string type = node.contains(
"type") && node[
"type"].is_string()
176 ? node[
"type"].get<std::string>()
178 if (type ==
"leaf" && node.contains(
"panels") &&
179 node[
"panels"].is_array()) {
180 auto& panels = node[
"panels"];
181 for (
auto it = panels.begin(); it != panels.end();) {
182 const bool erase = it->is_object() && it->contains(
"panel_id") &&
183 (*it)[
"panel_id"].is_string() &&
185 (*it)[
"panel_id"].get<std::string>());
187 it = panels.erase(it);
193 const int max_active =
194 panels.empty() ? 0 :
static_cast<int>(panels.size()) - 1;
195 const int active_tab_index =
196 node.contains(
"active_tab_index") &&
197 node[
"active_tab_index"].is_number_integer()
198 ? node[
"active_tab_index"].get<
int>()
200 const int clamped_active = std::clamp(active_tab_index, 0, max_active);
201 if (active_tab_index != clamped_active) {
202 node[
"active_tab_index"] = clamped_active;
206 if (node.contains(
"child_a")) {
207 self(self, node[
"child_a"]);
209 if (node.contains(
"child_b")) {
210 self(self, node[
"child_b"]);
213 prune_node(prune_node, parsed[
"root"]);
216 *json_body = parsed.dump();
224 return absl::InvalidArgumentError(
"prefs is null");
229 return absl::OkStatus();
232 std::istringstream ss(data);
237 auto to_float = [](
const std::string& s,
float fallback) {
240 }
catch (
const std::exception&) {
244 auto to_int = [](
const std::string& s,
int fallback) {
247 }
catch (
const std::exception&) {
251 while (std::getline(ss, line)) {
252 size_t eq_pos = line.find(
'=');
253 if (eq_pos == std::string::npos) {
257 std::string key = line.substr(0, eq_pos);
258 std::string val = line.substr(eq_pos + 1);
261 if (key ==
"font_global_scale") {
263 }
else if (key ==
"backup_rom") {
265 }
else if (key ==
"save_new_auto") {
267 }
else if (key ==
"autosave_enabled") {
269 }
else if (key ==
"autosave_interval") {
271 }
else if (key ==
"recent_files_limit") {
273 }
else if (key ==
"last_rom_path") {
275 }
else if (key ==
"last_project_path") {
277 }
else if (key ==
"show_welcome_on_startup") {
279 }
else if (key ==
"test_mode") {
281 }
else if (key ==
"restore_last_session") {
283 }
else if (key ==
"prefer_hmagic_sprite_names") {
285 }
else if (key ==
"feature_hints_enabled") {
287 }
else if (
constexpr std::string_view kDismissedHintPrefix =
288 "dismissed_feature_hint.";
289 key.rfind(kDismissedHintPrefix, 0) == 0) {
290 const std::string hint_id = key.substr(kDismissedHintPrefix.size());
291 if (!hint_id.empty() && val ==
"1") {
294 }
else if (key ==
"welcome_triforce_alpha") {
297 }
else if (key ==
"welcome_triforce_speed") {
300 }
else if (key ==
"welcome_triforce_size") {
303 }
else if (key ==
"welcome_particles_enabled") {
305 }
else if (key ==
"welcome_mouse_repel_enabled") {
307 }
else if (key ==
"reduced_motion") {
309 }
else if (key ==
"switch_motion_profile") {
311 }
else if (key ==
"last_theme_name") {
313 }
else if (key ==
"language_locale") {
315 }
else if (key ==
"font_family_index") {
319 else if (key ==
"backup_before_save") {
321 }
else if (key ==
"default_editor") {
325 else if (key ==
"vsync") {
326 prefs->
vsync = (val ==
"1");
327 }
else if (key ==
"target_fps") {
329 }
else if (key ==
"cache_size_mb") {
331 }
else if (key ==
"undo_history_size") {
335 else if (key ==
"ai_provider") {
337 }
else if (key ==
"ai_model") {
339 }
else if (key ==
"ollama_url") {
341 }
else if (key ==
"gemini_api_key") {
343 }
else if (key ==
"openai_api_key") {
345 }
else if (key ==
"anthropic_api_key") {
347 }
else if (key ==
"ai_temperature") {
349 }
else if (key ==
"ai_max_tokens") {
351 }
else if (key ==
"ai_proactive") {
353 }
else if (key ==
"ai_auto_learn") {
355 }
else if (key ==
"ai_multimodal") {
359 else if (key ==
"log_level") {
361 }
else if (key ==
"log_to_file") {
363 }
else if (key ==
"log_file_path") {
365 }
else if (key ==
"log_ai_requests") {
367 }
else if (key ==
"log_rom_operations") {
369 }
else if (key ==
"log_gui_automation") {
371 }
else if (key ==
"log_proposals") {
375 else if (key.substr(0, 15) ==
"panel_shortcut.") {
376 std::string panel_id = key.substr(15);
380 else if (key.substr(0, 14) ==
"card_shortcut.") {
381 std::string panel_id = key.substr(14);
385 else if (key ==
"sidebar_visible") {
387 }
else if (key ==
"sidebar_panel_expanded") {
389 }
else if (key ==
"sidebar_panel_width") {
391 }
else if (key ==
"panel_browser_category_width") {
394 }
else if (key ==
"panel_layout_defaults_revision") {
397 }
else if (key ==
"sidebar_active_category") {
399 }
else if (key.rfind(
"sidebar_context_collapsed.", 0) == 0) {
400 const std::string category =
401 key.substr(std::string(
"sidebar_context_collapsed.").size());
402 if (!category.empty()) {
409 }
else if (key ==
"dungeon_inspector_side") {
411 (val ==
"left") ? std::string(
"left") : std::string(
"right");
414 else if (key ==
"show_status_bar") {
416 }
else if (key ==
"emulator_keep_running_in_background") {
418 }
else if (key ==
"show_experimental_editors") {
422 else if (key.substr(0, 17) ==
"panel_visibility.") {
423 std::string rest = key.substr(17);
424 size_t dot_pos = rest.find(
'.');
425 if (dot_pos != std::string::npos) {
426 std::string editor_type = rest.substr(0, dot_pos);
427 std::string panel_id = rest.substr(dot_pos + 1);
432 else if (key.substr(0, 13) ==
"pinned_panel.") {
433 std::string panel_id = key.substr(13);
437 else if (key.substr(0, 18) ==
"right_panel_width.") {
438 std::string panel_key = key.substr(18);
442 else if (key.substr(0, 13) ==
"saved_layout.") {
443 std::string rest = key.substr(13);
444 size_t dot_pos = rest.find(
'.');
445 if (dot_pos != std::string::npos) {
446 std::string layout_name = rest.substr(0, dot_pos);
447 std::string panel_id = rest.substr(dot_pos + 1);
454 else if (key.substr(0, 13) ==
"named_layout.") {
455 std::string layout_name = key.substr(13);
457 }
else if (key ==
"last_applied_layout_name") {
462 return absl::OkStatus();
468 if (!ensure_status.ok()) {
469 return ensure_status;
472 std::ostringstream ss;
475 ss <<
"backup_rom=" << (prefs.
backup_rom ? 1 : 0) <<
"\n";
476 ss <<
"save_new_auto=" << (prefs.
save_new_auto ? 1 : 0) <<
"\n";
484 ss <<
"test_mode=" << (prefs.
test_mode ? 1 : 0) <<
"\n";
486 ss <<
"prefer_hmagic_sprite_names="
490 std::vector<std::string> dismissed_hints(
493 std::sort(dismissed_hints.begin(), dismissed_hints.end());
494 for (
const auto& hint_id : dismissed_hints) {
495 ss <<
"dismissed_feature_hint." << hint_id <<
"=1\n";
500 ss <<
"welcome_particles_enabled="
502 ss <<
"welcome_mouse_repel_enabled="
504 ss <<
"reduced_motion=" << (prefs.
reduced_motion ? 1 : 0) <<
"\n";
515 ss <<
"vsync=" << (prefs.
vsync ? 1 : 0) <<
"\n";
516 ss <<
"target_fps=" << prefs.
target_fps <<
"\n";
522 ss <<
"ai_model=" << prefs.
ai_model <<
"\n";
523 ss <<
"ollama_url=" << prefs.
ollama_url <<
"\n";
529 ss <<
"ai_proactive=" << (prefs.
ai_proactive ? 1 : 0) <<
"\n";
530 ss <<
"ai_auto_learn=" << (prefs.
ai_auto_learn ? 1 : 0) <<
"\n";
531 ss <<
"ai_multimodal=" << (prefs.
ai_multimodal ? 1 : 0) <<
"\n";
534 ss <<
"log_level=" << prefs.
log_level <<
"\n";
535 ss <<
"log_to_file=" << (prefs.
log_to_file ? 1 : 0) <<
"\n";
540 ss <<
"log_proposals=" << (prefs.
log_proposals ? 1 : 0) <<
"\n";
544 ss <<
"panel_shortcut." << panel_id <<
"=" << shortcut <<
"\n";
554 ss <<
"panel_layout_defaults_revision="
558 ss <<
"sidebar_context_collapsed." << category <<
"=1\n";
564 ss <<
"emulator_keep_running_in_background="
566 ss <<
"show_experimental_editors="
571 for (
const auto& [panel_id, visible] : panel_state) {
572 ss <<
"panel_visibility." << editor_type <<
"." << panel_id <<
"="
573 << (visible ? 1 : 0) <<
"\n";
579 ss <<
"pinned_panel." << panel_id <<
"=" << (pinned ? 1 : 0) <<
"\n";
583 ss <<
"right_panel_width." << panel_key <<
"=" << width <<
"\n";
587 for (
const auto& [layout_name, panel_state] : prefs.
saved_layouts) {
588 for (
const auto& [panel_id, visible] : panel_state) {
589 ss <<
"saved_layout." << layout_name <<
"." << panel_id <<
"="
590 << (visible ? 1 : 0) <<
"\n";
598 for (
const auto& [layout_name, json_body] : prefs.
named_layouts) {
601 std::string single_line;
602 single_line.reserve(json_body.size());
603 for (
char c : json_body) {
604 if (c ==
'\n' || c ==
'\r')
606 single_line.push_back(c);
608 ss <<
"named_layout." << layout_name <<
"=" << single_line <<
"\n";
611 std::ofstream file(path);
612 if (!file.is_open()) {
613 return absl::InternalError(
614 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
617 return absl::OkStatus();
634 UserSettings::Preferences::AiHost host;
635 host.id =
"ollama-local";
636 host.label =
"Ollama (local)";
638 host.api_type =
"ollama";
639 host.supports_tools =
true;
640 host.supports_streaming =
true;
645 UserSettings::Preferences::AiHost lmstudio;
646 lmstudio.id =
"lmstudio-local";
647 lmstudio.label =
"LM Studio (local)";
648 lmstudio.base_url =
"http://localhost:1234";
649 lmstudio.api_type =
"lmstudio";
650 lmstudio.supports_tools =
true;
651 lmstudio.supports_streaming =
true;
652 prefs->
ai_hosts.push_back(lmstudio);
659void EnsureDefaultAiProfiles(UserSettings::Preferences* prefs) {
663 if (!prefs->ai_profiles.empty()) {
664 if (prefs->active_ai_profile.empty()) {
665 prefs->active_ai_profile = prefs->ai_profiles.front().name;
669 if (!prefs->ai_model.empty()) {
670 UserSettings::Preferences::AiModelProfile profile;
671 profile.name =
"default";
672 profile.model = prefs->ai_model;
673 profile.temperature = prefs->ai_temperature;
674 profile.top_p = 0.95f;
675 profile.max_output_tokens = prefs->ai_max_tokens;
676 profile.supports_tools =
true;
677 prefs->ai_profiles.push_back(profile);
678 prefs->active_ai_profile = profile.name;
682void EnsureDefaultFilesystemRoots(UserSettings::Preferences* prefs) {
687 auto add_unique_root = [&](
const std::filesystem::path& path) {
691 const std::string path_str = path.string();
692 auto it = std::find(prefs->project_root_paths.begin(),
693 prefs->project_root_paths.end(), path_str);
694 if (it == prefs->project_root_paths.end()) {
695 prefs->project_root_paths.push_back(path_str);
701 add_unique_root(*docs_dir);
704 if (prefs->use_icloud_sync) {
707 if (icloud_dir.ok()) {
708 add_unique_root(*icloud_dir);
709 if (prefs->default_project_root.empty()) {
710 prefs->default_project_root = icloud_dir->string();
715 if (prefs->default_project_root.empty() &&
716 !prefs->project_root_paths.empty()) {
717 prefs->default_project_root = prefs->project_root_paths.front();
721void EnsureDefaultModelPaths(UserSettings::Preferences* prefs) {
725 if (!prefs->ai_model_paths.empty()) {
729 auto add_unique_path = [&](
const std::filesystem::path& path) {
733 const std::string path_str = path.string();
734 auto it = std::find(prefs->ai_model_paths.begin(),
735 prefs->ai_model_paths.end(), path_str);
736 if (it == prefs->ai_model_paths.end()) {
737 prefs->ai_model_paths.push_back(path_str);
742 if (!home_dir.empty() && home_dir !=
".") {
743 add_unique_path(home_dir /
"models");
744 add_unique_path(home_dir /
".lmstudio" /
"models");
745 add_unique_path(home_dir /
".ollama" /
"models");
749void LoadStringMap(
const json& src,
750 std::unordered_map<std::string, std::string>* target) {
751 if (!target || !src.is_object()) {
755 for (
const auto& [key, value] : src.items()) {
756 if (value.is_string()) {
757 (*target)[key] = value.get<std::string>();
762void LoadBoolMap(
const json& src,
763 std::unordered_map<std::string, bool>* target) {
764 if (!target || !src.is_object()) {
768 for (
const auto& [key, value] : src.items()) {
769 if (value.is_boolean()) {
770 (*target)[key] = value.get<
bool>();
775void LoadFloatMap(
const json& src,
776 std::unordered_map<std::string, float>* target) {
777 if (!target || !src.is_object()) {
781 for (
const auto& [key, value] : src.items()) {
782 if (value.is_number()) {
783 (*target)[key] = value.get<
float>();
788void LoadNestedBoolMap(
790 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
792 if (!target || !src.is_object()) {
796 for (
const auto& [outer_key, outer_val] : src.items()) {
797 if (!outer_val.is_object()) {
800 auto& inner = (*target)[outer_key];
802 for (
const auto& [inner_key, inner_val] : outer_val.items()) {
803 if (inner_val.is_boolean()) {
804 inner[inner_key] = inner_val.get<
bool>();
810json ToStringMap(
const std::unordered_map<std::string, std::string>& map) {
811 json obj = json::object();
812 for (
const auto& [key, value] : map) {
818json ToBoolMap(
const std::unordered_map<std::string, bool>& map) {
819 json obj = json::object();
820 for (
const auto& [key, value] : map) {
826json ToFloatMap(
const std::unordered_map<std::string, float>& map) {
827 json obj = json::object();
828 for (
const auto& [key, value] : map) {
834json ToNestedBoolMap(
const std::unordered_map<
835 std::string, std::unordered_map<std::string, bool>>& map) {
836 json obj = json::object();
837 for (
const auto& [outer_key, inner] : map) {
838 obj[outer_key] = ToBoolMap(inner);
843absl::Status LoadPreferencesFromJson(
const std::filesystem::path& path,
844 UserSettings::Preferences* prefs) {
846 return absl::InvalidArgumentError(
"prefs is null");
849 std::ifstream file(path);
850 if (!file.is_open()) {
851 return absl::NotFoundError(
852 absl::StrFormat(
"Settings file not found: %s", path.string()));
858 }
catch (
const std::exception& e) {
859 return absl::InternalError(
860 absl::StrFormat(
"Failed to parse settings.json: %s", e.what()));
863 if (root.contains(
"general")) {
864 const auto& g = root[
"general"];
865 prefs->font_global_scale =
866 g.value(
"font_global_scale", prefs->font_global_scale);
867 prefs->backup_rom = g.value(
"backup_rom", prefs->backup_rom);
868 prefs->save_new_auto = g.value(
"save_new_auto", prefs->save_new_auto);
869 prefs->autosave_enabled =
870 g.value(
"autosave_enabled", prefs->autosave_enabled);
871 prefs->autosave_interval =
872 g.value(
"autosave_interval", prefs->autosave_interval);
873 prefs->recent_files_limit =
874 g.value(
"recent_files_limit", prefs->recent_files_limit);
875 prefs->last_rom_path = g.value(
"last_rom_path", prefs->last_rom_path);
876 prefs->last_project_path =
877 g.value(
"last_project_path", prefs->last_project_path);
878 prefs->show_welcome_on_startup =
879 g.value(
"show_welcome_on_startup", prefs->show_welcome_on_startup);
880 prefs->test_mode = g.value(
"test_mode", prefs->test_mode);
881 prefs->restore_last_session =
882 g.value(
"restore_last_session", prefs->restore_last_session);
883 prefs->prefer_hmagic_sprite_names = g.value(
884 "prefer_hmagic_sprite_names", prefs->prefer_hmagic_sprite_names);
885 prefs->welcome_triforce_alpha =
886 g.value(
"welcome_triforce_alpha", prefs->welcome_triforce_alpha);
887 prefs->welcome_triforce_speed =
888 g.value(
"welcome_triforce_speed", prefs->welcome_triforce_speed);
889 prefs->welcome_triforce_size =
890 g.value(
"welcome_triforce_size", prefs->welcome_triforce_size);
891 prefs->welcome_particles_enabled =
892 g.value(
"welcome_particles_enabled", prefs->welcome_particles_enabled);
893 prefs->welcome_mouse_repel_enabled = g.value(
894 "welcome_mouse_repel_enabled", prefs->welcome_mouse_repel_enabled);
897 if (root.contains(
"onboarding")) {
898 const auto& onboarding = root[
"onboarding"];
899 prefs->feature_hints_enabled =
900 onboarding.value(
"feature_hints_enabled", prefs->feature_hints_enabled);
901 if (onboarding.contains(
"dismissed_feature_hints") &&
902 onboarding[
"dismissed_feature_hints"].is_array()) {
903 prefs->dismissed_feature_hints.clear();
904 for (
const auto& item : onboarding[
"dismissed_feature_hints"]) {
905 if (item.is_string()) {
906 prefs->dismissed_feature_hints.insert(item.get<std::string>());
912 if (root.contains(
"appearance")) {
913 const auto& appearance = root[
"appearance"];
914 prefs->reduced_motion =
915 appearance.value(
"reduced_motion", prefs->reduced_motion);
916 prefs->switch_motion_profile =
917 appearance.value(
"switch_motion_profile", prefs->switch_motion_profile);
918 prefs->last_theme_name =
919 appearance.value(
"last_theme_name", prefs->last_theme_name);
920 prefs->language_locale =
921 appearance.value(
"language_locale", prefs->language_locale);
922 prefs->font_family_index =
923 appearance.value(
"font_family_index", prefs->font_family_index);
926 if (root.contains(
"editor")) {
927 const auto& e = root[
"editor"];
928 prefs->backup_before_save =
929 e.value(
"backup_before_save", prefs->backup_before_save);
930 prefs->default_editor = e.value(
"default_editor", prefs->default_editor);
933 e.value(
"dungeon_inspector_side", prefs->dungeon_inspector_side);
934 prefs->dungeon_inspector_side = (side ==
"left") ?
"left" :
"right";
938 if (root.contains(
"performance")) {
939 const auto& p = root[
"performance"];
940 prefs->vsync = p.value(
"vsync", prefs->vsync);
941 prefs->target_fps = p.value(
"target_fps", prefs->target_fps);
942 prefs->cache_size_mb = p.value(
"cache_size_mb", prefs->cache_size_mb);
943 prefs->undo_history_size =
944 p.value(
"undo_history_size", prefs->undo_history_size);
947 if (root.contains(
"ai")) {
948 const auto& ai = root[
"ai"];
949 prefs->ai_provider = ai.value(
"provider", prefs->ai_provider);
950 prefs->ai_model = ai.value(
"model", prefs->ai_model);
951 prefs->ollama_url = ai.value(
"ollama_url", prefs->ollama_url);
952 prefs->gemini_api_key = ai.value(
"gemini_api_key", prefs->gemini_api_key);
953 prefs->openai_api_key = ai.value(
"openai_api_key", prefs->openai_api_key);
954 prefs->anthropic_api_key =
955 ai.value(
"anthropic_api_key", prefs->anthropic_api_key);
956 std::string google_key = ai.value(
"google_api_key", std::string());
957 if (prefs->gemini_api_key.empty() && !google_key.empty()) {
958 prefs->gemini_api_key = google_key;
960 prefs->ai_temperature = ai.value(
"temperature", prefs->ai_temperature);
961 prefs->ai_max_tokens = ai.value(
"max_tokens", prefs->ai_max_tokens);
962 prefs->ai_proactive = ai.value(
"proactive", prefs->ai_proactive);
963 prefs->ai_auto_learn = ai.value(
"auto_learn", prefs->ai_auto_learn);
964 prefs->ai_multimodal = ai.value(
"multimodal", prefs->ai_multimodal);
965 prefs->active_ai_host_id =
966 ai.value(
"active_host_id", prefs->active_ai_host_id);
967 prefs->active_ai_profile =
968 ai.value(
"active_profile", prefs->active_ai_profile);
969 prefs->remote_build_host_id =
970 ai.value(
"remote_build_host_id", prefs->remote_build_host_id);
971 if (ai.contains(
"model_paths") && ai[
"model_paths"].is_array()) {
972 prefs->ai_model_paths.clear();
973 for (
const auto& item : ai[
"model_paths"]) {
974 if (item.is_string()) {
975 prefs->ai_model_paths.push_back(item.get<std::string>());
980 if (ai.contains(
"hosts") && ai[
"hosts"].is_array()) {
981 prefs->ai_hosts.clear();
982 for (
const auto& host : ai[
"hosts"]) {
983 if (!host.is_object()) {
986 UserSettings::Preferences::AiHost entry;
987 entry.id = host.value(
"id",
"");
988 entry.label = host.value(
"label",
"");
989 entry.base_url = host.value(
"base_url",
"");
990 entry.api_type = host.value(
"api_type",
"");
991 entry.supports_vision =
992 host.value(
"supports_vision", entry.supports_vision);
993 entry.supports_tools =
994 host.value(
"supports_tools", entry.supports_tools);
995 entry.supports_streaming =
996 host.value(
"supports_streaming", entry.supports_streaming);
997 entry.allow_insecure =
998 host.value(
"allow_insecure", entry.allow_insecure);
999 entry.api_key = host.value(
"api_key",
"");
1000 entry.credential_id = host.value(
"credential_id",
"");
1001 prefs->ai_hosts.push_back(entry);
1005 if (ai.contains(
"profiles") && ai[
"profiles"].is_array()) {
1006 prefs->ai_profiles.clear();
1007 for (
const auto& profile : ai[
"profiles"]) {
1008 if (!profile.is_object()) {
1011 UserSettings::Preferences::AiModelProfile entry;
1012 entry.name = profile.value(
"name",
"");
1013 entry.model = profile.value(
"model",
"");
1014 entry.temperature = profile.value(
"temperature", entry.temperature);
1015 entry.top_p = profile.value(
"top_p", entry.top_p);
1016 entry.max_output_tokens =
1017 profile.value(
"max_output_tokens", entry.max_output_tokens);
1018 entry.supports_vision =
1019 profile.value(
"supports_vision", entry.supports_vision);
1020 entry.supports_tools =
1021 profile.value(
"supports_tools", entry.supports_tools);
1022 prefs->ai_profiles.push_back(entry);
1027 if (root.contains(
"logging")) {
1028 const auto& log = root[
"logging"];
1029 prefs->log_level = log.value(
"level", prefs->log_level);
1030 prefs->log_to_file = log.value(
"to_file", prefs->log_to_file);
1031 prefs->log_file_path = log.value(
"file_path", prefs->log_file_path);
1032 prefs->log_ai_requests = log.value(
"ai_requests", prefs->log_ai_requests);
1033 prefs->log_rom_operations =
1034 log.value(
"rom_operations", prefs->log_rom_operations);
1035 prefs->log_gui_automation =
1036 log.value(
"gui_automation", prefs->log_gui_automation);
1037 prefs->log_proposals = log.value(
"proposals", prefs->log_proposals);
1040 if (root.contains(
"shortcuts")) {
1041 const auto& shortcuts = root[
"shortcuts"];
1042 if (shortcuts.contains(
"panel")) {
1043 LoadStringMap(shortcuts[
"panel"], &prefs->panel_shortcuts);
1045 if (shortcuts.contains(
"global")) {
1046 LoadStringMap(shortcuts[
"global"], &prefs->global_shortcuts);
1048 if (shortcuts.contains(
"editor")) {
1049 LoadStringMap(shortcuts[
"editor"], &prefs->editor_shortcuts);
1053 if (root.contains(
"sidebar")) {
1054 const auto& sidebar = root[
"sidebar"];
1055 prefs->sidebar_visible = sidebar.value(
"visible", prefs->sidebar_visible);
1056 prefs->sidebar_panel_expanded =
1057 sidebar.value(
"panel_expanded", prefs->sidebar_panel_expanded);
1058 prefs->sidebar_panel_width =
1059 sidebar.value(
"panel_width", prefs->sidebar_panel_width);
1060 prefs->panel_browser_category_width = sidebar.value(
1061 "panel_browser_category_width", prefs->panel_browser_category_width);
1062 prefs->sidebar_active_category =
1063 sidebar.value(
"active_category", prefs->sidebar_active_category);
1065 if (sidebar.contains(
"order") && sidebar[
"order"].is_array()) {
1066 prefs->sidebar_order.clear();
1067 for (
const auto& item : sidebar[
"order"]) {
1068 if (item.is_string()) {
1069 prefs->sidebar_order.push_back(item.get<std::string>());
1073 if (sidebar.contains(
"hidden") && sidebar[
"hidden"].is_array()) {
1074 prefs->sidebar_hidden.clear();
1075 for (
const auto& item : sidebar[
"hidden"]) {
1076 if (item.is_string()) {
1077 prefs->sidebar_hidden.insert(item.get<std::string>());
1081 if (sidebar.contains(
"pinned") && sidebar[
"pinned"].is_array()) {
1082 prefs->sidebar_pinned.clear();
1083 for (
const auto& item : sidebar[
"pinned"]) {
1084 if (item.is_string()) {
1085 prefs->sidebar_pinned.insert(item.get<std::string>());
1089 if (sidebar.contains(
"context_collapsed") &&
1090 sidebar[
"context_collapsed"].is_array()) {
1091 prefs->sidebar_context_collapsed.clear();
1092 for (
const auto& item : sidebar[
"context_collapsed"]) {
1093 if (item.is_string()) {
1094 prefs->sidebar_context_collapsed.insert(item.get<std::string>());
1100 if (root.contains(
"status_bar")) {
1101 const auto& status_bar = root[
"status_bar"];
1102 prefs->show_status_bar =
1103 status_bar.value(
"visible", prefs->show_status_bar);
1106 if (root.contains(
"emulator")) {
1107 const auto& emulator = root[
"emulator"];
1108 prefs->emulator_keep_running_in_background =
1109 emulator.value(
"keep_running_in_background",
1110 prefs->emulator_keep_running_in_background);
1113 if (root.contains(
"editors")) {
1114 const auto& editors = root[
"editors"];
1115 prefs->show_experimental_editors =
1116 editors.value(
"show_experimental", prefs->show_experimental_editors);
1119 if (root.contains(
"layouts")) {
1120 const auto& layouts = root[
"layouts"];
1121 prefs->panel_layout_defaults_revision = layouts.value(
1122 "defaults_revision", prefs->panel_layout_defaults_revision);
1123 if (layouts.contains(
"panel_visibility")) {
1124 LoadNestedBoolMap(layouts[
"panel_visibility"],
1125 &prefs->panel_visibility_state);
1127 if (layouts.contains(
"pinned_panels")) {
1128 LoadBoolMap(layouts[
"pinned_panels"], &prefs->pinned_panels);
1130 if (layouts.contains(
"right_panel_widths")) {
1131 LoadFloatMap(layouts[
"right_panel_widths"], &prefs->right_panel_widths);
1133 if (layouts.contains(
"saved_layouts")) {
1134 LoadNestedBoolMap(layouts[
"saved_layouts"], &prefs->saved_layouts);
1136 if (layouts.contains(
"named_layouts") &&
1137 layouts[
"named_layouts"].is_object()) {
1138 prefs->named_layouts.clear();
1139 for (
auto it = layouts[
"named_layouts"].begin();
1140 it != layouts[
"named_layouts"].end(); ++it) {
1141 if (it.value().is_string()) {
1142 prefs->named_layouts[it.key()] = it.value().get<std::string>();
1143 }
else if (it.value().is_object() || it.value().is_array()) {
1146 prefs->named_layouts[it.key()] = it.value().dump();
1150 prefs->last_applied_layout_name = layouts.value(
1151 "last_applied_layout_name", prefs->last_applied_layout_name);
1154 if (root.contains(
"filesystem")) {
1155 const auto& fs = root[
"filesystem"];
1156 if (fs.contains(
"project_root_paths") &&
1157 fs[
"project_root_paths"].is_array()) {
1158 prefs->project_root_paths.clear();
1159 for (
const auto& item : fs[
"project_root_paths"]) {
1160 if (item.is_string()) {
1161 prefs->project_root_paths.push_back(item.get<std::string>());
1165 prefs->default_project_root =
1166 fs.value(
"default_project_root", prefs->default_project_root);
1167 prefs->use_files_app = fs.value(
"use_files_app", prefs->use_files_app);
1168 prefs->use_icloud_sync =
1169 fs.value(
"use_icloud_sync", prefs->use_icloud_sync);
1172 EnsureDefaultAiHosts(prefs);
1173 EnsureDefaultAiProfiles(prefs);
1174 EnsureDefaultFilesystemRoots(prefs);
1176 return absl::OkStatus();
1179absl::Status SavePreferencesToJson(
const std::filesystem::path& path,
1180 const UserSettings::Preferences& prefs) {
1182 if (!ensure_status.ok()) {
1183 return ensure_status;
1187 root[
"version"] = 1;
1189 {
"font_global_scale", prefs.font_global_scale},
1190 {
"backup_rom", prefs.backup_rom},
1191 {
"save_new_auto", prefs.save_new_auto},
1192 {
"autosave_enabled", prefs.autosave_enabled},
1193 {
"autosave_interval", prefs.autosave_interval},
1194 {
"recent_files_limit", prefs.recent_files_limit},
1195 {
"last_rom_path", prefs.last_rom_path},
1196 {
"last_project_path", prefs.last_project_path},
1197 {
"show_welcome_on_startup", prefs.show_welcome_on_startup},
1198 {
"test_mode", prefs.test_mode},
1199 {
"restore_last_session", prefs.restore_last_session},
1200 {
"prefer_hmagic_sprite_names", prefs.prefer_hmagic_sprite_names},
1201 {
"welcome_triforce_alpha", prefs.welcome_triforce_alpha},
1202 {
"welcome_triforce_speed", prefs.welcome_triforce_speed},
1203 {
"welcome_triforce_size", prefs.welcome_triforce_size},
1204 {
"welcome_particles_enabled", prefs.welcome_particles_enabled},
1205 {
"welcome_mouse_repel_enabled", prefs.welcome_mouse_repel_enabled},
1209 auto set_to_sorted_vec = [](
const std::unordered_set<std::string>& s)
1210 -> std::vector<std::string> {
1211 std::vector<std::string> v(s.begin(), s.end());
1212 std::sort(v.begin(), v.end());
1215 root[
"onboarding"] = {
1216 {
"feature_hints_enabled", prefs.feature_hints_enabled},
1217 {
"dismissed_feature_hints",
1218 set_to_sorted_vec(prefs.dismissed_feature_hints)},
1222 root[
"appearance"] = {
1223 {
"reduced_motion", prefs.reduced_motion},
1224 {
"switch_motion_profile", prefs.switch_motion_profile},
1225 {
"last_theme_name", prefs.last_theme_name},
1226 {
"language_locale", prefs.language_locale},
1227 {
"font_family_index", prefs.font_family_index},
1231 {
"backup_before_save", prefs.backup_before_save},
1232 {
"default_editor", prefs.default_editor},
1233 {
"dungeon_inspector_side", prefs.dungeon_inspector_side},
1236 root[
"performance"] = {
1237 {
"vsync", prefs.vsync},
1238 {
"target_fps", prefs.target_fps},
1239 {
"cache_size_mb", prefs.cache_size_mb},
1240 {
"undo_history_size", prefs.undo_history_size},
1243 json ai_hosts = json::array();
1244 for (
const auto& host : prefs.ai_hosts) {
1245 ai_hosts.push_back({
1247 {
"label", host.label},
1248 {
"base_url", host.base_url},
1249 {
"api_type", host.api_type},
1250 {
"supports_vision", host.supports_vision},
1251 {
"supports_tools", host.supports_tools},
1252 {
"supports_streaming", host.supports_streaming},
1253 {
"allow_insecure", host.allow_insecure},
1254 {
"api_key", host.api_key},
1255 {
"credential_id", host.credential_id},
1259 json ai_profiles = json::array();
1260 for (
const auto& profile : prefs.ai_profiles) {
1261 ai_profiles.push_back({
1262 {
"name", profile.name},
1263 {
"model", profile.model},
1264 {
"temperature", profile.temperature},
1265 {
"top_p", profile.top_p},
1266 {
"max_output_tokens", profile.max_output_tokens},
1267 {
"supports_vision", profile.supports_vision},
1268 {
"supports_tools", profile.supports_tools},
1273 {
"provider", prefs.ai_provider},
1274 {
"model", prefs.ai_model},
1275 {
"ollama_url", prefs.ollama_url},
1276 {
"gemini_api_key", prefs.gemini_api_key},
1277 {
"google_api_key", prefs.gemini_api_key},
1278 {
"openai_api_key", prefs.openai_api_key},
1279 {
"anthropic_api_key", prefs.anthropic_api_key},
1280 {
"temperature", prefs.ai_temperature},
1281 {
"max_tokens", prefs.ai_max_tokens},
1282 {
"proactive", prefs.ai_proactive},
1283 {
"auto_learn", prefs.ai_auto_learn},
1284 {
"multimodal", prefs.ai_multimodal},
1285 {
"hosts", ai_hosts},
1286 {
"active_host_id", prefs.active_ai_host_id},
1287 {
"profiles", ai_profiles},
1288 {
"active_profile", prefs.active_ai_profile},
1289 {
"remote_build_host_id", prefs.remote_build_host_id},
1290 {
"model_paths", prefs.ai_model_paths},
1294 {
"level", prefs.log_level},
1295 {
"to_file", prefs.log_to_file},
1296 {
"file_path", prefs.log_file_path},
1297 {
"ai_requests", prefs.log_ai_requests},
1298 {
"rom_operations", prefs.log_rom_operations},
1299 {
"gui_automation", prefs.log_gui_automation},
1300 {
"proposals", prefs.log_proposals},
1303 root[
"shortcuts"] = {
1304 {
"panel", ToStringMap(prefs.panel_shortcuts)},
1305 {
"global", ToStringMap(prefs.global_shortcuts)},
1306 {
"editor", ToStringMap(prefs.editor_shortcuts)},
1309 auto set_to_sorted_vec =
1310 [](
const std::unordered_set<std::string>& s) -> std::vector<std::string> {
1311 std::vector<std::string> v(s.begin(), s.end());
1312 std::sort(v.begin(), v.end());
1317 {
"visible", prefs.sidebar_visible},
1318 {
"panel_expanded", prefs.sidebar_panel_expanded},
1319 {
"panel_width", prefs.sidebar_panel_width},
1320 {
"panel_browser_category_width", prefs.panel_browser_category_width},
1321 {
"active_category", prefs.sidebar_active_category},
1322 {
"order", prefs.sidebar_order},
1323 {
"hidden", set_to_sorted_vec(prefs.sidebar_hidden)},
1324 {
"pinned", set_to_sorted_vec(prefs.sidebar_pinned)},
1325 {
"context_collapsed", set_to_sorted_vec(prefs.sidebar_context_collapsed)},
1328 root[
"status_bar"] = {
1329 {
"visible", prefs.show_status_bar},
1332 root[
"emulator"] = {
1333 {
"keep_running_in_background", prefs.emulator_keep_running_in_background},
1337 {
"show_experimental", prefs.show_experimental_editors},
1343 nlohmann::json named_layouts_json = nlohmann::json::object();
1344 for (
const auto& [layout_name, json_body] : prefs.named_layouts) {
1345 nlohmann::json parsed = nlohmann::json::parse(json_body,
nullptr,
false);
1346 if (parsed.is_discarded() || !parsed.is_object()) {
1347 named_layouts_json[layout_name] = json_body;
1349 named_layouts_json[layout_name] = std::move(parsed);
1354 {
"defaults_revision", prefs.panel_layout_defaults_revision},
1355 {
"panel_visibility", ToNestedBoolMap(prefs.panel_visibility_state)},
1356 {
"pinned_panels", ToBoolMap(prefs.pinned_panels)},
1357 {
"right_panel_widths", ToFloatMap(prefs.right_panel_widths)},
1358 {
"saved_layouts", ToNestedBoolMap(prefs.saved_layouts)},
1359 {
"named_layouts", std::move(named_layouts_json)},
1360 {
"last_applied_layout_name", prefs.last_applied_layout_name},
1363 root[
"filesystem"] = {
1364 {
"project_root_paths", prefs.project_root_paths},
1365 {
"default_project_root", prefs.default_project_root},
1366 {
"use_files_app", prefs.use_files_app},
1367 {
"use_icloud_sync", prefs.use_icloud_sync},
1370 std::ofstream file(path);
1371 if (!file.is_open()) {
1372 return absl::InternalError(
1373 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
1376 file << root.dump(2) <<
"\n";
1377 return absl::OkStatus();