Line data Source code
1 : // =============================================================================
2 : // Native file dialog implementations (Windows, macOS, Linux)
3 : // Folder dialog implementation
4 : // =============================================================================
5 :
6 : #include "gui/dialogs/file_dialog.h"
7 : #include <cstring>
8 :
9 : #ifdef _WIN32
10 : #define WIN32_LEAN_AND_MEAN
11 : #include <windows.h>
12 : #include <shlobj.h>
13 : #else
14 : // Required for popen, pclose, fgets, FILE on non-Windows builds
15 : #include <cstdio>
16 : #endif
17 :
18 : #ifdef __APPLE__
19 : #include <TargetConditionals.h>
20 : #include <unistd.h>
21 : #include <sys/wait.h>
22 : #include <fcntl.h>
23 : #endif
24 :
25 : #ifndef _WIN32
26 : #include <sys/wait.h>
27 : #endif
28 :
29 : namespace Amplitron {
30 :
31 : #ifdef AMPLITRON_HEADLESS
32 3 : std::string show_folder_dialog(const std::string&) {
33 4 : return "";
34 : }
35 : #else
36 :
37 : #ifdef _WIN32
38 0 : std::string show_folder_dialog(const std::string& title) {
39 0 : BROWSEINFOA bi;
40 0 : std::memset(&bi, 0, sizeof(bi));
41 0 : bi.lpszTitle = title.c_str();
42 0 : bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
43 :
44 0 : LPITEMIDLIST pidl = SHBrowseForFolderA(&bi);
45 0 : if (!pidl) return "";
46 :
47 0 : char path[MAX_PATH];
48 0 : bool ok = SHGetPathFromIDListA(pidl, path);
49 0 : CoTaskMemFree(pidl);
50 0 : return ok ? std::string(path) : "";
51 : }
52 :
53 : #elif defined(__APPLE__) && !TARGET_OS_IOS
54 0 : std::string show_folder_dialog(const std::string& title) {
55 : // Sanitize title for embedding in an AppleScript string literal:
56 : // escape backslashes first, then double-quotes.
57 0 : std::string safe_title;
58 0 : for (char c : title) {
59 0 : if (c == '\\') { safe_title += "\\\\"; }
60 0 : else if (c == '"') { safe_title += "\\\""; }
61 0 : else { safe_title += c; }
62 : }
63 :
64 0 : std::string script = "POSIX path of (choose folder with prompt \"" + safe_title + "\")";
65 :
66 : // Use fork+exec to invoke /usr/bin/osascript directly, bypassing /bin/sh
67 : // so the script string is never interpreted by a shell.
68 : int pipefd[2];
69 0 : if (pipe(pipefd) != 0) return "";
70 :
71 0 : pid_t pid = fork();
72 0 : if (pid < 0) {
73 0 : close(pipefd[0]);
74 0 : close(pipefd[1]);
75 0 : return "";
76 : }
77 :
78 0 : if (pid == 0) {
79 : // Child process
80 0 : close(pipefd[0]);
81 0 : dup2(pipefd[1], STDOUT_FILENO);
82 0 : close(pipefd[1]);
83 0 : int devnull = open("/dev/null", O_WRONLY);
84 0 : if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); }
85 0 : execl("/usr/bin/osascript", "osascript", "-e", script.c_str(), nullptr);
86 0 : _exit(1);
87 : }
88 :
89 : // Parent process
90 0 : close(pipefd[1]);
91 : char buf[1024];
92 0 : std::string result;
93 : ssize_t n;
94 0 : while ((n = read(pipefd[0], buf, sizeof(buf))) > 0)
95 0 : result.append(buf, static_cast<size_t>(n));
96 0 : close(pipefd[0]);
97 :
98 0 : int status = 0;
99 0 : waitpid(pid, &status, 0);
100 :
101 0 : while (!result.empty() && (result.back() == '\n' || result.back() == '\r'))
102 0 : result.pop_back();
103 : // osascript returns paths with trailing slash; strip it for consistency
104 0 : if (!result.empty() && result.back() == '/') result.pop_back();
105 :
106 0 : return result;
107 0 : }
108 :
109 : #else // Linux
110 0 : std::string show_folder_dialog(const std::string& title) {
111 : // Sanitize title for single-quote shell embedding: replace ' with '\''
112 0 : std::string safe_title;
113 0 : for (char c : title) {
114 0 : if (c == '\'') { safe_title += "'\\''"; }
115 0 : else { safe_title += c; }
116 : }
117 :
118 : std::string cmd = "zenity --file-selection --directory "
119 0 : "--title='" + safe_title + "' 2>/dev/null";
120 :
121 0 : FILE* pipe = popen(cmd.c_str(), "r");
122 0 : if (!pipe) return "";
123 :
124 : char buf[1024];
125 0 : std::string result;
126 0 : while (fgets(buf, sizeof(buf), pipe)) {
127 0 : result += buf;
128 : }
129 0 : int wait_status = pclose(pipe);
130 0 : int exit_code = WIFEXITED(wait_status) ? WEXITSTATUS(wait_status) : -1;
131 :
132 0 : if (exit_code != 0) {
133 : // Exit status 1 means the user cancelled in zenity — return empty.
134 0 : if (exit_code == 1) return "";
135 :
136 : // Any other non-zero status means zenity is unavailable; try kdialog.
137 0 : cmd = "kdialog --getexistingdirectory ~/ --title '" + safe_title + "' 2>/dev/null";
138 0 : pipe = popen(cmd.c_str(), "r");
139 0 : if (!pipe) return "";
140 0 : result.clear();
141 0 : while (fgets(buf, sizeof(buf), pipe)) {
142 0 : result += buf;
143 : }
144 0 : pclose(pipe);
145 : }
146 :
147 0 : while (!result.empty() && (result.back() == '\n' || result.back() == '\r'))
148 0 : result.pop_back();
149 :
150 0 : return result;
151 0 : }
152 : #endif
153 :
154 : #endif // AMPLITRON_HEADLESS
155 :
156 : } // namespace Amplitron
|