ohmu
·
2026-10-15
1#include "fastfetch.h"
2#include "common/io.h"
3#include "common/processing.h"
4#include "common/properties.h"
5#include "common/path.h"
6#include "common/strutil.h"
7#include "common/binary.h"
8
9#include <ctype.h>
10#include <stdint.h>
11
12#if _WIN32
13 #include "common/windows/registry.h"
14 #include "common/windows/version.h"
15 #include <windows.h>
16
17static bool getFileVersion(const FFstrbuf* exePath, const wchar_t* stringName, FFstrbuf* version) {
18 wchar_t exePathW[PATH_MAX + 1];
19 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(exePathW, (ULONG) sizeof(exePathW), nullptr, exePath->chars, (ULONG) exePath->length + 1))) {
20 return false;
21 }
22 return ffGetFileVersion(exePathW, stringName, version);
23}
24
25#elif __HAIKU__
26 #include "common/haiku/version.h"
27#elif __APPLE__
28 #include "common/apple/version.h"
29#endif
30
31static bool getExeVersionRaw(FFstrbuf* exe, FFstrbuf* version) {
32 return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) == nullptr && version->length > 0;
33}
34
35static bool getExeVersionGeneral(FFstrbuf* exe, FFstrbuf* version) {
36 if (!getExeVersionRaw(exe, version)) {
37 return false;
38 }
39
40 ffStrbufSubstrAfterFirstC(version, ' ');
41 ffStrbufSubstrBeforeFirstC(version, ' ');
42 return version->length > 0;
43}
44
45static bool extractBashVersion(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
46 if (!ffStrStartsWith(line, "@(#)Bash version ")) {
47 return true;
48 }
49 const char* start = line + strlen("@(#)Bash version ");
50 const char* end = strchr(start, '(');
51 if (!end) {
52 return true;
53 }
54 ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start);
55 return false;
56}
57
58static bool getShellVersionBash(FFstrbuf* exe, FFstrbuf* version) {
59 ffBinaryExtractStrings(exe->chars, extractBashVersion, version, (uint32_t) strlen("@(#)Bash version 0.0.0(0) release GNU"));
60 if (version->length > 0) {
61 return true;
62 }
63
64 if (!getExeVersionRaw(exe, version)) {
65 return false;
66 }
67
68 // GNU bash, version 5.1.16(1)-release (x86_64-pc-msys)\nCopyright...
69 ffStrbufSubstrBeforeFirstC(version, '('); // GNU bash, version 5.1.16
70 ffStrbufSubstrAfterLastC(version, ' '); // 5.1.16
71 return true;
72}
73
74static bool getShellVersionFish(FFstrbuf* exe, FFstrbuf* version) {
75 if (!getExeVersionRaw(exe, version)) {
76 return false;
77 }
78
79 // fish, version 4.0.2-1 (Built by MSYS2 project) // version can be localized if LC_ALL is set
80 if (version->length < strlen("fish, v") || !ffStrbufStartsWithS(version, "fish")) {
81 return false;
82 }
83 uint32_t index = ffStrbufFirstIndexC(version, ' '); // skip "fish,"
84 while (index + 1 < version->length && !ffCharIsDigit(version->chars[index + 1])) {
85 index = ffStrbufNextIndexC(version, index + 1, ' '); // skip "version"
86 }
87 if (index + 1 >= version->length) {
88 return false;
89 }
90
91 ffStrbufSubstrAfter(version, index);
92 ffStrbufSubstrBeforeFirstC(version, ' ');
93 return true;
94}
95
96static bool getShellVersionPwsh(FFstrbuf* exe, FFstrbuf* version) {
97 // Requires manually setting $POWERSHELL_VERSION
98 // $env:POWERSHELL_VERSION = $PSVersionTable.PSVersion.ToString(); fastfetch.exe
99 const char* env = getenv("POWERSHELL_VERSION");
100 if (env) {
101 ffStrbufSetS(version, env);
102 return true;
103 }
104
105#ifdef _WIN32
106 if (getFileVersion(exe, nullptr, version)) {
107 ffStrbufSubstrBeforeLastC(version, '.');
108 return true;
109 }
110#endif
111
112 if (!getExeVersionRaw(exe, version)) {
113 return false;
114 }
115
116 ffStrbufSubstrAfterLastC(version, ' ');
117 return true;
118}
119
120static bool getShellVersionKsh(FFstrbuf* exe, FFstrbuf* version) {
121 ffStrbufClear(version);
122
123 // ksh93 has used both stderr and stdout for --version, and the exit
124 // status is not consistent across releases. Parse any captured output
125 // before trying the next probe.
126 for (int attempt = 0; attempt < 4; ++attempt) {
127 ffStrbufClear(version);
128 if (attempt < 2 && (ffStrbufEndsWithS(exe, "mksh") || ffStrbufEndsWithS(exe, "pdksh") || ffStrbufEndsWithS(exe, "oksh")))
129 continue;
130
131 if (attempt == 0) {
132 ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", nullptr });
133 } else if (attempt == 1) {
134 ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr });
135 } else if (attempt == 2) {
136 ffProcessAppendStdOut(version, (char* const[]) {
137 exe->chars,
138 "-c",
139 "printf '%s\\n' \"${KSH_VERSION:-${SH_VERSION:-}}\"",
140 nullptr
141 });
142 } else {
143 ffProcessAppendStdOut(version, (char* const[]) {
144 exe->chars,
145 "-c",
146 "printf '%s\\n' \"${.sh.version}\"",
147 nullptr
148 });
149 }
150
151 const char* p = version->chars;
152 while (*p && isspace((unsigned char) *p))
153 ++p;
154 if (!*p)
155 continue;
156
157 // @(#)PD KSH v5.2.14, @(#)MIRBSD KSH R59, @(#)LEGACY KSH R59
158 if (ffStrStartsWith(p, "@(#)")) {
159 const char* marker = strstr(p, " KSH ");
160 if (!marker)
161 continue;
162
163 const char* start = marker + strlen(" KSH ");
164 while (*start && isspace((unsigned char) *start))
165 ++start;
166 const char* end = start;
167 while (*end && !isspace((unsigned char) *end))
168 ++end;
169
170 uint32_t length = (uint32_t) (end - start);
171 if (length > 1 && start[0] == 'v' && ffCharIsDigit(start[1])) {
172 ++start;
173 --length;
174 }
175 if (length == 0 || (!ffCharIsDigit(start[0]) && !(start[0] == 'R' && length > 1 && ffCharIsDigit(start[1]))))
176 continue;
177
178 ffStrbufSetNS(version, length, start);
179 return true;
180 }
181
182 const char* core = nullptr;
183 if (ffStrStartsWith(p, "version")) {
184 // ksh93 self-doc: version sh (AT&T [Labs] Research) ...
185 const char* selfDoc = strstr(p, " sh (");
186 const char* end = selfDoc ? strchr(selfDoc, ')') : nullptr;
187 if (!end)
188 continue;
189 core = end + 1;
190 } else if (ffStrStartsWith(p, "Version ")) {
191 // $KSH_VERSION / ${.sh.version}: Version <features> <version> <date>
192 core = p + strlen("Version ");
193 } else {
194 continue;
195 }
196
197 while (*core && isspace((unsigned char) *core))
198 ++core;
199
200 const char* token = core;
201 while (*token) {
202 while (*token && isspace((unsigned char) *token))
203 ++token;
204 if (!*token)
205 break;
206
207 const char* end = token;
208 while (*end && !isspace((unsigned char) *end))
209 ++end;
210 uint32_t length = (uint32_t) (end - token);
211
212 bool valid = false;
213 if (length >= 2 && token[0] == '9' && token[1] == '3') {
214 valid = true;
215 for (uint32_t i = 2; i < length; ++i) {
216 char c = token[i];
217 if (!(ffCharIsDigit(c) || ffCharIsEnglishAlphabet(c) || c == '+' || c == '.' || c == '_' || c == '/' || c == '-')) {
218 valid = false;
219 break;
220 }
221 }
222 } else if (length >= 5 && token[0] == 'M' && token[1] == '-') {
223 for (uint32_t i = 2; i + 2 < length; ++i) {
224 if (token[i] == '/' && token[i + 1] == '9' && token[i + 2] == '3') {
225 valid = true;
226 break;
227 }
228 }
229 } else if (length > 0 && ffCharIsDigit(token[0]) && (memchr(token, '.', length) || memchr(token, '-', length))) {
230 valid = true;
231 } else if (length > 1 && token[0] == 'v' && ffCharIsDigit(token[1])) {
232 valid = true;
233 }
234
235 if (valid) {
236 uint32_t resultLength = length;
237 bool date = length >= 10 &&
238 ffCharIsDigit(token[0]) && ffCharIsDigit(token[1]) &&
239 ffCharIsDigit(token[2]) && ffCharIsDigit(token[3]) && token[4] == '-' &&
240 ffCharIsDigit(token[5]) && ffCharIsDigit(token[6]) && token[7] == '-' &&
241 ffCharIsDigit(token[8]) && ffCharIsDigit(token[9]);
242 if (date) {
243 const char* next = end;
244 while (*next && isspace((unsigned char) *next))
245 ++next;
246 const char* nextEnd = next;
247 while (*nextEnd && !isspace((unsigned char) *nextEnd))
248 ++nextEnd;
249 uint32_t nextLength = (uint32_t) (nextEnd - next);
250 if (nextLength > 0 && nextLength <= 2) {
251 bool suffix = true;
252 for (uint32_t i = 0; i < nextLength; ++i) {
253 if (!ffCharIsEnglishAlphabet(next[i]) && next[i] != '+') {
254 suffix = false;
255 break;
256 }
257 }
258 if (suffix)
259 resultLength = (uint32_t) (nextEnd - token);
260 }
261 }
262
263 const char* start = token;
264 if (resultLength > 1 && start[0] == 'v' && ffCharIsDigit(start[1])) {
265 ++start;
266 --resultLength;
267 }
268 ffStrbufSetNS(version, resultLength, start);
269 return version->length > 0;
270 }
271
272 token = end;
273 }
274 }
275
276 ffStrbufClear(version);
277 return false;
278}
279
280static bool getShellVersionOksh(FFstrbuf* exe, FFstrbuf* version) {
281 // Homebrew version
282 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $OKSH_VERSION", nullptr }) != nullptr) {
283 return false;
284 }
285
286 // oksh 7.3
287 ffStrbufSubstrAfterFirstC(version, ' ');
288 return true;
289}
290
291static bool getShellVersionOils(FFstrbuf* exe, FFstrbuf* version) {
292 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) != nullptr) {
293 return false;
294 }
295
296 // Oils 0.18.0 https://www.oilshell.org/...
297 ffStrbufSubstrAfterFirstC(version, ' ');
298 ffStrbufSubstrBeforeFirstC(version, '\t');
299 return true;
300}
301
302static bool getShellVersionNushell(FFstrbuf* exe, FFstrbuf* version) {
303 ffStrbufSetS(version, getenv("NU_VERSION"));
304 if (version->length) {
305 return true;
306 }
307 return getExeVersionRaw(exe, version); // 0.73.0
308}
309
310static bool extractBusyboxVersion(const char* line, uint32_t len, void* userdata) {
311 if (!ffStrStartsWith(line, "BusyBox v")) {
312 return true;
313 }
314
315 line += strlen("BusyBox v");
316 len -= (uint32_t) strlen("BusyBox v");
317 const char* space = memchr(line, ' ', len);
318 if (space) {
319 len = (uint32_t) (space - line);
320 }
321
322 ffStrbufSetNS((FFstrbuf*) userdata, len, line);
323 return false;
324}
325
326static bool getShellVersionAsh(FFstrbuf* exe, FFstrbuf* version) {
327 ffBinaryExtractStrings(exe->chars, extractBusyboxVersion, version, (uint32_t) strlen("BusyBox v0.0.0"));
328 if (version->length > 0) {
329 return true;
330 }
331
332 const char* error = ffStrbufEndsWithS(exe, "busybox")
333 ? ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "ash", "--help", nullptr })
334 : ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--help", nullptr });
335 if (error != nullptr) {
336 return false;
337 }
338
339 // BusyBox v1.36.1 (2023-11-07 18:53:09 UTC) multi-call binary...
340 ffStrbufSubstrAfterFirstC(version, ' ');
341 ffStrbufSubstrBeforeFirstC(version, ' ');
342 ffStrbufTrimLeft(version, 'v');
343 return true;
344}
345
346static bool getShellVersionXonsh([[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
347 ffStrbufSetS(version, getenv("XONSH_VERSION"));
348 if (version->length) {
349 return true;
350 }
351
352 // exe is python here
353 if (ffProcessAppendStdErr(version, (char* const[]) { "xonsh", "--version", nullptr }) != nullptr) {
354 return false;
355 }
356
357 // xonsh/0.14.1
358 ffStrbufSubstrAfterFirstC(version, '/');
359 return true;
360}
361
362static bool extractZshVersion(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
363 if (!ffStrStartsWith(line, "zsh-")) {
364 return true;
365 }
366 const char* start = line + strlen("zsh-");
367 const char* end = strchr(start, '-');
368 if (!end) {
369 return true;
370 }
371
372 ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start);
373 return false;
374}
375
376static bool getShellVersionZsh(FFstrbuf* exe, FFstrbuf* version) {
377 ffBinaryExtractStrings(exe->chars, extractZshVersion, version, (uint32_t) strlen("zsh-0.0-0"));
378 if (version->length) {
379 return true;
380 }
381
382 return getExeVersionGeneral(exe, version); // zsh 5.9 (arm-apple-darwin21.3.0)
383}
384
385#ifdef _WIN32
386static bool getShellVersionWinPowerShell(FFstrbuf* exe, FFstrbuf* version) {
387 const char* env = getenv("POWERSHELL_VERSION");
388 if (env) {
389 ffStrbufSetS(version, env);
390 return true;
391 }
392
393 FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
394 if (ffRegOpenSubkeyForRead(ffRegGetRootKeyHandle(HKEY_LOCAL_MACHINE), L"SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", &hKey, nullptr) && ffRegReadStrbuf(hKey, L"PowerShellVersion", version, nullptr)) {
395 return true;
396 }
397
398 // Extremely slow
399 return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()", nullptr }) == nullptr;
400}
401#endif
402
403bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version) {
404 if (ffStrEqualsIgnCase(exeName, "sh")) { // #849
405 return false;
406 }
407
408 if (ffStrEqualsIgnCase(exeName, "bash")) {
409 return getShellVersionBash(exe, version);
410 }
411 if (ffStrEqualsIgnCase(exeName, "zsh")) {
412 return getShellVersionZsh(exe, version);
413 }
414 if (ffStrEqualsIgnCase(exeName, "fish")) {
415 return getShellVersionFish(exe, version);
416 }
417 if (ffStrEqualsIgnCase(exeName, "pwsh")) {
418 return getShellVersionPwsh(exe, version);
419 }
420 if (ffStrEqualsIgnCase(exeName, "csh") || ffStrEqualsIgnCase(exeName, "tcsh")) {
421 return getExeVersionGeneral(exe, version); // tcsh 6.24.07 (Astron) 2022-12-21 (aarch64-apple-darwin) options wide,nls,dl,al,kan,sm,rh,color,filec
422 }
423 if (ffStrEqualsIgnCase(exeName, "nu")) {
424 return getShellVersionNushell(exe, version);
425 }
426 if (ffStrEqualsIgnCase(exeName, "ksh") || ffStrEqualsIgnCase(exeName, "ksh93") || ffStrEqualsIgnCase(exeName, "mksh") || ffStrEqualsIgnCase(exeName, "pdksh")) {
427 return getShellVersionKsh(exe, version);
428 }
429 if (ffStrEqualsIgnCase(exeName, "oksh")) {
430 return getShellVersionOksh(exe, version);
431 }
432 if (ffStrEqualsIgnCase(exeName, "oil.ovm")) {
433 return getShellVersionOils(exe, version);
434 }
435 if (ffStrEqualsIgnCase(exeName, "elvish")) {
436 return getExeVersionRaw(exe, version);
437 }
438 if (ffStrEqualsIgnCase(exeName, "ash") || ffStrEqualsIgnCase(exeName, "busybox")) {
439 return getShellVersionAsh(exe, version);
440 }
441 if (ffStrEqualsIgnCase(exeName, "xonsh")) {
442 return getShellVersionXonsh(exe, version);
443 }
444 if (ffStrEqualsIgnCase(exeName, "brush")) {
445 return getExeVersionGeneral(exe, version); // brush 0.2.23 (git:2835487)
446 }
447
448#ifdef _WIN32
449 if (ffStrEqualsIgnCase(exeName, "powershell") || ffStrEqualsIgnCase(exeName, "powershell_ise")) {
450 return getShellVersionWinPowerShell(exe, version);
451 }
452
453 return getFileVersion(exe, nullptr, version);
454#endif
455
456 return false;
457}
458
459[[maybe_unused]] static bool getTerminalVersionTermux(FFstrbuf* version) {
460 ffStrbufSetS(version, getenv("TERMUX_VERSION"));
461 return version->length > 0;
462}
463
464static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
465 if (!ffCharIsDigit(str[0])) {
466 return true;
467 }
468 int count = 0;
469 sscanf(str, "%*d.%*d.%*d%n", &count);
470 if (count == 0) {
471 return true;
472 }
473 ffStrbufSetS((FFstrbuf*) userdata, str);
474 return false;
475}
476
477[[maybe_unused]] static bool getTerminalVersionGnome(FFstrbuf* exe, FFstrbuf* version) {
478 if (ffIsAbsolutePath(exe->chars)) {
479 ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0"));
480 if (version->length) {
481 return true;
482 }
483 }
484
485 if (ffProcessAppendStdOut(version, (char* const[]) { "gnome-terminal", "--version", nullptr })) {
486 return false;
487 }
488
489 // # GNOME Terminal 3.46.7 using VTE 0.70.2 +BIDI +GNUTLS +ICU +SYSTEMD
490 if (!ffStrbufSubstrAfterFirstS(version, "Terminal "))
491 return false;
492 ffStrbufSubstrBeforeFirstC(version, ' ');
493 return version->length > 0;
494}
495
496[[maybe_unused]] static bool getTerminalVersionXfce4Terminal(FFstrbuf* exe, FFstrbuf* version) {
497 if (ffIsAbsolutePath(exe->chars)) {
498 ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0"));
499 if (version->length) {
500 return true;
501 }
502 }
503
504 return getExeVersionGeneral(exe, version); // xfce4-terminal 1.0.4 (Xfce 4.18)...
505}
506
507[[maybe_unused]] static bool getTerminalVersionKgx(FFstrbuf* version) {
508 if (ffProcessAppendStdOut(version, (char* const[]) { "kgx", "--version", nullptr })) {
509 return false;
510 }
511
512 // # KGX 45.0 using VTE 0.74.0 +BIDI +GNUTLS +ICU +SYSTEMD
513 if (!ffStrbufSubstrAfterFirstS(version, "KGX "))
514 return false;
515 ffStrbufSubstrBeforeFirstC(version, ' ');
516 return version->length > 0;
517}
518
519[[maybe_unused]] static bool getTerminalVersionKonsole(FFstrbuf* exe, FFstrbuf* version) {
520 const char* konsoleVersion = getenv("KONSOLE_VERSION");
521 if (konsoleVersion) {
522 // 221201
523 char* end;
524 long major = strtol(konsoleVersion, &end, 10);
525 if (end != konsoleVersion && *end == '\0' && major >= 0) {
526 long patch = major % 100;
527 major /= 100;
528 long minor = major % 100;
529 major /= 100;
530 ffStrbufSetF(version, "%ld.%ld.%ld", major, minor, patch);
531 return true;
532 }
533 }
534
535 // Likely TDE konsole. See #2319
536 if (!getExeVersionRaw(exe, version)) {
537 return false;
538 }
539 return ffStrbufSubstrAfterLastC(version, ' ');
540}
541
542[[maybe_unused]] static bool getTerminalVersionFoot(FFstrbuf* exe, FFstrbuf* version) {
543 uint32_t major = 0, minor = 0, patch = 0;
544 if (ffGetTerminalResponse("\e[>c", 3, "\e[>1;%2u%2u%2u;0c", &major, &minor, &patch) == nullptr) {
545 ffStrbufSetF(version, "%u.%u.%u", major, minor, patch);
546 return true;
547 }
548
549 if (!getExeVersionRaw(exe, version)) {
550 return false;
551 }
552
553 // foot version: 1.13.1 -pgo +ime -graphemes -assertions
554 if (!ffStrbufSubstrAfterFirstS(version, "version: "))
555 return false;
556 ffStrbufSubstrBeforeFirstC(version, ' ');
557 return version->length > 0;
558}
559
560[[maybe_unused]] static bool getTerminalVersionMateTerminal(FFstrbuf* exe, FFstrbuf* version) {
561 ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0"));
562 if (version->length > 0) {
563 return true;
564 }
565
566 if (!getExeVersionRaw(exe, version)) {
567 return false;
568 }
569
570 // MATE Terminal 1.26.1
571 if (!ffStrbufSubstrAfterLastC(version, ' '))
572 return false;
573 return version->length > 0;
574}
575
576[[maybe_unused]] static bool getTerminalVersionCockpit(FFstrbuf* exe, FFstrbuf* version) {
577 if (!getExeVersionRaw(exe, version)) {
578 return false;
579 }
580
581 // Version: 295\n...
582 ffStrbufSubstrBeforeFirstC(version, '\n');
583 if (!ffStrbufSubstrAfterFirstC(version, ' '))
584 return false;
585 return version->length > 0;
586}
587
588[[maybe_unused]] static bool getTerminalVersionXterm(FFstrbuf* exe, FFstrbuf* version) {
589 ffStrbufSetS(version, getenv("XTERM_VERSION"));
590 if (!version->length) {
591 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-v", nullptr })) {
592 return false;
593 }
594 }
595
596 // xterm(273)
597 ffStrbufTrimRight(version, ')');
598 if (!ffStrbufSubstrAfterFirstC(version, '('))
599 return false;
600 return version->length > 0;
601}
602
603[[maybe_unused]] static bool getTerminalVersionBlackbox(FFstrbuf* exe, FFstrbuf* version) {
604 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr })) {
605 return false;
606 }
607
608 // BlackBox version 0.14.0 (flatpak)
609 if (!ffStrbufSubstrAfterFirstS(version, "version "))
610 return false;
611 ffStrbufSubstrBeforeFirstC(version, ' ');
612 return version->length > 0;
613}
614
615[[maybe_unused]] static bool getTerminalVersionUrxvt([[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
616 if (ffProcessAppendStdErr(version, (char* const[]) { "urxvt", // Don't use exe because of urxvtd
617 "-invalid",
618 nullptr })) {
619 return false;
620 }
621
622 // urxvt: "invalid": unknown or malformed option.
623 // rxvt-unicode (urxvt) v9.31 - released: 2023-01-02
624 if (!ffStrbufSubstrAfterFirstS(version, "(urxvt) v"))
625 return false;
626 ffStrbufSubstrBeforeFirstC(version, ' ');
627
628 return version->length > 0;
629}
630
631[[maybe_unused]] static bool getTerminalVersionSt([[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
632 if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "-v", nullptr })) {
633 return false;
634 }
635
636 // st 0.9
637 if (!ffStrbufSubstrAfterFirstC(version, ' '))
638 return false;
639
640 return version->length > 0;
641}
642
643[[maybe_unused]] static bool getTerminalVersionLxterminal(FFstrbuf* exe, FFstrbuf* version) {
644 if (!getExeVersionRaw(exe, version)) {
645 return false;
646 }
647 // lxterminal 0.3.2
648 if (!ffStrbufSubstrAfterFirstC(version, ' '))
649 return false;
650 return version->length > 0;
651}
652
653[[maybe_unused]] static bool getTerminalVersionWeston([[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
654 // weston-terminal doesn't report a version, use weston version instead
655 if (ffProcessAppendStdOut(version, (char* const[]) { "weston", "--version", nullptr })) {
656 return false;
657 }
658
659 // weston 8.0.0
660 if (!ffStrbufSubstrAfterFirstC(version, ' '))
661 return false;
662
663 return version->length > 0;
664}
665
666[[maybe_unused]] static bool extractKmsconVersion(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
667 if (!ffStrStartsWith(str, "v")) {
668 return true;
669 }
670 int count = 0;
671 sscanf(str, "v%*d.%*d.%*d%n", &count);
672 if (count == 0) {
673 return true;
674 }
675 ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) count - 1, str + 1);
676 return false;
677}
678
679[[maybe_unused]] static bool getTerminalVersionKmscon(FFstrbuf* exe, FFstrbuf* version) {
680 if (ffIsAbsolutePath(exe->chars)) {
681 ffBinaryExtractStrings(exe->chars, extractKmsconVersion, version, (uint32_t) strlen("v0.0.0"));
682 if (version->length) {
683 return true;
684 }
685 }
686
687 if (!getExeVersionRaw(exe, version)) {
688 return false;
689 }
690
691 // kmscon version v10.0.0
692 if (!ffStrbufSubstrAfterLastC(version, ' '))
693 return false;
694 return version->length > 0;
695}
696
697static bool getTerminalVersionContour(FFstrbuf* exe, FFstrbuf* version) {
698 const char* env = getenv("TERMINAL_VERSION_STRING");
699 if (env && *env) {
700 ffStrbufAppendS(version, env);
701 return version->length > 0;
702 }
703 if (!getExeVersionRaw(exe, version)) {
704 return false;
705 }
706 // Contour Terminal Emulator 0.3.12.262
707 if (!ffStrbufSubstrAfterLastC(version, ' '))
708 return false;
709 return version->length > 0;
710}
711
712static bool getTerminalVersionScreen(FFstrbuf* exe, FFstrbuf* version) {
713 if (!getExeVersionRaw(exe, version)) {
714 return false;
715 }
716 // Screen version 4.09.01 (GNU) 20-Aug-23
717 if (!ffStrbufSubstrAfterFirstS(version, "Screen version "))
718 return false;
719 ffStrbufSubstrBeforeFirstC(version, ' ');
720 return version->length > 0;
721}
722
723static bool getTerminalVersionTmux(FFstrbuf* exe, FFstrbuf* version) {
724 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-V", nullptr }) != nullptr) {
725 return false;
726 }
727
728 // tmux 3.4
729 if (!ffStrbufSubstrAfterFirstC(version, ' '))
730 return false;
731 return version->length > 0;
732}
733
734static bool getTerminalVersionZellij(FFstrbuf* exe, FFstrbuf* version) {
735 if (!getExeVersionRaw(exe, version)) {
736 return false;
737 }
738
739 // zellij 0.39.2
740 if (!ffStrbufSubstrAfterFirstC(version, ' '))
741 return false;
742 return version->length > 0;
743}
744
745static bool getTerminalVersionZed(FFstrbuf* exe, FFstrbuf* version) {
746 FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe);
747 ffStrbufSubstrBeforeLastC(&cli, '/');
748 ffStrbufAppendS(&cli, "/cli"
749#ifdef _WIN32
750 ".exe"
751#endif
752 );
753
754 if (ffProcessAppendStdOut(version, (char* const[]) { cli.chars, "--version", nullptr }) != nullptr) {
755 return false;
756 }
757
758 // Zed 0.142.6 – /Applications/Zed.app
759 if (!ffStrbufStartsWithS(version, "Zed "))
760 return false;
761 ffStrbufSubstrAfter(version, (uint32_t) strlen("Zed ") - 1);
762 ffStrbufSubstrBeforeFirstC(version, ' ');
763 return version->length > 0;
764}
765
766static bool extractSshdVersion(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
767 if (!ffStrStartsWith(str, "OpenSSH_") || !ffCharIsDigit(str[strlen("OpenSSH_")])) {
768 return true;
769 }
770 str += strlen("OpenSSH_");
771 int count = 0;
772 sscanf(str, "%*d.%*dp%*d%n", &count);
773 if (count == 0) {
774 return true;
775 }
776 ffStrbufSetS((FFstrbuf*) userdata, str);
777 return false;
778}
779
780static bool getTerminalVersionSshd(FFstrbuf* exe, FFstrbuf* version) {
781 FF_STRBUF_AUTO_DESTROY exePath = ffStrbufCreate();
782 if (ffIsAbsolutePath(exe->chars)) {
783 ffStrbufSet(&exePath, exe);
784 } else if (ffFindExecutableInPath("sshd", &exePath) != nullptr) {
785 return false;
786 }
787
788 ffBinaryExtractStrings(exePath.chars, extractSshdVersion, version, (uint32_t) strlen("OpenSSH0.0"));
789 if (version->length) {
790 return true;
791 }
792
793 if (ffProcessAppendStdErr(version, (char* const[]) { exePath.chars, "-V", nullptr }) != nullptr) {
794 return false;
795 }
796
797 if (ffStrbufStartsWithS(version, "unknown ")) { // `unknown option -- V` (ancient OpenSSH version)
798 if (!ffStrbufSubstrAfterFirstC(version, '\n'))
799 return false;
800 }
801
802 if (!ffStrbufStartsWithS(version, "OpenSSH_"))
803 return false;
804 ffStrbufSubstrAfter(version, (uint32_t) strlen("OpenSSH_") - 1);
805
806 // OpenSSH_10.0p2 Ubuntu-5ubuntu5, OpenSSL 3.5.3 16 Sep 2025
807 ffStrbufSubstrBeforeFirstC(version, ',');
808 return version->length > 0;
809}
810
811#ifndef _WIN32
812static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
813 #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__GNU__)
814 char buffer[1024] = {};
815 if (
816 #if __linux__ || __GNU__
817 ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib64/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) ||
818 ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer)
819 #else
820 ffReadFileData(FF_PATH_PKG_BASE "/share/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer)
821 #endif
822 ) {
823 // Starts from version 0.17.0
824 // https://github.com/kovidgoyal/kitty/blob/master/kitty/constants.py#L25
825 const char* p = memmem(buffer, ARRAY_SIZE(buffer) - 1, "version: Version = Version(", strlen("version: Version = Version("));
826 if (p) {
827 p += strlen("version: Version = Version(");
828 int major, minor, patch;
829 if (sscanf(p, "%d,%d,%d", &major, &minor, &patch) == 3) {
830 ffStrbufSetF(version, "%d.%d.%d", major, minor, patch);
831 return true;
832 }
833 }
834 }
835 #elif __APPLE__
836 if (ffGetAppNameAndVersion(exe->chars, nullptr, version) && version->length > 0) {
837 return true;
838 }
839 #endif
840
841 char versionHex[64];
842 // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233
843 if (ffGetTerminalResponse(
844 "\eP+q6b697474792d71756572792d76657273696f6e\e\\", // kitty-query-version
845 1,
846 "\eP1+r%*[^=]=%63[^\e]\e\\\\",
847 versionHex) == nullptr) {
848 // decode hex string
849 for (const char* p = versionHex; p[0] && p[1]; p += 2) {
850 unsigned value;
851 if (sscanf(p, "%2x", &value) == 1) {
852 ffStrbufAppendC(version, (char) value);
853 }
854 }
855 if (version->length > 0)
856 return true;
857 }
858
859 // kitty 0.21.2 created by Kovid Goyal
860 return getExeVersionGeneral(exe, version);
861}
862
863[[maybe_unused]] static bool getTerminalVersionPtyxis([[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
864 if (ffProcessAppendStdOut(version, (char* const[]) { "ptyxis", "--version", nullptr }) != nullptr) {
865 return false;
866 }
867
868 ffStrbufSubstrBeforeFirstC(version, '\n');
869 if (!ffStrbufSubstrAfterFirstC(version, ' '))
870 return false;
871 return version->length > 0;
872}
873
874[[maybe_unused]] static bool getTerminalVersionTilix(FFstrbuf* exe, FFstrbuf* version) {
875 if (ffIsAbsolutePath(exe->chars)) {
876 ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0"));
877 if (version->length) {
878 return true;
879 }
880 }
881
882 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) != nullptr) {
883 return false;
884 }
885
886 uint32_t index = ffStrbufFirstIndexS(version, "Tilix version: ");
887 if (index == version->length) {
888 return false;
889 }
890
891 index += (uint32_t) strlen("Tilix version:");
892 uint32_t end = ffStrbufNextIndexC(version, index, '\n');
893
894 ffStrbufSubstrBefore(version, end);
895 ffStrbufSubstrAfter(version, index);
896 return version->length > 0;
897}
898
899[[maybe_unused]] static bool getTerminalVersionSakura(FFstrbuf* exe, FFstrbuf* version) {
900 if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", nullptr }) != nullptr) { // sakura version is 3.8.8
901 return false;
902 }
903
904 if (!ffStrbufSubstrAfterLastC(version, ' '))
905 return false;
906 return version->length > 0;
907}
908
909[[maybe_unused]] static bool getTerminalVersionTermite(FFstrbuf* exe, FFstrbuf* version) {
910 if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) != nullptr) { // termite v16.9\nvte 0.78.1 +BIDI +GNUTLS +ICU +SYSTEMD
911 return false;
912 }
913
914 ffStrbufSubstrBeforeFirstC(version, '\n');
915 if (!ffStrbufSubstrAfterLastC(version, 'v'))
916 return false;
917 return version->length > 0;
918}
919#endif
920
921#ifdef _WIN32
922
923static bool getTerminalVersionWindowsTerminal(FFstrbuf* exe, FFstrbuf* version) {
924 FF_STRBUF_AUTO_DESTROY buildInfoPath;
925 ffStrbufInitNS(&buildInfoPath, ffStrbufLastIndexC(exe, '\\') + 1, exe->chars);
926 ffStrbufAppendS(&buildInfoPath, "BuildInfo.xml");
927
928 if (ffParsePropFile(buildInfoPath.chars, "StoreVersion=\"", version) && version->length > 0) {
929 ffStrbufTrimRight(version, '"');
930 return version->length > 0;
931 }
932
933 return getFileVersion(exe, nullptr, version) && version->length > 0;
934}
935
936static bool getTerminalVersionConEmu(FFstrbuf* exe, FFstrbuf* version) {
937 ffStrbufSetS(version, getenv("ConEmuBuild"));
938
939 if (version->length) {
940 return true;
941 }
942
943 return getFileVersion(exe, nullptr, version) && version->length > 0;
944}
945
946#endif
947
948bool fftsGetTerminalVersion(FFstrbuf* processName, [[maybe_unused]] FFstrbuf* exe, FFstrbuf* version) {
949#ifdef __ANDROID__
950
951 if (ffStrbufEqualS(processName, "com.termux")) {
952 return getTerminalVersionTermux(version);
953 }
954
955#endif
956
957#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__HAIKU__) || defined(__GNU__)
958
959 if (ffStrbufStartsWithIgnCaseS(processName, "gnome-terminal")) {
960 return getTerminalVersionGnome(exe, version);
961 }
962
963 if (ffStrbufIgnCaseEqualS(processName, "konsole")) {
964 return getTerminalVersionKonsole(exe, version);
965 }
966
967 if (ffStrbufIgnCaseEqualS(processName, "yakuake")) {
968 return getTerminalVersionKonsole(exe, version); // yakuake shares code with konsole
969 }
970
971 if (ffStrbufIgnCaseEqualS(processName, "xfce4-terminal")) {
972 return getTerminalVersionXfce4Terminal(exe, version);
973 }
974
975 if (ffStrbufIgnCaseEqualS(processName, "terminator")) {
976 return getExeVersionGeneral(exe, version); // terminator 2.1.3
977 }
978
979 if (ffStrbufIgnCaseEqualS(processName, "deepin-terminal")) {
980 return getExeVersionGeneral(exe, version); // deepin-terminal 5.4.36
981 }
982
983 if (ffStrbufIgnCaseEqualS(processName, "foot")) {
984 return getTerminalVersionFoot(exe, version);
985 }
986
987 if (ffStrbufIgnCaseEqualS(processName, "qterminal")) {
988 return getExeVersionRaw(exe, version); // 1.2.0
989 }
990
991 if (ffStrbufIgnCaseEqualS(processName, "mate-terminal")) {
992 return getTerminalVersionMateTerminal(exe, version);
993 }
994
995 if (ffStrbufIgnCaseEqualS(processName, "cockpit-bridge")) {
996 return getTerminalVersionCockpit(exe, version);
997 }
998
999 if (ffStrbufIgnCaseEqualS(processName, "xterm")) {
1000 return getTerminalVersionXterm(exe, version);
1001 }
1002
1003 if (ffStrbufIgnCaseEqualS(processName, "blackbox")) {
1004 return getTerminalVersionBlackbox(exe, version);
1005 }
1006
1007 if (ffStrbufIgnCaseEqualS(processName, "st")) {
1008 return getTerminalVersionSt(exe, version);
1009 }
1010
1011 if (ffStrbufIgnCaseEqualS(processName, "lxterminal")) {
1012 return getTerminalVersionLxterminal(exe, version);
1013 }
1014
1015 if (ffStrbufIgnCaseEqualS(processName, "weston-terminal")) {
1016 return getTerminalVersionWeston(exe, version);
1017 }
1018
1019 if (ffStrbufIgnCaseEqualS(processName, "urxvt") ||
1020 ffStrbufIgnCaseEqualS(processName, "urxvtd") ||
1021 ffStrbufIgnCaseEqualS(processName, "rxvt") ||
1022 ffStrbufIgnCaseEqualS(processName, "rxvt-unicode")) {
1023 return getTerminalVersionUrxvt(exe, version);
1024 }
1025
1026 if (ffStrbufIgnCaseEqualS(processName, "ptyxis-agent")) {
1027 return getTerminalVersionPtyxis(exe, version);
1028 }
1029
1030 if (ffStrbufIgnCaseEqualS(processName, "tilix")) {
1031 return getTerminalVersionTilix(exe, version);
1032 }
1033
1034 if (ffStrbufIgnCaseEqualS(processName, "sakura")) {
1035 return getTerminalVersionSakura(exe, version);
1036 }
1037
1038 if (ffStrbufIgnCaseEqualS(processName, "termite")) {
1039 return getTerminalVersionTermite(exe, version);
1040 }
1041
1042 if (ffStrbufIgnCaseEqualS(processName, "cosmic-term")) {
1043 return getTerminalVersionTmux(exe, version);
1044 }
1045
1046#endif
1047
1048#ifdef __linux__
1049
1050 if (ffStrbufIgnCaseEqualS(processName, "kmscon")) {
1051 return getTerminalVersionKmscon(exe, version);
1052 }
1053
1054#endif
1055
1056#ifdef _WIN32
1057
1058 if (ffStrbufIgnCaseEqualS(processName, "WindowsTerminal.exe")) {
1059 return getTerminalVersionWindowsTerminal(exe, version);
1060 }
1061
1062 if (ffStrbufStartsWithIgnCaseS(processName, "ConEmu")) {
1063 return getTerminalVersionConEmu(exe, version);
1064 }
1065
1066 if (ffStrbufIgnCaseEqualS(processName, "warp.exe")) {
1067 return getFileVersion(exe, L"ProductVersion", version) && version->length > 0;
1068 }
1069
1070#endif
1071
1072#ifndef _WIN32
1073
1074 if (ffStrbufIgnCaseEqualS(processName, "kitty")) {
1075 return getTerminalVersionKitty(exe, version);
1076 }
1077
1078 if (ffStrbufIgnCaseEqualS(processName, "Tabby") && getExeVersionRaw(exe, version)) {
1079 return true;
1080 }
1081
1082#endif
1083
1084 if (ffStrbufStartsWithIgnCaseS(processName, "alacritty")) {
1085 return getExeVersionGeneral(exe, version);
1086 }
1087
1088 if (ffStrbufStartsWithIgnCaseS(processName, "contour")) {
1089 return getTerminalVersionContour(exe, version);
1090 }
1091
1092 if (ffStrbufStartsWithIgnCaseS(processName, "screen")) {
1093 return getTerminalVersionScreen(exe, version);
1094 }
1095
1096 if (ffStrbufStartsWithIgnCaseS(processName, "zellij")) {
1097 return getTerminalVersionZellij(exe, version);
1098 }
1099
1100 if (ffStrbufStartsWithIgnCaseS(processName, "zed")) {
1101 return getTerminalVersionZed(exe, version);
1102 }
1103
1104#if __HAIKU__
1105 if (ffStrbufEqualS(processName, "Terminal")) {
1106 return ffGetFileVersion(exe->chars, version);
1107 }
1108#endif
1109
1110 const char* termProgramVersion = getenv("TERM_PROGRAM_VERSION");
1111 if (termProgramVersion && *termProgramVersion) {
1112 const char* termProgram = getenv("TERM_PROGRAM");
1113 if (termProgram) {
1114 if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows
1115 (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) ||
1116#ifdef __APPLE__
1117 (ffStrEquals(termProgram, "iTerm.app") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-")) ||
1118#elif defined(__linux__)
1119 (ffStrEquals(termProgram, "WarpTerminal") && ffStrbufEqualS(processName, "warp")) ||
1120#endif
1121 false) {
1122 ffStrbufSetS(version, termProgramVersion);
1123 return version->length > 0;
1124 }
1125 }
1126 }
1127
1128 termProgramVersion = getenv("LC_TERMINAL_VERSION");
1129 if (termProgramVersion && *termProgramVersion) {
1130 const char* termProgram = getenv("LC_TERMINAL");
1131 if (termProgram) {
1132 if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows
1133 (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) ||
1134 (ffStrStartsWith(termProgram, "iTerm") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-"))) {
1135 ffStrbufSetS(version, termProgramVersion);
1136 return version->length > 0;
1137 }
1138 }
1139 }
1140
1141 if (ffStrbufStartsWithIgnCaseS(processName, "tmux")) {
1142 return getTerminalVersionTmux(exe, version);
1143 }
1144
1145 if (ffStrbufIgnCaseEqualS(processName, "sshd") || ffStrbufStartsWithIgnCaseS(processName, "sshd-")) {
1146 return getTerminalVersionSshd(exe, version);
1147 }
1148
1149#ifdef _WIN32
1150
1151 return getFileVersion(exe, nullptr, version) && version->length > 0;
1152
1153#elif __APPLE__
1154
1155 return ffGetAppNameAndVersion(exe->chars, nullptr, version) && version->length > 0;
1156
1157#else
1158
1159 return false;
1160
1161#endif
1162}