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