32 auto parent = path.parent_path();
34 return absl::OkStatus();
39bool IsTransientPanelVisibilityId(
const std::string& panel_id) {
40 constexpr char kRoomPanelPrefix[] =
"dungeon.room_";
41 constexpr size_t kRoomPanelPrefixLength = 13;
42 if (panel_id.rfind(kRoomPanelPrefix, 0) != 0 ||
43 panel_id.size() <= kRoomPanelPrefixLength) {
46 return std::all_of(panel_id.begin() + kRoomPanelPrefixLength, panel_id.end(),
47 [](
char ch) { return ch >=
'0' && ch <=
'9'; });
51 std::unordered_map<std::string, bool>* panel_state) {
55 for (
auto it = panel_state->begin(); it != panel_state->end();) {
56 if (IsTransientPanelVisibilityId(it->first)) {
57 it = panel_state->erase(it);
65 return panel_id ==
"dungeon.object_editor" ||
66 panel_id ==
"dungeon.settings" || panel_id ==
"dungeon.dungeon_map";
70 std::unordered_map<std::string, bool>* panel_state) {
74 for (
auto it = panel_state->begin(); it != panel_state->end();) {
76 it = panel_state->erase(it);
84 if (!json_body || json_body->empty()) {
87 auto parsed = nlohmann::json::parse(*json_body,
nullptr,
false);
88 if (parsed.is_discarded() || !parsed.is_object() ||
89 !parsed.contains(
"root")) {
94 auto prune_node = [&changed](
auto&& self, nlohmann::json& node) ->
void {
95 if (!node.is_object()) {
98 const std::string type = node.contains(
"type") && node[
"type"].is_string()
99 ? node[
"type"].get<std::string>()
101 if (type ==
"leaf" && node.contains(
"panels") &&
102 node[
"panels"].is_array()) {
103 auto& panels = node[
"panels"];
104 for (
auto it = panels.begin(); it != panels.end();) {
105 const bool erase = it->is_object() && it->contains(
"panel_id") &&
106 (*it)[
"panel_id"].is_string() &&
108 (*it)[
"panel_id"].get<std::string>());
110 it = panels.erase(it);
116 const int max_active =
117 panels.empty() ? 0 :
static_cast<int>(panels.size()) - 1;
118 const int active_tab_index =
119 node.contains(
"active_tab_index") &&
120 node[
"active_tab_index"].is_number_integer()
121 ? node[
"active_tab_index"].get<
int>()
123 const int clamped_active = std::clamp(active_tab_index, 0, max_active);
124 if (active_tab_index != clamped_active) {
125 node[
"active_tab_index"] = clamped_active;
129 if (node.contains(
"child_a")) {
130 self(self, node[
"child_a"]);
132 if (node.contains(
"child_b")) {
133 self(self, node[
"child_b"]);
136 prune_node(prune_node, parsed[
"root"]);
139 *json_body = parsed.dump();
147 return absl::InvalidArgumentError(
"prefs is null");
152 return absl::OkStatus();
155 std::istringstream ss(data);
160 auto to_float = [](
const std::string& s,
float fallback) {
163 }
catch (
const std::exception&) {
167 auto to_int = [](
const std::string& s,
int fallback) {
170 }
catch (
const std::exception&) {
174 while (std::getline(ss, line)) {
175 size_t eq_pos = line.find(
'=');
176 if (eq_pos == std::string::npos) {
180 std::string key = line.substr(0, eq_pos);
181 std::string val = line.substr(eq_pos + 1);
184 if (key ==
"font_global_scale") {
186 }
else if (key ==
"backup_rom") {
188 }
else if (key ==
"save_new_auto") {
190 }
else if (key ==
"autosave_enabled") {
192 }
else if (key ==
"autosave_interval") {
194 }
else if (key ==
"recent_files_limit") {
196 }
else if (key ==
"last_rom_path") {
198 }
else if (key ==
"last_project_path") {
200 }
else if (key ==
"show_welcome_on_startup") {
202 }
else if (key ==
"restore_last_session") {
204 }
else if (key ==
"prefer_hmagic_sprite_names") {
206 }
else if (key ==
"welcome_triforce_alpha") {
209 }
else if (key ==
"welcome_triforce_speed") {
212 }
else if (key ==
"welcome_triforce_size") {
215 }
else if (key ==
"welcome_particles_enabled") {
217 }
else if (key ==
"welcome_mouse_repel_enabled") {
219 }
else if (key ==
"reduced_motion") {
221 }
else if (key ==
"switch_motion_profile") {
223 }
else if (key ==
"last_theme_name") {
225 }
else if (key ==
"language_locale") {
227 }
else if (key ==
"font_family_index") {
231 else if (key ==
"backup_before_save") {
233 }
else if (key ==
"default_editor") {
237 else if (key ==
"vsync") {
238 prefs->
vsync = (val ==
"1");
239 }
else if (key ==
"target_fps") {
241 }
else if (key ==
"cache_size_mb") {
243 }
else if (key ==
"undo_history_size") {
247 else if (key ==
"ai_provider") {
249 }
else if (key ==
"ai_model") {
251 }
else if (key ==
"ollama_url") {
253 }
else if (key ==
"gemini_api_key") {
255 }
else if (key ==
"openai_api_key") {
257 }
else if (key ==
"anthropic_api_key") {
259 }
else if (key ==
"ai_temperature") {
261 }
else if (key ==
"ai_max_tokens") {
263 }
else if (key ==
"ai_proactive") {
265 }
else if (key ==
"ai_auto_learn") {
267 }
else if (key ==
"ai_multimodal") {
271 else if (key ==
"log_level") {
273 }
else if (key ==
"log_to_file") {
275 }
else if (key ==
"log_file_path") {
277 }
else if (key ==
"log_ai_requests") {
279 }
else if (key ==
"log_rom_operations") {
281 }
else if (key ==
"log_gui_automation") {
283 }
else if (key ==
"log_proposals") {
287 else if (key.substr(0, 15) ==
"panel_shortcut.") {
288 std::string panel_id = key.substr(15);
292 else if (key.substr(0, 14) ==
"card_shortcut.") {
293 std::string panel_id = key.substr(14);
297 else if (key ==
"sidebar_visible") {
299 }
else if (key ==
"sidebar_panel_expanded") {
301 }
else if (key ==
"sidebar_panel_width") {
303 }
else if (key ==
"panel_browser_category_width") {
306 }
else if (key ==
"panel_layout_defaults_revision") {
309 }
else if (key ==
"sidebar_active_category") {
311 }
else if (key ==
"dungeon_inspector_side") {
313 (val ==
"left") ? std::string(
"left") : std::string(
"right");
316 else if (key ==
"show_status_bar") {
320 else if (key.substr(0, 17) ==
"panel_visibility.") {
321 std::string rest = key.substr(17);
322 size_t dot_pos = rest.find(
'.');
323 if (dot_pos != std::string::npos) {
324 std::string editor_type = rest.substr(0, dot_pos);
325 std::string panel_id = rest.substr(dot_pos + 1);
330 else if (key.substr(0, 13) ==
"pinned_panel.") {
331 std::string panel_id = key.substr(13);
335 else if (key.substr(0, 18) ==
"right_panel_width.") {
336 std::string panel_key = key.substr(18);
340 else if (key.substr(0, 13) ==
"saved_layout.") {
341 std::string rest = key.substr(13);
342 size_t dot_pos = rest.find(
'.');
343 if (dot_pos != std::string::npos) {
344 std::string layout_name = rest.substr(0, dot_pos);
345 std::string panel_id = rest.substr(dot_pos + 1);
352 else if (key.substr(0, 13) ==
"named_layout.") {
353 std::string layout_name = key.substr(13);
355 }
else if (key ==
"last_applied_layout_name") {
360 return absl::OkStatus();
366 if (!ensure_status.ok()) {
367 return ensure_status;
370 std::ostringstream ss;
373 ss <<
"backup_rom=" << (prefs.
backup_rom ? 1 : 0) <<
"\n";
374 ss <<
"save_new_auto=" << (prefs.
save_new_auto ? 1 : 0) <<
"\n";
383 ss <<
"prefer_hmagic_sprite_names="
388 ss <<
"welcome_particles_enabled="
390 ss <<
"welcome_mouse_repel_enabled="
392 ss <<
"reduced_motion=" << (prefs.
reduced_motion ? 1 : 0) <<
"\n";
403 ss <<
"vsync=" << (prefs.
vsync ? 1 : 0) <<
"\n";
404 ss <<
"target_fps=" << prefs.
target_fps <<
"\n";
410 ss <<
"ai_model=" << prefs.
ai_model <<
"\n";
411 ss <<
"ollama_url=" << prefs.
ollama_url <<
"\n";
417 ss <<
"ai_proactive=" << (prefs.
ai_proactive ? 1 : 0) <<
"\n";
418 ss <<
"ai_auto_learn=" << (prefs.
ai_auto_learn ? 1 : 0) <<
"\n";
419 ss <<
"ai_multimodal=" << (prefs.
ai_multimodal ? 1 : 0) <<
"\n";
422 ss <<
"log_level=" << prefs.
log_level <<
"\n";
423 ss <<
"log_to_file=" << (prefs.
log_to_file ? 1 : 0) <<
"\n";
428 ss <<
"log_proposals=" << (prefs.
log_proposals ? 1 : 0) <<
"\n";
432 ss <<
"panel_shortcut." << panel_id <<
"=" << shortcut <<
"\n";
442 ss <<
"panel_layout_defaults_revision="
452 for (
const auto& [panel_id, visible] : panel_state) {
453 ss <<
"panel_visibility." << editor_type <<
"." << panel_id <<
"="
454 << (visible ? 1 : 0) <<
"\n";
460 ss <<
"pinned_panel." << panel_id <<
"=" << (pinned ? 1 : 0) <<
"\n";
464 ss <<
"right_panel_width." << panel_key <<
"=" << width <<
"\n";
468 for (
const auto& [layout_name, panel_state] : prefs.
saved_layouts) {
469 for (
const auto& [panel_id, visible] : panel_state) {
470 ss <<
"saved_layout." << layout_name <<
"." << panel_id <<
"="
471 << (visible ? 1 : 0) <<
"\n";
479 for (
const auto& [layout_name, json_body] : prefs.
named_layouts) {
482 std::string single_line;
483 single_line.reserve(json_body.size());
484 for (
char c : json_body) {
485 if (c ==
'\n' || c ==
'\r')
487 single_line.push_back(c);
489 ss <<
"named_layout." << layout_name <<
"=" << single_line <<
"\n";
492 std::ofstream file(path);
493 if (!file.is_open()) {
494 return absl::InternalError(
495 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
498 return absl::OkStatus();
515 UserSettings::Preferences::AiHost host;
516 host.id =
"ollama-local";
517 host.label =
"Ollama (local)";
519 host.api_type =
"ollama";
520 host.supports_tools =
true;
521 host.supports_streaming =
true;
526 UserSettings::Preferences::AiHost lmstudio;
527 lmstudio.id =
"lmstudio-local";
528 lmstudio.label =
"LM Studio (local)";
529 lmstudio.base_url =
"http://localhost:1234";
530 lmstudio.api_type =
"lmstudio";
531 lmstudio.supports_tools =
true;
532 lmstudio.supports_streaming =
true;
533 prefs->
ai_hosts.push_back(lmstudio);
540void EnsureDefaultAiProfiles(UserSettings::Preferences* prefs) {
544 if (!prefs->ai_profiles.empty()) {
545 if (prefs->active_ai_profile.empty()) {
546 prefs->active_ai_profile = prefs->ai_profiles.front().name;
550 if (!prefs->ai_model.empty()) {
551 UserSettings::Preferences::AiModelProfile profile;
552 profile.name =
"default";
553 profile.model = prefs->ai_model;
554 profile.temperature = prefs->ai_temperature;
555 profile.top_p = 0.95f;
556 profile.max_output_tokens = prefs->ai_max_tokens;
557 profile.supports_tools =
true;
558 prefs->ai_profiles.push_back(profile);
559 prefs->active_ai_profile = profile.name;
563void EnsureDefaultFilesystemRoots(UserSettings::Preferences* prefs) {
568 auto add_unique_root = [&](
const std::filesystem::path& path) {
572 const std::string path_str = path.string();
573 auto it = std::find(prefs->project_root_paths.begin(),
574 prefs->project_root_paths.end(), path_str);
575 if (it == prefs->project_root_paths.end()) {
576 prefs->project_root_paths.push_back(path_str);
582 add_unique_root(*docs_dir);
585 if (prefs->use_icloud_sync) {
588 if (icloud_dir.ok()) {
589 add_unique_root(*icloud_dir);
590 if (prefs->default_project_root.empty()) {
591 prefs->default_project_root = icloud_dir->string();
596 if (prefs->default_project_root.empty() &&
597 !prefs->project_root_paths.empty()) {
598 prefs->default_project_root = prefs->project_root_paths.front();
602void EnsureDefaultModelPaths(UserSettings::Preferences* prefs) {
606 if (!prefs->ai_model_paths.empty()) {
610 auto add_unique_path = [&](
const std::filesystem::path& path) {
614 const std::string path_str = path.string();
615 auto it = std::find(prefs->ai_model_paths.begin(),
616 prefs->ai_model_paths.end(), path_str);
617 if (it == prefs->ai_model_paths.end()) {
618 prefs->ai_model_paths.push_back(path_str);
623 if (!home_dir.empty() && home_dir !=
".") {
624 add_unique_path(home_dir /
"models");
625 add_unique_path(home_dir /
".lmstudio" /
"models");
626 add_unique_path(home_dir /
".ollama" /
"models");
630void LoadStringMap(
const json& src,
631 std::unordered_map<std::string, std::string>* target) {
632 if (!target || !src.is_object()) {
636 for (
const auto& [key, value] : src.items()) {
637 if (value.is_string()) {
638 (*target)[
key] = value.get<std::string>();
643void LoadBoolMap(
const json& src,
644 std::unordered_map<std::string, bool>* target) {
645 if (!target || !src.is_object()) {
649 for (
const auto& [key, value] : src.items()) {
650 if (value.is_boolean()) {
651 (*target)[
key] = value.get<
bool>();
656void LoadFloatMap(
const json& src,
657 std::unordered_map<std::string, float>* target) {
658 if (!target || !src.is_object()) {
662 for (
const auto& [key, value] : src.items()) {
663 if (value.is_number()) {
664 (*target)[
key] = value.get<
float>();
669void LoadNestedBoolMap(
671 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
673 if (!target || !src.is_object()) {
677 for (
const auto& [outer_key, outer_val] : src.items()) {
678 if (!outer_val.is_object()) {
681 auto& inner = (*target)[outer_key];
683 for (
const auto& [inner_key, inner_val] : outer_val.items()) {
684 if (inner_val.is_boolean()) {
685 inner[inner_key] = inner_val.get<
bool>();
691json ToStringMap(
const std::unordered_map<std::string, std::string>& map) {
692 json obj = json::object();
693 for (
const auto& [key, value] : map) {
699json ToBoolMap(
const std::unordered_map<std::string, bool>& map) {
700 json obj = json::object();
701 for (
const auto& [key, value] : map) {
707json ToFloatMap(
const std::unordered_map<std::string, float>& map) {
708 json obj = json::object();
709 for (
const auto& [key, value] : map) {
715json ToNestedBoolMap(
const std::unordered_map<
716 std::string, std::unordered_map<std::string, bool>>& map) {
717 json obj = json::object();
718 for (
const auto& [outer_key, inner] : map) {
719 obj[outer_key] = ToBoolMap(inner);
724absl::Status LoadPreferencesFromJson(
const std::filesystem::path& path,
725 UserSettings::Preferences* prefs) {
727 return absl::InvalidArgumentError(
"prefs is null");
730 std::ifstream file(path);
731 if (!file.is_open()) {
732 return absl::NotFoundError(
733 absl::StrFormat(
"Settings file not found: %s", path.string()));
739 }
catch (
const std::exception& e) {
740 return absl::InternalError(
741 absl::StrFormat(
"Failed to parse settings.json: %s", e.what()));
744 if (root.contains(
"general")) {
745 const auto& g = root[
"general"];
746 prefs->font_global_scale =
747 g.value(
"font_global_scale", prefs->font_global_scale);
748 prefs->backup_rom = g.value(
"backup_rom", prefs->backup_rom);
749 prefs->save_new_auto = g.value(
"save_new_auto", prefs->save_new_auto);
750 prefs->autosave_enabled =
751 g.value(
"autosave_enabled", prefs->autosave_enabled);
752 prefs->autosave_interval =
753 g.value(
"autosave_interval", prefs->autosave_interval);
754 prefs->recent_files_limit =
755 g.value(
"recent_files_limit", prefs->recent_files_limit);
756 prefs->last_rom_path = g.value(
"last_rom_path", prefs->last_rom_path);
757 prefs->last_project_path =
758 g.value(
"last_project_path", prefs->last_project_path);
759 prefs->show_welcome_on_startup =
760 g.value(
"show_welcome_on_startup", prefs->show_welcome_on_startup);
761 prefs->restore_last_session =
762 g.value(
"restore_last_session", prefs->restore_last_session);
763 prefs->prefer_hmagic_sprite_names = g.value(
764 "prefer_hmagic_sprite_names", prefs->prefer_hmagic_sprite_names);
765 prefs->welcome_triforce_alpha =
766 g.value(
"welcome_triforce_alpha", prefs->welcome_triforce_alpha);
767 prefs->welcome_triforce_speed =
768 g.value(
"welcome_triforce_speed", prefs->welcome_triforce_speed);
769 prefs->welcome_triforce_size =
770 g.value(
"welcome_triforce_size", prefs->welcome_triforce_size);
771 prefs->welcome_particles_enabled =
772 g.value(
"welcome_particles_enabled", prefs->welcome_particles_enabled);
773 prefs->welcome_mouse_repel_enabled = g.value(
774 "welcome_mouse_repel_enabled", prefs->welcome_mouse_repel_enabled);
777 if (root.contains(
"appearance")) {
778 const auto& appearance = root[
"appearance"];
779 prefs->reduced_motion =
780 appearance.value(
"reduced_motion", prefs->reduced_motion);
781 prefs->switch_motion_profile =
782 appearance.value(
"switch_motion_profile", prefs->switch_motion_profile);
783 prefs->last_theme_name =
784 appearance.value(
"last_theme_name", prefs->last_theme_name);
785 prefs->language_locale =
786 appearance.value(
"language_locale", prefs->language_locale);
787 prefs->font_family_index =
788 appearance.value(
"font_family_index", prefs->font_family_index);
791 if (root.contains(
"editor")) {
792 const auto& e = root[
"editor"];
793 prefs->backup_before_save =
794 e.value(
"backup_before_save", prefs->backup_before_save);
795 prefs->default_editor = e.value(
"default_editor", prefs->default_editor);
798 e.value(
"dungeon_inspector_side", prefs->dungeon_inspector_side);
799 prefs->dungeon_inspector_side = (side ==
"left") ?
"left" :
"right";
803 if (root.contains(
"performance")) {
804 const auto& p = root[
"performance"];
805 prefs->vsync = p.value(
"vsync", prefs->vsync);
806 prefs->target_fps = p.value(
"target_fps", prefs->target_fps);
807 prefs->cache_size_mb = p.value(
"cache_size_mb", prefs->cache_size_mb);
808 prefs->undo_history_size =
809 p.value(
"undo_history_size", prefs->undo_history_size);
812 if (root.contains(
"ai")) {
813 const auto& ai = root[
"ai"];
814 prefs->ai_provider = ai.value(
"provider", prefs->ai_provider);
815 prefs->ai_model = ai.value(
"model", prefs->ai_model);
816 prefs->ollama_url = ai.value(
"ollama_url", prefs->ollama_url);
817 prefs->gemini_api_key = ai.value(
"gemini_api_key", prefs->gemini_api_key);
818 prefs->openai_api_key = ai.value(
"openai_api_key", prefs->openai_api_key);
819 prefs->anthropic_api_key =
820 ai.value(
"anthropic_api_key", prefs->anthropic_api_key);
821 std::string google_key = ai.value(
"google_api_key", std::string());
822 if (prefs->gemini_api_key.empty() && !google_key.empty()) {
823 prefs->gemini_api_key = google_key;
825 prefs->ai_temperature = ai.value(
"temperature", prefs->ai_temperature);
826 prefs->ai_max_tokens = ai.value(
"max_tokens", prefs->ai_max_tokens);
827 prefs->ai_proactive = ai.value(
"proactive", prefs->ai_proactive);
828 prefs->ai_auto_learn = ai.value(
"auto_learn", prefs->ai_auto_learn);
829 prefs->ai_multimodal = ai.value(
"multimodal", prefs->ai_multimodal);
830 prefs->active_ai_host_id =
831 ai.value(
"active_host_id", prefs->active_ai_host_id);
832 prefs->active_ai_profile =
833 ai.value(
"active_profile", prefs->active_ai_profile);
834 prefs->remote_build_host_id =
835 ai.value(
"remote_build_host_id", prefs->remote_build_host_id);
836 if (ai.contains(
"model_paths") && ai[
"model_paths"].is_array()) {
837 prefs->ai_model_paths.clear();
838 for (
const auto& item : ai[
"model_paths"]) {
839 if (item.is_string()) {
840 prefs->ai_model_paths.push_back(item.get<std::string>());
845 if (ai.contains(
"hosts") && ai[
"hosts"].is_array()) {
846 prefs->ai_hosts.clear();
847 for (
const auto& host : ai[
"hosts"]) {
848 if (!host.is_object()) {
851 UserSettings::Preferences::AiHost entry;
852 entry.id = host.value(
"id",
"");
853 entry.label = host.value(
"label",
"");
854 entry.base_url = host.value(
"base_url",
"");
855 entry.api_type = host.value(
"api_type",
"");
856 entry.supports_vision =
857 host.value(
"supports_vision", entry.supports_vision);
858 entry.supports_tools =
859 host.value(
"supports_tools", entry.supports_tools);
860 entry.supports_streaming =
861 host.value(
"supports_streaming", entry.supports_streaming);
862 entry.allow_insecure =
863 host.value(
"allow_insecure", entry.allow_insecure);
864 entry.api_key = host.value(
"api_key",
"");
865 entry.credential_id = host.value(
"credential_id",
"");
866 prefs->ai_hosts.push_back(entry);
870 if (ai.contains(
"profiles") && ai[
"profiles"].is_array()) {
871 prefs->ai_profiles.clear();
872 for (
const auto& profile : ai[
"profiles"]) {
873 if (!profile.is_object()) {
876 UserSettings::Preferences::AiModelProfile entry;
877 entry.name = profile.value(
"name",
"");
878 entry.model = profile.value(
"model",
"");
879 entry.temperature = profile.value(
"temperature", entry.temperature);
880 entry.top_p = profile.value(
"top_p", entry.top_p);
881 entry.max_output_tokens =
882 profile.value(
"max_output_tokens", entry.max_output_tokens);
883 entry.supports_vision =
884 profile.value(
"supports_vision", entry.supports_vision);
885 entry.supports_tools =
886 profile.value(
"supports_tools", entry.supports_tools);
887 prefs->ai_profiles.push_back(entry);
892 if (root.contains(
"logging")) {
893 const auto& log = root[
"logging"];
894 prefs->log_level = log.value(
"level", prefs->log_level);
895 prefs->log_to_file = log.value(
"to_file", prefs->log_to_file);
896 prefs->log_file_path = log.value(
"file_path", prefs->log_file_path);
897 prefs->log_ai_requests = log.value(
"ai_requests", prefs->log_ai_requests);
898 prefs->log_rom_operations =
899 log.value(
"rom_operations", prefs->log_rom_operations);
900 prefs->log_gui_automation =
901 log.value(
"gui_automation", prefs->log_gui_automation);
902 prefs->log_proposals = log.value(
"proposals", prefs->log_proposals);
905 if (root.contains(
"shortcuts")) {
906 const auto& shortcuts = root[
"shortcuts"];
907 if (shortcuts.contains(
"panel")) {
908 LoadStringMap(shortcuts[
"panel"], &prefs->panel_shortcuts);
910 if (shortcuts.contains(
"global")) {
911 LoadStringMap(shortcuts[
"global"], &prefs->global_shortcuts);
913 if (shortcuts.contains(
"editor")) {
914 LoadStringMap(shortcuts[
"editor"], &prefs->editor_shortcuts);
918 if (root.contains(
"sidebar")) {
919 const auto& sidebar = root[
"sidebar"];
920 prefs->sidebar_visible = sidebar.value(
"visible", prefs->sidebar_visible);
921 prefs->sidebar_panel_expanded =
922 sidebar.value(
"panel_expanded", prefs->sidebar_panel_expanded);
923 prefs->sidebar_panel_width =
924 sidebar.value(
"panel_width", prefs->sidebar_panel_width);
925 prefs->panel_browser_category_width = sidebar.value(
926 "panel_browser_category_width", prefs->panel_browser_category_width);
927 prefs->sidebar_active_category =
928 sidebar.value(
"active_category", prefs->sidebar_active_category);
930 if (sidebar.contains(
"order") && sidebar[
"order"].is_array()) {
931 prefs->sidebar_order.clear();
932 for (
const auto& item : sidebar[
"order"]) {
933 if (item.is_string()) {
934 prefs->sidebar_order.push_back(item.get<std::string>());
938 if (sidebar.contains(
"hidden") && sidebar[
"hidden"].is_array()) {
939 prefs->sidebar_hidden.clear();
940 for (
const auto& item : sidebar[
"hidden"]) {
941 if (item.is_string()) {
942 prefs->sidebar_hidden.insert(item.get<std::string>());
946 if (sidebar.contains(
"pinned") && sidebar[
"pinned"].is_array()) {
947 prefs->sidebar_pinned.clear();
948 for (
const auto& item : sidebar[
"pinned"]) {
949 if (item.is_string()) {
950 prefs->sidebar_pinned.insert(item.get<std::string>());
956 if (root.contains(
"status_bar")) {
957 const auto& status_bar = root[
"status_bar"];
958 prefs->show_status_bar =
959 status_bar.value(
"visible", prefs->show_status_bar);
962 if (root.contains(
"layouts")) {
963 const auto& layouts = root[
"layouts"];
964 prefs->panel_layout_defaults_revision = layouts.value(
965 "defaults_revision", prefs->panel_layout_defaults_revision);
966 if (layouts.contains(
"panel_visibility")) {
967 LoadNestedBoolMap(layouts[
"panel_visibility"],
968 &prefs->panel_visibility_state);
970 if (layouts.contains(
"pinned_panels")) {
971 LoadBoolMap(layouts[
"pinned_panels"], &prefs->pinned_panels);
973 if (layouts.contains(
"right_panel_widths")) {
974 LoadFloatMap(layouts[
"right_panel_widths"], &prefs->right_panel_widths);
976 if (layouts.contains(
"saved_layouts")) {
977 LoadNestedBoolMap(layouts[
"saved_layouts"], &prefs->saved_layouts);
979 if (layouts.contains(
"named_layouts") &&
980 layouts[
"named_layouts"].is_object()) {
981 prefs->named_layouts.clear();
982 for (
auto it = layouts[
"named_layouts"].begin();
983 it != layouts[
"named_layouts"].end(); ++it) {
984 if (it.value().is_string()) {
985 prefs->named_layouts[it.key()] = it.value().get<std::string>();
986 }
else if (it.value().is_object() || it.value().is_array()) {
989 prefs->named_layouts[it.key()] = it.value().dump();
993 prefs->last_applied_layout_name = layouts.value(
994 "last_applied_layout_name", prefs->last_applied_layout_name);
997 if (root.contains(
"filesystem")) {
998 const auto& fs = root[
"filesystem"];
999 if (fs.contains(
"project_root_paths") &&
1000 fs[
"project_root_paths"].is_array()) {
1001 prefs->project_root_paths.clear();
1002 for (
const auto& item : fs[
"project_root_paths"]) {
1003 if (item.is_string()) {
1004 prefs->project_root_paths.push_back(item.get<std::string>());
1008 prefs->default_project_root =
1009 fs.value(
"default_project_root", prefs->default_project_root);
1010 prefs->use_files_app = fs.value(
"use_files_app", prefs->use_files_app);
1011 prefs->use_icloud_sync =
1012 fs.value(
"use_icloud_sync", prefs->use_icloud_sync);
1015 EnsureDefaultAiHosts(prefs);
1016 EnsureDefaultAiProfiles(prefs);
1017 EnsureDefaultFilesystemRoots(prefs);
1019 return absl::OkStatus();
1022absl::Status SavePreferencesToJson(
const std::filesystem::path& path,
1023 const UserSettings::Preferences& prefs) {
1025 if (!ensure_status.ok()) {
1026 return ensure_status;
1030 root[
"version"] = 1;
1032 {
"font_global_scale", prefs.font_global_scale},
1033 {
"backup_rom", prefs.backup_rom},
1034 {
"save_new_auto", prefs.save_new_auto},
1035 {
"autosave_enabled", prefs.autosave_enabled},
1036 {
"autosave_interval", prefs.autosave_interval},
1037 {
"recent_files_limit", prefs.recent_files_limit},
1038 {
"last_rom_path", prefs.last_rom_path},
1039 {
"last_project_path", prefs.last_project_path},
1040 {
"show_welcome_on_startup", prefs.show_welcome_on_startup},
1041 {
"restore_last_session", prefs.restore_last_session},
1042 {
"prefer_hmagic_sprite_names", prefs.prefer_hmagic_sprite_names},
1043 {
"welcome_triforce_alpha", prefs.welcome_triforce_alpha},
1044 {
"welcome_triforce_speed", prefs.welcome_triforce_speed},
1045 {
"welcome_triforce_size", prefs.welcome_triforce_size},
1046 {
"welcome_particles_enabled", prefs.welcome_particles_enabled},
1047 {
"welcome_mouse_repel_enabled", prefs.welcome_mouse_repel_enabled},
1050 root[
"appearance"] = {
1051 {
"reduced_motion", prefs.reduced_motion},
1052 {
"switch_motion_profile", prefs.switch_motion_profile},
1053 {
"last_theme_name", prefs.last_theme_name},
1054 {
"language_locale", prefs.language_locale},
1055 {
"font_family_index", prefs.font_family_index},
1059 {
"backup_before_save", prefs.backup_before_save},
1060 {
"default_editor", prefs.default_editor},
1061 {
"dungeon_inspector_side", prefs.dungeon_inspector_side},
1064 root[
"performance"] = {
1065 {
"vsync", prefs.vsync},
1066 {
"target_fps", prefs.target_fps},
1067 {
"cache_size_mb", prefs.cache_size_mb},
1068 {
"undo_history_size", prefs.undo_history_size},
1071 json ai_hosts = json::array();
1072 for (
const auto& host : prefs.ai_hosts) {
1073 ai_hosts.push_back({
1075 {
"label", host.label},
1076 {
"base_url", host.base_url},
1077 {
"api_type", host.api_type},
1078 {
"supports_vision", host.supports_vision},
1079 {
"supports_tools", host.supports_tools},
1080 {
"supports_streaming", host.supports_streaming},
1081 {
"allow_insecure", host.allow_insecure},
1082 {
"api_key", host.api_key},
1083 {
"credential_id", host.credential_id},
1087 json ai_profiles = json::array();
1088 for (
const auto& profile : prefs.ai_profiles) {
1089 ai_profiles.push_back({
1090 {
"name", profile.name},
1091 {
"model", profile.model},
1092 {
"temperature", profile.temperature},
1093 {
"top_p", profile.top_p},
1094 {
"max_output_tokens", profile.max_output_tokens},
1095 {
"supports_vision", profile.supports_vision},
1096 {
"supports_tools", profile.supports_tools},
1101 {
"provider", prefs.ai_provider},
1102 {
"model", prefs.ai_model},
1103 {
"ollama_url", prefs.ollama_url},
1104 {
"gemini_api_key", prefs.gemini_api_key},
1105 {
"google_api_key", prefs.gemini_api_key},
1106 {
"openai_api_key", prefs.openai_api_key},
1107 {
"anthropic_api_key", prefs.anthropic_api_key},
1108 {
"temperature", prefs.ai_temperature},
1109 {
"max_tokens", prefs.ai_max_tokens},
1110 {
"proactive", prefs.ai_proactive},
1111 {
"auto_learn", prefs.ai_auto_learn},
1112 {
"multimodal", prefs.ai_multimodal},
1113 {
"hosts", ai_hosts},
1114 {
"active_host_id", prefs.active_ai_host_id},
1115 {
"profiles", ai_profiles},
1116 {
"active_profile", prefs.active_ai_profile},
1117 {
"remote_build_host_id", prefs.remote_build_host_id},
1118 {
"model_paths", prefs.ai_model_paths},
1122 {
"level", prefs.log_level},
1123 {
"to_file", prefs.log_to_file},
1124 {
"file_path", prefs.log_file_path},
1125 {
"ai_requests", prefs.log_ai_requests},
1126 {
"rom_operations", prefs.log_rom_operations},
1127 {
"gui_automation", prefs.log_gui_automation},
1128 {
"proposals", prefs.log_proposals},
1131 root[
"shortcuts"] = {
1132 {
"panel", ToStringMap(prefs.panel_shortcuts)},
1133 {
"global", ToStringMap(prefs.global_shortcuts)},
1134 {
"editor", ToStringMap(prefs.editor_shortcuts)},
1137 auto set_to_sorted_vec =
1138 [](
const std::unordered_set<std::string>& s) -> std::vector<std::string> {
1139 std::vector<std::string> v(s.begin(), s.end());
1140 std::sort(v.begin(), v.end());
1145 {
"visible", prefs.sidebar_visible},
1146 {
"panel_expanded", prefs.sidebar_panel_expanded},
1147 {
"panel_width", prefs.sidebar_panel_width},
1148 {
"panel_browser_category_width", prefs.panel_browser_category_width},
1149 {
"active_category", prefs.sidebar_active_category},
1150 {
"order", prefs.sidebar_order},
1151 {
"hidden", set_to_sorted_vec(prefs.sidebar_hidden)},
1152 {
"pinned", set_to_sorted_vec(prefs.sidebar_pinned)},
1155 root[
"status_bar"] = {
1156 {
"visible", prefs.show_status_bar},
1162 nlohmann::json named_layouts_json = nlohmann::json::object();
1163 for (
const auto& [layout_name, json_body] : prefs.named_layouts) {
1164 nlohmann::json parsed = nlohmann::json::parse(json_body,
nullptr,
false);
1165 if (parsed.is_discarded() || !parsed.is_object()) {
1166 named_layouts_json[layout_name] = json_body;
1168 named_layouts_json[layout_name] = std::move(parsed);
1173 {
"defaults_revision", prefs.panel_layout_defaults_revision},
1174 {
"panel_visibility", ToNestedBoolMap(prefs.panel_visibility_state)},
1175 {
"pinned_panels", ToBoolMap(prefs.pinned_panels)},
1176 {
"right_panel_widths", ToFloatMap(prefs.right_panel_widths)},
1177 {
"saved_layouts", ToNestedBoolMap(prefs.saved_layouts)},
1178 {
"named_layouts", std::move(named_layouts_json)},
1179 {
"last_applied_layout_name", prefs.last_applied_layout_name},
1182 root[
"filesystem"] = {
1183 {
"project_root_paths", prefs.project_root_paths},
1184 {
"default_project_root", prefs.default_project_root},
1185 {
"use_files_app", prefs.use_files_app},
1186 {
"use_icloud_sync", prefs.use_icloud_sync},
1189 std::ofstream file(path);
1190 if (!file.is_open()) {
1191 return absl::InternalError(
1192 absl::StrFormat(
"Failed to open settings file: %s", path.string()));
1195 file << root.dump(2) <<
"\n";
1196 return absl::OkStatus();