master ohmu/fastfetch-openbsd / src / detection / wm / wm_linux.c
ohmu  ·  2026-10-15
  1#include "wm.h"
  2
  3#include "common/processing.h"
  4#include "common/io.h"
  5#include "common/binary.h"
  6#include "common/path.h"
  7#include "common/strutil.h"
  8#include "common/debug.h"
  9
 10const char* ffDetectWMPlugin([[maybe_unused]] FFstrbuf* pluginName) {
 11    FF_DEBUG("Not supported on this platform");
 12    return "Not supported on this platform";
 13}
 14
 15static bool extractCommonWmVersion(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
 16    int count = 0;
 17    sscanf(line, "%*d.%*d.%*d%n", &count);
 18    if (count == 0) {
 19        return true;
 20    }
 21
 22    ffStrbufSetNS((FFstrbuf*) userdata, len, line);
 23    return false;
 24}
 25
 26#if !__ANDROID__
 27static bool extractHyprlandVersion(const char* line, uint32_t len, void* userdata) {
 28    if (line[0] != 'v') {
 29        return true;
 30    }
 31    ++line;
 32    --len;
 33    int count = 0;
 34    sscanf(line, "%*d.%*d.%*d%n", &count);
 35    if (count == 0) {
 36        return true;
 37    }
 38
 39    ffStrbufSetNS((FFstrbuf*) userdata, len, line);
 40    return false;
 41}
 42
 43static const char* getHyprland(FFstrbuf* result) {
 44    FF_DEBUG("Detecting Hyprland version");
 45    FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
 46
 47    FF_DEBUG("Checking for " FF_PATH_PKG_BASE "/include/hyprland/src/version.h file");
 48    if (ffReadFileBuffer(FF_PATH_PKG_BASE "/include/hyprland/src/version.h", result)) {
 49        FF_DEBUG("Found version.h file, extracting version");
 50        if (ffStrbufSubstrAfterFirstS(result, "\n#define GIT_TAG ")) {
 51            ffStrbufSubstrAfterFirstC(result, '"');
 52            ffStrbufSubstrBeforeFirstC(result, '"');
 53            ffStrbufTrimLeft(result, 'v');
 54            FF_DEBUG("Extracted version from version.h: %s", result->chars);
 55            return nullptr;
 56        }
 57        FF_DEBUG("Failed to extract version from version.h");
 58        ffStrbufClear(result);
 59    } else {
 60        FF_DEBUG("version.h file not found, trying Hyprland executable");
 61    }
 62
 63    const char* error = ffFindExecutableInPath("Hyprland", &buffer);
 64    if (error) {
 65        FF_DEBUG("Error finding Hyprland executable: %s", error);
 66        return "Failed to find Hyprland executable path";
 67    }
 68    FF_DEBUG("Found Hyprland executable at: %s", buffer.chars);
 69
 70    ffBinaryExtractStrings(buffer.chars, extractHyprlandVersion, result, (uint32_t) strlen("v0.0.0"));
 71    if (result->length > 0) {
 72        FF_DEBUG("Extracted version from binary strings: %s", result->chars);
 73        return nullptr;
 74    }
 75    FF_DEBUG("Failed to extract version from binary strings, trying --version option");
 76
 77    if (ffProcessAppendStdOut(result, (char* const[]) { buffer.chars, "--version", nullptr }) == nullptr) {
 78        // Hyprland 0.48.1 built from branch  at commit 29e2e59...
 79        // Date: ...
 80        // Tag: v0.48.1, commits: 5937
 81        // ...
 82
 83        FF_DEBUG("Raw version output: %s", result->chars);
 84        // Use tag if available
 85        if (ffStrbufSubstrAfterFirstS(result, "\nTag: v")) {
 86            ffStrbufSubstrBeforeFirstC(result, ',');
 87            FF_DEBUG("Extracted version from Tag: %s", result->chars);
 88        } else {
 89            ffStrbufSubstrAfterFirstC(result, ' ');
 90            ffStrbufSubstrBeforeFirstC(result, ' ');
 91            FF_DEBUG("Extracted version from output: %s", result->chars);
 92        }
 93        return nullptr;
 94    }
 95    FF_DEBUG("Failed to run Hyprland --version command");
 96
 97    return "Failed to run command `Hyprland --version`";
 98}
 99
