PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
LauncherConfig.cpp
Go to the documentation of this file.
1// LauncherConfig.cpp - Configuration management implementation
2// Copyright (c) 2026 Shane T. Mueller
3// Licensed under GPL
4
5#include "LauncherConfig.h"
6#include "BinReloc.h"
7#include <fstream>
8#include <sstream>
9#include <cstdlib>
10#include <cstring>
11#include <ctime>
12#include <filesystem>
13
14namespace fs = std::filesystem;
15
16#include <sys/stat.h>
17#include <sys/types.h>
18
19#ifdef _WIN32
20#include <windows.h>
21#include <shlobj.h>
22#include <direct.h>
23// Windows compatibility for stat
24#ifndef stat
25#define stat _stat
26#endif
27#ifndef S_ISDIR
28#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR)
29#endif
30#else
31#include <unistd.h>
32#include <pwd.h>
33#endif
34
36 : mExperimentDirectory("")
37 , mSubjectCode("test")
38 , mLanguage("en")
39 , mFullscreen(false)
40 , mAutoUpload(false)
41 , mUploadToken("")
42 , mUploadURL("https://peblhub.online/api/upload")
43 , mWorkspacePath("")
44 , mBatteryPath("")
45 , mPeblExecutablePath("")
46 , mDataOutputPath("")
47 , mFontSize(16)
48 , mWindowWidth(1280)
49 , mWindowHeight(720)
50 , mCurrentStudyPath("")
51 , mCurrentChainName("")
52{
53 // Set platform-specific default external editor
54#ifdef _WIN32
55 mExternalEditor = "start";
56#elif __APPLE__
57 mExternalEditor = "open";
58#else
59 mExternalEditor = "xdg-open";
60#endif
61
62 // Detect battery path using BinReloc (relative to launcher executable)
63 mBatteryPath = DetectPEBLInstallation();
64
65 // For backward compatibility, also set experiment directory to battery path
66 mExperimentDirectory = mBatteryPath;
67
68 // Detect PEBL executable path (same directory as launcher)
69#ifdef _WIN32
70 char exePath[MAX_PATH];
71 DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
72 if (len > 0 && len < MAX_PATH) {
73 std::string exePathStr(exePath);
74 size_t lastSep = exePathStr.find_last_of("/\\");
75 if (lastSep != std::string::npos) {
76 std::string exeDir = exePathStr.substr(0, lastSep);
77 mPeblExecutablePath = exeDir + "\\pebl2.exe";
78 struct stat st;
79 if (stat(mPeblExecutablePath.c_str(), &st) == 0) {
80 printf("Found PEBL executable at: %s\n", mPeblExecutablePath.c_str());
81 } else {
82 printf("Warning: PEBL executable not found at: %s\n", mPeblExecutablePath.c_str());
83 mPeblExecutablePath = "";
84 }
85 }
86 }
87#else
88 // On Linux, use BinReloc to find launcher location
89 BrInitError error;
90 if (br_init(&error) != 0) {
91 char* exeDir = br_find_exe_dir("/usr/local/bin");
92 if (exeDir) {
93 mPeblExecutablePath = std::string(exeDir) + "/pebl2";
94 free(exeDir);
95 struct stat st;
96 if (stat(mPeblExecutablePath.c_str(), &st) == 0) {
97 printf("Found PEBL executable at: %s\n", mPeblExecutablePath.c_str());
98 } else {
99 printf("Warning: PEBL executable not found at: %s\n", mPeblExecutablePath.c_str());
100 mPeblExecutablePath = "";
101 }
102 }
103 }
104#endif
105
106 // Set workspace path based on mode
107 if (IsPortableMode()) {
108 // In portable mode, workspace is the portable root directory
109 // This allows access to PEBL/battery, PEBL/demo, PEBL/tutorial, etc.
110 std::string portableRoot = GetPortableWorkspacePath();
111
112 // Convert relative path to absolute
113#ifdef _WIN32
114 char absPath[MAX_PATH];
115 if (GetFullPathNameA(portableRoot.c_str(), MAX_PATH, absPath, NULL) > 0) {
116 mWorkspacePath = absPath;
117 } else {
118 mWorkspacePath = portableRoot;
119 }
120#else
121 char* absPath = realpath(portableRoot.c_str(), nullptr);
122 if (absPath) {
123 mWorkspacePath = absPath;
124 free(absPath);
125 } else {
126 mWorkspacePath = portableRoot;
127 }
128#endif
129 printf("Portable mode: workspace set to %s\n", mWorkspacePath.c_str());
130
131 // In portable mode, data output goes to workspace/my_studies
132 if (!mWorkspacePath.empty()) {
133#ifdef _WIN32
134 mDataOutputPath = mWorkspacePath + "\\my_studies";
135#else
136 mDataOutputPath = mWorkspacePath + "/my_studies";
137#endif
138 }
139 } else {
140 // Installed mode: use Documents/pebl-exp.VERSION/
141 std::string documentsPath = GetDocumentsPath();
142 if (!documentsPath.empty()) {
143#ifdef _WIN32
144 mWorkspacePath = documentsPath + "\\pebl-exp." + PEBL_VERSION;
145#else
146 mWorkspacePath = documentsPath + "/pebl-exp." + PEBL_VERSION;
147#endif
148 }
149
150 // Set default data output path (workspace/my_studies)
151 if (!mWorkspacePath.empty()) {
152#ifdef _WIN32
153 mDataOutputPath = mWorkspacePath + "\\my_studies";
154#else
155 mDataOutputPath = mWorkspacePath + "/my_studies";
156#endif
157 }
158 }
159}
160
164
165std::string LauncherConfig::GetDocumentsPath() const
166{
167 std::string documentsPath;
168
169#ifdef _WIN32
170 // Windows: Documents folder
171 char docPath[MAX_PATH];
172 if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, 0, docPath))) {
173 documentsPath = docPath;
174 }
175#else
176 // Linux/macOS: ~/Documents or ~ if Documents doesn't exist
177 const char* homeDir = getenv("HOME");
178 if (!homeDir) {
179 struct passwd* pw = getpwuid(getuid());
180 homeDir = pw->pw_dir;
181 }
182
183 std::string docDir = std::string(homeDir) + "/Documents";
184 struct stat st;
185 if (stat(docDir.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
186 documentsPath = docDir;
187 } else {
188 // Fallback to home directory
189 documentsPath = homeDir;
190 }
191#endif
192
193 return documentsPath;
194}
195
196std::string LauncherConfig::DetectPEBLInstallation() const
197{
198 struct stat st;
199
200 // Only check for portable-style PEBL directory if explicitly in portable mode
201 if (IsPortableMode()) {
202#ifdef _WIN32
203 const char* peblBattery = "PEBL\\battery";
204 const char* parentPeblBattery = "..\\PEBL\\battery";
205#else
206 const char* peblBattery = "PEBL/battery";
207 const char* parentPeblBattery = "../PEBL/battery";
208#endif
209
210 if (stat(peblBattery, &st) == 0 && S_ISDIR(st.st_mode)) {
211 printf("Found PEBL battery in portable mode: %s\n", peblBattery);
212 fflush(stdout);
213 return peblBattery;
214 }
215
216 if (stat(parentPeblBattery, &st) == 0 && S_ISDIR(st.st_mode)) {
217 printf("Found PEBL battery in portable mode: %s\n", parentPeblBattery);
218 fflush(stdout);
219 return parentPeblBattery;
220 }
221 }
222
223#ifdef _WIN32
224 // On Windows, use GetModuleFileName to find the launcher's location
225 char exePath[MAX_PATH];
226 DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
227 if (len > 0 && len < MAX_PATH) {
228 std::string exePathStr(exePath);
229
230 // Find the directory containing the executable
231 size_t lastSep = exePathStr.find_last_of("/\\");
232 if (lastSep != std::string::npos) {
233 std::string exeDir = exePathStr.substr(0, lastSep);
234
235 // Check for battery in same directory (non-portable installed mode)
236 std::string batteryPath = exeDir + "\\battery";
237 if (stat(batteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
238 printf("Found PEBL battery at: %s\n", batteryPath.c_str());
239 return batteryPath;
240 }
241
242 // Check one level up (if launcher is in bin/ subdirectory)
243 size_t parentSep = exeDir.find_last_of("/\\");
244 if (parentSep != std::string::npos) {
245 std::string parentDir = exeDir.substr(0, parentSep);
246 batteryPath = parentDir + "\\battery";
247 if (stat(batteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
248 printf("Found PEBL battery at: %s\n", batteryPath.c_str());
249 return batteryPath;
250 }
251 }
252 }
253 }
254
255 // Fallback: Check for Windows installed mode (Program Files)
256 const char* programFiles[] = {
257 "C:\\Program Files\\pebl2\\battery",
258 "C:\\Program Files (x86)\\pebl2\\battery",
259 "C:\\Program Files\\PEBL2\\battery",
260 "C:\\Program Files (x86)\\PEBL2\\battery"
261 };
262
263 for (const char* path : programFiles) {
264 if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
265 printf("Found PEBL battery at: %s\n", path);
266 return path;
267 }
268 }
269#else
270 // On Linux, use BinReloc to find the executable location
271 BrInitError error;
272 if (br_init(&error) != 0) {
273 // BinReloc initialized successfully
274 char* prefix = br_find_prefix("/usr/local");
275 if (prefix) {
276 std::string batteryPath = std::string(prefix) + "/battery";
277 free(prefix);
278 if (stat(batteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
279 printf("Found PEBL battery at: %s\n", batteryPath.c_str());
280 return batteryPath;
281 }
282 }
283
284 // Try exe directory
285 char* exeDir = br_find_exe_dir("/usr/local/bin");
286 if (exeDir) {
287 std::string batteryPath = std::string(exeDir) + "/battery";
288 if (stat(batteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
289 free(exeDir);
290 printf("Found PEBL battery at: %s\n", batteryPath.c_str());
291 return batteryPath;
292 }
293
294 // Check one level up
295 std::string exeDirStr(exeDir);
296 free(exeDir);
297 size_t lastSep = exeDirStr.find_last_of('/');
298 if (lastSep != std::string::npos) {
299 std::string parentDir = exeDirStr.substr(0, lastSep);
300 batteryPath = parentDir + "/battery";
301 if (stat(batteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
302 printf("Found PEBL battery at: %s\n", batteryPath.c_str());
303 return batteryPath;
304 }
305 }
306 }
307 }
308
309 // Fallback: check standard installation paths
310 const char* linuxPaths[] = {
311 "/usr/local/pebl2/battery",
312 "/usr/pebl2/battery",
313 "/usr/local/share/pebl2/battery",
314 "/usr/share/pebl2/battery"
315 };
316
317 for (const char* path : linuxPaths) {
318 if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
319 printf("Found PEBL battery at: %s\n", path);
320 return path;
321 }
322 }
323#endif
324
325 printf("Warning: Could not locate PEBL battery directory\n");
326 return "";
327}
328
329std::string LauncherConfig::GetConfigFilePath() const
330{
331 // In portable mode, store config in workspace/settings/
332 if (IsPortableMode()) {
333 std::string portableWorkspace = GetPortableWorkspacePath();
334#ifdef _WIN32
335 return portableWorkspace + "\\settings\\launcher.cfg";
336#else
337 return portableWorkspace + "/settings/launcher.cfg";
338#endif
339 }
340
341 // Store config file in Documents/pebl-exp.VERSION/settings/launcher.cfg
342 std::string documentsPath = GetDocumentsPath();
343 if (!documentsPath.empty()) {
344#ifdef _WIN32
345 return documentsPath + "\\pebl-exp." + PEBL_VERSION + "\\settings\\launcher.cfg";
346#else
347 return documentsPath + "/pebl-exp." + PEBL_VERSION + "/settings/launcher.cfg";
348#endif
349 }
350
351 // Fallback to ~/.pebl/settings/launcher.cfg
352#ifdef _WIN32
353 // Windows: %APPDATA%\PEBL\settings\launcher.cfg
354 char appDataPath[MAX_PATH];
355 if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appDataPath))) {
356 return std::string(appDataPath) + "\\PEBL\\settings\\launcher.cfg";
357 }
358 return "";
359#else
360 // Linux/macOS: ~/.pebl/settings/launcher.cfg
361 const char* homeDir = getenv("HOME");
362 if (!homeDir) {
363 struct passwd* pw = getpwuid(getuid());
364 homeDir = pw->pw_dir;
365 }
366 return std::string(homeDir) + "/.pebl/settings/launcher.cfg";
367#endif
368}
369
370bool LauncherConfig::IsPortableMode() const
371{
372 struct stat st;
373
374 // Check for STANDALONE.txt marker file
375 if (stat("./STANDALONE.txt", &st) == 0 || stat("../STANDALONE.txt", &st) == 0 ||
376 stat("../../STANDALONE.txt", &st) == 0) {
377 return true;
378 }
379
380 // Check for PORTABLE marker file
381 if (stat("./PORTABLE.txt", &st) == 0 || stat("../PORTABLE.txt", &st) == 0 ||
382 stat("../../PORTABLE.txt", &st) == 0) {
383 return true;
384 }
385
386 // Check for PEBL_PORTABLE environment variable
387 const char* portableEnv = getenv("PEBL_PORTABLE");
388 if (portableEnv && strcmp(portableEnv, "1") == 0) {
389 return true;
390 }
391
392 // No automatic detection - only explicit marker files or environment variable
393 return false;
394}
395
396std::string LauncherConfig::GetPortableWorkspacePath() const
397{
398 struct stat st;
399
400 // Check where the marker file is located to determine workspace root
401 // Marker file location determines workspace location
402
403 // Check if marker file is in current directory (running from root)
404 if (stat("./STANDALONE.txt", &st) == 0 || stat("./PORTABLE.txt", &st) == 0) {
405 return ".";
406 }
407
408 // Check if marker file is one level up
409 if (stat("../STANDALONE.txt", &st) == 0 || stat("../PORTABLE.txt", &st) == 0) {
410 return "..";
411 }
412
413 // Check if marker file is two levels up (we're in PEBL/bin/)
414 if (stat("../../STANDALONE.txt", &st) == 0 || stat("../../PORTABLE.txt", &st) == 0) {
415 return "../..";
416 }
417
418 // If PEBL_PORTABLE env var is set, use current directory
419 // Fallback: current directory
420 return ".";
421}
422
424{
425 std::string configPath = GetConfigFilePath();
426 std::ifstream configFile(configPath);
427
428 if (!configFile.is_open()) {
429 // Config file doesn't exist yet, use defaults
430 return false;
431 }
432
433 // In portable mode, skip loading path-related settings
434 // They should be re-detected each time based on current location
435 bool portable = IsPortableMode();
436
437 std::string line;
438 std::string currentSection = "";
439
440 while (std::getline(configFile, line)) {
441 // Skip empty lines and comments
442 if (line.empty() || line[0] == '#') {
443 continue;
444 }
445
446 // Check for section headers
447 if (line[0] == '[' && line[line.length()-1] == ']') {
448 currentSection = line.substr(1, line.length()-2);
449 continue;
450 }
451
452 // Parse key=value pairs
453 size_t equalPos = line.find('=');
454 if (equalPos == std::string::npos) {
455 continue;
456 }
457
458 std::string key = line.substr(0, equalPos);
459 std::string value = line.substr(equalPos + 1);
460
461 // Trim whitespace
462 key.erase(0, key.find_first_not_of(" \t"));
463 key.erase(key.find_last_not_of(" \t") + 1);
464 value.erase(0, value.find_first_not_of(" \t"));
465 value.erase(value.find_last_not_of(" \t") + 1);
466
467 // Apply configuration values based on section
468 if (currentSection == "") {
469 // In portable mode, skip path-related settings (except relative study paths)
470 if (portable) {
471 if (key == "experiment_directory" || key == "workspace_path" ||
472 key == "battery_path" || key == "pebl_executable_path" ||
473 key == "data_output_path") {
474 continue; // Skip - will be re-detected
475 }
476 // Note: current_study_path is handled specially in its own case below
477 }
478
479 if (key == "experiment_directory") {
480 mExperimentDirectory = value;
481 } else if (key == "subject_code") {
482 mSubjectCode = value;
483 } else if (key == "language") {
484 mLanguage = value;
485 } else if (key == "fullscreen") {
486 mFullscreen = (value == "true" || value == "1");
487 } else if (key == "auto_upload") {
488 mAutoUpload = (value == "true" || value == "1");
489 } else if (key == "upload_token") {
490 mUploadToken = value;
491 } else if (key == "upload_url") {
492 mUploadURL = value;
493 } else if (key == "workspace_path") {
494 mWorkspacePath = value;
495 } else if (key == "battery_path") {
496 mBatteryPath = value;
497 } else if (key == "pebl_executable_path") {
498 mPeblExecutablePath = value;
499 } else if (key == "data_output_path") {
500 mDataOutputPath = value;
501 } else if (key == "font_size") {
502 try {
503 mFontSize = std::stoi(value);
504 } catch (...) {
505 printf("Warning: Invalid font_size value, using default\n");
506 }
507 } else if (key == "window_width") {
508 try {
509 mWindowWidth = std::stoi(value);
510 } catch (...) {
511 printf("Warning: Invalid window_width value, using default\n");
512 }
513 } else if (key == "window_height") {
514 try {
515 mWindowHeight = std::stoi(value);
516 } catch (...) {
517 printf("Warning: Invalid window_height value, using default\n");
518 }
519 } else if (key == "current_study_path") {
520 if (portable && !value.empty()) {
521 // In portable mode, convert relative path to absolute
522 // Check if it's already absolute
523 if (value[0] != '/' && !(value.length() > 1 && value[1] == ':')) {
524 // It's a relative path - make it absolute relative to workspace
525 try {
526 fs::path relativePath(value);
527 fs::path absolutePath = fs::absolute(fs::path(mWorkspacePath) / relativePath);
528 mCurrentStudyPath = absolutePath.string();
529 } catch (...) {
530 mCurrentStudyPath = value; // Fallback to as-is
531 }
532 } else {
533 // Absolute path in portable mode - skip it
534 mCurrentStudyPath = "";
535 }
536 } else {
537 mCurrentStudyPath = value;
538 }
539 } else if (key == "current_chain_name") {
540 mCurrentChainName = value;
541 } else if (key == "external_editor") {
542 mExternalEditor = value;
543 }
544 } else if (currentSection == "recent") {
545 // Parse recent experiment entry: name|path|timestamp
546 size_t pipe1 = value.find('|');
547 if (pipe1 != std::string::npos) {
548 size_t pipe2 = value.find('|', pipe1 + 1);
549 if (pipe2 != std::string::npos) {
550 try {
551 std::string timestampStr = value.substr(pipe2 + 1);
552 // Skip if timestamp is empty or invalid
553 if (timestampStr.empty()) {
554 continue;
555 }
556
557 RecentExperiment recent;
558 recent.name = value.substr(0, pipe1);
559 recent.path = value.substr(pipe1 + 1, pipe2 - pipe1 - 1);
560 recent.lastRun = std::stol(timestampStr);
561 mRecentExperiments.push_back(recent);
562 } catch (const std::invalid_argument& e) {
563 // Skip malformed entry
564 printf("Warning: Skipping malformed recent experiment entry: %s\n", value.c_str());
565 } catch (const std::out_of_range& e) {
566 // Skip out of range timestamp
567 printf("Warning: Skipping out-of-range timestamp in recent experiment entry: %s\n", value.c_str());
568 }
569 }
570 }
571 }
572 }
573
574 configFile.close();
575
576 // Validate loaded paths — a saved path may point to a now-gone AppImage mount
577 // (e.g. /tmp/.mount_PEBL-xxx/...) or a previous installation that no longer exists.
578 // If either critical path is missing, re-run auto-detection so the launcher works
579 // correctly regardless of how it was first invoked.
580 struct stat st;
581 if (!mBatteryPath.empty() &&
582 !(stat(mBatteryPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)))
583 {
584 printf("Warning: Saved battery_path no longer exists: %s\n", mBatteryPath.c_str());
585 printf("Re-detecting PEBL installation...\n");
586 mBatteryPath = DetectPEBLInstallation();
587 if (!mBatteryPath.empty()) {
588 printf("Re-detected battery at: %s\n", mBatteryPath.c_str());
589 } else {
590 printf("Warning: Could not auto-detect battery path.\n");
591 }
592 }
593
594 if (!mPeblExecutablePath.empty() &&
595 !(stat(mPeblExecutablePath.c_str(), &st) == 0 && S_ISREG(st.st_mode)))
596 {
597 printf("Warning: Saved pebl_executable_path no longer exists: %s\n", mPeblExecutablePath.c_str());
598 // Derive pebl2 location from battery path: battery/ is a sibling of bin/
599 if (!mBatteryPath.empty()) {
600#ifdef _WIN32
601 std::string candidate = mBatteryPath + "\\..\\bin\\pebl2.exe";
602#else
603 std::string candidate = mBatteryPath + "/../bin/pebl2";
604#endif
605 if (stat(candidate.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
606 mPeblExecutablePath = candidate;
607 printf("Re-detected PEBL executable at: %s\n", mPeblExecutablePath.c_str());
608 } else {
609 printf("Warning: Could not auto-detect PEBL executable path.\n");
610 mPeblExecutablePath = "";
611 }
612 }
613 }
614
615 return true;
616}
617
619{
620 std::string configPath = GetConfigFilePath();
621 bool portable = IsPortableMode();
622
623 // Create directory (and any parent directories) if it doesn't exist
624 fs::path configDir = fs::path(configPath).parent_path();
625 if (!configDir.empty()) {
626 try {
627 fs::create_directories(configDir);
628 } catch (const fs::filesystem_error& e) {
629 printf("Warning: Could not create config directory: %s\n", e.what());
630 }
631 }
632
633 std::ofstream configFile(configPath);
634 if (!configFile.is_open()) {
635 return false;
636 }
637
638 // Write configuration
639 configFile << "# PEBL Launcher Configuration" << std::endl;
640 configFile << "# Auto-generated - edit at your own risk" << std::endl;
641 if (portable) {
642 configFile << "# Portable mode - path settings are auto-detected on launch" << std::endl;
643 }
644 configFile << std::endl;
645
646 // In portable mode, don't save absolute path settings
647 // They will be re-detected based on the launcher's location
648 if (!portable) {
649 configFile << "experiment_directory=" << mExperimentDirectory << std::endl;
650 }
651 configFile << "subject_code=" << mSubjectCode << std::endl;
652 configFile << "language=" << mLanguage << std::endl;
653 configFile << "fullscreen=" << (mFullscreen ? "true" : "false") << std::endl;
654 configFile << std::endl;
655
656 // In portable mode, skip path settings entirely
657 if (!portable) {
658 configFile << "# File paths" << std::endl;
659 configFile << "workspace_path=" << mWorkspacePath << std::endl;
660 configFile << "battery_path=" << mBatteryPath << std::endl;
661 configFile << "pebl_executable_path=" << mPeblExecutablePath << std::endl;
662 configFile << "data_output_path=" << mDataOutputPath << std::endl;
663 configFile << std::endl;
664 }
665
666 configFile << "# Upload settings" << std::endl;
667 configFile << "auto_upload=" << (mAutoUpload ? "true" : "false") << std::endl;
668 configFile << "upload_token=" << mUploadToken << std::endl;
669 configFile << "upload_url=" << mUploadURL << std::endl;
670 configFile << std::endl;
671
672 configFile << "# UI settings" << std::endl;
673 configFile << "font_size=" << mFontSize << std::endl;
674 configFile << "window_width=" << mWindowWidth << std::endl;
675 configFile << "window_height=" << mWindowHeight << std::endl;
676 configFile << "external_editor=" << mExternalEditor << std::endl;
677 configFile << std::endl;
678
679 configFile << "# Session state" << std::endl;
680 // Save study path - in portable mode, convert to relative path
681 if (!mCurrentStudyPath.empty()) {
682 if (portable) {
683 // Convert absolute path to relative path for portable mode
684 try {
685 fs::path studyPath(mCurrentStudyPath);
686 fs::path workspacePath(mWorkspacePath);
687 fs::path relativePath = fs::relative(studyPath, workspacePath);
688 configFile << "current_study_path=" << relativePath.string() << std::endl;
689 } catch (...) {
690 // If relative path conversion fails, skip saving
691 printf("Warning: Could not convert study path to relative path\n");
692 }
693 } else {
694 configFile << "current_study_path=" << mCurrentStudyPath << std::endl;
695 }
696 }
697 configFile << "current_chain_name=" << mCurrentChainName << std::endl;
698
699 // Save recent experiments
700 if (!mRecentExperiments.empty()) {
701 configFile << std::endl;
702 configFile << "[recent]" << std::endl;
703 for (size_t i = 0; i < mRecentExperiments.size(); i++) {
704 const RecentExperiment& recent = mRecentExperiments[i];
705 configFile << "recent" << i << "=" << recent.name << "|"
706 << recent.path << "|" << recent.lastRun << std::endl;
707 }
708 }
709
710 configFile.close();
711 return true;
712}
713
714void LauncherConfig::AddRecentExperiment(const std::string& path, const std::string& name)
715{
716 // Check if already in list
717 for (auto it = mRecentExperiments.begin(); it != mRecentExperiments.end(); ++it) {
718 if (it->path == path) {
719 // Update timestamp and move to front
720 it->lastRun = time(nullptr);
721 RecentExperiment recent = *it;
722 mRecentExperiments.erase(it);
723 mRecentExperiments.insert(mRecentExperiments.begin(), recent);
724 return;
725 }
726 }
727
728 // Add new entry at front
729 RecentExperiment recent;
730 recent.path = path;
731 recent.name = name;
732 recent.lastRun = time(nullptr);
733 mRecentExperiments.insert(mRecentExperiments.begin(), recent);
734
735 // Limit to MAX_RECENT_EXPERIMENTS
736 if ((int)mRecentExperiments.size() > MAX_RECENT_EXPERIMENTS) {
737 mRecentExperiments.resize(MAX_RECENT_EXPERIMENTS);
738 }
739}
char * br_find_prefix(const char *default_prefix)
Definition BinReloc.cpp:433
#define NULL
Definition BinReloc.cpp:317
int br_init(BrInitError *error)
Definition BinReloc.cpp:338
char * br_find_exe_dir(const char *default_dir)
Definition BinReloc.cpp:405
BrInitError
Definition BinReloc.h:22
#define PEBL_VERSION
void AddRecentExperiment(const std::string &path, const std::string &name)