33 auto parent = path.parent_path();
35 return absl::OkStatus();
40bool IsTransientPanelVisibilityId(
const std::string& panel_id) {
41 constexpr char kRoomPanelPrefix[] =
"dungeon.room_";
42 constexpr size_t kRoomPanelPrefixLength = 13;
43 if (panel_id.rfind(kRoomPanelPrefix, 0) != 0 ||
44 panel_id.size() <= kRoomPanelPrefixLength) {
47 return std::all_of(panel_id.begin() + kRoomPanelPrefixLength, panel_id.end(),
48 [](
char ch) { return ch >=
'0' && ch <=
'9'; });
52 std::unordered_map<std::string, bool>* panel_state) {
56 for (
auto it = panel_state->begin(); it != panel_state->end();) {
57 if (IsTransientPanelVisibilityId(it->first)) {
58 it = panel_state->erase(it);
66 return panel_id ==
"dungeon.object_editor" ||
67 panel_id ==
"dungeon.settings" || panel_id ==
"dungeon.dungeon_map";
71 std::unordered_map<std::string, bool>* panel_state) {
75 for (
auto it = panel_state->begin(); it != panel_state->end();) {
77 it = panel_state->erase(it);
85 "dungeon.room_selector",
"dungeon.room_matrix",
86 "dungeon.object_selector",
"dungeon.sprite_editor",
87 "dungeon.item_editor",
"dungeon.room_graphics",
88 "dungeon.door_editor",
"dungeon.palette_editor",
89 "dungeon.entrance_list",
"dungeon.entrance_properties",
93 std::unordered_map<std::string, bool>* panel_state) {
97 (*panel_state)[
"dungeon.workbench"] =
true;
99 (*panel_state)[panel_id] =
false;
104 if (!json_body || json_body->empty()) {
107 auto parsed = nlohmann::json::parse(*json_body,
nullptr,
false);
108 if (parsed.is_discarded() || !parsed.is_object() ||
109 !parsed.contains(
"root")) {
113 bool changed =
false;
114 auto prune_node = [&changed](
auto&& self, nlohmann::json& node) ->
void {
115 if (!node.is_object()) {
118 const std::string type = node.contains(
"type") && node[
"type"].is_string()
119 ? node[
"type"].get<std::string>()
121 if (type ==
"leaf" && node.contains(
"panels") &&
122 node[
"panels"].is_array()) {
123 auto& panels = node[
"panels"];
124 for (
auto it = panels.begin(); it != panels.end();) {
125 const bool erase = it->is_object() && it->contains(
"panel_id") &&
126 (*it)[
"panel_id"].is_string() &&
128 (*it)[
"panel_id"].get<std::string>());
130 it = panels.erase(it);
136 const int max_active =
137 panels.empty() ? 0 :
static_cast<int>(panels.size()) - 1;
138 const int active_tab_index =
139 node.contains(
"active_tab_index") &&
140 node[
"active_tab_index"].is_number_integer()
141 ? node[
"active_tab_index"].get<
int>()
143 const int clamped_active = std::clamp(active_tab_index, 0, max_active);
144 if (active_tab_index != clamped_active) {
145 node[
"active_tab_index"] = clamped_active;
149 if (node.contains(
"child_a")) {
150 self(self, node[
"child_a"]);
152 if (node.contains(
"child_b")) {
153 self(self, node[
"child_b"]);
156 prune_node(prune_node, parsed[
"root"]);
159 *json_body = parsed.dump();
167 return absl::InvalidArgumentError(
"prefs is null");
172 return absl::OkStatus();
175 std::istringstream ss(data);
180 auto to_float = [](
const std::string& s,
float fallback) {
183 }
catch (
const std::exception&) {
187 auto to_int = [](
const std::string& s,
int fallback) {
190 }
catch (
const std::exception&) {
194 while (std::getline(ss, line)) {
195 size_t eq_pos = line.find(
'=');
196 if (eq_pos == std::string::npos) {
200 std::string key = line.substr(0, eq_pos);
201 std::string val = line.substr(eq_pos + 1);
204 if (key ==
"font_global_scale") {
206 }
else if (key ==
"backup_rom") {
208 }
else if (key ==
"save_new_auto") {
210 }
else if (key ==
"autosave_enabled") {
212 }
else if (key ==
"autosave_interval") {
214 }
else if (key ==
"recent_files_limit") {
216 }
else if (key ==
"last_rom_path") {
218 }
else if (key ==
"last_project_path") {
220 }
else if (key ==
"show_welcome_on_startup") {
222 }
else if (key ==
"restore_last_session") {
224 }
else if (key ==
"prefer_hmagic_sprite_names") {
226 }
else if (key ==
"welcome_triforce_alpha") {
229 }
else if (key ==
"welcome_triforce_speed") {
232 }
else if (key ==
"welcome_triforce_size") {
235 }
else if (key ==
"welcome_particles_enabled") {
237 }
else if (key ==
"welcome_mouse_repel_enabled") {
239 }
else if (key ==
"reduced_motion") {
241 }
else if (key ==
"switch_motion_profile") {
243 }
else if (key ==
"last_theme_name") {
245 }
else if (key ==
"language_locale") {
247 }
else if (key ==
"font_family_index") {
251 else if (key ==
"backup_before_save") {
253 }
else if (key ==
"default_editor") {
257 else if (key ==
"vsync") {
258 prefs->
vsync = (val ==
"1");
259 }
else if (key ==
"target_fps") {
261 }
else if (key ==
"cache_size_mb") {
263 }
else if (key ==
"undo_history_size") {
267 else if (key ==
"ai_provider") {
269 }
else if (key ==
"ai_model") {
271 }
else if (key ==
"ollama_url") {
273 }
else if (key ==
"gemini_api_key") {
275 }
else if (key ==
"openai_api_key") {
277 }
else if (key ==
"anthropic_api_key") {
279 }
else if (key ==
"ai_temperature") {
281 }
else if (key ==
"ai_max_tokens") {
283 }
else if (key ==
"ai_proactive") {
285 }
else if (key ==
"ai_auto_learn") {
287 }
else if (key ==
"ai_multimodal") {
291 else if (key ==
"log_level") {
293 }
else if (key ==
"log_to_file") {
295 }
else if (key ==
"log_file_path") {
297 }
else if (key ==
"log_ai_requests") {
299 }
else if (key ==
"log_rom_operations") {
301 }
else if (key ==
"log_gui_automation") {
303 }
else if (key ==
"log_proposals") {
307 else if (key.substr(0, 15) ==
"panel_shortcut.") {
308 std::string panel_id = key.substr(15);
312 else if (key.substr(0, 14) ==
"card_shortcut.") {
313 std::string panel_id = key.substr(14);
317 else if (key ==
"sidebar_visible") {
319 }
else if (key ==
"sidebar_panel_expanded") {
321 }
else if (key ==
"sidebar_panel_width") {
323 }
else if (key ==
"panel_browser_category_width") {
326 }
else if (key ==
"panel_layout_defaults_revision") {
329 }
else if (key ==
"sidebar_active_category") {
331 }
else if (key ==
"dungeon_inspector_side") {
333 (val ==
"left") ? std::string(
"left") : std::string(
"right");
336 else if (key ==
"show_status_bar") {
338 }
else if (key ==
"emulator_keep_running_in_background") {
340 }
else if (key ==
"show_experimental_editors") {
344 else if (key.substr(0, 17) ==
"panel_visibility.") {
345 std::string rest = key.substr(17);
346 size_t dot_pos = rest.find(
'.');
347 if (dot_pos != std::string::npos) {
348 std::string editor_type = rest.substr(0, dot_pos);
349 std::string panel_id = rest.substr(dot_pos + 1);
354 else if (key.substr(0, 13) ==
"pinned_panel.") {
355 std::string panel_id = key.substr(13);
359 else if (key.substr(0, 18) ==
"right_panel_width.") {
360 std::string panel_key = key.substr(18);
364 else if (key.substr(0, 13) ==
"saved_layout.") {
365 std::string rest = key.substr(13);
366 size_t dot_pos = rest.find(
'.');
367 if (dot_pos != std::string::npos) {
368 std::string layout_name = rest.substr(0, dot_pos);
369 std::string panel_id = rest.substr(dot_pos + 1);
376 else if (key.substr(0, 13) ==
"named_layout.") {
377 std::string layout_name = key.substr(13);
379 }
else if (key ==
"last_applied_layout_name") {
384 return absl::OkStatus();
390 if (!ensure_status.ok()) {
391 return ensure_status;
394 std::ostringstream ss;
397 ss <<
"backup_rom=" << (prefs.
backup_rom ? 1 : 0) <<
"\n";
398 ss <<
"save_new_auto=" << (prefs.
save_new_auto ? 1 : 0) <<
"\n";
407 ss <<
"prefer_hmagic_sprite_names="
412 ss <<
"welcome_particles_enabled="
414 ss <<
"welcome_mouse_repel_enabled="
416 ss <<
"reduced_motion=" << (prefs.
reduced_motion ? 1 : 0) <<
"\n";
427 ss <<
"vsync=" << (prefs.
vsync ? 1 : 0) <<
"\n";
428 ss <<
"target_fps=" << prefs.
target_fps <<
"\n";
434 ss <<
"ai_model=" << prefs.
ai_model <<
"\n";
435 ss <<
"ollama_url=" << prefs.
ollama_url <<
"\n";
441 ss <<
"ai_proactive=" << (prefs.
ai_proactive ? 1 : 0) <<
"\n";
442 ss <<
"ai_auto_learn=" << (prefs.
ai_auto_learn ? 1 : 0) <<
"\n";
443 ss <<
"ai_multimodal=" << (prefs.
ai_multimodal ? 1 : 0) <<
"\n";
446 ss <<
"log_level=" << prefs.
log_level <<
"\n";
447 ss <<
"log_to_file=" << (prefs.
log_to_file ? 1 : 0) <<
"\n";
452 ss <<
"log_proposals=" << (prefs.
log_proposals ? 1 : 0) <<
"\n";
456 ss <<
"panel_shortcut." << panel_id <<
"=" << shortcut <<
"\n";
466 ss <<
"panel_layout_defaults_revision="
473 ss <<
"emulator_keep_running_in_background="
475 ss <<
"show_experimental_editors="
480 for (
const auto& [panel_id, visible] : panel_state) {
481 ss <<
"panel_visibility." << editor_type <<
"." << panel_id <<
"="
482 << (visible ? 1 : 0) <<
"\n";
488 ss <<
"pinned_panel." << panel_id <<
"=" << (pinned ? 1 : 0) <<
"\n";
492 ss <<
"right_panel_width." << panel_key <<
"=" << width <<
"\n";
496 for (
const auto& [layout_name, panel_state] : prefs.
saved_layouts) {
497 for (
const auto& [panel_id, visible] : panel_state) {
498 ss <<
"saved_layout." << layout_name <<
"." << panel_id <<
"="
499 << (visible ? 1 : 0) <<
"\n";
507 for (
const auto& [layout_name, json_body] : prefs.
named_layouts) {
510 std::string single_line;
511 single_line.reserve(json_body.size());
512 for (
char c : json_body) {
513 if (c ==
'\n' || c ==
'\r')
515 single_line.push_back(c);
517 ss <<
"named_layout." << layout_name <<
"=" << single_line <<
"\n";
520 std::ofstream file(path);
521 if (!file.is_open()) {
522 return absl::InternalError(
523 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
526 return absl::OkStatus();
543 UserSettings::Preferences::AiHost host;
544 host.id =
"ollama-local";
545 host.label =
"Ollama (local)";
547 host.api_type =
"ollama";
548 host.supports_tools =
true;
549 host.supports_streaming =
true;
554 UserSettings::Preferences::AiHost lmstudio;
555 lmstudio.id =
"lmstudio-local";
556 lmstudio.label =
"LM Studio (local)";
557 lmstudio.base_url =
"http://localhost:1234";
558 lmstudio.api_type =
"lmstudio";
559 lmstudio.supports_tools =
true;
560 lmstudio.supports_streaming =
true;
561 prefs->
ai_hosts.push_back(lmstudio);
568void EnsureDefaultAiProfiles(UserSettings::Preferences* prefs) {
572 if (!prefs->ai_profiles.empty()) {
573 if (prefs->active_ai_profile.empty()) {
574 prefs->active_ai_profile = prefs->ai_profiles.front().name;
578 if (!prefs->ai_model.empty()) {
579 UserSettings::Preferences::AiModelProfile profile;
580 profile.name =
"default";
581 profile.model = prefs->ai_model;
582 profile.temperature = prefs->ai_temperature;
583 profile.top_p = 0.95f;
584 profile.max_output_tokens = prefs->ai_max_tokens;
585 profile.supports_tools =
true;
586 prefs->ai_profiles.push_back(profile);
587 prefs->active_ai_profile = profile.name;
591void EnsureDefaultFilesystemRoots(UserSettings::Preferences* prefs) {
596 auto add_unique_root = [&](
const std::filesystem::path& path) {
600 const std::string path_str = path.string();
601 auto it = std::find(prefs->project_root_paths.begin(),
602 prefs->project_root_paths.end(), path_str);
603 if (it == prefs->project_root_paths.end()) {
604 prefs->project_root_paths.push_back(path_str);
610 add_unique_root(*docs_dir);
613 if (prefs->use_icloud_sync) {
616 if (icloud_dir.ok()) {
617 add_unique_root(*icloud_dir);
618 if (prefs->default_project_root.empty()) {
619 prefs->default_project_root = icloud_dir->string();
624 if (prefs->default_project_root.empty() &&
625 !prefs->project_root_paths.empty()) {
626 prefs->default_project_root = prefs->project_root_paths.front();
630void EnsureDefaultModelPaths(UserSettings::Preferences* prefs) {
634 if (!prefs->ai_model_paths.empty()) {
638 auto add_unique_path = [&](
const std::filesystem::path& path) {
642 const std::string path_str = path.string();
643 auto it = std::find(prefs->ai_model_paths.begin(),
644 prefs->ai_model_paths.end(), path_str);
645 if (it == prefs->ai_model_paths.end()) {
646 prefs->ai_model_paths.push_back(path_str);
651 if (!home_dir.empty() && home_dir !=
".") {
652 add_unique_path(home_dir /
"models");
653 add_unique_path(home_dir /
".lmstudio" /
"models");
654 add_unique_path(home_dir /
".ollama" /
"models");
658void LoadStringMap(
const json& src,
659 std::unordered_map<std::string, std::string>* target) {
660 if (!target || !src.is_object()) {
664 for (
const auto& [key, value] : src.items()) {
665 if (value.is_string()) {
666 (*target)[
key] = value.get<std::string>();
671void LoadBoolMap(
const json& src,
672 std::unordered_map<std::string, bool>* target) {
673 if (!target || !src.is_object()) {
677 for (
const auto& [key, value] : src.items()) {
678 if (value.is_boolean()) {
679 (*target)[
key] = value.get<
bool>();
684void LoadFloatMap(
const json& src,
685 std::unordered_map<std::string, float>* target) {
686 if (!target || !src.is_object()) {
690 for (
const auto& [key, value] : src.items()) {
691 if (value.is_number()) {
692 (*target)[
key] = value.get<
float>();
697void LoadNestedBoolMap(
699 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
701 if (!target || !src.is_object()) {
705 for (
const auto& [outer_key, outer_val] : src.items()) {
706 if (!outer_val.is_object()) {
709 auto& inner = (*target)[outer_key];
711 for (
const auto& [inner_key, inner_val] : outer_val.items()) {
712 if (inner_val.is_boolean()) {
713 inner[inner_key] = inner_val.get<
bool>();
719json ToStringMap(
const std::unordered_map<std::string, std::string>& map) {
720 json obj = json::object();
721 for (
const auto& [key, value] : map) {
727json ToBoolMap(
const std::unordered_map<std::string, bool>& map) {
728 json obj = json::object();
729 for (
const auto& [key, value] : map) {
735json ToFloatMap(
const std::unordered_map<std::string, float>& map) {
736 json obj = json::object();
737 for (
const auto& [key, value] : map) {
743json ToNestedBoolMap(
const std::unordered_map<
744 std::string, std::unordered_map<std::string, bool>>& map) {
745 json obj = json::object();
746 for (
const auto& [outer_key, inner] : map) {
747 obj[outer_key] = ToBoolMap(inner);
752absl::Status LoadPreferencesFromJson(
const std::filesystem::path& path,
753 UserSettings::Preferences* prefs) {
755 return absl::InvalidArgumentError(
"prefs is null");
758 std::ifstream file(path);
759 if (!file.is_open()) {
760 return absl::NotFoundError(
761 absl::StrFormat(
"Settings file not found: %s", path.string()));
767 }
catch (
const std::exception& e) {
768 return absl::InternalError(
769 absl::StrFormat(
"Failed to parse settings.json: %s", e.what()));
772 if (root.contains(
"general")) {
773 const auto& g = root[
"general"];
774 prefs->font_global_scale =
775 g.value(
"font_global_scale", prefs->font_global_scale);
776 prefs->backup_rom = g.value(
"backup_rom", prefs->backup_rom);
777 prefs->save_new_auto = g.value(
"save_new_auto", prefs->save_new_auto);
778 prefs->autosave_enabled =
779 g.value(
"autosave_enabled", prefs->autosave_enabled);
780 prefs->autosave_interval =
781 g.value(
"autosave_interval", prefs->autosave_interval);
782 prefs->recent_files_limit =
783 g.value(
"recent_files_limit", prefs->recent_files_limit);
784 prefs->last_rom_path = g.value(
"last_rom_path", prefs->last_rom_path);
785 prefs->last_project_path =
786 g.value(
"last_project_path", prefs->last_project_path);
787 prefs->show_welcome_on_startup =
788 g.value(
"show_welcome_on_startup", prefs->show_welcome_on_startup);
789 prefs->restore_last_session =
790 g.value(
"restore_last_session", prefs->restore_last_session);
791 prefs->prefer_hmagic_sprite_names = g.value(
792 "prefer_hmagic_sprite_names", prefs->prefer_hmagic_sprite_names);
793 prefs->welcome_triforce_alpha =
794 g.value(
"welcome_triforce_alpha", prefs->welcome_triforce_alpha);
795 prefs->welcome_triforce_speed =
796 g.value(
"welcome_triforce_speed", prefs->welcome_triforce_speed);
797 prefs->welcome_triforce_size =
798 g.value(
"welcome_triforce_size", prefs->welcome_triforce_size);
799 prefs->welcome_particles_enabled =
800 g.value(
"welcome_particles_enabled", prefs->welcome_particles_enabled);
801 prefs->welcome_mouse_repel_enabled = g.value(
802 "welcome_mouse_repel_enabled", prefs->welcome_mouse_repel_enabled);
805 if (root.contains(
"appearance")) {
806 const auto& appearance = root[
"appearance"];
807 prefs->reduced_motion =
808 appearance.value(
"reduced_motion", prefs->reduced_motion);
809 prefs->switch_motion_profile =
810 appearance.value(
"switch_motion_profile", prefs->switch_motion_profile);
811 prefs->last_theme_name =
812 appearance.value(
"last_theme_name", prefs->last_theme_name);
813 prefs->language_locale =
814 appearance.value(
"language_locale", prefs->language_locale);
815 prefs->font_family_index =
816 appearance.value(
"font_family_index", prefs->font_family_index);
819 if (root.contains(
"editor")) {
820 const auto& e = root[
"editor"];
821 prefs->backup_before_save =
822 e.value(
"backup_before_save", prefs->backup_before_save);
823 prefs->default_editor = e.value(
"default_editor", prefs->default_editor);
826 e.value(
"dungeon_inspector_side", prefs->dungeon_inspector_side);
827 prefs->dungeon_inspector_side = (side ==
"left") ?
"left" :
"right";
831 if (root.contains(
"performance")) {
832 const auto& p = root[
"performance"];
833 prefs->vsync = p.value(
"vsync", prefs->vsync);
834 prefs->target_fps = p.value(
"target_fps", prefs->target_fps);
835 prefs->cache_size_mb = p.value(
"cache_size_mb", prefs->cache_size_mb);
836 prefs->undo_history_size =
837 p.value(
"undo_history_size", prefs->undo_history_size);
840 if (root.contains(
"ai")) {
841 const auto& ai = root[
"ai"];
842 prefs->ai_provider = ai.value(
"provider", prefs->ai_provider);
843 prefs->ai_model = ai.value(
"model", prefs->ai_model);
844 prefs->ollama_url = ai.value(
"ollama_url", prefs->ollama_url);
845 prefs->gemini_api_key = ai.value(
"gemini_api_key", prefs->gemini_api_key);
846 prefs->openai_api_key = ai.value(
"openai_api_key", prefs->openai_api_key);
847 prefs->anthropic_api_key =
848 ai.value(
"anthropic_api_key", prefs->anthropic_api_key);
849 std::string google_key = ai.value(
"google_api_key", std::string());
850 if (prefs->gemini_api_key.empty() && !google_key.empty()) {
851 prefs->gemini_api_key = google_key;
853 prefs->ai_temperature = ai.value(
"temperature", prefs->ai_temperature);
854 prefs->ai_max_tokens = ai.value(
"max_tokens", prefs->ai_max_tokens);
855 prefs->ai_proactive = ai.value(
"proactive", prefs->ai_proactive);
856 prefs->ai_auto_learn = ai.value(
"auto_learn", prefs->ai_auto_learn);
857 prefs->ai_multimodal = ai.value(
"multimodal", prefs->ai_multimodal);
858 prefs->active_ai_host_id =
859 ai.value(
"active_host_id", prefs->active_ai_host_id);
860 prefs->active_ai_profile =
861 ai.value(
"active_profile", prefs->active_ai_profile);
862 prefs->remote_build_host_id =
863 ai.value(
"remote_build_host_id", prefs->remote_build_host_id);
864 if (ai.contains(
"model_paths") && ai[
"model_paths"].is_array()) {
865 prefs->ai_model_paths.clear();
866 for (
const auto& item : ai[
"model_paths"]) {
867 if (item.is_string()) {
868 prefs->ai_model_paths.push_back(item.get<std::string>());
873 if (ai.contains(
"hosts") && ai[
"hosts"].is_array()) {
874 prefs->ai_hosts.clear();
875 for (
const auto& host : ai[
"hosts"]) {
876 if (!host.is_object()) {
879 UserSettings::Preferences::AiHost entry;
880 entry.id = host.value(
"id",
"");
881 entry.label = host.value(
"label",
"");
882 entry.base_url = host.value(
"base_url",
"");
883 entry.api_type = host.value(
"api_type",
"");
884 entry.supports_vision =
885 host.value(
"supports_vision", entry.supports_vision);
886 entry.supports_tools =
887 host.value(
"supports_tools", entry.supports_tools);
888 entry.supports_streaming =
889 host.value(
"supports_streaming", entry.supports_streaming);
890 entry.allow_insecure =
891 host.value(
"allow_insecure", entry.allow_insecure);
892 entry.api_key = host.value(
"api_key",
"");
893 entry.credential_id = host.value(
"credential_id",
"");
894 prefs->ai_hosts.push_back(entry);
898 if (ai.contains(
"profiles") && ai[
"profiles"].is_array()) {
899 prefs->ai_profiles.clear();
900 for (
const auto& profile : ai[
"profiles"]) {
901 if (!profile.is_object()) {
904 UserSettings::Preferences::AiModelProfile entry;
905 entry.name = profile.value(
"name",
"");
906 entry.model = profile.value(
"model",
"");
907 entry.temperature = profile.value(
"temperature", entry.temperature);
908 entry.top_p = profile.value(
"top_p", entry.top_p);
909 entry.max_output_tokens =
910 profile.value(
"max_output_tokens", entry.max_output_tokens);
911 entry.supports_vision =
912 profile.value(
"supports_vision", entry.supports_vision);
913 entry.supports_tools =
914 profile.value(
"supports_tools", entry.supports_tools);
915 prefs->ai_profiles.push_back(entry);
920 if (root.contains(
"logging")) {
921 const auto& log = root[
"logging"];
922 prefs->log_level = log.value(
"level", prefs->log_level);
923 prefs->log_to_file = log.value(
"to_file", prefs->log_to_file);
924 prefs->log_file_path = log.value(
"file_path", prefs->log_file_path);
925 prefs->log_ai_requests = log.value(
"ai_requests", prefs->log_ai_requests);
926 prefs->log_rom_operations =
927 log.value(
"rom_operations", prefs->log_rom_operations);
928 prefs->log_gui_automation =
929 log.value(
"gui_automation", prefs->log_gui_automation);
930 prefs->log_proposals = log.value(
"proposals", prefs->log_proposals);
933 if (root.contains(
"shortcuts")) {
934 const auto& shortcuts = root[
"shortcuts"];
935 if (shortcuts.contains(
"panel")) {
936 LoadStringMap(shortcuts[
"panel"], &prefs->panel_shortcuts);
938 if (shortcuts.contains(
"global")) {
939 LoadStringMap(shortcuts[
"global"], &prefs->global_shortcuts);
941 if (shortcuts.contains(
"editor")) {
942 LoadStringMap(shortcuts[
"editor"], &prefs->editor_shortcuts);
946 if (root.contains(
"sidebar")) {
947 const auto& sidebar = root[
"sidebar"];
948 prefs->sidebar_visible = sidebar.value(
"visible", prefs->sidebar_visible);
949 prefs->sidebar_panel_expanded =
950 sidebar.value(
"panel_expanded", prefs->sidebar_panel_expanded);
951 prefs->sidebar_panel_width =
952 sidebar.value(
"panel_width", prefs->sidebar_panel_width);
953 prefs->panel_browser_category_width = sidebar.value(
954 "panel_browser_category_width", prefs->panel_browser_category_width);
955 prefs->sidebar_active_category =
956 sidebar.value(
"active_category", prefs->sidebar_active_category);
958 if (sidebar.contains(
"order") && sidebar[
"order"].is_array()) {
959 prefs->sidebar_order.clear();
960 for (
const auto& item : sidebar[
"order"]) {
961 if (item.is_string()) {
962 prefs->sidebar_order.push_back(item.get<std::string>());
966 if (sidebar.contains(
"hidden") && sidebar[
"hidden"].is_array()) {
967 prefs->sidebar_hidden.clear();
968 for (
const auto& item : sidebar[
"hidden"]) {
969 if (item.is_string()) {
970 prefs->sidebar_hidden.insert(item.get<std::string>());
974 if (sidebar.contains(
"pinned") && sidebar[
"pinned"].is_array()) {
975 prefs->sidebar_pinned.clear();
976 for (
const auto& item : sidebar[
"pinned"]) {
977 if (item.is_string()) {
978 prefs->sidebar_pinned.insert(item.get<std::string>());
984 if (root.contains(
"status_bar")) {
985 const auto& status_bar = root[
"status_bar"];
986 prefs->show_status_bar =
987 status_bar.value(
"visible", prefs->show_status_bar);
990 if (root.contains(
"emulator")) {
991 const auto& emulator = root[
"emulator"];
992 prefs->emulator_keep_running_in_background =
993 emulator.value(
"keep_running_in_background",
994 prefs->emulator_keep_running_in_background);
997 if (root.contains(
"editors")) {
998 const auto& editors = root[
"editors"];
999 prefs->show_experimental_editors =
1000 editors.value(
"show_experimental", prefs->show_experimental_editors);
1003 if (root.contains(
"layouts")) {
1004 const auto& layouts = root[
"layouts"];
1005 prefs->panel_layout_defaults_revision = layouts.value(
1006 "defaults_revision", prefs->panel_layout_defaults_revision);
1007 if (layouts.contains(
"panel_visibility")) {
1008 LoadNestedBoolMap(layouts[
"panel_visibility"],
1009 &prefs->panel_visibility_state);
1011 if (layouts.contains(
"pinned_panels")) {
1012 LoadBoolMap(layouts[
"pinned_panels"], &prefs->pinned_panels);
1014 if (layouts.contains(
"right_panel_widths")) {
1015 LoadFloatMap(layouts[
"right_panel_widths"], &prefs->right_panel_widths);
1017 if (layouts.contains(
"saved_layouts")) {
1018 LoadNestedBoolMap(layouts[
"saved_layouts"], &prefs->saved_layouts);
1020 if (layouts.contains(
"named_layouts") &&
1021 layouts[
"named_layouts"].is_object()) {
1022 prefs->named_layouts.clear();
1023 for (
auto it = layouts[
"named_layouts"].begin();
1024 it != layouts[
"named_layouts"].end(); ++it) {
1025 if (it.value().is_string()) {
1026 prefs->named_layouts[it.key()] = it.value().get<std::string>();
1027 }
else if (it.value().is_object() || it.value().is_array()) {
1030 prefs->named_layouts[it.key()] = it.value().dump();
1034 prefs->last_applied_layout_name = layouts.value(
1035 "last_applied_layout_name", prefs->last_applied_layout_name);
1038 if (root.contains(
"filesystem")) {
1039 const auto& fs = root[
"filesystem"];
1040 if (fs.contains(
"project_root_paths") &&
1041 fs[
"project_root_paths"].is_array()) {
1042 prefs->project_root_paths.clear();
1043 for (
const auto& item : fs[
"project_root_paths"]) {
1044 if (item.is_string()) {
1045 prefs->project_root_paths.push_back(item.get<std::string>());
1049 prefs->default_project_root =
1050 fs.value(
"default_project_root", prefs->default_project_root);
1051 prefs->use_files_app = fs.value(
"use_files_app", prefs->use_files_app);
1052 prefs->use_icloud_sync =
1053 fs.value(
"use_icloud_sync", prefs->use_icloud_sync);
1056 EnsureDefaultAiHosts(prefs);
1057 EnsureDefaultAiProfiles(prefs);
1058 EnsureDefaultFilesystemRoots(prefs);
1060 return absl::OkStatus();
1063absl::Status SavePreferencesToJson(
const std::filesystem::path& path,
1064 const UserSettings::Preferences& prefs) {
1066 if (!ensure_status.ok()) {
1067 return ensure_status;
1071 root[
"version"] = 1;
1073 {
"font_global_scale", prefs.font_global_scale},
1074 {
"backup_rom", prefs.backup_rom},
1075 {
"save_new_auto", prefs.save_new_auto},
1076 {
"autosave_enabled", prefs.autosave_enabled},
1077 {
"autosave_interval", prefs.autosave_interval},
1078 {
"recent_files_limit", prefs.recent_files_limit},
1079 {
"last_rom_path", prefs.last_rom_path},
1080 {
"last_project_path", prefs.last_project_path},
1081 {
"show_welcome_on_startup", prefs.show_welcome_on_startup},
1082 {
"restore_last_session", prefs.restore_last_session},
1083 {
"prefer_hmagic_sprite_names", prefs.prefer_hmagic_sprite_names},
1084 {
"welcome_triforce_alpha", prefs.welcome_triforce_alpha},
1085 {
"welcome_triforce_speed", prefs.welcome_triforce_speed},
1086 {
"welcome_triforce_size", prefs.welcome_triforce_size},
1087 {
"welcome_particles_enabled", prefs.welcome_particles_enabled},
1088 {
"welcome_mouse_repel_enabled", prefs.welcome_mouse_repel_enabled},
1091 root[
"appearance"] = {
1092 {
"reduced_motion", prefs.reduced_motion},
1093 {
"switch_motion_profile", prefs.switch_motion_profile},
1094 {
"last_theme_name", prefs.last_theme_name},
1095 {
"language_locale", prefs.language_locale},
1096 {
"font_family_index", prefs.font_family_index},
1100 {
"backup_before_save", prefs.backup_before_save},
1101 {
"default_editor", prefs.default_editor},
1102 {
"dungeon_inspector_side", prefs.dungeon_inspector_side},
1105 root[
"performance"] = {
1106 {
"vsync", prefs.vsync},
1107 {
"target_fps", prefs.target_fps},
1108 {
"cache_size_mb", prefs.cache_size_mb},
1109 {
"undo_history_size", prefs.undo_history_size},
1112 json ai_hosts = json::array();
1113 for (
const auto& host : prefs.ai_hosts) {
1114 ai_hosts.push_back({
1116 {
"label", host.label},
1117 {
"base_url", host.base_url},
1118 {
"api_type", host.api_type},
1119 {
"supports_vision", host.supports_vision},
1120 {
"supports_tools", host.supports_tools},
1121 {
"supports_streaming", host.supports_streaming},
1122 {
"allow_insecure", host.allow_insecure},
1123 {
"api_key", host.api_key},
1124 {
"credential_id", host.credential_id},
1128 json ai_profiles = json::array();
1129 for (
const auto& profile : prefs.ai_profiles) {
1130 ai_profiles.push_back({
1131 {
"name", profile.name},
1132 {
"model", profile.model},
1133 {
"temperature", profile.temperature},
1134 {
"top_p", profile.top_p},
1135 {
"max_output_tokens", profile.max_output_tokens},
1136 {
"supports_vision", profile.supports_vision},
1137 {
"supports_tools", profile.supports_tools},
1142 {
"provider", prefs.ai_provider},
1143 {
"model", prefs.ai_model},
1144 {
"ollama_url", prefs.ollama_url},
1145 {
"gemini_api_key", prefs.gemini_api_key},
1146 {
"google_api_key", prefs.gemini_api_key},
1147 {
"openai_api_key", prefs.openai_api_key},
1148 {
"anthropic_api_key", prefs.anthropic_api_key},
1149 {
"temperature", prefs.ai_temperature},
1150 {
"max_tokens", prefs.ai_max_tokens},
1151 {
"proactive", prefs.ai_proactive},
1152 {
"auto_learn", prefs.ai_auto_learn},
1153 {
"multimodal", prefs.ai_multimodal},
1154 {
"hosts", ai_hosts},
1155 {
"active_host_id", prefs.active_ai_host_id},
1156 {
"profiles", ai_profiles},
1157 {
"active_profile", prefs.active_ai_profile},
1158 {
"remote_build_host_id", prefs.remote_build_host_id},
1159 {
"model_paths", prefs.ai_model_paths},
1163 {
"level", prefs.log_level},
1164 {
"to_file", prefs.log_to_file},
1165 {
"file_path", prefs.log_file_path},
1166 {
"ai_requests", prefs.log_ai_requests},
1167 {
"rom_operations", prefs.log_rom_operations},
1168 {
"gui_automation", prefs.log_gui_automation},
1169 {
"proposals", prefs.log_proposals},
1172 root[
"shortcuts"] = {
1173 {
"panel", ToStringMap(prefs.panel_shortcuts)},
1174 {
"global", ToStringMap(prefs.global_shortcuts)},
1175 {
"editor", ToStringMap(prefs.editor_shortcuts)},
1178 auto set_to_sorted_vec =
1179 [](
const std::unordered_set<std::string>& s) -> std::vector<std::string> {
1180 std::vector<std::string> v(s.begin(), s.end());
1181 std::sort(v.begin(), v.end());
1186 {
"visible", prefs.sidebar_visible},
1187 {
"panel_expanded", prefs.sidebar_panel_expanded},
1188 {
"panel_width", prefs.sidebar_panel_width},
1189 {
"panel_browser_category_width", prefs.panel_browser_category_width},
1190 {
"active_category", prefs.sidebar_active_category},
1191 {
"order", prefs.sidebar_order},
1192 {
"hidden", set_to_sorted_vec(prefs.sidebar_hidden)},
1193 {
"pinned", set_to_sorted_vec(prefs.sidebar_pinned)},
1196 root[
"status_bar"] = {
1197 {
"visible", prefs.show_status_bar},
1200 root[
"emulator"] = {
1201 {
"keep_running_in_background", prefs.emulator_keep_running_in_background},
1205 {
"show_experimental", prefs.show_experimental_editors},
1211 nlohmann::json named_layouts_json = nlohmann::json::object();
1212 for (
const auto& [layout_name, json_body] : prefs.named_layouts) {
1213 nlohmann::json parsed = nlohmann::json::parse(json_body,
nullptr,
false);
1214 if (parsed.is_discarded() || !parsed.is_object()) {
1215 named_layouts_json[layout_name] = json_body;
1217 named_layouts_json[layout_name] = std::move(parsed);
1222 {
"defaults_revision", prefs.panel_layout_defaults_revision},
1223 {
"panel_visibility", ToNestedBoolMap(prefs.panel_visibility_state)},
1224 {
"pinned_panels", ToBoolMap(prefs.pinned_panels)},
1225 {
"right_panel_widths", ToFloatMap(prefs.right_panel_widths)},
1226 {
"saved_layouts", ToNestedBoolMap(prefs.saved_layouts)},
1227 {
"named_layouts", std::move(named_layouts_json)},
1228 {
"last_applied_layout_name", prefs.last_applied_layout_name},
1231 root[
"filesystem"] = {
1232 {
"project_root_paths", prefs.project_root_paths},
1233 {
"default_project_root", prefs.default_project_root},
1234 {
"use_files_app", prefs.use_files_app},
1235 {
"use_icloud_sync", prefs.use_icloud_sync},
1238 std::ofstream file(path);
1239 if (!file.is_open()) {
1240 return absl::InternalError(
1241 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
1244 file << root.dump(2) <<
"\n";
1245 return absl::OkStatus();