100static bool extractSwayVersion(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
101    FFstrbuf* result = (FFstrbuf*) userdata;
102    if (!ffStrStartsWith(line, "sway")) {
103        return true;
104    }
105    if (ffStrStartsWith(line + 4, " version ")) {
106        ffStrbufSetNS(result, len - (uint32_t) strlen("sway version "), line + strlen("sway version "));
107        ffStrbufTrimRightSpace(result);
108        return false;
109    } else {
110        char swayfxVer[32], swayVer[32];
111        if (sscanf(line + 4, "fx version %31[^ ] (based on sway %31[^)])", swayfxVer, swayVer) == 2) {
112            ffStrbufSetF(result, "%s [swayfx %s]", swayVer, swayfxVer);
113            return false;
114        }
115    }
116
117    return true;
118}
119
120static const char* getSway(FFstrbuf* result) {
121    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
122    const char* error = ffFindExecutableInPath("sway", &path);
123    if (error) {
124        FF_DEBUG("Failed to find sway executable: %s", error);
125        return "Failed to find sway executable path";
126    }
127
128    ffBinaryExtractStrings(path.chars, extractSwayVersion, result, (uint32_t) strlen("sway version 0.0.0"));
129    if (result->length > 0) {
130        return nullptr;
131    }
132
133    FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
134    if (ffProcessAppendStdOut(&buffer, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // sway version 1.10
135        return extractSwayVersion(buffer.chars, buffer.length, result) ? "Failed to parse sway version output" : nullptr;
136    }
137
138    FF_DEBUG("Failed to run command `sway --version`");
139    return "Failed to run command `sway --version`";
140}
141
142static const char* getLabwc(FFstrbuf* result) {
143    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
144    const char* error = ffFindExecutableInPath("labwc", &path);
145    if (error) {
146        FF_DEBUG("Failed to find labwc executable: %s", error);
147        return "Failed to find labwc executable path";
148    }
149
150    ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0"));
151    if (result->length > 0) {
152        return nullptr;
153    }
154
155    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // labwc 0.9.0 (+xwayland +nls +rsvg +libsfdo)
156        ffStrbufSubstrAfterFirstC(result, ' ');
157        ffStrbufSubstrBeforeFirstC(result, ' ');
158        return nullptr;
159    }
160
161    FF_DEBUG("Failed to run command `labwc --version`");
162    return "Failed to run command `labwc --version`";
163}
164
165static const char* getNiri(FFstrbuf* result) {
166    if (ffProcessAppendStdOut(result, (char* const[]) { "niri", "--version", nullptr }) == nullptr) { // niri 25.11 (commit b35bcae)
167        ffStrbufSubstrAfterFirstC(result, ' ');
168        ffStrbufSubstrBeforeLastC(result, '(');
169        ffStrbufTrimRightSpace(result);
170        return nullptr;
171    }
172
173    FF_DEBUG("Failed to run command `niri --version`");
174    return "Failed to run command `niri --version`";
175}
176
177static const char* getUmbriel(FFstrbuf* result) {
178    if (ffProcessAppendStdOut(result, (char* const[]) { "umbriel", "--version", nullptr }) == nullptr) { // umbriel 0.1.0 (7a448abe550e)
179        ffStrbufSubstrAfterFirstC(result, ' ');
180        ffStrbufSubstrBeforeLastC(result, '(');
181        ffStrbufTrimRightSpace(result);
182        return nullptr;
183    }
184
185    FF_DEBUG("Failed to run command `umbriel --version`");
186    return "Failed to run command `umbriel --version`";
187}
188
189static const char* getWeston(FFstrbuf* result) {
190    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
191    const char* error = ffFindExecutableInPath("weston", &path);
192    if (error) {
193        FF_DEBUG("Failed to find weston executable: %s", error);
194        return "Failed to find weston executable path";
195    }
196
197    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // weston 8.0.0\n...
198        ffStrbufSubstrBeforeFirstC(result, '\n');
199        ffStrbufSubstrAfterLastC(result, ' ');
200        return nullptr;
201    }
202
203    FF_DEBUG("Failed to run command `weston --version`");
204    return "Failed to run command `weston --version`";
205}
206
207    #ifdef __linux__
208static const char* getWslg(FFstrbuf* result) {
209    if (!ffAppendFileBuffer("/mnt/wslg/versions.txt", result)) {
210        FF_DEBUG("Failed to read /mnt/wslg/versions.txt");
211        return "Failed to read /mnt/wslg/versions.txt";
212    }
213
214    if (ffStrbufStartsWithS(result, "WSLg: ")) { // WSL 2.9.3+
215        ffStrbufSubstrBeforeFirstC(result, '\n');
216        ffStrbufSubstrAfter(result, (uint32_t) (strlen("WSLg: ") - 1));
217    } else if (ffStrbufStartsWithS(result, "WSLg ")) {
218        ffStrbufSubstrBeforeFirstC(result, '\n');
219        ffStrbufSubstrBeforeFirstC(result, '+');
220        ffStrbufSubstrAfterFirstC(result, ':');
221        ffStrbufTrimLeft(result, ' ');
222    } else {
223        ffStrbufClear(result);
224        FF_DEBUG("Failed to parse WSLg version from /mnt/wslg/versions.txt");
225        return "Failed to parse WSLg version from /mnt/wslg/versions.txt";
226    }
227
228    return nullptr;
229}
230    #endif
231
232#endif // !__ANDROID__
233
234static bool extractI3Version(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
235    int count = 0;
236    sscanf(line, "%*d.%*d%n", &count);
237    if (count == 0) {
238        return true;
239    }
240
241    ffStrbufSetNS((FFstrbuf*) userdata, len, line);
242    return false;
243}
244
245static const char* getI3(FFstrbuf* result) {
246    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
247    const char* error = ffFindExecutableInPath("i3", &path);
248    if (error) {
249        FF_DEBUG("Failed to find i3 executable: %s", error);
250        return "Failed to find i3 executable path";
251    }
252
253    ffBinaryExtractStrings(path.chars, extractI3Version, result, (uint32_t) strlen("0.0"));
254    if (result->length > 0) {
255        return nullptr;
256    }
257
258    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // i3 version 1.10 C 2009...
259        ffStrbufSubstrAfterFirstS(result, "version ");
260        ffStrbufSubstrBeforeFirstC(result, ' ');
261        return nullptr;
262    }
263
264    FF_DEBUG("Failed to run command `i3 --version`");
265    return "Failed to run command `i3 --version`";
266}
267
268static const char* getCtwm(FFstrbuf* result) {
269    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
270    const char* error = ffFindExecutableInPath("ctwm", &path);
271    if (error) {
272        FF_DEBUG("Failed to find ctwm executable: %s", error);
273        return "Failed to find ctwm executable path";
274    }
275
276    ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0"));
277    if (result->length > 0) {
278        return nullptr;
279    }
280
281    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // ctwm version 4.0.1\n...
282        ffStrbufSubstrBeforeFirstC(result, '\n');
283        ffStrbufSubstrAfterLastC(result, ' ');
284        return nullptr;
285    }
286
287    FF_DEBUG("Failed to run command `ctwm --version`");
288    return "Failed to run command `ctwm --version`";
289}
290
291static const char* getFvwm(FFstrbuf* result) {
292    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
293    const char* error = ffFindExecutableInPath("fvwm", &path);
294    if (error) {
295        FF_DEBUG("Failed to find fvwm executable: %s", error);
296        return "Failed to find fvwm executable path";
297    }
298
299    ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0"));
300    if (result->length > 0) {
301        return nullptr;
302    }
303
304    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "-version", nullptr }) == nullptr) { // [FVWM][main]: fvwm Version 2.2.5\n...
305        ffStrbufSubstrBeforeFirstC(result, '\n');
306        ffStrbufSubstrAfterLastC(result, ' ');
307        return nullptr;
308    }
309
310    FF_DEBUG("Failed to run command `fvwm -version`");
311    return "Failed to run command `fvwm -version`";
312}
313
314static const char* getOpenbox(FFstrbuf* result) {
315    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
316    const char* error = ffFindExecutableInPath("openbox", &path);
317    if (error) {
318        FF_DEBUG("Failed to find openbox executable: %s", error);
319        return "Failed to find openbox executable path";
320    }
321
322    ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0"));
323    if (result->length > 0) {
324        return nullptr;
325    }
326
327    if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", nullptr }) == nullptr) { // Openbox 3.6.1\n...
328        ffStrbufSubstrBeforeFirstC(result, '\n');
329        ffStrbufSubstrAfterLastC(result, ' ');
330        return nullptr;
331    }
332
333    FF_DEBUG("Failed to run command `openbox --version`");
334    return "Failed to run command `openbox --version`";
335}
336
337const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_unused]] FFWMOptions* options) {
338    if (!wmName) {
339        FF_DEBUG("No WM detected");
340        return "No WM detected";
341    }
342
343#if !__ANDROID__
344    // Wayland compositors
345    if (ffStrbufIgnCaseEqualS(wmName, "Hyprland")) {
346        return getHyprland(result);
347    }
348
349    if (ffStrbufEqualS(wmName, "sway")) {
350        return getSway(result);
351    }
352
353    if (ffStrbufEqualS(wmName, "labwc")) {
354        return getLabwc(result);
355    }
356
357    if (ffStrbufEqualS(wmName, "niri")) {
358        return getNiri(result);
359    }
360
361    if (ffStrbufEqualS(wmName, "umbriel")) {
362        return getUmbriel(result);
363    }
364
365    if (ffStrbufEqualS(wmName, "weston")) {
366        return getWeston(result);
367    }
368
369    #if __linux__
370    if (ffStrbufEqualS(wmName, "WSLg")) {
371        return getWslg(result);
372    }
373    #endif
374#endif
375
376    // X11 WMs
377    if (ffStrbufEqualS(wmName, "i3")) {
378        return getI3(result);
379    }
380
381    if (ffStrbufEqualS(wmName, "ctwm")) {
382        return getCtwm(result);
383    }
384
385    if (ffStrbufEqualS(wmName, "fvwm")) {
386        return getFvwm(result);
387    }
388
389    if (ffStrbufEqualS(wmName, "Openbox")) {
390        return getOpenbox(result);
391    }
392
393    FF_DEBUG("Unsupported WM: %s", wmName->chars);
394    return "Unsupported WM";
395}