From 76424950e373d3b04ac3dd13019151bfba3e8423 Mon Sep 17 00:00:00 2001 From: sumuel Date: Mon, 17 Aug 2026 20:44:55 +0000 Subject: Add the files --- src/detection/terminalshell/terminalshell.c | 949 +++++++++++++++++++++ src/detection/terminalshell/terminalshell.h | 35 + src/detection/terminalshell/terminalshell_linux.c | 449 ++++++++++ .../terminalshell/terminalshell_windows.c | 379 ++++++++ 4 files changed, 1812 insertions(+) create mode 100644 src/detection/terminalshell/terminalshell.c create mode 100644 src/detection/terminalshell/terminalshell.h create mode 100644 src/detection/terminalshell/terminalshell_linux.c create mode 100644 src/detection/terminalshell/terminalshell_windows.c (limited to 'src/detection/terminalshell') diff --git a/src/detection/terminalshell/terminalshell.c b/src/detection/terminalshell/terminalshell.c new file mode 100644 index 0000000..2222216 --- /dev/null +++ b/src/detection/terminalshell/terminalshell.c @@ -0,0 +1,949 @@ +#include "fastfetch.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/properties.h" +#include "common/path.h" +#include "common/strutil.h" +#include "common/binary.h" + +#include +#include +#ifdef __FreeBSD__ + #include + #ifndef _PATH_LOCALBASE + #define _PATH_LOCALBASE "/usr/local" + #endif +#elif __OpenBSD__ + #define _PATH_LOCALBASE "/usr/local" +#elif __NetBSD__ + #define _PATH_LOCALBASE "/usr/pkg" +#elif _WIN32 + + #include "common/windows/version.h" + #include + +static bool getFileVersion(const FFstrbuf* exePath, const wchar_t* stringName, FFstrbuf* version) { + wchar_t exePathW[PATH_MAX + 1]; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(exePathW, (ULONG) sizeof(exePathW), NULL, exePath->chars, (ULONG) exePath->length + 1))) { + return false; + } + return ffGetFileVersion(exePathW, stringName, version); +} + +#elif __HAIKU__ + #include "common/haiku/version.h" +#elif __APPLE__ + #include "common/apple/version.h" +#endif + +static bool getExeVersionRaw(FFstrbuf* exe, FFstrbuf* version) { + return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) == NULL; +} + +static bool getExeVersionGeneral(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool extractBashVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "@(#)Bash version ")) { + return true; + } + const char* start = line + strlen("@(#)Bash version "); + const char* end = strchr(start, '('); + if (!end) { + return true; + } + ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start); + return false; +} + +static bool getShellVersionBash(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractBashVersion, version, (uint32_t) strlen("@(#)Bash version 0.0.0(0) release GNU")); + if (version->length > 0) { + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // GNU bash, version 5.1.16(1)-release (x86_64-pc-msys)\nCopyright... + ffStrbufSubstrBeforeFirstC(version, '('); // GNU bash, version 5.1.16 + ffStrbufSubstrAfterLastC(version, ' '); // 5.1.16 + return true; +} + +static bool getShellVersionFish(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // fish, version 4.0.2-1 (Built by MSYS2 project) // version can be localized if LC_ALL is set + if (version->length < strlen("fish, v") || !ffStrbufStartsWithS(version, "fish")) { + return false; + } + uint32_t index = ffStrbufNextIndexC(version, strlen("fish, "), ' '); + ffStrbufSubstrAfter(version, index); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool getShellVersionPwsh(FFstrbuf* exe, FFstrbuf* version) { + // Requires manually setting $POWERSHELL_VERSION + // $env:POWERSHELL_VERSION = $PSVersionTable.PSVersion.ToString(); fastfetch.exe + const char* env = getenv("POWERSHELL_VERSION"); + if (env) { + ffStrbufSetS(version, env); + return true; + } + +#ifdef _WIN32 + if (getFileVersion(exe, NULL, version)) { + ffStrbufSubstrBeforeLastC(version, '.'); + return true; + } +#endif + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + ffStrbufSubstrAfterLastC(version, ' '); + return true; +} + +static bool getShellVersionKsh(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", NULL }) == NULL && ffStrbufSubstrAfterFirstS(version, " (AT&T Research) ")) { + // version sh (AT&T Research) 93u+ 2012-08-01 + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; + } + + ffStrbufClear(version); + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $KSH_VERSION", NULL }) == NULL && ffStrbufSubstrAfterFirstS(version, " KSH ")) { + // OKSH: @(#)PD KSH v5.2.14 99/07/13.2 + // MKSH: @(#)MIRBSD KSH R59 2025/04/26 +Debian + // $OKSH_VERSION doesn't exist on OpenBSD + ffStrbufSubstrBeforeFirstC(version, ' '); + ffStrbufTrimLeft(version, 'v'); + return true; + } + + return false; +} + +static bool getShellVersionOksh(FFstrbuf* exe, FFstrbuf* version) { + // Homebrew version + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $OKSH_VERSION", NULL }) != NULL) { + return false; + } + + // oksh 7.3 + ffStrbufSubstrAfterFirstC(version, ' '); + return true; +} + +static bool getShellVersionOils(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { + return false; + } + + // Oils 0.18.0 https://www.oilshell.org/... + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, '\t'); + return true; +} + +static bool getShellVersionNushell(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("NU_VERSION")); + if (version->length) { + return true; + } + return getExeVersionRaw(exe, version); // 0.73.0 +} + +static bool extractBusyboxVersion(const char* line, uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "BusyBox v")) { + return true; + } + + line += strlen("BusyBox v"); + len -= (uint32_t) strlen("BusyBox v"); + const char* space = memchr(line, ' ', len); + if (space) { + len = (uint32_t) (space - line); + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +static bool getShellVersionAsh(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractBusyboxVersion, version, (uint32_t) strlen("BusyBox v0.0.0")); + + const char* error = ffStrbufEndsWithS(exe, "busybox") + ? ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "ash", "--help", NULL }) + : ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--help", NULL }); + if (error != NULL) { + return false; + } + + // BusyBox v1.36.1 (2023-11-07 18:53:09 UTC) multi-call binary... + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + ffStrbufTrimLeft(version, 'v'); + return true; +} + +static bool getShellVersionXonsh(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("XONSH_VERSION")); + if (version->length) { + return true; + } + + // exe is python here + if (ffProcessAppendStdErr(version, (char* const[]) { "xonsh", "--version", NULL }) != NULL) { + return false; + } + + // xonsh/0.14.1 + ffStrbufSubstrAfterFirstC(version, '/'); + return true; +} + +static bool extractZshVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "zsh-")) { + return true; + } + const char* start = line + strlen("zsh-"); + const char* end = strchr(start, '-'); + if (!end) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start); + return false; +} + +static bool getShellVersionZsh(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractZshVersion, version, (uint32_t) strlen("zsh-0.0-0")); + if (version->length) { + return true; + } + + return getExeVersionGeneral(exe, version); // zsh 5.9 (arm-apple-darwin21.3.0) +} + +#ifdef _WIN32 +static bool getShellVersionWinPowerShell(FFstrbuf* exe, FFstrbuf* version) { + const char* env = getenv("POWERSHELL_VERSION"); + if (env) { + ffStrbufSetS(version, env); + return true; + } + + return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()", NULL }) == NULL; +} +#endif + +bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version) { + if (ffStrEqualsIgnCase(exeName, "sh")) { // #849 + return false; + } + + if (ffStrEqualsIgnCase(exeName, "bash")) { + return getShellVersionBash(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "zsh")) { + return getShellVersionZsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "fish")) { + return getShellVersionFish(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "pwsh")) { + return getShellVersionPwsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "csh") || ffStrEqualsIgnCase(exeName, "tcsh")) { + 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 + } + if (ffStrEqualsIgnCase(exeName, "nu")) { + return getShellVersionNushell(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "ksh") || ffStrEqualsIgnCase(exeName, "mksh")) { + return getShellVersionKsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "oksh")) { + return getShellVersionOksh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "oil.ovm")) { + return getShellVersionOils(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "elvish")) { + return getExeVersionRaw(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "ash") || ffStrEqualsIgnCase(exeName, "busybox")) { + return getShellVersionAsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "xonsh")) { + return getShellVersionXonsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "brush")) { + return getExeVersionGeneral(exe, version); // brush 0.2.23 (git:2835487) + } + +#ifdef _WIN32 + if (ffStrEqualsIgnCase(exeName, "powershell") || ffStrEqualsIgnCase(exeName, "powershell_ise")) { + return getShellVersionWinPowerShell(exe, version); + } + + return getFileVersion(exe, NULL, version); +#endif + + return false; +} + +FF_A_UNUSED static bool getTerminalVersionTermux(FFstrbuf* version) { + ffStrbufSetS(version, getenv("TERMUX_VERSION")); + return version->length > 0; +} + +static bool extractGeneralVersion(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffCharIsDigit(str[0])) { + return true; + } + int count = 0; + sscanf(str, "%*d.%*d.%*d%n", &count); + if (count == 0) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str); + return false; +} + +FF_A_UNUSED static bool getTerminalVersionGnome(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + if (ffProcessAppendStdOut(version, (char* const[]) { "gnome-terminal", "--version", NULL })) { + return false; + } + + // # GNOME Terminal 3.46.7 using VTE 0.70.2 +BIDI +GNUTLS +ICU +SYSTEMD + ffStrbufSubstrAfterFirstS(version, "Terminal "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionXfce4Terminal(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + return getExeVersionGeneral(exe, version); // xfce4-terminal 1.0.4 (Xfce 4.18)... +} + +FF_A_UNUSED static bool getTerminalVersionKgx(FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { "kgx", "--version", NULL })) { + return false; + } + + // # KGX 45.0 using VTE 0.74.0 +BIDI +GNUTLS +ICU +SYSTEMD + ffStrbufSubstrAfterFirstS(version, "KGX "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionKonsole(FFstrbuf* exe, FFstrbuf* version) { + const char* konsoleVersion = getenv("KONSOLE_VERSION"); + if (konsoleVersion) { + // 221201 + long major = strtol(konsoleVersion, NULL, 10); + if (major >= 0) { + long patch = major % 100; + major /= 100; + long minor = major % 100; + major /= 100; + ffStrbufSetF(version, "%ld.%ld.%ld", major, minor, patch); + return true; + } + } + + // Likely TDE konsole. See #2319 + if (!getExeVersionRaw(exe, version)) { + return false; + } + return ffStrbufSubstrAfterLastC(version, ' '); +} + +FF_A_UNUSED static bool getTerminalVersionFoot(FFstrbuf* exe, FFstrbuf* version) { + uint32_t major = 0, minor = 0, patch = 0; + if (ffGetTerminalResponse("\e[>c", 3, "\e[>1;%2u%2u%2u;0c", &major, &minor, &patch) == NULL) { + ffStrbufSetF(version, "%u.%u.%u", major, minor, patch); + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // foot version: 1.13.1 -pgo +ime -graphemes -assertions + ffStrbufSubstrAfterFirstS(version, "version: "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionMateTerminal(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length > 0) { + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // MATE Terminal 1.26.1 + ffStrbufSubstrAfterLastC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionCockpit(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // Version: 295\n... + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionXterm(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("XTERM_VERSION")); + if (!version->length) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-v", NULL })) { + return false; + } + } + + // xterm(273) + ffStrbufTrimRight(version, ')'); + ffStrbufSubstrAfterFirstC(version, '('); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionBlackbox(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL })) { + return false; + } + + // BlackBox version 0.14.0 (flatpak) + ffStrbufSubstrAfterFirstS(version, "version "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionUrxvt(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { "urxvt", // Don't use exe because of urxvtd + "-invalid", + NULL })) { + return false; + } + + // urxvt: "invalid": unknown or malformed option. + // rxvt-unicode (urxvt) v9.31 - released: 2023-01-02 + ffStrbufSubstrAfterFirstS(version, "(urxvt) v"); + ffStrbufSubstrBeforeFirstC(version, ' '); + + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionSt(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "-v", NULL })) { + return false; + } + + // st 0.9 + ffStrbufSubstrAfterFirstC(version, ' '); + + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionLxterminal(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + // lxterminal 0.3.2 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionWeston(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + // weston-terminal doesn't report a version, use weston version instead + if (ffProcessAppendStdOut(version, (char* const[]) { "weston", "--version", NULL })) { + return false; + } + + // weston 8.0.0 + ffStrbufSubstrAfterFirstC(version, ' '); + + return version->length > 0; +} + +static bool getTerminalVersionContour(FFstrbuf* exe, FFstrbuf* version) { + const char* env = getenv("TERMINAL_VERSION_STRING"); + if (env) { + ffStrbufAppendS(version, env); + return true; + } + if (!getExeVersionRaw(exe, version)) { + return false; + } + // Contour Terminal Emulator 0.3.12.262 + ffStrbufSubstrAfterLastC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionScreen(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + // Screen version 4.09.01 (GNU) 20-Aug-23 + ffStrbufSubstrAfter(version, (uint32_t) strlen("Screen version ") - 1); + ffStrbufSubstrBeforeFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionTmux(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-V", NULL }) != NULL) { + return false; + } + + // tmux 3.4 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionZellij(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // zellij 0.39.2 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionZed(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe); + ffStrbufSubstrBeforeLastC(&cli, '/'); + ffStrbufAppendS(&cli, "/cli" +#ifdef _WIN32 + ".exe" +#endif + ); + + if (ffProcessAppendStdOut(version, (char* const[]) { cli.chars, "--version", NULL }) != NULL) { + return false; + } + + // Zed 0.142.6 – /Applications/Zed.app + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool extractSshdVersion(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "OpenSSH_") || !ffCharIsDigit(str[strlen("OpenSSH_")])) { + return true; + } + str += strlen("OpenSSH_"); + int count = 0; + sscanf(str, "%*d.%*dp%*d%n", &count); + if (count == 0) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str); + return false; +} + +static bool getTerminalVersionSshd(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY exePath = ffStrbufCreate(); + if (ffIsAbsolutePath(exe->chars)) { + ffStrbufSet(&exePath, exe); + } else if (ffFindExecutableInPath("sshd", &exePath) != NULL) { + return false; + } + + ffBinaryExtractStrings(exePath.chars, extractSshdVersion, version, (uint32_t) strlen("OpenSSH0.0")); + if (version->length) { + return true; + } + + if (ffProcessAppendStdOut(version, (char* const[]) { exePath.chars, "-V", NULL }) != NULL) { + return false; + } + + if (ffStrbufStartsWithS(version, "unknown ")) { // `unknown option -- V` (ancient OpenSSH version) + ffStrbufSubstrAfterFirstC(version, '\n'); + } + + // OpenSSH_10.0p2 Ubuntu-5ubuntu5, OpenSSL 3.5.3 16 Sep 2025 + ffStrbufSubstrAfterFirstC(version, '_'); + ffStrbufSubstrBeforeFirstC(version, ','); + return true; +} + +#ifndef _WIN32 +static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) { + #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__GNU__) + char buffer[1024] = {}; + if ( + #if __linux__ || __GNU__ + ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib64/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) || + ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) + #else + ffReadFileData(_PATH_LOCALBASE "/share/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) + #endif + ) { + // Starts from version 0.17.0 + // https://github.com/kovidgoyal/kitty/blob/master/kitty/constants.py#L25 + const char* p = memmem(buffer, ARRAY_SIZE(buffer) - 1, "version: Version = Version(", strlen("version: Version = Version(")); + if (p) { + p += strlen("version: Version = Version("); + int major, minor, patch; + if (sscanf(p, "%d,%d,%d", &major, &minor, &patch) == 3) { + ffStrbufSetF(version, "%d.%d.%d", major, minor, patch); + return true; + } + } + } + #elif __APPLE__ + if (ffGetAppNameAndVersion(exe->chars, NULL, version)) { + return true; + } + #endif + + char versionHex[64]; + // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233 + if (ffGetTerminalResponse( + "\eP+q6b697474792d71756572792d76657273696f6e\e\\", // kitty-query-version + 1, + "\eP1+r%*[^=]=%63[^\e]\e\\\\", + versionHex) == NULL) { + // decode hex string + for (const char* p = versionHex; p[0] && p[1]; p += 2) { + unsigned value; + if (sscanf(p, "%2x", &value) == 1) { + ffStrbufAppendC(version, (char) value); + } + } + return true; + } + + // kitty 0.21.2 created by Kovid Goyal + return getExeVersionGeneral(exe, version); +} + +FF_A_UNUSED static bool getTerminalVersionPtyxis(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { "ptyxis", "--version", NULL }) != NULL) { + return false; + } + + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionTilix(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { + return false; + } + + uint32_t index = ffStrbufFirstIndexS(version, "Tilix version: "); + if (index == version->length) { + return false; + } + + index += (uint32_t) strlen("Tilix version:"); + uint32_t end = ffStrbufNextIndexC(version, index, '\n'); + + ffStrbufSubstrBefore(version, end); + ffStrbufSubstrAfter(version, index); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionSakura(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { // sakura version is 3.8.8 + return false; + } + + ffStrbufSubstrAfterLastC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionTermite(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { // termite v16.9\nvte 0.78.1 +BIDI +GNUTLS +ICU +SYSTEMD + return false; + } + + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterLastC(version, 'v'); + return true; +} +#endif + +#ifdef _WIN32 + +static bool getTerminalVersionWindowsTerminal(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY buildInfoPath; + ffStrbufInitNS(&buildInfoPath, ffStrbufLastIndexC(exe, '\\') + 1, exe->chars); + ffStrbufAppendS(&buildInfoPath, "BuildInfo.xml"); + + if (ffParsePropFile(buildInfoPath.chars, "StoreVersion=\"", version)) { + ffStrbufTrimRight(version, '"'); + return true; + } + + return getFileVersion(exe, NULL, version); +} + +static bool getTerminalVersionConEmu(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("ConEmuBuild")); + + if (version->length) { + return true; + } + + return getFileVersion(exe, NULL, version); +} + +#endif + +bool fftsGetTerminalVersion(FFstrbuf* processName, FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { +#ifdef __ANDROID__ + + if (ffStrbufEqualS(processName, "com.termux")) { + return getTerminalVersionTermux(version); + } + +#endif + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__HAIKU__) || defined(__GNU__) + + if (ffStrbufStartsWithIgnCaseS(processName, "gnome-terminal")) { + return getTerminalVersionGnome(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "konsole")) { + return getTerminalVersionKonsole(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "yakuake")) { + return getTerminalVersionKonsole(exe, version); // yakuake shares code with konsole + } + + if (ffStrbufIgnCaseEqualS(processName, "xfce4-terminal")) { + return getTerminalVersionXfce4Terminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "terminator")) { + return getExeVersionGeneral(exe, version); // terminator 2.1.3 + } + + if (ffStrbufIgnCaseEqualS(processName, "deepin-terminal")) { + return getExeVersionGeneral(exe, version); // deepin-terminal 5.4.36 + } + + if (ffStrbufIgnCaseEqualS(processName, "foot")) { + return getTerminalVersionFoot(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "qterminal")) { + return getExeVersionRaw(exe, version); // 1.2.0 + } + + if (ffStrbufIgnCaseEqualS(processName, "mate-terminal")) { + return getTerminalVersionMateTerminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "cockpit-bridge")) { + return getTerminalVersionCockpit(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "xterm")) { + return getTerminalVersionXterm(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "blackbox")) { + return getTerminalVersionBlackbox(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "st")) { + return getTerminalVersionSt(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "lxterminal")) { + return getTerminalVersionLxterminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "weston-terminal")) { + return getTerminalVersionWeston(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "urxvt") || + ffStrbufIgnCaseEqualS(processName, "urxvtd") || + ffStrbufIgnCaseEqualS(processName, "rxvt") || + ffStrbufIgnCaseEqualS(processName, "rxvt-unicode")) { + return getTerminalVersionUrxvt(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "ptyxis-agent")) { + return getTerminalVersionPtyxis(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "tilix")) { + return getTerminalVersionTilix(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "sakura")) { + return getTerminalVersionSakura(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "termite")) { + return getTerminalVersionTermite(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "cosmic-term")) { + return getTerminalVersionTmux(exe, version); + } + +#endif + +#ifdef _WIN32 + + if (ffStrbufIgnCaseEqualS(processName, "WindowsTerminal.exe")) { + return getTerminalVersionWindowsTerminal(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "ConEmu")) { + return getTerminalVersionConEmu(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "warp.exe")) { + return getFileVersion(exe, L"ProductVersion", version); + } + +#endif + +#ifndef _WIN32 + + if (ffStrbufIgnCaseEqualS(processName, "kitty")) { + return getTerminalVersionKitty(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "Tabby") && getExeVersionRaw(exe, version)) { + return true; + } + +#endif + + if (ffStrbufStartsWithIgnCaseS(processName, "alacritty")) { + return getExeVersionGeneral(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "contour")) { + return getTerminalVersionContour(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "screen")) { + return getTerminalVersionScreen(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "zellij")) { + return getTerminalVersionZellij(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "zed")) { + return getTerminalVersionZed(exe, version); + } + +#if __HAIKU__ + if (ffStrbufEqualS(processName, "Terminal")) { + return ffGetFileVersion(exe->chars, version); + } +#endif + + const char* termProgramVersion = getenv("TERM_PROGRAM_VERSION"); + if (termProgramVersion) { + const char* termProgram = getenv("TERM_PROGRAM"); + if (termProgram) { + if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows + (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) || +#ifdef __APPLE__ + (ffStrEquals(termProgram, "iTerm.app") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-")) || +#elif defined(__linux__) + (ffStrEquals(termProgram, "WarpTerminal") && ffStrbufEqualS(processName, "warp")) || +#endif + false) { + ffStrbufSetS(version, termProgramVersion); + return true; + } + } + } + + termProgramVersion = getenv("LC_TERMINAL_VERSION"); + if (termProgramVersion) { + const char* termProgram = getenv("LC_TERMINAL"); + if (termProgram) { + if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows + (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) || + (ffStrStartsWith(termProgram, "iTerm") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-"))) { + ffStrbufSetS(version, termProgramVersion); + return true; + } + } + } + + if (ffStrbufStartsWithIgnCaseS(processName, "tmux")) { + return getTerminalVersionTmux(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "sshd") || ffStrbufStartsWithIgnCaseS(processName, "sshd-")) { + return getTerminalVersionSshd(exe, version); + } + +#ifdef _WIN32 + + return getFileVersion(exe, NULL, version); + +#elif __APPLE__ + + return ffGetAppNameAndVersion(exe->chars, NULL, version); + +#else + + return false; + +#endif +} diff --git a/src/detection/terminalshell/terminalshell.h b/src/detection/terminalshell/terminalshell.h new file mode 100644 index 0000000..008e30c --- /dev/null +++ b/src/detection/terminalshell/terminalshell.h @@ -0,0 +1,35 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/terminal/option.h" +#include "modules/shell/option.h" + +typedef struct FFShellResult { + FFstrbuf processName; + FFstrbuf exe; // Actually arg0 in *nix + const char* exeName; // pointer to a char in exe + FFstrbuf exePath; // Full real path to executable file + FFstrbuf prettyName; + FFstrbuf version; + uint32_t pid; + uint32_t ppid; + int32_t tty; +} FFShellResult; + +typedef struct FFTerminalResult { + FFstrbuf processName; + FFstrbuf exe; + FFstrbuf prettyName; + const char* exeName; // pointer to a char in exe + FFstrbuf exePath; // Full real path to executable file + FFstrbuf version; + FFstrbuf tty; + uint32_t pid; + uint32_t ppid; +} FFTerminalResult; + +const FFShellResult* ffDetectShell(); +const FFTerminalResult* ffDetectTerminal(); + +bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version); +bool fftsGetTerminalVersion(FFstrbuf* processName, FFstrbuf* exe, FFstrbuf* version); diff --git a/src/detection/terminalshell/terminalshell_linux.c b/src/detection/terminalshell/terminalshell_linux.c new file mode 100644 index 0000000..1eccf26 --- /dev/null +++ b/src/detection/terminalshell/terminalshell_linux.c @@ -0,0 +1,449 @@ +#include "terminalshell.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/processing.h" +#include "common/thread.h" +#include "common/strutil.h" + +#include +#include +#include + +static void setExeName(FFstrbuf* exe, const char** exeName) { + assert(exe->length > 0); + uint32_t lastSlashIndex = ffStrbufLastIndexC(exe, '/'); + if (lastSlashIndex < exe->length) { + *exeName = exe->chars + lastSlashIndex + 1; + } +} + +static pid_t getShellInfo(FFShellResult* result, pid_t pid) { + pid_t ppid = 0; + int32_t tty = -1; + + const char* userShellName = NULL; + { + uint32_t index = ffStrbufLastIndexC(&instance.state.platform.userShell, '/'); + if (index == instance.state.platform.userShell.length) { + userShellName = instance.state.platform.userShell.chars; + } else { + userShellName = instance.state.platform.userShell.chars + index + 1; + } + } + + while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, &tty) == NULL) { + if (!ffStrbufEqualS(&result->processName, userShellName)) { + // Common programs that are between terminal and own process, but are not the shell + if ( + // tty < 0 || //A shell should connect to a tty + pid == 1 || // init/systemd + ffStrbufEqualS(&result->processName, "sh") || // This prevents us from detecting things like pipes and redirects, i hope nobody uses plain `sh` as shell + ffStrbufEqualS(&result->processName, "sudo") || + ffStrbufEqualS(&result->processName, "su") || + ffStrbufEqualS(&result->processName, "strace") || + ffStrbufEqualS(&result->processName, "gdb") || + ffStrbufEqualS(&result->processName, "lldb") || + ffStrbufEqualS(&result->processName, "lldb-mi") || + ffStrbufEqualS(&result->processName, "login") || + ffStrbufEqualS(&result->processName, "ltrace") || + ffStrbufEqualS(&result->processName, "perf") || + ffStrbufEqualS(&result->processName, "guake-wrapped") || + ffStrbufEqualS(&result->processName, "time") || + ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289 + ffStrbufEqualS(&result->processName, "valgrind") || + ffStrbufEqualS(&result->processName, "fastfetch") || // #994 + ffStrbufEqualS(&result->processName, "flashfetch") || + ffStrbufEqualS(&result->processName, "proot") || + ffStrbufEqualS(&result->processName, "script") || +#ifdef __linux__ + ffStrbufEqualS(&result->processName, "run-parts") || +#endif + ffStrbufContainS(&result->processName, "debug") || + ffStrbufContainS(&result->processName, "command-not-") || + ffStrbufEndsWithS(&result->processName, ".sh")) { + pid = ppid; + ffStrbufClear(&result->processName); + continue; + } + } + + result->pid = (uint32_t) pid; + result->ppid = (uint32_t) ppid; + result->tty = tty; + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + break; + } + return pid > 1 ? ppid : 0; +} + +static pid_t getTerminalInfo(FFTerminalResult* result, pid_t pid) { + pid_t ppid = 0; + + while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, NULL) == NULL) { + // Known shells + if ( + pid == 1 || // init/systemd + ffStrbufEqualS(&result->processName, "sudo") || + ffStrbufEqualS(&result->processName, "su") || + ffStrbufEqualS(&result->processName, "sh") || + ffStrbufEqualS(&result->processName, "ash") || + ffStrbufEqualS(&result->processName, "bash") || + ffStrbufEqualS(&result->processName, "zsh") || + ffStrbufEqualS(&result->processName, "ksh") || + ffStrbufEqualS(&result->processName, "mksh") || + ffStrbufEqualS(&result->processName, "oksh") || + ffStrbufEqualS(&result->processName, "csh") || + ffStrbufEqualS(&result->processName, "tcsh") || + ffStrbufEqualS(&result->processName, "fish") || + ffStrbufEqualS(&result->processName, "dash") || + ffStrbufEqualS(&result->processName, "pwsh") || + ffStrbufEqualS(&result->processName, "nu") || + ffStrbufEqualS(&result->processName, "git-shell") || + ffStrbufEqualS(&result->processName, "elvish") || + ffStrbufEqualS(&result->processName, "oil.ovm") || + ffStrbufEqualS(&result->processName, "xonsh") || // works in Linux but not in macOS because kernel returns `Python` in this case + ffStrbufEqualS(&result->processName, "login") || + ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289 + ffStrbufEqualS(&result->processName, "chezmoi") || // #762 + ffStrbufEqualS(&result->processName, "proot") || + ffStrbufEqualS(&result->processName, "script") || +#ifdef __linux__ + ffStrbufStartsWithS(&result->processName, "Relay(") || // Unknown process in WSL2 + ffStrbufStartsWithS(&result->processName, "flatpak-") || // #707 + ffStrbufEqualS(&result->processName, "run-parts") || // #2048 +#endif + ffStrbufEndsWithS(&result->processName, ".sh")) { + pid = ppid; + ffStrbufClear(&result->processName); + continue; + } + +#ifdef __APPLE__ + // https://github.com/fastfetch-cli/fastfetch/discussions/501 + const char* pLeft = strstr(result->processName.chars, " ("); + if (pLeft) { + pLeft += 2; + const char* pRight = strstr(pLeft, "term)"); + if (pRight && pRight[5] == '\0') { + for (; pLeft < pRight; ++pLeft) { + if (*pLeft < 'a' || *pLeft > 'z') { + break; + } + } + if (pLeft == pRight && ffProcessGetBasicInfoLinux(ppid, &result->processName, &ppid, NULL) != NULL) { + return 0; + } + } + } +#endif + + result->pid = (uint32_t) pid; + result->ppid = (uint32_t) ppid; + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + break; + } + return pid > 1 ? ppid : 0; +} + +static bool getTerminalInfoByPidEnv(FFTerminalResult* result, const char* pidEnv) { + const char* envStr = getenv(pidEnv); + if (envStr == NULL) { + return false; + } + + pid_t pid = (pid_t) strtol(envStr, NULL, 10); + result->pid = (uint32_t) pid; + if (ffProcessGetBasicInfoLinux(pid, &result->processName, (pid_t*) &result->ppid, NULL) == NULL) { + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + return true; + } + + return false; +} + +static void getTerminalFromEnv(FFTerminalResult* result) { + if (result->processName.length > 0) { + if (!ffStrbufStartsWithS(&result->processName, "login") && + !ffStrbufEqualS(&result->processName, "(login)") && + +#ifdef __APPLE__ + !ffStrbufEqualS(&result->processName, "launchd") && +#else + !ffStrbufEqualS(&result->processName, "systemd") && + !ffStrbufEqualS(&result->processName, "init") && + !ffStrbufEqualS(&result->processName, "(init)") && + !ffStrbufEqualS(&result->processName, "SessionLeader") && // #750 +#endif + + !ffStrbufEqualS(&result->processName, "0")) + return; + + ffStrbufClear(&result->processName); + ffStrbufClear(&result->exe); + result->exeName = result->exe.chars; + ffStrbufClear(&result->exePath); + result->pid = result->ppid = 0; + } + + const char* term = NULL; + + // SSH + if ( + getenv("SSH_TTY") != NULL) { + term = getenv("SSH_TTY"); + } else if ( + getenv("KITTY_PID") != NULL || + getenv("KITTY_INSTALLATION_DIR") != NULL) { + if (getTerminalInfoByPidEnv(result, "KITTY_PID")) { + return; + } + term = "kitty"; + } + +#ifdef __linux__ // WSL + // Windows Terminal + else if ( + getenv("WT_SESSION") != NULL || + getenv("WT_PROFILE_ID") != NULL) + term = "Windows Terminal"; + + // ConEmu + else if ( + getenv("ConEmuPID") != NULL) { + term = "ConEmu"; + } +#endif + + // Alacritty + else if ( + getenv("ALACRITTY_SOCKET") != NULL || + getenv("ALACRITTY_LOG") != NULL || + getenv("ALACRITTY_WINDOW_ID") != NULL) { + term = "Alacritty"; + } +#ifdef __ANDROID__ + // Termux + else if ( + getenv("TERMUX_VERSION") != NULL || + getenv("TERMUX_MAIN_PACKAGE_FORMAT") != NULL) { + if (getTerminalInfoByPidEnv(result, "TERMUX_APP__PID")) { + return; + } + term = "com.termux"; + } +#endif + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) + // Konsole + else if ( + getenv("KONSOLE_VERSION") != NULL) + term = "konsole"; + + else if ( + getenv("GNOME_TERMINAL_SCREEN") != NULL || + getenv("GNOME_TERMINAL_SERVICE") != NULL) { + term = "gnome-terminal"; + } +#endif + + // MacOS, mintty + else if (getenv("TERM_PROGRAM") != NULL) { + term = getenv("TERM_PROGRAM"); + } + + else if (getenv("LC_TERMINAL") != NULL) { + term = getenv("LC_TERMINAL"); + } + + // Normal Terminal + else { + term = getenv("TERM"); + // TTY + if (!ffStrSet(term) || ffStrEquals(term, "linux")) { + term = ttyname(STDIN_FILENO); + } + } + + if (ffStrSet(term)) { + ffStrbufSetS(&result->processName, term); + ffStrbufSetS(&result->exe, term); + setExeName(&result->exe, &result->exeName); + } +} + +static void getUserShellFromEnv(FFShellResult* result) { + // If shell detection via processes failed + if (result->processName.length == 0 && instance.state.platform.userShell.length > 0) { + ffStrbufSet(&result->exe, &instance.state.platform.userShell); + setExeName(&result->exe, &result->exeName); + ffStrbufAppendS(&result->processName, result->exeName); + } +} + +static void setShellInfoDetails(FFShellResult* result) { + if (ffStrbufEqualS(&result->processName, "pwsh")) { + ffStrbufInitStatic(&result->prettyName, "PowerShell"); + } else if (ffStrbufEqualS(&result->processName, "nu")) { + ffStrbufInitStatic(&result->prettyName, "nushell"); + } else if (ffStrbufEqualS(&result->processName, "oil.ovm")) { + ffStrbufInitStatic(&result->prettyName, "Oils"); + } else if (ffStrbufEqualS(&result->processName, "busybox")) { + ffStrbufInitStatic(&result->prettyName, "ash"); + } else { + // https://github.com/fastfetch-cli/fastfetch/discussions/280#discussioncomment-3831734 + ffStrbufInitS(&result->prettyName, result->exeName); + } +} + +static void setTerminalInfoDetails(FFTerminalResult* result) { + // For Nixpkgs. Ref: #510 and https://github.com/NixOS/nixpkgs/pull/249428 + // We use processName when detecting version and font, overriding it for simplification + if (ffStrbufStartsWithC(&result->processName, '.') && ffStrbufStartsWithS(&result->exePath,"/nix/store")) { + ffStrbufSubstrAfter(&result->processName, 0); + if (strlen(result->exeName) < 15) { + ffStrbufSubstrBeforeLastC(&result->processName, '-'); + } + } + + if (ffStrbufEqualS(&result->processName, "wezterm-gui")) { + ffStrbufInitStatic(&result->prettyName, "WezTerm"); + } else if (ffStrbufStartsWithS(&result->processName, "tmux:")) { + ffStrbufInitStatic(&result->prettyName, "tmux"); + } else if (ffStrbufStartsWithS(&result->processName, "screen-")) { + ffStrbufInitStatic(&result->prettyName, "screen"); + } else if (ffStrbufEqualS(&result->processName, "sshd") || ffStrbufStartsWithS(&result->processName, "sshd-")) { + if (result->tty.length) { + ffStrbufInitCopy(&result->prettyName, &result->tty); + } else { + ffStrbufSetStatic(&result->prettyName, "sshd"); + } + } + +#if defined(__ANDROID__) + + else if (ffStrbufEqualS(&result->processName, "com.termux")) + ffStrbufInitStatic(&result->prettyName, "Termux"); + +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) + + else if (ffStrbufStartsWithS(&result->processName, "gnome-terminal")) + ffStrbufInitStatic(&result->prettyName, "GNOME Terminal"); + else if (ffStrbufStartsWithS(&result->processName, "kgx")) { + ffStrbufInitStatic(&result->prettyName, "GNOME Console"); + } else if (ffStrbufEqualS(&result->processName, "urxvt") || + ffStrbufEqualS(&result->processName, "urxvtd") || + ffStrbufEqualS(&result->processName, "rxvt")) { + ffStrbufInitStatic(&result->prettyName, "rxvt-unicode"); + } else if (ffStrbufStartsWithS(&result->processName, "ptyxis-agent")) { + ffStrbufInitStatic(&result->prettyName, "Ptyxis"); + } + +#elif defined(__APPLE__) + + else if (ffStrbufEqualS(&result->processName, "iTerm.app") || ffStrbufStartsWithS(&result->processName, "iTermServer-")) + ffStrbufInitStatic(&result->prettyName, "iTerm"); + else if (ffStrbufEndsWithS(&result->exePath, "Terminal.app/Contents/MacOS/Terminal")) { + ffStrbufSetStatic(&result->processName, "Apple_Terminal"); // $TERM_PROGRAM, for terminal font detection + ffStrbufInitStatic(&result->prettyName, "Apple Terminal"); + } else if (ffStrbufEqualS(&result->processName, "Apple_Terminal")) { + ffStrbufInitStatic(&result->prettyName, "Apple Terminal"); + } else if (ffStrbufEndsWithS(&result->exePath, "Warp.app/Contents/MacOS/stable")) { + ffStrbufSetStatic(&result->processName, "WarpTerminal"); // $TERM_PROGRAM, for terminal font detection + ffStrbufInitStatic(&result->prettyName, "Warp"); + } else if (ffStrbufEqualS(&result->processName, "WarpTerminal")) { + ffStrbufInitStatic(&result->prettyName, "Warp"); + } + +#elif defined(__HAIKU__) + + else if (ffStrbufEqualS(&result->processName, "Terminal")) + ffStrbufInitStatic(&result->prettyName, "Haiku Terminal"); + +#endif + + else if (strncmp(result->exeName, result->processName.chars, result->processName.length) == 0 || (ffStrbufStartsWithS(&result->exePath,"/nix/store") && strlen(result->exeName) > 15)) { // if exeName starts with processName, print it. Otherwise print processName. For nixpkgs, use exeName if processName can't be unwrapped + ffStrbufInitS(&result->prettyName, result->exeName); + } else { + ffStrbufInitCopy(&result->prettyName, &result->processName); + } +} + +#if defined(MAXPATH) + #define FF_EXE_PATH_LEN MAXPATH +#elif defined(PATH_MAX) + #define FF_EXE_PATH_LEN PATH_MAX +#else + #define FF_EXE_PATH_LEN 260 +#endif + +const FFShellResult* ffDetectShell() { + static FFShellResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); + result.exeName = result.exe.chars; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.version); + result.pid = 0; + result.ppid = 0; + result.tty = -1; + + pid_t ppid = getppid(); + + const char* ignoreParent = getenv("FFTS_IGNORE_PARENT"); + if (ignoreParent && ffStrEquals(ignoreParent, "1")) { + FF_STRBUF_AUTO_DESTROY _ = ffStrbufCreate(); + ffProcessGetBasicInfoLinux(ppid, &_, &ppid, NULL); + } + + ppid = getShellInfo(&result, ppid); + getUserShellFromEnv(&result); + + if (result.processName.length > 0) { + setShellInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, result.exeName, &result.version); + } + } + + return &result; +} + +const FFTerminalResult* ffDetectTerminal() { + static FFTerminalResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); + result.exeName = result.exe.chars; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.version); + ffStrbufInitS(&result.tty, ttyname(STDOUT_FILENO)); + result.pid = 0; + result.ppid = 0; + + pid_t ppid = (pid_t) ffDetectShell()->ppid; + + if (ppid) { + ppid = getTerminalInfo(&result, ppid); + } + getTerminalFromEnv(&result); + + if (result.processName.length > 0) { + setTerminalInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + } + } + + return &result; +} diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c new file mode 100644 index 0000000..d037663 --- /dev/null +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -0,0 +1,379 @@ +#include "terminalshell.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/thread.h" +#include "common/mallocHelper.h" +#include "common/windows/registry.h" +#include "common/windows/unicode.h" +#include "common/windows/version.h" +#include "common/windows/nt.h" +#include "common/strutil.h" + +#include +#include +#include +#include +#include +#include +#include + +static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) { + uint32_t ppid = 0; + bool gui = false; + + while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + + // Common programs that are between terminal and own process, but are not the shell + if ( + !gui && (ffStrbufIgnCaseEqualS(&result->prettyName, "sudo") || ffStrbufIgnCaseEqualS(&result->prettyName, "su") || ffStrbufIgnCaseEqualS(&result->prettyName, "gdb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb-dap") || ffStrbufIgnCaseEqualS(&result->prettyName, "python") || // python on windows generates shim executables + ffStrbufIgnCaseEqualS(&result->prettyName, "fastfetch") || // scoop warps the real binaries with a "shim" exe + ffStrbufIgnCaseEqualS(&result->prettyName, "flashfetch") || ffStrbufContainIgnCaseS(&result->prettyName, "debug") || ffStrbufContainIgnCaseS(&result->prettyName, "time") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "ConEmuC") // https://github.com/fastfetch-cli/fastfetch/issues/488#issuecomment-1619982014 + )) { + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + result->exeName = NULL; + pid = ppid; + continue; + } + + result->pid = pid; + + if (gui) { + // Started without shell + // In this case, terminal process will be created by fastfetch itself. + ppid = 0; + if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetS(&result->prettyName, "Windows Explorer"); + } + } else { + result->ppid = ppid; + } + + break; + } + return ppid; +} + +static void setShellInfoDetails(FFShellResult* result) { + if (ffStrbufIgnCaseEqualS(&result->prettyName, "pwsh")) { + ffStrbufSetS(&result->prettyName, "PowerShell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell")) { + ffStrbufSetS(&result->prettyName, "Windows PowerShell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell_ise")) { + ffStrbufSetS(&result->prettyName, "Windows PowerShell ISE"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "cmd")) { + ffStrbufSetS(&result->prettyName, "CMD"); + + if (instance.config.general.detectVersion) { + FF_AUTO_CLOSE_FD HANDLE snapshot = NULL; + while (!(snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, result->pid)) && GetLastError() == ERROR_BAD_LENGTH) {} + + if (snapshot) { + MODULEENTRY32W module; + module.dwSize = sizeof(module); + for (BOOL success = Module32FirstW(snapshot, &module); success; success = Module32NextW(snapshot, &module)) { + if (wcsncmp(module.szModule, L"clink_dll_", strlen("clink_dll_")) == 0) { + FF_STRBUF_AUTO_DESTROY clinkVersion = ffStrbufCreate(); + if (ffGetFileVersion(module.szExePath, NULL, &clinkVersion)) { + ffStrbufAppendF(&result->prettyName, " (with Clink %s)", clinkVersion.chars); + } else { + ffStrbufAppendS(&result->prettyName, " (with Clink)"); + } + break; + } + } + } + } + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "nu")) { + ffStrbufSetS(&result->prettyName, "nushell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetS(&result->prettyName, "Windows Explorer"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "busybox")) { + ffStrbufInitStatic(&result->prettyName, "ash"); + } +} + +static bool getTerminalFromEnv(FFTerminalResult* result) { + if ( + result->processName.length > 0 && + ffStrbufIgnCaseCompS(&result->processName, "explorer") != 0) { + return false; + } + + const char* term = getenv("ConEmuPID"); + + if (term) { + // ConEmu + uint32_t pid = (uint32_t) strtoul(term, NULL, 10); + result->pid = pid; + if (ffProcessGetInfoWindows(pid, NULL, &result->processName, &result->exe, &result->exeName, &result->exePath, NULL)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + return true; + } else { + term = "ConEmu"; + } + } + + // SSH + if (getenv("SSH_TTY") != NULL) { + term = getenv("SSH_TTY"); + } + + // Windows Terminal + if (!term && (getenv("WT_SESSION") != NULL || getenv("WT_PROFILE_ID") != NULL)) { + term = "WindowsTerminal"; + } + + // Alacritty + if (!term && (getenv("ALACRITTY_SOCKET") != NULL || getenv("ALACRITTY_LOG") != NULL || getenv("ALACRITTY_WINDOW_ID") != NULL)) { + term = "Alacritty"; + } + + if (!term) { + term = getenv("TERM_PROGRAM"); + } + + // Normal Terminal + if (!term) { + term = getenv("TERM"); + } + + if (term) { + ffStrbufSetS(&result->processName, term); + ffStrbufSetS(&result->prettyName, term); + ffStrbufSetS(&result->exe, term); + result->exeName = ""; + return true; + } + + return false; +} + +static bool detectDefaultTerminal(FFTerminalResult* result) { + wchar_t regPath[128] = L"SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\"; + wchar_t* uuid = regPath + strlen("SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\"); + FF_AUTO_CLOSE_FD HANDLE hkcu = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Console\\%%Startup", &hkcu, NULL) && + ffRegReadData(hkcu, L"DelegationTerminal", &(FFArgBuffer) { + .data = uuid, + .length = (uint32_t) (sizeof(regPath) - (size_t) (uuid - regPath) * sizeof(wchar_t)), + }, + NULL)) { + if (wcscmp(uuid, L"{00000000-0000-0000-0000-000000000000}") == 0 || // Let Windows decide + wcscmp(uuid, L"{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}") == 0) // Conhost + { + goto conhost; + } + + FF_AUTO_CLOSE_FD HANDLE hklm = NULL; + if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, regPath, &hklm, NULL)) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + if (ffRegGetSubKey(hklm, 0, &path, NULL)) { + if (ffStrbufStartsWithS(&path, "Microsoft.WindowsTerminal")) { + ffStrbufSetS(&result->processName, "WindowsTerminal.exe"); + ffStrbufSetS(&result->prettyName, "WindowsTerminal"); + + PWSTR programFiles = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, NULL, &programFiles))) { + ffStrbufSetWS(&result->exe, programFiles); + CoTaskMemFree(programFiles); + programFiles = NULL; + + ffStrbufAppendS(&result->exe, "\\WindowsApps\\"); + ffStrbufAppend(&result->exe, &path); + ffStrbufAppendS(&result->exe, "\\WindowsTerminal.exe"); + + if (ffPathExists(result->exe.chars, FF_PATHTYPE_FILE)) { + result->exeName = result->exe.chars + ffStrbufLastIndexC(&result->exe, '\\') + 1; + ffStrbufSet(&result->exePath, &result->exe); + } else { + ffStrbufDestroy(&result->exe); + ffStrbufInitMove(&result->exe, &path); + result->exeName = ""; + } + } + return true; + } + } + } + } + +conhost:; + ULONG_PTR conhostPid = 0; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(), ProcessConsoleHostProcess, &conhostPid, sizeof(conhostPid), &size)) && conhostPid != 0) { + // For Windows Terminal, it reports the PID of OpenConsole + if (ffProcessGetInfoWindows((uint32_t) conhostPid, NULL, &result->processName, &result->exe, &result->exeName, &result->exePath, NULL)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + return true; + } + } + + ffStrbufClear(&result->exe); + return false; +} + +static uint32_t getTerminalInfo(FFTerminalResult* result, uint32_t pid) { + if (getenv("MSYSTEM")) { + // Don't try to detect terminals in MSYS shell + // It won't work because MSYS doesn't follow process tree of native Windows programs + return 0; + } + + uint32_t ppid = 0; + bool gui; + + while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) { + if (!gui) { + // We are in nested shell + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + ffStrbufClear(&result->exePath); + result->exeName = ""; + pid = ppid; + continue; + } + + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + + if (ffStrbufIgnCaseEqualS(&result->prettyName, "sihost") || + ffStrbufIgnCaseEqualS(&result->prettyName, "explorer") || + ffStrbufIgnCaseEqualS(&result->prettyName, "wininit")) { + // A CUI program created by Windows Explorer will spawn a conhost as its child. + // However the conhost process is just a placeholder; + // The true terminal can be Windows Terminal or others. + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + ffStrbufClear(&result->exePath); + result->exeName = ""; + return 0; + } else { + result->pid = pid; + result->ppid = ppid; + } + + break; + } + return ppid; +} + +static void setTerminalInfoDetails(FFTerminalResult* result) { + if (ffStrbufIgnCaseEqualS(&result->prettyName, "WindowsTerminal")) { + ffStrbufSetStatic(&result->prettyName, ffStrbufContainIgnCaseS(&result->exe, ".WindowsTerminalPreview_") ? "Windows Terminal Preview" : "Windows Terminal"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "conhost")) { + ffStrbufSetStatic(&result->prettyName, "Windows Console"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "Code")) { + ffStrbufSetStatic(&result->prettyName, "Visual Studio Code"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetStatic(&result->prettyName, "Windows Explorer"); + } else if (ffStrbufEqualS(&result->prettyName, "wezterm-gui")) { + ffStrbufSetStatic(&result->prettyName, "WezTerm"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "sshd") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "sshd-")) { + const char* tty = getenv("SSH_TTY"); + if (tty) { + ffStrbufSetS(&result->prettyName, tty); + } + } +} + +const FFShellResult* ffDetectShell(void) { + static FFShellResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, MAX_PATH); + result.exeName = ""; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.prettyName); + ffStrbufInit(&result.version); + result.pid = 0; + result.ppid = 0; + result.tty = -1; + + uint32_t ppid; + if (!ffProcessGetInfoWindows(0, &ppid, NULL, NULL, NULL, NULL, NULL)) { + return &result; + } + + const char* ignoreParent = getenv("FFTS_IGNORE_PARENT"); + if (ignoreParent && ffStrEquals(ignoreParent, "1")) { + ffProcessGetInfoWindows(ppid, &ppid, NULL, NULL, NULL, NULL, NULL); + } + + ppid = getShellInfo(&result, ppid); + + if (result.processName.length > 0) { + setShellInfoDetails(&result); + char tmp[MAX_PATH]; + strcpy(tmp, result.exeName); + char* ext = strrchr(tmp, '.'); + if (ext) { + *ext = '\0'; + } + if (instance.config.general.detectVersion) { + fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, tmp, &result.version); + } + } + + return &result; +} + +const FFTerminalResult* ffDetectTerminal(void) { + static FFTerminalResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, MAX_PATH); + result.exeName = ""; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.prettyName); + ffStrbufInit(&result.version); + ffStrbufInit(&result.tty); + result.pid = 0; + result.ppid = 0; + + uint32_t ppid = ffDetectShell()->ppid; + if (ppid) { + getTerminalInfo(&result, ppid); + } + + if (result.processName.length == 0) { + getTerminalFromEnv(&result); + } + if (result.processName.length == 0) { + detectDefaultTerminal(&result); + } + + if (result.processName.length > 0) { + setTerminalInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + } + } + + return &result; +} -- cgit v1.2.3