1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#include "../SDL_dialog_utils.h"
24
25#define X11_HANDLE_MAX_WIDTH 28
26typedef struct
27{
28 SDL_DialogFileCallback callback;
29 void *userdata;
30 void *argv;
31
32 /* Zenity only works with X11 handles apparently */
33 char x11_window_handle[X11_HANDLE_MAX_WIDTH];
34 /* These are part of argv, but are tracked separately for deallocation purposes */
35 int nfilters;
36 char **filters_slice;
37 char *filename;
38 char *title;
39 char *accept;
40 char *cancel;
41} zenityArgs;
42
43static char *zenity_clean_name(const char *name)
44{
45 char *newname = SDL_strdup(name);
46
47 /* Filter out "|", which Zenity considers a special character. Let's hope
48 there aren't others. TODO: find something better. */
49 for (char *c = newname; *c; c++) {
50 if (*c == '|') {
51 // Zenity doesn't support escaping with '\'
52 *c = '/';
53 }
54 }
55
56 return newname;
57}
58
59static bool get_x11_window_handle(SDL_PropertiesID props, char *out)
60{
61 SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
62 if (!window) {
63 return false;
64 }
65 SDL_PropertiesID window_props = SDL_GetWindowProperties(window);
66 if (!window_props) {
67 return false;
68 }
69 Uint64 handle = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
70 if (!handle) {
71 return false;
72 }
73 if (SDL_snprintf(out, X11_HANDLE_MAX_WIDTH, "0x%" SDL_PRIx64, handle) >= X11_HANDLE_MAX_WIDTH) {
74 return false;
75 };
76 return true;
77}
78
79/* Exec call format:
80 *
81 * zenity --file-selection --separator=\n [--multiple]
82 * [--directory] [--save --confirm-overwrite]
83 * [--filename FILENAME] [--modal --attach 0x11w1nd0w]
84 * [--title TITLE] [--ok-label ACCEPT]
85 * [--cancel-label CANCEL]
86 * [--file-filter=Filter Name | *.filt *.fn ...]...
87 */
88static zenityArgs *create_zenity_args(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
89{
90 zenityArgs *args = SDL_calloc(1, sizeof(*args));
91 if (!args) {
92 return NULL;
93 }
94 args->callback = callback;
95 args->userdata = userdata;
96 args->nfilters = SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
97
98 const char **argv = SDL_malloc(
99 sizeof(*argv) * (3 /* zenity --file-selection --separator=\n */
100 + 1 /* --multiple */
101 + 2 /* --directory | --save --confirm-overwrite */
102 + 2 /* --filename [file] */
103 + 3 /* --modal --attach [handle] */
104 + 2 /* --title [title] */
105 + 2 /* --ok-label [label] */
106 + 2 /* --cancel-label [label] */
107 + args->nfilters + 1 /* NULL */));
108 if (!argv) {
109 goto cleanup;
110 }
111 args->argv = argv;
112
113 /* Properties can be destroyed as soon as the function returns; copy over what we need. */
114#define COPY_STRING_PROPERTY(dst, prop) \
115 { \
116 const char *str = SDL_GetStringProperty(props, prop, NULL); \
117 if (str) { \
118 dst = SDL_strdup(str); \
119 if (!dst) { \
120 goto cleanup; \
121 } \
122 } \
123 }
124
125 COPY_STRING_PROPERTY(args->filename, SDL_PROP_FILE_DIALOG_LOCATION_STRING);
126 COPY_STRING_PROPERTY(args->title, SDL_PROP_FILE_DIALOG_TITLE_STRING);
127 COPY_STRING_PROPERTY(args->accept, SDL_PROP_FILE_DIALOG_ACCEPT_STRING);
128 COPY_STRING_PROPERTY(args->cancel, SDL_PROP_FILE_DIALOG_CANCEL_STRING);
129#undef COPY_STRING_PROPERTY
130
131 // ARGV PASS
132 int argc = 0;
133 argv[argc++] = "zenity";
134 argv[argc++] = "--file-selection";
135 argv[argc++] = "--separator=\n";
136
137 if (SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false)) {
138 argv[argc++] = "--multiple";
139 }
140
141 switch (type) {
142 case SDL_FILEDIALOG_OPENFILE:
143 break;
144
145 case SDL_FILEDIALOG_SAVEFILE:
146 argv[argc++] = "--save";
147 /* Asking before overwriting while saving seems like a sane default */
148 argv[argc++] = "--confirm-overwrite";
149 break;
150
151 case SDL_FILEDIALOG_OPENFOLDER:
152 argv[argc++] = "--directory";
153 break;
154 };
155
156 if (args->filename) {
157 argv[argc++] = "--filename";
158 argv[argc++] = args->filename;
159 }
160
161 if (get_x11_window_handle(props, args->x11_window_handle)) {
162 argv[argc++] = "--modal";
163 argv[argc++] = "--attach";
164 argv[argc++] = args->x11_window_handle;
165 }
166
167 if (args->title) {
168 argv[argc++] = "--title";
169 argv[argc++] = args->title;
170 }
171
172 if (args->accept) {
173 argv[argc++] = "--ok-label";
174 argv[argc++] = args->accept;
175 }
176
177 if (args->cancel) {
178 argv[argc++] = "--cancel-label";
179 argv[argc++] = args->cancel;
180 }
181
182 const SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
183 if (filters) {
184 args->filters_slice = (char **)&argv[argc];
185 for (int i = 0; i < args->nfilters; i++) {
186 char *filter_str = convert_filter(filters[i],
187 zenity_clean_name,
188 "--file-filter=", " | ", "",
189 "*.", " *.", "");
190
191 if (!filter_str) {
192 while (i--) {
193 SDL_free(args->filters_slice[i]);
194 }
195 goto cleanup;
196 }
197
198 args->filters_slice[i] = filter_str;
199 }
200 argc += args->nfilters;
201 }
202
203 argv[argc] = NULL;
204 return args;
205
206cleanup:
207 SDL_free(args->filename);
208 SDL_free(args->title);
209 SDL_free(args->accept);
210 SDL_free(args->cancel);
211 SDL_free(argv);
212 SDL_free(args);
213 return NULL;
214}
215
216// TODO: Zenity survives termination of the parent
217
218static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *argv)
219{
220 SDL_Process *process = NULL;
221 SDL_Environment *env = NULL;
222 int status = -1;
223 size_t bytes_read = 0;
224 char *container = NULL;
225 size_t narray = 1;
226 char **array = NULL;
227 bool result = false;
228
229 env = SDL_CreateEnvironment(true);
230 if (!env) {
231 goto done;
232 }
233
234 /* Recent versions of Zenity have different exit codes, but picks up
235 different codes from the environment */
236 SDL_SetEnvironmentVariable(env, "ZENITY_OK", "0", true);
237 SDL_SetEnvironmentVariable(env, "ZENITY_CANCEL", "1", true);
238 SDL_SetEnvironmentVariable(env, "ZENITY_ESC", "1", true);
239 SDL_SetEnvironmentVariable(env, "ZENITY_EXTRA", "2", true);
240 SDL_SetEnvironmentVariable(env, "ZENITY_ERROR", "2", true);
241 SDL_SetEnvironmentVariable(env, "ZENITY_TIMEOUT", "2", true);
242
243 SDL_PropertiesID props = SDL_CreateProperties();
244 SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv);
245 SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env);
246 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
247 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
248 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL);
249 process = SDL_CreateProcessWithProperties(props);
250 SDL_DestroyProperties(props);
251 if (!process) {
252 goto done;
253 }
254
255 container = SDL_ReadProcess(process, &bytes_read, &status);
256 if (!container) {
257 goto done;
258 }
259
260 array = (char **)SDL_malloc((narray + 1) * sizeof(char *));
261 if (!array) {
262 goto done;
263 }
264 array[0] = container;
265 array[1] = NULL;
266
267 for (int i = 0; i < bytes_read; i++) {
268 if (container[i] == '\n') {
269 container[i] = '\0';
270 // Reading from a process often leaves a trailing \n, so ignore the last one
271 if (i < bytes_read - 1) {
272 array[narray] = container + i + 1;
273 narray++;
274 char **new_array = (char **)SDL_realloc(array, (narray + 1) * sizeof(char *));
275 if (!new_array) {
276 goto done;
277 }
278 array = new_array;
279 array[narray] = NULL;
280 }
281 }
282 }
283
284 // 0 = the user chose one or more files, 1 = the user canceled the dialog
285 if (status == 0 || status == 1) {
286 callback(userdata, (const char *const *)array, -1);
287 } else {
288 SDL_SetError("Could not run zenity: exit code %d", status);
289 callback(userdata, NULL, -1);
290 }
291
292 result = true;
293
294done:
295 SDL_free(array);
296 SDL_free(container);
297 SDL_DestroyEnvironment(env);
298 SDL_DestroyProcess(process);
299
300 if (!result) {
301 callback(userdata, NULL, -1);
302 }
303}
304
305static void free_zenity_args(zenityArgs *args)
306{
307 if (args->filters_slice) {
308 for (int i = 0; i < args->nfilters; i++) {
309 SDL_free(args->filters_slice[i]);
310 }
311 }
312 SDL_free(args->filename);
313 SDL_free(args->title);
314 SDL_free(args->accept);
315 SDL_free(args->cancel);
316 SDL_free(args->argv);
317 SDL_free(args);
318}
319
320static int run_zenity_thread(void *ptr)
321{
322 zenityArgs *args = ptr;
323 run_zenity(args->callback, args->userdata, args->argv);
324 free_zenity_args(args);
325 return 0;
326}
327
328void SDL_Zenity_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
329{
330 zenityArgs *args = create_zenity_args(type, callback, userdata, props);
331 if (!args) {
332 callback(userdata, NULL, -1);
333 return;
334 }
335
336 SDL_Thread *thread = SDL_CreateThread(run_zenity_thread, "SDL_ZenityFileDialog", (void *)args);
337
338 if (!thread) {
339 free_zenity_args(args);
340 callback(userdata, NULL, -1);
341 return;
342 }
343
344 SDL_DetachThread(thread);
345}
346
347bool SDL_Zenity_detect(void)
348{
349 const char *args[] = {
350 "zenity", "--version", NULL
351 };
352 int status = -1;
353
354 SDL_PropertiesID props = SDL_CreateProperties();
355 SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, args);
356 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
357 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL);
358 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL);
359 SDL_Process *process = SDL_CreateProcessWithProperties(props);
360 SDL_DestroyProperties(props);
361 if (process) {
362 SDL_WaitProcess(process, true, &status);
363 SDL_DestroyProcess(process);
364 }
365 return (status == 0);
366}
367