yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
user_settings.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11#include "util/file_util.h"
12#include "util/log.h"
13#include "util/platform_paths.h"
14
15#ifdef YAZE_WITH_JSON
16#include "nlohmann/json.hpp"
17#endif
18
19namespace yaze {
20namespace editor {
21
22#ifdef YAZE_WITH_JSON
23using json = nlohmann::json;
24#endif
25
26namespace {
27
28absl::Status EnsureParentDirectory(const std::filesystem::path& path) {
29 auto parent = path.parent_path();
30 if (parent.empty()) {
31 return absl::OkStatus();
32 }
34}
35
36absl::Status LoadPreferencesFromIni(const std::filesystem::path& path,
38 if (!prefs) {
39 return absl::InvalidArgumentError("prefs is null");
40 }
41
42 auto data = util::LoadFile(path.string());
43 if (data.empty()) {
44 return absl::OkStatus();
45 }
46
47 std::istringstream ss(data);
48 std::string line;
49 while (std::getline(ss, line)) {
50 size_t eq_pos = line.find('=');
51 if (eq_pos == std::string::npos) {
52 continue;
53 }
54
55 std::string key = line.substr(0, eq_pos);
56 std::string val = line.substr(eq_pos + 1);
57
58 // General
59 if (key == "font_global_scale") {
60 prefs->font_global_scale = std::stof(val);
61 } else if (key == "backup_rom") {
62 prefs->backup_rom = (val == "1");
63 } else if (key == "save_new_auto") {
64 prefs->save_new_auto = (val == "1");
65 } else if (key == "autosave_enabled") {
66 prefs->autosave_enabled = (val == "1");
67 } else if (key == "autosave_interval") {
68 prefs->autosave_interval = std::stof(val);
69 } else if (key == "recent_files_limit") {
70 prefs->recent_files_limit = std::stoi(val);
71 } else if (key == "last_rom_path") {
72 prefs->last_rom_path = val;
73 } else if (key == "last_project_path") {
74 prefs->last_project_path = val;
75 } else if (key == "show_welcome_on_startup") {
76 prefs->show_welcome_on_startup = (val == "1");
77 } else if (key == "restore_last_session") {
78 prefs->restore_last_session = (val == "1");
79 } else if (key == "prefer_hmagic_sprite_names") {
80 prefs->prefer_hmagic_sprite_names = (val == "1");
81 } else if (key == "reduced_motion") {
82 prefs->reduced_motion = (val == "1");
83 } else if (key == "switch_motion_profile") {
84 prefs->switch_motion_profile = std::stoi(val);
85 }
86 // Editor Behavior
87 else if (key == "backup_before_save") {
88 prefs->backup_before_save = (val == "1");
89 } else if (key == "default_editor") {
90 prefs->default_editor = std::stoi(val);
91 }
92 // Performance
93 else if (key == "vsync") {
94 prefs->vsync = (val == "1");
95 } else if (key == "target_fps") {
96 prefs->target_fps = std::stoi(val);
97 } else if (key == "cache_size_mb") {
98 prefs->cache_size_mb = std::stoi(val);
99 } else if (key == "undo_history_size") {
100 prefs->undo_history_size = std::stoi(val);
101 }
102 // AI Agent
103 else if (key == "ai_provider") {
104 prefs->ai_provider = std::stoi(val);
105 } else if (key == "ai_model") {
106 prefs->ai_model = val;
107 } else if (key == "ollama_url") {
108 prefs->ollama_url = val;
109 } else if (key == "gemini_api_key") {
110 prefs->gemini_api_key = val;
111 } else if (key == "openai_api_key") {
112 prefs->openai_api_key = val;
113 } else if (key == "anthropic_api_key") {
114 prefs->anthropic_api_key = val;
115 } else if (key == "ai_temperature") {
116 prefs->ai_temperature = std::stof(val);
117 } else if (key == "ai_max_tokens") {
118 prefs->ai_max_tokens = std::stoi(val);
119 } else if (key == "ai_proactive") {
120 prefs->ai_proactive = (val == "1");
121 } else if (key == "ai_auto_learn") {
122 prefs->ai_auto_learn = (val == "1");
123 } else if (key == "ai_multimodal") {
124 prefs->ai_multimodal = (val == "1");
125 }
126 // CLI Logging
127 else if (key == "log_level") {
128 prefs->log_level = std::stoi(val);
129 } else if (key == "log_to_file") {
130 prefs->log_to_file = (val == "1");
131 } else if (key == "log_file_path") {
132 prefs->log_file_path = val;
133 } else if (key == "log_ai_requests") {
134 prefs->log_ai_requests = (val == "1");
135 } else if (key == "log_rom_operations") {
136 prefs->log_rom_operations = (val == "1");
137 } else if (key == "log_gui_automation") {
138 prefs->log_gui_automation = (val == "1");
139 } else if (key == "log_proposals") {
140 prefs->log_proposals = (val == "1");
141 }
142 // Panel Shortcuts (format: panel_shortcut.panel_id=shortcut)
143 else if (key.substr(0, 15) == "panel_shortcut.") {
144 std::string panel_id = key.substr(15);
145 prefs->panel_shortcuts[panel_id] = val;
146 }
147 // Backward compatibility for card_shortcut
148 else if (key.substr(0, 14) == "card_shortcut.") {
149 std::string panel_id = key.substr(14);
150 prefs->panel_shortcuts[panel_id] = val;
151 }
152 // Sidebar State
153 else if (key == "sidebar_visible") {
154 prefs->sidebar_visible = (val == "1");
155 } else if (key == "sidebar_panel_expanded") {
156 prefs->sidebar_panel_expanded = (val == "1");
157 } else if (key == "sidebar_panel_width") {
158 prefs->sidebar_panel_width = std::stof(val);
159 } else if (key == "panel_browser_category_width") {
160 prefs->panel_browser_category_width = std::stof(val);
161 } else if (key == "panel_layout_defaults_revision") {
162 prefs->panel_layout_defaults_revision = std::stoi(val);
163 } else if (key == "sidebar_active_category") {
164 prefs->sidebar_active_category = val;
165 }
166 // Status Bar
167 else if (key == "show_status_bar") {
168 prefs->show_status_bar = (val == "1");
169 }
170 // Panel Visibility State (format: panel_visibility.EditorType.panel_id=1)
171 else if (key.substr(0, 17) == "panel_visibility.") {
172 std::string rest = key.substr(17);
173 size_t dot_pos = rest.find('.');
174 if (dot_pos != std::string::npos) {
175 std::string editor_type = rest.substr(0, dot_pos);
176 std::string panel_id = rest.substr(dot_pos + 1);
177 prefs->panel_visibility_state[editor_type][panel_id] = (val == "1");
178 }
179 }
180 // Pinned Panels (format: pinned_panel.panel_id=1)
181 else if (key.substr(0, 13) == "pinned_panel.") {
182 std::string panel_id = key.substr(13);
183 prefs->pinned_panels[panel_id] = (val == "1");
184 }
185 // Right panel widths (format: right_panel_width.panel_key=420.0)
186 else if (key.substr(0, 18) == "right_panel_width.") {
187 std::string panel_key = key.substr(18);
188 prefs->right_panel_widths[panel_key] = std::stof(val);
189 }
190 // Saved Layouts (format: saved_layout.LayoutName.panel_id=1)
191 else if (key.substr(0, 13) == "saved_layout.") {
192 std::string rest = key.substr(13);
193 size_t dot_pos = rest.find('.');
194 if (dot_pos != std::string::npos) {
195 std::string layout_name = rest.substr(0, dot_pos);
196 std::string panel_id = rest.substr(dot_pos + 1);
197 prefs->saved_layouts[layout_name][panel_id] = (val == "1");
198 }
199 }
200 }
201
202 return absl::OkStatus();
203}
204
205absl::Status SavePreferencesToIni(const std::filesystem::path& path,
206 const UserSettings::Preferences& prefs) {
207 auto ensure_status = EnsureParentDirectory(path);
208 if (!ensure_status.ok()) {
209 return ensure_status;
210 }
211
212 std::ostringstream ss;
213 // General
214 ss << "font_global_scale=" << prefs.font_global_scale << "\n";
215 ss << "backup_rom=" << (prefs.backup_rom ? 1 : 0) << "\n";
216 ss << "save_new_auto=" << (prefs.save_new_auto ? 1 : 0) << "\n";
217 ss << "autosave_enabled=" << (prefs.autosave_enabled ? 1 : 0) << "\n";
218 ss << "autosave_interval=" << prefs.autosave_interval << "\n";
219 ss << "recent_files_limit=" << prefs.recent_files_limit << "\n";
220 ss << "last_rom_path=" << prefs.last_rom_path << "\n";
221 ss << "last_project_path=" << prefs.last_project_path << "\n";
222 ss << "show_welcome_on_startup=" << (prefs.show_welcome_on_startup ? 1 : 0)
223 << "\n";
224 ss << "restore_last_session=" << (prefs.restore_last_session ? 1 : 0) << "\n";
225 ss << "prefer_hmagic_sprite_names="
226 << (prefs.prefer_hmagic_sprite_names ? 1 : 0) << "\n";
227 ss << "reduced_motion=" << (prefs.reduced_motion ? 1 : 0) << "\n";
228 ss << "switch_motion_profile=" << prefs.switch_motion_profile << "\n";
229
230 // Editor Behavior
231 ss << "backup_before_save=" << (prefs.backup_before_save ? 1 : 0) << "\n";
232 ss << "default_editor=" << prefs.default_editor << "\n";
233
234 // Performance
235 ss << "vsync=" << (prefs.vsync ? 1 : 0) << "\n";
236 ss << "target_fps=" << prefs.target_fps << "\n";
237 ss << "cache_size_mb=" << prefs.cache_size_mb << "\n";
238 ss << "undo_history_size=" << prefs.undo_history_size << "\n";
239
240 // AI Agent
241 ss << "ai_provider=" << prefs.ai_provider << "\n";
242 ss << "ai_model=" << prefs.ai_model << "\n";
243 ss << "ollama_url=" << prefs.ollama_url << "\n";
244 ss << "gemini_api_key=" << prefs.gemini_api_key << "\n";
245 ss << "openai_api_key=" << prefs.openai_api_key << "\n";
246 ss << "anthropic_api_key=" << prefs.anthropic_api_key << "\n";
247 ss << "ai_temperature=" << prefs.ai_temperature << "\n";
248 ss << "ai_max_tokens=" << prefs.ai_max_tokens << "\n";
249 ss << "ai_proactive=" << (prefs.ai_proactive ? 1 : 0) << "\n";
250 ss << "ai_auto_learn=" << (prefs.ai_auto_learn ? 1 : 0) << "\n";
251 ss << "ai_multimodal=" << (prefs.ai_multimodal ? 1 : 0) << "\n";
252
253 // CLI Logging
254 ss << "log_level=" << prefs.log_level << "\n";
255 ss << "log_to_file=" << (prefs.log_to_file ? 1 : 0) << "\n";
256 ss << "log_file_path=" << prefs.log_file_path << "\n";
257 ss << "log_ai_requests=" << (prefs.log_ai_requests ? 1 : 0) << "\n";
258 ss << "log_rom_operations=" << (prefs.log_rom_operations ? 1 : 0) << "\n";
259 ss << "log_gui_automation=" << (prefs.log_gui_automation ? 1 : 0) << "\n";
260 ss << "log_proposals=" << (prefs.log_proposals ? 1 : 0) << "\n";
261
262 // Panel Shortcuts
263 for (const auto& [panel_id, shortcut] : prefs.panel_shortcuts) {
264 ss << "panel_shortcut." << panel_id << "=" << shortcut << "\n";
265 }
266
267 // Sidebar State
268 ss << "sidebar_visible=" << (prefs.sidebar_visible ? 1 : 0) << "\n";
269 ss << "sidebar_panel_expanded=" << (prefs.sidebar_panel_expanded ? 1 : 0)
270 << "\n";
271 ss << "sidebar_panel_width=" << prefs.sidebar_panel_width << "\n";
272 ss << "panel_browser_category_width="
273 << prefs.panel_browser_category_width << "\n";
274 ss << "panel_layout_defaults_revision="
275 << prefs.panel_layout_defaults_revision << "\n";
276 ss << "sidebar_active_category=" << prefs.sidebar_active_category << "\n";
277
278 // Status Bar
279 ss << "show_status_bar=" << (prefs.show_status_bar ? 1 : 0) << "\n";
280
281 // Panel Visibility State
282 for (const auto& [editor_type, panel_state] : prefs.panel_visibility_state) {
283 for (const auto& [panel_id, visible] : panel_state) {
284 ss << "panel_visibility." << editor_type << "." << panel_id << "="
285 << (visible ? 1 : 0) << "\n";
286 }
287 }
288
289 // Pinned Panels
290 for (const auto& [panel_id, pinned] : prefs.pinned_panels) {
291 ss << "pinned_panel." << panel_id << "=" << (pinned ? 1 : 0) << "\n";
292 }
293
294 for (const auto& [panel_key, width] : prefs.right_panel_widths) {
295 ss << "right_panel_width." << panel_key << "=" << width << "\n";
296 }
297
298 // Saved Layouts
299 for (const auto& [layout_name, panel_state] : prefs.saved_layouts) {
300 for (const auto& [panel_id, visible] : panel_state) {
301 ss << "saved_layout." << layout_name << "." << panel_id << "="
302 << (visible ? 1 : 0) << "\n";
303 }
304 }
305
306 std::ofstream file(path);
307 if (!file.is_open()) {
308 return absl::InternalError(
309 absl::StrFormat("Failed to open settings file: %s", path.string()));
310 }
311 file << ss.str();
312 return absl::OkStatus();
313}
314
315#ifdef YAZE_WITH_JSON
316void EnsureDefaultAiHosts(UserSettings::Preferences* prefs) {
317 if (!prefs) {
318 return;
319 }
320
321 if (!prefs->ai_hosts.empty()) {
322 if (prefs->active_ai_host_id.empty()) {
323 prefs->active_ai_host_id = prefs->ai_hosts.front().id;
324 }
325 return;
326 }
327
328 if (!prefs->ollama_url.empty()) {
329 UserSettings::Preferences::AiHost host;
330 host.id = "ollama-local";
331 host.label = "Ollama (local)";
332 host.base_url = prefs->ollama_url;
333 host.api_type = "ollama";
334 host.supports_tools = true;
335 host.supports_streaming = true;
336 prefs->ai_hosts.push_back(host);
337 }
338
339 // Provide a local OpenAI-compatible host for LM Studio by default.
340 UserSettings::Preferences::AiHost lmstudio;
341 lmstudio.id = "lmstudio-local";
342 lmstudio.label = "LM Studio (local)";
343 lmstudio.base_url = "http://localhost:1234";
344 lmstudio.api_type = "lmstudio";
345 lmstudio.supports_tools = true;
346 lmstudio.supports_streaming = true;
347 prefs->ai_hosts.push_back(lmstudio);
348
349 if (!prefs->ai_hosts.empty() && prefs->active_ai_host_id.empty()) {
350 prefs->active_ai_host_id = prefs->ai_hosts.front().id;
351 }
352}
353
354void EnsureDefaultAiProfiles(UserSettings::Preferences* prefs) {
355 if (!prefs) {
356 return;
357 }
358 if (!prefs->ai_profiles.empty()) {
359 if (prefs->active_ai_profile.empty()) {
360 prefs->active_ai_profile = prefs->ai_profiles.front().name;
361 }
362 return;
363 }
364 if (!prefs->ai_model.empty()) {
365 UserSettings::Preferences::AiModelProfile profile;
366 profile.name = "default";
367 profile.model = prefs->ai_model;
368 profile.temperature = prefs->ai_temperature;
369 profile.top_p = 0.95f;
370 profile.max_output_tokens = prefs->ai_max_tokens;
371 profile.supports_tools = true;
372 prefs->ai_profiles.push_back(profile);
373 prefs->active_ai_profile = profile.name;
374 }
375}
376
377void EnsureDefaultFilesystemRoots(UserSettings::Preferences* prefs) {
378 if (!prefs) {
379 return;
380 }
381
382 auto add_unique_root = [&](const std::filesystem::path& path) {
383 if (path.empty()) {
384 return;
385 }
386 const std::string path_str = path.string();
387 auto it = std::find(prefs->project_root_paths.begin(),
388 prefs->project_root_paths.end(), path_str);
389 if (it == prefs->project_root_paths.end()) {
390 prefs->project_root_paths.push_back(path_str);
391 }
392 };
393
395 if (docs_dir.ok()) {
396 add_unique_root(*docs_dir);
397 }
398
399 if (prefs->use_icloud_sync) {
400 auto icloud_dir =
402 if (icloud_dir.ok()) {
403 add_unique_root(*icloud_dir);
404 if (prefs->default_project_root.empty()) {
405 prefs->default_project_root = icloud_dir->string();
406 }
407 }
408 }
409
410 if (prefs->default_project_root.empty() &&
411 !prefs->project_root_paths.empty()) {
412 prefs->default_project_root = prefs->project_root_paths.front();
413 }
414}
415
416void EnsureDefaultModelPaths(UserSettings::Preferences* prefs) {
417 if (!prefs) {
418 return;
419 }
420 if (!prefs->ai_model_paths.empty()) {
421 return;
422 }
423
424 auto add_unique_path = [&](const std::filesystem::path& path) {
425 if (path.empty()) {
426 return;
427 }
428 const std::string path_str = path.string();
429 auto it = std::find(prefs->ai_model_paths.begin(),
430 prefs->ai_model_paths.end(), path_str);
431 if (it == prefs->ai_model_paths.end()) {
432 prefs->ai_model_paths.push_back(path_str);
433 }
434 };
435
436 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
437 if (!home_dir.empty() && home_dir != ".") {
438 add_unique_path(home_dir / "models");
439 add_unique_path(home_dir / ".lmstudio" / "models");
440 add_unique_path(home_dir / ".ollama" / "models");
441 }
442}
443
444void LoadStringMap(const json& src,
445 std::unordered_map<std::string, std::string>* target) {
446 if (!target || !src.is_object()) {
447 return;
448 }
449 target->clear();
450 for (const auto& [key, value] : src.items()) {
451 if (value.is_string()) {
452 (*target)[key] = value.get<std::string>();
453 }
454 }
455}
456
457void LoadBoolMap(const json& src,
458 std::unordered_map<std::string, bool>* target) {
459 if (!target || !src.is_object()) {
460 return;
461 }
462 target->clear();
463 for (const auto& [key, value] : src.items()) {
464 if (value.is_boolean()) {
465 (*target)[key] = value.get<bool>();
466 }
467 }
468}
469
470void LoadFloatMap(const json& src,
471 std::unordered_map<std::string, float>* target) {
472 if (!target || !src.is_object()) {
473 return;
474 }
475 target->clear();
476 for (const auto& [key, value] : src.items()) {
477 if (value.is_number()) {
478 (*target)[key] = value.get<float>();
479 }
480 }
481}
482
483void LoadNestedBoolMap(
484 const json& src,
485 std::unordered_map<std::string, std::unordered_map<std::string, bool>>*
486 target) {
487 if (!target || !src.is_object()) {
488 return;
489 }
490 target->clear();
491 for (const auto& [outer_key, outer_val] : src.items()) {
492 if (!outer_val.is_object()) {
493 continue;
494 }
495 auto& inner = (*target)[outer_key];
496 inner.clear();
497 for (const auto& [inner_key, inner_val] : outer_val.items()) {
498 if (inner_val.is_boolean()) {
499 inner[inner_key] = inner_val.get<bool>();
500 }
501 }
502 }
503}
504
505json ToStringMap(const std::unordered_map<std::string, std::string>& map) {
506 json obj = json::object();
507 for (const auto& [key, value] : map) {
508 obj[key] = value;
509 }
510 return obj;
511}
512
513json ToBoolMap(const std::unordered_map<std::string, bool>& map) {
514 json obj = json::object();
515 for (const auto& [key, value] : map) {
516 obj[key] = value;
517 }
518 return obj;
519}
520
521json ToFloatMap(const std::unordered_map<std::string, float>& map) {
522 json obj = json::object();
523 for (const auto& [key, value] : map) {
524 obj[key] = value;
525 }
526 return obj;
527}
528
529json ToNestedBoolMap(const std::unordered_map<
530 std::string, std::unordered_map<std::string, bool>>& map) {
531 json obj = json::object();
532 for (const auto& [outer_key, inner] : map) {
533 obj[outer_key] = ToBoolMap(inner);
534 }
535 return obj;
536}
537
538absl::Status LoadPreferencesFromJson(const std::filesystem::path& path,
539 UserSettings::Preferences* prefs) {
540 if (!prefs) {
541 return absl::InvalidArgumentError("prefs is null");
542 }
543
544 std::ifstream file(path);
545 if (!file.is_open()) {
546 return absl::NotFoundError(
547 absl::StrFormat("Settings file not found: %s", path.string()));
548 }
549
550 json root;
551 try {
552 file >> root;
553 } catch (const std::exception& e) {
554 return absl::InternalError(
555 absl::StrFormat("Failed to parse settings.json: %s", e.what()));
556 }
557
558 if (root.contains("general")) {
559 const auto& g = root["general"];
560 prefs->font_global_scale =
561 g.value("font_global_scale", prefs->font_global_scale);
562 prefs->backup_rom = g.value("backup_rom", prefs->backup_rom);
563 prefs->save_new_auto = g.value("save_new_auto", prefs->save_new_auto);
564 prefs->autosave_enabled =
565 g.value("autosave_enabled", prefs->autosave_enabled);
566 prefs->autosave_interval =
567 g.value("autosave_interval", prefs->autosave_interval);
568 prefs->recent_files_limit =
569 g.value("recent_files_limit", prefs->recent_files_limit);
570 prefs->last_rom_path = g.value("last_rom_path", prefs->last_rom_path);
571 prefs->last_project_path =
572 g.value("last_project_path", prefs->last_project_path);
573 prefs->show_welcome_on_startup =
574 g.value("show_welcome_on_startup", prefs->show_welcome_on_startup);
575 prefs->restore_last_session =
576 g.value("restore_last_session", prefs->restore_last_session);
577 prefs->prefer_hmagic_sprite_names = g.value(
578 "prefer_hmagic_sprite_names", prefs->prefer_hmagic_sprite_names);
579 }
580
581 if (root.contains("appearance")) {
582 const auto& appearance = root["appearance"];
583 prefs->reduced_motion =
584 appearance.value("reduced_motion", prefs->reduced_motion);
585 prefs->switch_motion_profile = appearance.value(
586 "switch_motion_profile", prefs->switch_motion_profile);
587 }
588
589 if (root.contains("editor")) {
590 const auto& e = root["editor"];
591 prefs->backup_before_save =
592 e.value("backup_before_save", prefs->backup_before_save);
593 prefs->default_editor = e.value("default_editor", prefs->default_editor);
594 }
595
596 if (root.contains("performance")) {
597 const auto& p = root["performance"];
598 prefs->vsync = p.value("vsync", prefs->vsync);
599 prefs->target_fps = p.value("target_fps", prefs->target_fps);
600 prefs->cache_size_mb = p.value("cache_size_mb", prefs->cache_size_mb);
601 prefs->undo_history_size =
602 p.value("undo_history_size", prefs->undo_history_size);
603 }
604
605 if (root.contains("ai")) {
606 const auto& ai = root["ai"];
607 prefs->ai_provider = ai.value("provider", prefs->ai_provider);
608 prefs->ai_model = ai.value("model", prefs->ai_model);
609 prefs->ollama_url = ai.value("ollama_url", prefs->ollama_url);
610 prefs->gemini_api_key = ai.value("gemini_api_key", prefs->gemini_api_key);
611 prefs->openai_api_key = ai.value("openai_api_key", prefs->openai_api_key);
612 prefs->anthropic_api_key =
613 ai.value("anthropic_api_key", prefs->anthropic_api_key);
614 std::string google_key = ai.value("google_api_key", std::string());
615 if (prefs->gemini_api_key.empty() && !google_key.empty()) {
616 prefs->gemini_api_key = google_key;
617 }
618 prefs->ai_temperature = ai.value("temperature", prefs->ai_temperature);
619 prefs->ai_max_tokens = ai.value("max_tokens", prefs->ai_max_tokens);
620 prefs->ai_proactive = ai.value("proactive", prefs->ai_proactive);
621 prefs->ai_auto_learn = ai.value("auto_learn", prefs->ai_auto_learn);
622 prefs->ai_multimodal = ai.value("multimodal", prefs->ai_multimodal);
623 prefs->active_ai_host_id =
624 ai.value("active_host_id", prefs->active_ai_host_id);
625 prefs->active_ai_profile =
626 ai.value("active_profile", prefs->active_ai_profile);
627 prefs->remote_build_host_id =
628 ai.value("remote_build_host_id", prefs->remote_build_host_id);
629 if (ai.contains("model_paths") && ai["model_paths"].is_array()) {
630 prefs->ai_model_paths.clear();
631 for (const auto& item : ai["model_paths"]) {
632 if (item.is_string()) {
633 prefs->ai_model_paths.push_back(item.get<std::string>());
634 }
635 }
636 }
637
638 if (ai.contains("hosts") && ai["hosts"].is_array()) {
639 prefs->ai_hosts.clear();
640 for (const auto& host : ai["hosts"]) {
641 if (!host.is_object()) {
642 continue;
643 }
644 UserSettings::Preferences::AiHost entry;
645 entry.id = host.value("id", "");
646 entry.label = host.value("label", "");
647 entry.base_url = host.value("base_url", "");
648 entry.api_type = host.value("api_type", "");
649 entry.supports_vision =
650 host.value("supports_vision", entry.supports_vision);
651 entry.supports_tools =
652 host.value("supports_tools", entry.supports_tools);
653 entry.supports_streaming =
654 host.value("supports_streaming", entry.supports_streaming);
655 entry.allow_insecure =
656 host.value("allow_insecure", entry.allow_insecure);
657 entry.api_key = host.value("api_key", "");
658 entry.credential_id = host.value("credential_id", "");
659 prefs->ai_hosts.push_back(entry);
660 }
661 }
662
663 if (ai.contains("profiles") && ai["profiles"].is_array()) {
664 prefs->ai_profiles.clear();
665 for (const auto& profile : ai["profiles"]) {
666 if (!profile.is_object()) {
667 continue;
668 }
669 UserSettings::Preferences::AiModelProfile entry;
670 entry.name = profile.value("name", "");
671 entry.model = profile.value("model", "");
672 entry.temperature = profile.value("temperature", entry.temperature);
673 entry.top_p = profile.value("top_p", entry.top_p);
674 entry.max_output_tokens =
675 profile.value("max_output_tokens", entry.max_output_tokens);
676 entry.supports_vision =
677 profile.value("supports_vision", entry.supports_vision);
678 entry.supports_tools =
679 profile.value("supports_tools", entry.supports_tools);
680 prefs->ai_profiles.push_back(entry);
681 }
682 }
683 }
684
685 if (root.contains("logging")) {
686 const auto& log = root["logging"];
687 prefs->log_level = log.value("level", prefs->log_level);
688 prefs->log_to_file = log.value("to_file", prefs->log_to_file);
689 prefs->log_file_path = log.value("file_path", prefs->log_file_path);
690 prefs->log_ai_requests = log.value("ai_requests", prefs->log_ai_requests);
691 prefs->log_rom_operations =
692 log.value("rom_operations", prefs->log_rom_operations);
693 prefs->log_gui_automation =
694 log.value("gui_automation", prefs->log_gui_automation);
695 prefs->log_proposals = log.value("proposals", prefs->log_proposals);
696 }
697
698 if (root.contains("shortcuts")) {
699 const auto& shortcuts = root["shortcuts"];
700 if (shortcuts.contains("panel")) {
701 LoadStringMap(shortcuts["panel"], &prefs->panel_shortcuts);
702 }
703 if (shortcuts.contains("global")) {
704 LoadStringMap(shortcuts["global"], &prefs->global_shortcuts);
705 }
706 if (shortcuts.contains("editor")) {
707 LoadStringMap(shortcuts["editor"], &prefs->editor_shortcuts);
708 }
709 }
710
711 if (root.contains("sidebar")) {
712 const auto& sidebar = root["sidebar"];
713 prefs->sidebar_visible = sidebar.value("visible", prefs->sidebar_visible);
714 prefs->sidebar_panel_expanded =
715 sidebar.value("panel_expanded", prefs->sidebar_panel_expanded);
716 prefs->sidebar_panel_width =
717 sidebar.value("panel_width", prefs->sidebar_panel_width);
718 prefs->panel_browser_category_width = sidebar.value(
719 "panel_browser_category_width", prefs->panel_browser_category_width);
720 prefs->sidebar_active_category =
721 sidebar.value("active_category", prefs->sidebar_active_category);
722 }
723
724 if (root.contains("status_bar")) {
725 const auto& status_bar = root["status_bar"];
726 prefs->show_status_bar =
727 status_bar.value("visible", prefs->show_status_bar);
728 }
729
730 if (root.contains("layouts")) {
731 const auto& layouts = root["layouts"];
732 prefs->panel_layout_defaults_revision =
733 layouts.value("defaults_revision", prefs->panel_layout_defaults_revision);
734 if (layouts.contains("panel_visibility")) {
735 LoadNestedBoolMap(layouts["panel_visibility"],
736 &prefs->panel_visibility_state);
737 }
738 if (layouts.contains("pinned_panels")) {
739 LoadBoolMap(layouts["pinned_panels"], &prefs->pinned_panels);
740 }
741 if (layouts.contains("right_panel_widths")) {
742 LoadFloatMap(layouts["right_panel_widths"], &prefs->right_panel_widths);
743 }
744 if (layouts.contains("saved_layouts")) {
745 LoadNestedBoolMap(layouts["saved_layouts"], &prefs->saved_layouts);
746 }
747 }
748
749 if (root.contains("filesystem")) {
750 const auto& fs = root["filesystem"];
751 if (fs.contains("project_root_paths") &&
752 fs["project_root_paths"].is_array()) {
753 prefs->project_root_paths.clear();
754 for (const auto& item : fs["project_root_paths"]) {
755 if (item.is_string()) {
756 prefs->project_root_paths.push_back(item.get<std::string>());
757 }
758 }
759 }
760 prefs->default_project_root =
761 fs.value("default_project_root", prefs->default_project_root);
762 prefs->use_files_app = fs.value("use_files_app", prefs->use_files_app);
763 prefs->use_icloud_sync =
764 fs.value("use_icloud_sync", prefs->use_icloud_sync);
765 }
766
767 EnsureDefaultAiHosts(prefs);
768 EnsureDefaultAiProfiles(prefs);
769 EnsureDefaultFilesystemRoots(prefs);
770
771 return absl::OkStatus();
772}
773
774absl::Status SavePreferencesToJson(const std::filesystem::path& path,
775 const UserSettings::Preferences& prefs) {
776 auto ensure_status = EnsureParentDirectory(path);
777 if (!ensure_status.ok()) {
778 return ensure_status;
779 }
780
781 json root;
782 root["version"] = 1;
783 root["general"] = {
784 {"font_global_scale", prefs.font_global_scale},
785 {"backup_rom", prefs.backup_rom},
786 {"save_new_auto", prefs.save_new_auto},
787 {"autosave_enabled", prefs.autosave_enabled},
788 {"autosave_interval", prefs.autosave_interval},
789 {"recent_files_limit", prefs.recent_files_limit},
790 {"last_rom_path", prefs.last_rom_path},
791 {"last_project_path", prefs.last_project_path},
792 {"show_welcome_on_startup", prefs.show_welcome_on_startup},
793 {"restore_last_session", prefs.restore_last_session},
794 {"prefer_hmagic_sprite_names", prefs.prefer_hmagic_sprite_names},
795 };
796
797 root["appearance"] = {
798 {"reduced_motion", prefs.reduced_motion},
799 {"switch_motion_profile", prefs.switch_motion_profile},
800 };
801
802 root["editor"] = {
803 {"backup_before_save", prefs.backup_before_save},
804 {"default_editor", prefs.default_editor},
805 };
806
807 root["performance"] = {
808 {"vsync", prefs.vsync},
809 {"target_fps", prefs.target_fps},
810 {"cache_size_mb", prefs.cache_size_mb},
811 {"undo_history_size", prefs.undo_history_size},
812 };
813
814 json ai_hosts = json::array();
815 for (const auto& host : prefs.ai_hosts) {
816 ai_hosts.push_back({
817 {"id", host.id},
818 {"label", host.label},
819 {"base_url", host.base_url},
820 {"api_type", host.api_type},
821 {"supports_vision", host.supports_vision},
822 {"supports_tools", host.supports_tools},
823 {"supports_streaming", host.supports_streaming},
824 {"allow_insecure", host.allow_insecure},
825 {"api_key", host.api_key},
826 {"credential_id", host.credential_id},
827 });
828 }
829
830 json ai_profiles = json::array();
831 for (const auto& profile : prefs.ai_profiles) {
832 ai_profiles.push_back({
833 {"name", profile.name},
834 {"model", profile.model},
835 {"temperature", profile.temperature},
836 {"top_p", profile.top_p},
837 {"max_output_tokens", profile.max_output_tokens},
838 {"supports_vision", profile.supports_vision},
839 {"supports_tools", profile.supports_tools},
840 });
841 }
842
843 root["ai"] = {
844 {"provider", prefs.ai_provider},
845 {"model", prefs.ai_model},
846 {"ollama_url", prefs.ollama_url},
847 {"gemini_api_key", prefs.gemini_api_key},
848 {"google_api_key", prefs.gemini_api_key},
849 {"openai_api_key", prefs.openai_api_key},
850 {"anthropic_api_key", prefs.anthropic_api_key},
851 {"temperature", prefs.ai_temperature},
852 {"max_tokens", prefs.ai_max_tokens},
853 {"proactive", prefs.ai_proactive},
854 {"auto_learn", prefs.ai_auto_learn},
855 {"multimodal", prefs.ai_multimodal},
856 {"hosts", ai_hosts},
857 {"active_host_id", prefs.active_ai_host_id},
858 {"profiles", ai_profiles},
859 {"active_profile", prefs.active_ai_profile},
860 {"remote_build_host_id", prefs.remote_build_host_id},
861 {"model_paths", prefs.ai_model_paths},
862 };
863
864 root["logging"] = {
865 {"level", prefs.log_level},
866 {"to_file", prefs.log_to_file},
867 {"file_path", prefs.log_file_path},
868 {"ai_requests", prefs.log_ai_requests},
869 {"rom_operations", prefs.log_rom_operations},
870 {"gui_automation", prefs.log_gui_automation},
871 {"proposals", prefs.log_proposals},
872 };
873
874 root["shortcuts"] = {
875 {"panel", ToStringMap(prefs.panel_shortcuts)},
876 {"global", ToStringMap(prefs.global_shortcuts)},
877 {"editor", ToStringMap(prefs.editor_shortcuts)},
878 };
879
880 root["sidebar"] = {
881 {"visible", prefs.sidebar_visible},
882 {"panel_expanded", prefs.sidebar_panel_expanded},
883 {"panel_width", prefs.sidebar_panel_width},
884 {"panel_browser_category_width", prefs.panel_browser_category_width},
885 {"active_category", prefs.sidebar_active_category},
886 };
887
888 root["status_bar"] = {
889 {"visible", prefs.show_status_bar},
890 };
891
892 root["layouts"] = {
893 {"defaults_revision", prefs.panel_layout_defaults_revision},
894 {"panel_visibility", ToNestedBoolMap(prefs.panel_visibility_state)},
895 {"pinned_panels", ToBoolMap(prefs.pinned_panels)},
896 {"right_panel_widths", ToFloatMap(prefs.right_panel_widths)},
897 {"saved_layouts", ToNestedBoolMap(prefs.saved_layouts)},
898 };
899
900 root["filesystem"] = {
901 {"project_root_paths", prefs.project_root_paths},
902 {"default_project_root", prefs.default_project_root},
903 {"use_files_app", prefs.use_files_app},
904 {"use_icloud_sync", prefs.use_icloud_sync},
905 };
906
907 std::ofstream file(path);
908 if (!file.is_open()) {
909 return absl::InternalError(
910 absl::StrFormat("Failed to open settings file: %s", path.string()));
911 }
912
913 file << root.dump(2) << "\n";
914 return absl::OkStatus();
915}
916#endif // YAZE_WITH_JSON
917
918} // namespace
919
921 auto docs_dir_status = util::PlatformPaths::GetUserDocumentsDirectory();
922 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
923 if (docs_dir_status.ok()) {
924 settings_file_path_ = (*docs_dir_status / "settings.json").string();
925 } else if (config_dir_status.ok()) {
926 settings_file_path_ = (*config_dir_status / "settings.json").string();
927 } else {
928 LOG_WARN("UserSettings",
929 "Could not determine user documents or config directory. Using "
930 "local settings.json.");
931 settings_file_path_ = "settings.json";
932 }
933
934 if (config_dir_status.ok()) {
936 (*config_dir_status / "yaze_settings.ini").string();
937 } else {
938 legacy_settings_file_path_ = "yaze_settings.ini";
939 }
940}
941
942absl::Status UserSettings::Load() {
943 try {
944 bool loaded = false;
945#ifdef YAZE_WITH_JSON
947 if (json_exists) {
948 auto status = LoadPreferencesFromJson(settings_file_path_, &prefs_);
949 if (status.ok()) {
950 loaded = true;
951 } else {
952 LOG_WARN("UserSettings", "Failed to load settings.json: %s",
953 status.ToString().c_str());
954 }
955 }
956#endif
957
959 auto status = LoadPreferencesFromIni(legacy_settings_file_path_, &prefs_);
960 if (!status.ok()) {
961 return status;
962 }
963 loaded = true;
964#ifdef YAZE_WITH_JSON
966 (void)SavePreferencesToJson(settings_file_path_, prefs_);
967 }
968#endif
969 }
970
971 if (!loaded) {
972#if defined(__APPLE__) && \
973 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
974 prefs_.sidebar_visible = false;
976#endif
977 LOG_INFO("UserSettings", "Settings not found, creating defaults at: %s",
978 settings_file_path_.c_str());
979 return Save();
980 }
981
982#ifdef YAZE_WITH_JSON
983 EnsureDefaultAiHosts(&prefs_);
984 EnsureDefaultAiProfiles(&prefs_);
985 EnsureDefaultFilesystemRoots(&prefs_);
986 EnsureDefaultModelPaths(&prefs_);
987#endif
988
990 std::clamp(prefs_.switch_motion_profile, 0, 2);
991
992 if (ImGui::GetCurrentContext() != nullptr) {
993 ImGui::GetIO().FontGlobalScale = prefs_.font_global_scale;
994 } else {
995 LOG_WARN("UserSettings",
996 "ImGui context not available; skipping FontGlobalScale update");
997 }
998 } catch (const std::exception& e) {
999 return absl::InternalError(
1000 absl::StrFormat("Failed to load user settings: %s", e.what()));
1001 }
1002 return absl::OkStatus();
1003}
1004
1006 if (target_revision <= 0 ||
1007 prefs_.panel_layout_defaults_revision >= target_revision) {
1008 return false;
1009 }
1010
1011 prefs_.sidebar_visible = true;
1016
1018 prefs_.pinned_panels.clear();
1019 prefs_.right_panel_widths.clear();
1020 prefs_.saved_layouts.clear();
1021
1022 prefs_.panel_layout_defaults_revision = target_revision;
1023 return true;
1024}
1025
1026absl::Status UserSettings::Save() {
1027 try {
1028 absl::Status status = absl::OkStatus();
1029#ifdef YAZE_WITH_JSON
1030 status = SavePreferencesToJson(settings_file_path_, prefs_);
1031 if (!status.ok()) {
1032 return status;
1033 }
1034#endif
1035 status = SavePreferencesToIni(legacy_settings_file_path_, prefs_);
1036 if (!status.ok()) {
1037 return status;
1038 }
1039 } catch (const std::exception& e) {
1040 return absl::InternalError(
1041 absl::StrFormat("Failed to save user settings: %s", e.what()));
1042 }
1043 return absl::OkStatus();
1044}
1045
1046} // namespace editor
1047} // namespace yaze
bool ApplyPanelLayoutDefaultsRevision(int target_revision)
std::string legacy_settings_file_path_
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsSubdirectory(const std::string &subdir)
Get a subdirectory within the user documents folder.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsDirectory()
Get the user's Documents directory.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
static bool Exists(const std::filesystem::path &path)
Check if a file or directory exists.
static std::filesystem::path GetHomeDirectory()
Get the user's home directory in a cross-platform way.
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
absl::Status SavePreferencesToIni(const std::filesystem::path &path, const UserSettings::Preferences &prefs)
absl::Status LoadPreferencesFromIni(const std::filesystem::path &path, UserSettings::Preferences *prefs)
absl::Status EnsureParentDirectory(const std::filesystem::path &path)
std::string LoadFile(const std::string &filename)
Loads the entire contents of a file into a string.
Definition file_util.cc:23
std::unordered_map< std::string, std::string > panel_shortcuts
std::unordered_map< std::string, std::unordered_map< std::string, bool > > saved_layouts
std::unordered_map< std::string, float > right_panel_widths
std::unordered_map< std::string, std::unordered_map< std::string, bool > > panel_visibility_state
std::unordered_map< std::string, bool > pinned_panels