ohmu
·
2026-10-15
1#include "displayserver_linux.h"
2#include "common/io.h"
3#include "common/properties.h"
4#include "common/strutil.h"
5#include "common/mallocHelper.h"
6
7#include <stdlib.h>
8#include <string.h>
9#include <unistd.h>
10#include <libgen.h>
11
12#if __FreeBSD__
13 #include <sys/sysctl.h>
14 #include <sys/types.h>
15 #include <sys/user.h>
16#elif __OpenBSD__
17 #include <sys/param.h>
18 #include <sys/sysctl.h>
19#elif __sun
20 #include <procfs.h>
21#elif __NetBSD__
22 #include <sys/types.h>
23 #include <sys/sysctl.h>
24#endif
25
26static const char* parseEnv(void) {
27 const char* env;
28
29 env = getenv("XDG_CURRENT_DESKTOP");
30 if (ffStrSet(env)) {
31 return env;
32 }
33
34 env = getenv("XDG_SESSION_DESKTOP");
35 if (ffStrSet(env)) {
36 return env;
37 }
38
39 env = getenv("CURRENT_DESKTOP");
40 if (ffStrSet(env)) {
41 return env;
42 }
43
44 env = getenv("SESSION_DESKTOP");
45 if (ffStrSet(env)) {
46 return env;
47 }
48
49 env = getenv("DESKTOP_SESSION");
50 if (ffStrSet(env)) {
51 return env;
52 }
53
54 if (getenv("KDE_FULL_SESSION") != nullptr || getenv("KDE_SESSION_UID") != nullptr || getenv("KDE_SESSION_VERSION") != nullptr) {
55 return "KDE";
56 }
57
58 if (getenv("GNOME_DESKTOP_SESSION_ID") != nullptr) {
59 return "GNOME";
60 }
61
62 if (getenv("MATE_DESKTOP_SESSION_ID") != nullptr) {
63 return "Mate";
64 }
65
66 if (getenv("TDE_FULL_SESSION") != nullptr) {
67 return "Trinity";
68 }
69
70 if (getenv("HYPRLAND_CMD") != nullptr) {
71 return "Hyprland";
72 }
73
74 if (getenv("SWAYSOCK") != nullptr) {
75 return "Sway";
76 }
77
78 if (getenv("COSMIC_VERSION") != nullptr) {
79 return "COSMIC";
80 }
81
82#if __linux__ && !__ANDROID__
83 if (
84 getenv("WAYLAND_DISPLAY") != nullptr &&
85 ffPathExists("/mnt/wslg/", FF_PATHTYPE_DIRECTORY)) {
86 return "WSLg";
87 }
88#endif
89
90 return nullptr;
91}
92
93static void applyPrettyNameIfWM(FFDisplayServerResult* result, const char* name) {
94 if (!ffStrSet(name)) {
95 return;
96 }
97
98 if (
99 ffStrEqualsIgnCase(name, "kwin") ||
100 ffStrStartsWithIgnCase(name, "kwin_") ||
101 ffStrEndsWithIgnCase(name, "-kwin_wayland") ||
102 ffStrEndsWithIgnCase(name, "-kwin_x11")) {
103 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_KWIN);
104 } else if (
105 ffStrEqualsIgnCase(name, "gnome-shell") ||
106 ffStrEqualsIgnCase(name, "gnome shell") ||
107 ffStrEqualsIgnCase(name, "gnome-session-binary") ||
108 ffStrEqualsIgnCase(name, "Mutter")) {
109 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MUTTER);
110 } else if (
111 ffStrEqualsIgnCase(name, "cinnamon") ||
112 ffStrStartsWithIgnCase(name, "cinnamon-") ||
113 ffStrEqualsIgnCase(name, "Muffin") ||
114 ffStrEqualsIgnCase(name, "Mutter (Muffin)")) {
115 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MUFFIN);
116 } else if (ffStrEqualsIgnCase(name, "sway")) {
117 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_SWAY);
118 } else if (ffStrEqualsIgnCase(name, "weston")) {
119 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WESTON);
120 } else if (ffStrEqualsIgnCase(name, "wayfire")) {
121 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WAYFIRE);
122 } else if (ffStrEqualsIgnCase(name, "openbox")) {
123 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_OPENBOX);
124 } else if (ffStrEqualsIgnCase(name, "xfwm4")) {
125 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_XFWM4);
126 } else if (ffStrEqualsIgnCase(name, "Marco") ||
127 ffStrEqualsIgnCase(name, "Metacity (Marco)")) {
128 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MARCO);
129 } else if (ffStrEqualsIgnCase(name, "xmonad")) {
130 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_XMONAD);
131 } else if (ffStrEqualsIgnCase(name, "WSLg")) {
132 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WSLG);
133 } else if (ffStrEqualsIgnCase(name, "dwm")) {
134 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_DWM);
135 } else if (ffStrEqualsIgnCase(name, "bspwm")) {
136 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_BSPWM);
137 } else if (ffStrEqualsIgnCase(name, "tinywm")) {
138 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_TINYWM);
139 } else if (ffStrEqualsIgnCase(name, "qtile")) {
140 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_QTILE);
141 } else if (ffStrEqualsIgnCase(name, "herbstluftwm")) {
142 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_HERBSTLUFTWM);
143 } else if (ffStrEqualsIgnCase(name, "icewm")) {
144 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_ICEWM);
145 } else if (ffStrEqualsIgnCase(name, "dtwm")) {
146 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_DTWM);
147 } else if (ffStrEqualsIgnCase(name, "fvwm")) {
148 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_FVWM);
149 } else if (ffStrEqualsIgnCase(name, "ctwm")) {
150 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_CTWM);
151 } else if (ffStrEqualsIgnCase(name, "hyprland")) {
152 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_HYPRLAND);
153 } else if (ffStrEqualsIgnCase(name, "ratpoison")) {
154 ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_RATPOISON);
155 }
156}
157
158static void applyNameIfWM(FFDisplayServerResult* result, const char* processName) {
159 applyPrettyNameIfWM(result, processName);
160 if (result->wmPrettyName.length > 0) {
161 ffStrbufSetS(&result->wmProcessName, processName);
162 }
163}
164
165static void applyBetterWM(FFDisplayServerResult* result, const char* processName) {
166 if (!ffStrSet(processName)) {
167 return;
168 }
169
170 ffStrbufSetS(&result->wmProcessName, processName);
171
172 // If it is a known wm, this will set the pretty name
173 applyPrettyNameIfWM(result, processName);
174
175 // If it isn't a known wm, set the pretty name to the process name
176 if (result->wmPrettyName.length == 0) {
177 ffStrbufAppend(&result->wmPrettyName, &result->wmProcessName);
178 }
179}
180
181static void applyPrettyNameIfDE(FFDisplayServerResult* result, const char* name) {
182 if (!ffStrSet(name)) {
183 return;
184 }
185
186 else if (
187 ffStrEqualsIgnCase(name, "KDE") ||
188 ffStrEqualsIgnCase(name, "plasma") ||
189 ffStrEqualsIgnCase(name, "plasmashell") ||
190 ffStrEqualsIgnCase(name, "plasmawayland")) {
191 ffStrbufSetStatic(&result->deProcessName, "plasmashell");
192 ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_PLASMA);
193 applyBetterWM(result, getenv("KDEWM"));
194 }
195
196 else if (
197 ffStrEqualsIgnCase(name, "GNOME") ||
198 ffStrEqualsIgnCase(name, "ubuntu:GNOME") ||
199 ffStrEqualsIgnCase(name, "ubuntu") ||
200 ffStrEqualsIgnCase(name, "gnome-shell")) {
201 ffStrbufSetStatic(&result->deProcessName, "gnome-shell");
202 const char* sessionMode = getenv("GNOME_SHELL_SESSION_MODE");
203 if (sessionMode && ffStrEquals(sessionMode, "classic")) {
204 ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_GNOME_CLASSIC);
205 } else {
206 ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_GNOME);
207 }
208 }
209
210 else if (
211 ffStrEqualsIgnCase(name, "X-Cinnamon") ||
212 ffStrEqualsIgnCase(name, "Cinnamon")) {
213 ffStrbufSetS(&result->deProcessName, "cinnamon");
214 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_CINNAMON);
215 }
216
217 else if (
218 ffStrEqualsIgnCase(name, "XFCE") ||
219 ffStrEqualsIgnCase(name, "X-XFCE") ||
220 ffStrEqualsIgnCase(name, "XFCE4") ||
221 ffStrEqualsIgnCase(name, "X-XFCE4") ||
222 ffStrEqualsIgnCase(name, "xfce4-session")) {
223 ffStrbufSetS(&result->deProcessName, "xfce4-session");
224 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_XFCE4);
225 }
226
227 else if (
228 ffStrEqualsIgnCase(name, "MATE") ||
229 ffStrEqualsIgnCase(name, "X-MATE") ||
230 ffStrEqualsIgnCase(name, "mate-session")) {
231 ffStrbufSetS(&result->deProcessName, "mate-session");
232 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_MATE);
233 }
234
235 else if (
236 ffStrEqualsIgnCase(name, "LXQt") ||
237 ffStrEqualsIgnCase(name, "X-LXQt") ||
238 ffStrEqualsIgnCase(name, "lxqt-session")) {
239 ffStrbufSetS(&result->deProcessName, "lxqt-session");
240 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_LXQT);
241 if (result->wmProcessName.length == 0) {
242 FF_STRBUF_AUTO_DESTROY wmProcessNameBuffer = ffStrbufCreate();
243 ffParsePropFileConfig("lxqt/session.conf", "window_manager =", &wmProcessNameBuffer);
244 applyBetterWM(result, wmProcessNameBuffer.chars);
245 }
246 }
247
248 else if (
249 ffStrEqualsIgnCase(name, "Budgie") ||
250 ffStrEqualsIgnCase(name, "X-Budgie") ||
251 ffStrEqualsIgnCase(name, "budgie-desktop") ||
252 ffStrEqualsIgnCase(name, "Budgie:GNOME")) {
253 ffStrbufSetS(&result->deProcessName, "budgie-desktop");
254 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_BUDGIE);
255 }
256
257 else if (
258 ffStrEqualsIgnCase(name, "dtsession")) {
259 ffStrbufSetS(&result->deProcessName, "dtsession");
260 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_CDE);
261 }
262
263 else if (
264 ffStrEqualsIgnCase(name, "ukui-session")) {
265 ffStrbufSetS(&result->deProcessName, "ukui-session");
266 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_UKUI);
267 }
268
269 else if (
270 ffStrStartsWithIgnCase(name, "Unity:Unity")) {
271 ffStrbufSetS(&result->deProcessName, "unity-session");
272 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_UNITY);
273 }
274
275 else if (ffStrEqualsIgnCase(name, "Enlightenment")) {
276 ffStrbufSetS(&result->deProcessName, "enlightenment_start");
277 ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_ENLIGHTENMENT);
278 }
279}
280
281static const char* getFromProcesses(FFDisplayServerResult* result) {
282 uint32_t userId = instance.state.platform.uid;
283
284#if __FreeBSD__
285 #ifdef __DragonFly__
286 #define ki_comm kp_comm
287 #endif
288
289 int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_UID, (int) userId };
290 size_t length = 0;
291
292 if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) {
293 return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed";
294 }
295
296 FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length);
297 if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) {
298 return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed";
299 }
300
301 length /= sizeof(*procs);
302
303 for (struct kinfo_proc* proc = procs; proc < procs + length; ++proc) {
304 if (result->dePrettyName.length == 0) {
305 applyPrettyNameIfDE(result, proc->ki_comm);
306 }
307
308 if (result->wmPrettyName.length == 0) {
309 applyNameIfWM(result, proc->ki_comm);
310 }
311
312 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
313 break;
314 }
315 }
316#elif __OpenBSD__
317 int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_UID, (int) userId, (int) sizeof(struct kinfo_proc), 0 };
318 size_t length = 0;
319
320 if (sysctl(request, ARRAY_SIZE(request), nullptr, &length, nullptr, 0) != 0) {
321 return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, nullptr) failed";
322 }
323
324 FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length);
325 request[5] = (int) (length / sizeof(struct kinfo_proc)); // count must be non-zero for data fetch
326 if (sysctl(request, ARRAY_SIZE(request), procs, &length, nullptr, 0) != 0) {
327 return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed";
328 }
329
330 int count = (int) (length / sizeof(struct kinfo_proc));
331 for (int i = 0; i < count; ++i) {
332 if (result->dePrettyName.length == 0) {
333 applyPrettyNameIfDE(result, procs[i].p_comm);
334 }
335
336 if (result->wmPrettyName.length == 0) {
337 applyNameIfWM(result, procs[i].p_comm);
338 }
339
340 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
341 break;
342 }
343 }
344#elif __sun
345 FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc");
346 if (procdir == nullptr) {
347 return "opendir(\"/proc\") failed";
348 }
349
350 FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreateA(64);
351 ffStrbufAppendS(&procPath, "/proc/");
352
353 uint32_t procPathLength = procPath.length;
354
355 struct dirent* dirent;
356 while ((dirent = readdir(procdir)) != nullptr) {
357 if (!ffCharIsDigit(dirent->d_name[0])) {
358 continue;
359 }
360
361 ffStrbufAppendS(&procPath, dirent->d_name);
362 ffStrbufAppendS(&procPath, "/psinfo");
363 psinfo_t proc;
364 if (ffReadFileData(procPath.chars, sizeof(proc), &proc) == sizeof(proc)) {
365 ffStrbufSubstrBefore(&procPath, procPathLength);
366
367 if (proc.pr_uid != userId) {
368 continue;
369 }
370
371 if (result->dePrettyName.length == 0) {
372 applyPrettyNameIfDE(result, proc.pr_fname);
373 }
374
375 if (result->wmPrettyName.length == 0) {
376 applyNameIfWM(result, proc.pr_fname);
377 }
378
379 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
380 break;
381 }
382 }
383 }
384#elif __linux__ || __GNU__
385 FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc");
386 if (procdir == nullptr) {
387 return "opendir(\"/proc\") failed";
388 }
389
390 FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreateA(64);
391 int procfd = dirfd(procdir);
392
393 struct dirent* dirent;
394 while ((dirent = readdir(procdir)) != nullptr) {
395 // Match only folders starting with a number (the pid folders)
396 if ((dirent->d_type != DT_DIR && dirent->d_type != DT_UNKNOWN) || !ffCharIsDigit(dirent->d_name[0])) {
397 continue;
398 }
399
400 ffStrbufSetS(&procPath, dirent->d_name);
401 uint32_t procFolderPathLength = procPath.length;
402
403 // Don't check for processes not owned by the current user.
404 char loginuid[32];
405 ffStrbufAppendS(&procPath, "/loginuid");
406 ssize_t bytesRead = ffReadFileDataRelative(procfd, procPath.chars, sizeof(loginuid) - 1, loginuid);
407 if (bytesRead <= 0) {
408 continue;
409 }
410 loginuid[bytesRead] = '\0';
411 if (strtol(loginuid, nullptr, 10) != (long) userId) {
412 continue;
413 }
414
415 ffStrbufSubstrBefore(&procPath, procFolderPathLength);
416
417 // We check the realpath for the process name
418 ffStrbufAppendS(&procPath, "/exe");
419 char exePath[PATH_MAX];
420 ssize_t exePathLength = readlinkat(procfd, procPath.chars, exePath, sizeof(exePath) - 1);
421 const char* processName;
422 if (exePathLength >= 0) {
423 exePath[exePathLength] = '\0';
424 processName = basename(exePath);
425 } else {
426 ffStrbufSubstrBefore(&procPath, procFolderPathLength);
427 ffStrbufAppendS(&procPath, "/comm");
428 exePathLength = ffReadFileDataRelative(procfd, procPath.chars, sizeof(exePath) - 1, exePath);
429 if (exePathLength <= 0) {
430 continue;
431 }
432 if (exePath[exePathLength - 1] == '\n') {
433 --exePathLength;
434 }
435 exePath[exePathLength] = '\0';
436 processName = exePath;
437 }
438
439 if (result->dePrettyName.length == 0) {
440 applyPrettyNameIfDE(result, processName);
441 }
442
443 if (result->wmPrettyName.length == 0) {
444 applyNameIfWM(result, processName);
445 }
446
447 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
448 break;
449 }
450 }
451#elif __NetBSD__
452 int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_UID, (int) userId, sizeof(struct kinfo_proc2), INT_MAX };
453
454 size_t size = 0;
455 if (sysctl(request, ARRAY_SIZE(request), nullptr, &size, nullptr, 0) != 0) {
456 return "sysctl(KERN_PROC_UID, nullptr) failed";
457 }
458
459 FF_AUTO_FREE struct kinfo_proc2* procs = malloc(size);
460
461 if (sysctl(request, ARRAY_SIZE(request), procs, &size, nullptr, 0) != 0) {
462 return "sysctl(KERN_PROC_UID, procs) failed";
463 }
464
465 for (struct kinfo_proc2* proc = procs; proc < procs + (size / sizeof(struct kinfo_proc2)); proc++) {
466 if (result->dePrettyName.length == 0) {
467 applyPrettyNameIfDE(result, proc->p_comm);
468 }
469
470 if (result->wmPrettyName.length == 0) {
471 applyNameIfWM(result, proc->p_comm);
472 }
473
474 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
475 break;
476 }
477 }
478#endif
479
480 return nullptr;
481}
482
483void ffdsDetectWMDE(FFDisplayServerResult* result) {
484#if __ANDROID__
485 if (ffStrbufIgnCaseEqualS(&result->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) {
486 return; // Only supported when connected to X11
487 }
488#endif
489
490 const char* env = parseEnv();
491
492 if (result->wmProcessName.length > 0) {
493 // If we found the processName via display server, use it.
494 // This will set the pretty name if it is a known WM, otherwise the prettyName to the processName
495 applyPrettyNameIfWM(result, result->wmProcessName.chars);
496 if (result->wmPrettyName.length == 0) {
497 ffStrbufSet(&result->wmPrettyName, &result->wmProcessName);
498 }
499 } else {
500 // if env is a known WM, use it
501 applyNameIfWM(result, env);
502 }
503
504 // Connecting to a display server only gives WM results, not DE results.
505 // If we find it in the environment, use that.
506 applyPrettyNameIfDE(result, env);
507
508 // If WM was found by connection to the sever, and DE in the environment, we can return
509 // This way we never call getFromProcDir(), which has slow initialization time
510 if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) {
511 return;
512 }
513
514 // Get missing WM / DE from processes.
515 getFromProcesses(result);
516
517 // Return if both wm and de are set, or if env doesn't contain anything
518 if (
519 (result->wmPrettyName.length > 0 && result->dePrettyName.length > 0) ||
520 !ffStrSet(env)) {
521 return;
522 }
523
524 // If nothing is set, use env as WM
525 else if (result->wmPrettyName.length == 0 && result->dePrettyName.length == 0) {
526 ffStrbufSetS(&result->wmProcessName, env);
527 ffStrbufSetS(&result->wmPrettyName, env);
528 }
529
530 // If only WM is not set, and DE doesn't equal env, use env as WM
531 else if (
532 result->wmPrettyName.length == 0 &&
533 ffStrbufIgnCaseCompS(&result->deProcessName, env) != 0 &&
534 ffStrbufIgnCaseCompS(&result->dePrettyName, env) != 0) {
535 ffStrbufSetS(&result->wmProcessName, env);
536 ffStrbufSetS(&result->wmPrettyName, env);
537 }
538
539 // If only DE is not set, and WM doesn't equal env, use env as DE
540 else if (
541 result->dePrettyName.length == 0 &&
542 ffStrbufIgnCaseCompS(&result->wmProcessName, env) != 0 &&
543 ffStrbufIgnCaseCompS(&result->wmPrettyName, env) != 0) {
544 ffStrbufSetS(&result->deProcessName, env);
545 ffStrbufSetS(&result->dePrettyName, env);
546 }
547}