#include "terminalshell.h" #include "common/FFcache.h" #include "common/debug.h" #include "common/io.h" #include "common/parsing.h" #include "common/processing.h" #include "common/thread.h" #include "common/strutil.h" #ifdef __ANDROID__ #include "common/android/applabel.h" #endif #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 = nullptr; { 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) == nullptr) { 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, "mise") || ffStrbufEqualS(&result->processName, "mise-shim") || ffStrbufEqualS(&result->processName, "atuin") || 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, nullptr) == nullptr) { // 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, "ksh93") || ffStrbufEqualS(&result->processName, "mksh") || ffStrbufEqualS(&result->processName, "pdksh") || 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, nullptr) != nullptr) { 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 == nullptr) { return false; } pid_t pid = (pid_t) strtol(envStr, nullptr, 10); result->pid = (uint32_t) pid; if (ffProcessGetBasicInfoLinux(pid, &result->processName, (pid_t*) &result->ppid, nullptr) == nullptr) { 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 = nullptr; // SSH if ( getenv("SSH_TTY") != nullptr) { term = getenv("SSH_TTY"); } else if ( getenv("KITTY_PID") != nullptr || getenv("KITTY_INSTALLATION_DIR") != nullptr) { if (getTerminalInfoByPidEnv(result, "KITTY_PID")) { return; } term = "kitty"; } #ifdef __linux__ // WSL // Windows Terminal else if ( getenv("WT_SESSION") != nullptr || getenv("WT_PROFILE_ID") != nullptr) term = "Windows Terminal"; // ConEmu else if ( getenv("ConEmuPID") != nullptr) { term = "ConEmu"; } #endif // Alacritty else if ( getenv("ALACRITTY_SOCKET") != nullptr || getenv("ALACRITTY_LOG") != nullptr || getenv("ALACRITTY_WINDOW_ID") != nullptr) { term = "Alacritty"; } #ifdef __ANDROID__ // Termux else if ( getenv("TERMUX_VERSION") != nullptr || getenv("TERMUX_MAIN_PACKAGE_FORMAT") != nullptr) { 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") != nullptr) term = "konsole"; else if ( getenv("GNOME_TERMINAL_SCREEN") != nullptr || getenv("GNOME_TERMINAL_SERVICE") != nullptr) { term = "gnome-terminal"; } #endif // MacOS, mintty else if (getenv("TERM_PROGRAM") != nullptr) { term = getenv("TERM_PROGRAM"); } else if (getenv("LC_TERMINAL") != nullptr) { 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")) { // The app is installed under a label rather than under its package name, and the label is // what every screen the user sees calls it -- "Termux:Launcher" on the test device, not // "Termux". The package name is the fallback for a build whose label cannot be read, which // includes an app whose label is a literal string in its manifest rather than a resource // reference; see common/android/applabel.h for what that read costs and when it fails. ffStrbufInitStatic(&result->prettyName, "Termux"); const char* labelError = ffAndroidGetAppLabel("com.termux", &result->prettyName); if (labelError != nullptr) { FF_DEBUG("The label of com.termux could not be resolved: %s", labelError); } } #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 static FFShellResult shellResult; static void destroyShellResult(void* storage) { FFShellResult* result = storage; ffStrbufDestroy(&result->processName); ffStrbufDestroy(&result->exe); result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again ffStrbufDestroy(&result->exePath); ffStrbufDestroy(&result->prettyName); ffStrbufDestroy(&result->version); result->pid = 0; result->ppid = 0; result->tty = -1; } static FFcacheEntry ffCacheEntryShell = { .name = "shell", .storage = &shellResult, .destroy = destroyShellResult, }; const FFShellResult* ffDetectShell() { FFShellResult* result = &shellResult; if (!ffCacheBeginInit(&ffCacheEntryShell)) { return result; } 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, nullptr); } ppid = getShellInfo(result, ppid); getUserShellFromEnv(result); if (result->processName.length > 0) { setShellInfoDetails(result); if (instance.config.general.detectVersion) { if (!fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, result->exeName, &result->version)) { ffStrbufClear(&result->version); } } } return result; } static FFTerminalResult terminalResult; static void destroyTerminalResult(void* storage) { FFTerminalResult* result = storage; ffStrbufDestroy(&result->processName); ffStrbufDestroy(&result->exe); result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again ffStrbufDestroy(&result->exePath); ffStrbufDestroy(&result->prettyName); ffStrbufDestroy(&result->version); ffStrbufDestroy(&result->tty); result->pid = 0; result->ppid = 0; } static FFcacheEntry ffCacheEntryTerminal = { .name = "terminal", .storage = &terminalResult, .destroy = destroyTerminalResult, }; const FFTerminalResult* ffDetectTerminal() { FFTerminalResult* result = &terminalResult; if (!ffCacheBeginInit(&ffCacheEntryTerminal)) { return result; } 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) { if (!fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version)) { ffStrbufClear(&result->version); } } } return result; }