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}