summaryrefslogtreecommitdiffstats
path: root/src/detection/terminalshell/terminalshell_linux.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/terminalshell/terminalshell_linux.c
Add the files
Diffstat (limited to 'src/detection/terminalshell/terminalshell_linux.c')
-rw-r--r--src/detection/terminalshell/terminalshell_linux.c449
1 files changed, 449 insertions, 0 deletions
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;
+}