summaryrefslogtreecommitdiffstats
path: root/src/detection/terminalshell
diff options
context:
space:
mode:
Diffstat (limited to 'src/detection/terminalshell')
-rw-r--r--src/detection/terminalshell/terminalshell.c949
-rw-r--r--src/detection/terminalshell/terminalshell.h35
-rw-r--r--src/detection/terminalshell/terminalshell_linux.c449
-rw-r--r--src/detection/terminalshell/terminalshell_windows.c379
4 files changed, 1812 insertions, 0 deletions
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 <ctype.h>
+#include <stdint.h>
+#ifdef __FreeBSD__
+ #include <paths.h>
+ #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 <windows.h>
+
+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 <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+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 <stdalign.h>
+#include <windows.h>
+#include <wchar.h>
+#include <tlhelp32.h>
+#include <ntstatus.h>
+#include <winternl.h>
+#include <shlobj.h>
+
+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;
+}