master ohmu/fastfetch-openbsd / src / detection / displayserver / linux / wayland / wayland.c
ohmu  ·  2026-10-15
  1#include "../displayserver_linux.h"
  2#include "common/io.h"
  3#include "common/edidHelper.h"
  4#include "common/strutil.h"
  5
  6#include <stdlib.h>
  7#include <string.h>
  8
  9#ifdef FF_HAVE_WAYLAND
 10
 11    #include <sys/socket.h>
 12
 13    #include "common/properties.h"
 14
 15    #include "wayland.h"
 16    #include "kde-output-device-v2-client-protocol.h"
 17    #include "xdg-output-unstable-v1-client-protocol.h"
 18    #include "wp-color-management-v1-client-protocol.h"
 19
 20    #if __FreeBSD__
 21        #include <sys/un.h>
 22        #include <sys/ucred.h>
 23        #include <sys/sysctl.h>
 24    #endif
 25
 26static bool waylandDetectWM(int fd, FFDisplayServerResult* result) {
 27    #if __linux__ || __GNU__ || (__FreeBSD__ && !__DragonFly__)
 28
 29        #if __linux__ || __GNU__
 30    struct ucred ucred = {};
 31    socklen_t len = sizeof(ucred);
 32    if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == -1 || ucred.pid <= 0) {
 33        return false;
 34    }
 35
 36    FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreate();
 37    ffStrbufAppendF(&procPath, "/proc/%d/cmdline", ucred.pid); // We check the cmdline for the process name, because it is not trimmed.
 38    if (!ffReadFileBuffer(procPath.chars, &result->wmProcessName)) {
 39        return false;
 40    }
 41        #else
 42    struct xucred ucred = {};
 43    socklen_t len = sizeof(ucred);
 44    if (getsockopt(fd, AF_UNSPEC, LOCAL_PEERCRED, &ucred, &len) == -1 || ucred.cr_pid <= 0) {
 45        return false;
 46    }
 47
 48    size_t size = 4096;
 49    ffStrbufEnsureFixedLengthFree(&result->wmProcessName, (uint32_t) size);
 50
 51    if (sysctl((int[]) { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, ucred.cr_pid }, 4, result->wmProcessName.chars, &size, nullptr, 0) != 0) {
 52        return false;
 53    }
 54    result->wmProcessName.length = (uint32_t) size - 1;
 55        #endif
 56
 57    // #1135: wl-restart is a special case
 58    const char* filename = strrchr(result->wmProcessName.chars, '/');
 59    if (filename) {
 60        filename++;
 61    } else {
 62        filename = result->wmProcessName.chars;
 63    }
 64
 65    if (ffStrEquals(filename, "wl-restart")) {
 66        ffStrbufSubstrAfterLastC(&result->wmProcessName, '\0');
 67    }
 68
 69    ffStrbufSubstrBeforeFirstC(&result->wmProcessName, '\0'); // Trim the arguments
 70    ffStrbufSubstrAfterLastC(&result->wmProcessName, '/');    // Trim the path
 71
 72    return true;
 73
 74    #else
 75    FF_UNUSED(fd, result);
 76    return false;
 77    #endif
 78}
 79
 80static void waylandGlobalAddListener(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
 81    WaylandData* wldata = data;
 82
 83    if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL) && ffStrEquals(interface, wl_output_interface.name)) {
 84        wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_GLOBAL;
 85        if (ffWaylandHandleGlobalOutput(wldata, registry, name, version) != nullptr) {
 86            wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
 87        }
 88    } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, kde_output_device_registry_v2_interface.name)) {
 89        wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_KDE_REGISTRY;
 90        if (ffWaylandHandleKdeOutputRegistry(wldata, registry, name, version) != nullptr) {
 91            wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
 92        }
 93    } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_KDE_DEPRECATED) && ffStrEquals(interface, kde_output_device_v2_interface.name)) {
 94        wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_KDE_DEPRECATED;
 95        if (ffWaylandHandleKdeOutput(wldata, registry, name, version) != nullptr) {
 96            wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE;
 97        }
 98    } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, zxdg_output_manager_v1_interface.name)) {
 99        ffWaylandHandleZxdgOutput(wldata, registry, name, version);
100    } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, wp_color_manager_v1_interface.name)) {
101        ffWaylandHandleWpColor(wldata, registry, name, version);
102    }
103}
104
105[[maybe_unused]] static bool matchDrmConnector(const char* connName, WaylandDisplay* wldata) {
106    // https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_output-event-name
107    // The doc says that "do not assume that the name is a reflection of an underlying DRM connector, X11 connection, etc."
108    // However I can't find a better method to get the edid data
109    const char* drmDirPath = "/sys/class/drm/";
110
111    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(drmDirPath);
112    if (dirp == nullptr) {
113        return false;
114    }
115
116    struct dirent* entry;
117    while ((entry = readdir(dirp)) != nullptr) {
118        const char* plainName = entry->d_name;
119        if (ffStrStartsWith(plainName, "card")) {
120            const char* tmp = strchr(plainName + strlen("card"), '-');
121            if (tmp) {
122                plainName = tmp + 1;
123            }
124        }
125        if (ffStrEquals(plainName, connName)) {
126            FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateF("%s%s/edid", drmDirPath, entry->d_name);
127
128            uint8_t edidData[512];
129            ssize_t edidLength = ffReadFileData(path.chars, ARRAY_SIZE(edidData), edidData);
130            if (edidLength > 0 && ffEdidIsValid(edidData, (uint32_t) edidLength)) {
131                ffEdidGetName(edidData, &wldata->edidName);
132                wldata->hdrSupported = ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength);
133                ffEdidGetManufactureDate((const uint8_t*) edidData, &wldata->myear, &wldata->mweek);
134                ffEdidGetSerial((const uint8_t*) edidData, &wldata->serial);
135                if (wldata->preferredWidth == 0) {
136                    uint32_t preferredWidth = 0, preferredHeight = 0;
137                    double preferredRefreshRate = 0;
138                    ffEdidGetPreferredResolutionAndRefreshRate(edidData, &preferredWidth, &preferredHeight, &preferredRefreshRate);
139                    wldata->preferredWidth = (int32_t) preferredWidth;
140                    wldata->preferredHeight = (int32_t) preferredHeight;
141                    wldata->preferredRefreshRate = (int32_t) (preferredRefreshRate * 1000.);
142                }
143                wldata->hdrInfoAvailable = true;
144                return true;
145            }
146            break;
147        }
148    }
149    return false;
150}
151
152void ffWaylandOutputNameListener(void* data, [[maybe_unused]] void* output, const char* name) {
153    WaylandDisplay* display = data;
154    if (display->id) {
155        return;
156    }
157
158    display->type = ffdsGetDisplayType(name);
159    #if __linux__
160    if (!display->edidName.length) {
161        matchDrmConnector(name, display);
162    }
163    #endif
164    display->id = ffWaylandGenerateIdFromName(name);
165    ffStrbufAppendS(&display->name, name);
166}
167
168void ffWaylandOutputDescriptionListener(void* data, [[maybe_unused]] void* output, const char* description) {
169    WaylandDisplay* display = data;
170    if (display->description.length) {
171        return;
172    }
173
174    while (*description == ' ') {
175        ++description;
176    }
177    if (!ffStrEquals(description, "Unknown Display") && !ffStrContains(description, "(null)")) {
178        ffStrbufAppendS(&display->description, description);
179    }
180}
181
182uint32_t ffWaylandHandleRotation(WaylandDisplay* display) {
183    uint32_t rotation;
184    switch (display->transform) {
185        case WL_OUTPUT_TRANSFORM_FLIPPED_90:
186        case WL_OUTPUT_TRANSFORM_90:
187            rotation = 90;
188            break;
189        case WL_OUTPUT_TRANSFORM_FLIPPED_180:
190        case WL_OUTPUT_TRANSFORM_180:
191            rotation = 180;
192            break;
193        case WL_OUTPUT_TRANSFORM_FLIPPED_270:
194        case WL_OUTPUT_TRANSFORM_270:
195            rotation = 270;
196            break;
197        default:
198            rotation = 0;
199            break;
200    }
201
202    switch (rotation) {
203        case 90:
204        case 270: {
205            int32_t temp = display->width;
206            display->width = display->height;
207            display->height = temp;
208
209            temp = display->physicalWidth;
210            display->physicalWidth = display->physicalHeight;
211            display->physicalHeight = temp;
212            break;
213        }
214        default:
215            break;
216    }
217    return rotation;
218}
219
220const char* ffWaylandWaitForDone(WaylandDisplay* display) {
221    int32_t timeout = instance.config.general.processingTimeout;
222    struct timespec ts;
223    if (timeout == 0) {
224        return "timeout is disabled";
225    } else if (timeout > 0) {
226        if (!display->parent->ffwl_display_dispatch_timeout) {
227            return "timeout is enabled, but the libwayland-client version is too old to support it";
228        }
229        ts.tv_sec = timeout / 1000;
230        ts.tv_nsec = (timeout % 1000) * 1000000;
231    }
232
233    WaylandData* wldata = display->parent;
234
235    // Some compositors emit the final output state asynchronously after the roundtrip callback (#2074)
236    while (!display->done) {
237        if (timeout > 0) {
238            if (wldata->ffwl_display_dispatch_timeout(wldata->display, &ts) < 0) {
239                return "wl_display_dispatch_timeout() failed";
240            }
241        } else if (timeout < 0) {
242            if (wldata->ffwl_display_dispatch(wldata->display) < 0) {
243                return "wl_display_dispatch() failed";
244            }
245        } else {
246            return "Timeout exceeded, but the compositor did not send a done event";
247        }
248    }
249
250    return nullptr;
251}
252
253const char* ffdsConnectWayland(FFDisplayServerResult* result) {
254    if (getenv("XDG_RUNTIME_DIR") == nullptr) {
255        return "Wayland requires $XDG_RUNTIME_DIR being set";
256    }
257
258    FF_LIBRARY_LOAD_MESSAGE(wayland, "libwayland-client" FF_LIBRARY_EXTENSION, 1)
259
260    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_connect)
261    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_get_fd)
262    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_proxy_marshal_constructor)
263    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_disconnect)
264
265    WaylandData data = {};
266
267    FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_marshal_constructor_versioned)
268    FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_add_listener)
269    FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_destroy)
270    FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_display_roundtrip)
271
272    if (instance.config.general.processingTimeout < 0) {
273        FF_LIBRARY_LOAD_SYMBOL_VAR_LAZY(wayland, data, wl_display_dispatch)
274    } else if (instance.config.general.processingTimeout > 0) {
275        FF_LIBRARY_LOAD_SYMBOL_VAR_LAZY(wayland, data, wl_display_dispatch_timeout)
276    }
277
278    data.display = ffwl_display_connect(nullptr);
279    if (data.display == nullptr) {
280        return "wl_display_connect returned nullptr";
281    }
282
283    waylandDetectWM(ffwl_display_get_fd(data.display), result);
284
285    struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, nullptr);
286    if (registry == nullptr) {
287        ffwl_display_disconnect(data.display);
288        return "wl_display_get_registry returned nullptr";
289    }
290
291    data.result = result;
292
293    struct wl_registry_listener registry_listener = {
294        .global = waylandGlobalAddListener,
295        .global_remove = (void*) ffUnused
296    };
297
298    data.ffwl_proxy_add_listener(registry, (void (**)(void)) &registry_listener, &data);
299    data.ffwl_display_roundtrip(data.display);
300
301    if (data.zxdgOutputManager) {
302        data.ffwl_proxy_destroy(data.zxdgOutputManager);
303    }
304
305    if (data.wpColorManager) {
306        data.ffwl_proxy_destroy(data.wpColorManager);
307    }
308
309    data.ffwl_proxy_destroy(registry);
310    ffwl_display_disconnect(data.display);
311
312    {
313        const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell")
314            ? "monitors.xml"
315            : ffStrbufEqualS(&result->wmProcessName, "cinnamon")
316            ? "cinnamon-monitors.xml"
317            : nullptr;
318        if (fileName) {
319            FF_STRBUF_AUTO_DESTROY monitorsXml = ffStrbufCreate();
320            FF_LIST_FOR_EACH (FFstrbuf, basePath, instance.state.platform.configDirs) {
321                char path[1024];
322                snprintf(path, ARRAY_SIZE(path), "%s%s", basePath->chars, fileName);
323                if (ffReadFileBuffer(path, &monitorsXml)) {
324                    break;
325                }
326            }
327            if (monitorsXml.length) {
328                // <monitors version="2">
329                // <configuration>
330                //     <logicalmonitor>
331                //     <x>0</x>
332                //     <y>0</y>
333                //     <scale>1.7489879131317139</scale>
334                //     <primary>yes</primary>
335                //     <monitor>
336                //         <monitorspec>
337                //         <connector>Virtual-1</connector>
338                //         <vendor>unknown</vendor>
339                //         <product>unknown</product>
340                //         <serial>unknown</serial>
341                //         </monitorspec>
342                //         <mode>
343                //         <width>3456</width>
344                //         <height>2160</height>
345                //         <rate>60.000068664550781</rate>
346                //         </mode>
347                //     </monitor>
348                //     </logicalmonitor>
349                // </configuration>
350                // </monitors>
351                uint32_t start = ffStrbufFirstIndexS(&monitorsXml, "<primary>yes</primary>");
352                if (start < monitorsXml.length) {
353                    start = ffStrbufNextIndexS(&monitorsXml, start, "<connector>");
354                    if (start < monitorsXml.length) {
355                        uint32_t end = ffStrbufNextIndexS(&monitorsXml, start, "</connector>");
356                        if (end < monitorsXml.length) {
357                            ffStrbufSubstrBefore(&monitorsXml, end);
358                            const char* name = monitorsXml.chars + start + strlen("<connector>");
359                            uint64_t primaryDisplayId = ffWaylandGenerateIdFromName(name);
360
361                            FF_LIST_FOR_EACH (FFDisplayResult, d, data.result->displays) {
362                                if (d->id == primaryDisplayId) {
363                                    d->primary = true;
364                                    break;
365                                }
366                            }
367                        }
368                    }
369                }
370            }
371        }
372    }
373
374    // We successfully connected to wayland and detected the display.
375    // So we can set set the session type to wayland.
376    // This is used as an indicator that we are running wayland by the x11 backends.
377    ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND);
378    return nullptr;
379}
380
381#else
382
383const char* ffdsConnectWayland([[maybe_unused]] FFDisplayServerResult* result) {
384    return "Fastfetch was compiled without Wayland support";
385}
386
387#endif