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
22#include "SDL_internal.h"
23
24#ifdef SDL_VIDEO_DRIVER_WAYLAND
25
26#include "SDL_waylandmessagebox.h"
27
28#define ZENITY_VERSION_LEN 32 // Number of bytes to read from zenity --version (including NUL)
29
30#define MAX_BUTTONS 8 // Maximum number of buttons supported
31
32static bool parse_zenity_version(const char *version, int *major, int *minor)
33{
34 /* We expect the version string is in the form of MAJOR.MINOR.MICRO
35 * as described in meson.build. We'll ignore everything after that.
36 */
37 const char *version_ptr = version;
38 char *end_ptr = NULL;
39 int tmp = (int) SDL_strtol(version_ptr, &end_ptr, 10);
40 if (tmp == 0 && end_ptr == version_ptr) {
41 return SDL_SetError("failed to get zenity major version number");
42 }
43 *major = tmp;
44
45 if (*end_ptr == '.') {
46 version_ptr = end_ptr + 1; // skip the dot
47 tmp = (int) SDL_strtol(version_ptr, &end_ptr, 10);
48 if (tmp == 0 && end_ptr == version_ptr) {
49 return SDL_SetError("failed to get zenity minor version number");
50 }
51 *minor = tmp;
52 } else {
53 *minor = 0;
54 }
55 return true;
56}
57
58static bool get_zenity_version(int *major, int *minor)
59{
60 const char *argv[] = { "zenity", "--version", NULL };
61 bool result = false;
62
63 SDL_Process *process = SDL_CreateProcess(argv, true);
64 if (!process) {
65 return false;
66 }
67
68 char *output = SDL_ReadProcess(process, NULL, NULL);
69 if (output) {
70 result = parse_zenity_version(output, major, minor);
71 SDL_free(output);
72 }
73 SDL_DestroyProcess(process);
74
75 return result;
76}
77
78bool Wayland_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID)
79{
80 int zenity_major = 0, zenity_minor = 0, output_len = 0;
81 int argc = 5, i;
82 const char *argv[5 + 2 /* icon name */ + 2 /* title */ + 2 /* message */ + 2 * MAX_BUTTONS + 1 /* NULL */] = {
83 "zenity", "--question", "--switch", "--no-wrap", "--no-markup"
84 };
85 SDL_Process *process;
86
87 // Are we trying to connect to or are currently in a Wayland session?
88 if (!SDL_getenv("WAYLAND_DISPLAY")) {
89 const char *session = SDL_getenv("XDG_SESSION_TYPE");
90 if (session && SDL_strcasecmp(session, "wayland") != 0) {
91 return SDL_SetError("Not on a wayland display");
92 }
93 }
94
95 if (messageboxdata->numbuttons > MAX_BUTTONS) {
96 return SDL_SetError("Too many buttons (%d max allowed)", MAX_BUTTONS);
97 }
98
99 // get zenity version so we know which arg to use
100 if (!get_zenity_version(&zenity_major, &zenity_minor)) {
101 return false; // get_zenity_version() calls SDL_SetError(), so message is already set
102 }
103
104 /* https://gitlab.gnome.org/GNOME/zenity/-/commit/c686bdb1b45e95acf010efd9ca0c75527fbb4dea
105 * This commit removed --icon-name without adding a deprecation notice.
106 * We need to handle it gracefully, otherwise no message box will be shown.
107 */
108 argv[argc++] = zenity_major > 3 || (zenity_major == 3 && zenity_minor >= 90) ? "--icon" : "--icon-name";
109 switch (messageboxdata->flags & (SDL_MESSAGEBOX_ERROR | SDL_MESSAGEBOX_WARNING | SDL_MESSAGEBOX_INFORMATION)) {
110 case SDL_MESSAGEBOX_ERROR:
111 argv[argc++] = "dialog-error";
112 break;
113 case SDL_MESSAGEBOX_WARNING:
114 argv[argc++] = "dialog-warning";
115 break;
116 case SDL_MESSAGEBOX_INFORMATION:
117 default:
118 argv[argc++] = "dialog-information";
119 break;
120 }
121
122 if (messageboxdata->title && messageboxdata->title[0]) {
123 argv[argc++] = "--title";
124 argv[argc++] = messageboxdata->title;
125 } else {
126 argv[argc++] = "--title=";
127 }
128
129 if (messageboxdata->message && messageboxdata->message[0]) {
130 argv[argc++] = "--text";
131 argv[argc++] = messageboxdata->message;
132 } else {
133 argv[argc++] = "--text=";
134 }
135
136 for (i = 0; i < messageboxdata->numbuttons; ++i) {
137 if (messageboxdata->buttons[i].text && messageboxdata->buttons[i].text[0]) {
138 int len = SDL_strlen(messageboxdata->buttons[i].text);
139 if (len > output_len) {
140 output_len = len;
141 }
142
143 argv[argc++] = "--extra-button";
144 argv[argc++] = messageboxdata->buttons[i].text;
145 } else {
146 argv[argc++] = "--extra-button=";
147 }
148 }
149 if (messageboxdata->numbuttons == 0) {
150 argv[argc++] = "--extra-button=OK";
151 }
152 argv[argc] = NULL;
153
154 SDL_PropertiesID props = SDL_CreateProperties();
155 if (!props) {
156 return false;
157 }
158 SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv);
159 // If buttonID is set we need to wait and read the results
160 if (buttonID) {
161 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
162 } else {
163 SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL);
164 }
165 process = SDL_CreateProcessWithProperties(props);
166 SDL_DestroyProperties(props);
167 if (!process) {
168 return false;
169 }
170 if (buttonID) {
171 char *output = SDL_ReadProcess(process, NULL, NULL);
172 if (output) {
173 // It likes to add a newline...
174 char *tmp = SDL_strrchr(output, '\n');
175 if (tmp) {
176 *tmp = '\0';
177 }
178
179 // Check which button got pressed
180 for (i = 0; i < messageboxdata->numbuttons; i += 1) {
181 if (messageboxdata->buttons[i].text) {
182 if (SDL_strcmp(output, messageboxdata->buttons[i].text) == 0) {
183 *buttonID = messageboxdata->buttons[i].buttonID;
184 break;
185 }
186 }
187 }
188 SDL_free(output);
189 }
190 }
191 SDL_DestroyProcess(process);
192
193 return true;
194}
195
196#endif // SDL_VIDEO_DRIVER_WAYLAND
197