master ohmu/fastfetch-openbsd / src / detection / terminalshell / terminalshell.c
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}