diff options
| author | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
|---|---|---|
| committer | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
| commit | 76424950e373d3b04ac3dd13019151bfba3e8423 (patch) | |
| tree | 4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /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.c | 449 |
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; +} |