Files
fastfetch/src/detection/terminalshell/terminalshell_windows.c
T
2026-04-28 19:45:19 +08:00

378 lines
15 KiB
C

#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/stringUtils.h"
#include <stdalign.h>
#include <windows.h>
#include <wchar.h>
#include <tlhelp32.h>
#include <ntstatus.h>
#include <winternl.h>
#include <shlobj.h>
bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version);
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");
}
}
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);
}
}
}
bool fftsGetTerminalVersion(FFstrbuf* processName, FFstrbuf* exe, FFstrbuf* version);
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';
}
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);
fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version);
}
return &result;
}