master ohmu/fastfetch-openbsd / src / detection / terminalshell / terminalshell_windows.c
ohmu  ·  2026-10-15
  1#include "terminalshell.h"
  2#include "common/FFcache.h"
  3#include "common/io.h"
  4#include "common/processing.h"
  5#include "common/thread.h"
  6#include "common/mallocHelper.h"
  7#include "common/windows/folders.h"
  8#include "common/windows/registry.h"
  9#include "common/windows/unicode.h"
 10#include "common/windows/version.h"
 11#include "common/windows/nt.h"
 12#include "common/strutil.h"
 13
 14#include <stdalign.h>
 15#include <windows.h>
 16#include <wchar.h>
 17#include <tlhelp32.h>
 18#include <ntstatus.h>
 19#include <winternl.h>
 20
 21static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) {
 22    uint32_t ppid = 0;
 23    bool gui = false;
 24
 25    while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) {
 26        ffStrbufSet(&result->prettyName, &result->processName);
 27        if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) {
 28            ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4);
 29        }
 30
 31        // Common programs that are between terminal and own process, but are not the shell
 32        if (
 33            !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
 34                        ffStrbufIgnCaseEqualS(&result->prettyName, "fastfetch") ||                                                                                                                                                                                                                                                                          // scoop warps the real binaries with a "shim" exe
 35                        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
 36                        )) {
 37            ffStrbufClear(&result->processName);
 38            ffStrbufClear(&result->prettyName);
 39            ffStrbufClear(&result->exe);
 40            result->exeName = nullptr;
 41            pid = ppid;
 42            continue;
 43        }
 44
 45        result->pid = pid;
 46
 47        if (gui) {
 48            // Started without shell
 49            // In this case, terminal process will be created by fastfetch itself.
 50            ppid = 0;
 51            if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) {
 52                ffStrbufSetS(&result->prettyName, "Windows Explorer");
 53            }
 54        } else {
 55            result->ppid = ppid;
 56        }
 57
 58        break;
 59    }
 60    return ppid;
 61}
 62
 63static void setShellInfoDetails(FFShellResult* result) {
 64    if (ffStrbufIgnCaseEqualS(&result->prettyName, "pwsh")) {
 65        ffStrbufSetS(&result->prettyName, "PowerShell");
 66    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell")) {
 67        ffStrbufSetS(&result->prettyName, "Windows PowerShell");
 68    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell_ise")) {
 69        ffStrbufSetS(&result->prettyName, "Windows PowerShell ISE");
 70    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "cmd")) {
 71        ffStrbufSetS(&result->prettyName, "CMD");
 72
 73        if (instance.config.general.detectVersion) {
 74            FF_AUTO_CLOSE_FD HANDLE snapshot = nullptr;
 75            while (!(snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, result->pid)) && GetLastError() == ERROR_BAD_LENGTH) {}
 76
 77            if (snapshot) {
 78                MODULEENTRY32W module;
 79                module.dwSize = sizeof(module);
 80                for (BOOL success = Module32FirstW(snapshot, &module); success; success = Module32NextW(snapshot, &module)) {
 81                    if (wcsncmp(module.szModule, L"clink_dll_", strlen("clink_dll_")) == 0) {
 82                        FF_STRBUF_AUTO_DESTROY clinkVersion = ffStrbufCreate();
 83                        if (ffGetFileVersion(module.szExePath, nullptr, &clinkVersion)) {
 84                            ffStrbufAppendF(&result->prettyName, " (with Clink %s)", clinkVersion.chars);
 85                        } else {
 86                            ffStrbufAppendS(&result->prettyName, " (with Clink)");
 87                        }
 88                        break;
 89                    }
 90                }
 91            }
 92        }
 93    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "nu")) {
 94        ffStrbufSetS(&result->prettyName, "nushell");
 95    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) {
 96        ffStrbufSetS(&result->prettyName, "Windows Explorer");
 97    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "busybox")) {
 98        ffStrbufInitStatic(&result->prettyName, "ash");
 99    }
100}
101
102static bool getTerminalFromEnv(FFTerminalResult* result) {
103    if (
104        result->processName.length > 0 &&
105        ffStrbufIgnCaseCompS(&result->processName, "explorer") != 0) {
106        return false;
107    }
108
109    const char* term = getenv("ConEmuPID");
110
111    if (term) {
112        // ConEmu
113        uint32_t pid = (uint32_t) strtoul(term, nullptr, 10);
114        result->pid = pid;
115        if (ffProcessGetInfoWindows(pid, nullptr, &result->processName, &result->exe, &result->exeName, &result->exePath, nullptr)) {
116            ffStrbufSet(&result->prettyName, &result->processName);
117            if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) {
118                ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4);
119            }
120            return true;
121        } else {
122            term = "ConEmu";
123        }
124    }
125
126    // SSH
127    if (getenv("SSH_CONNECTION") != nullptr) {
128        term = getenv("SSH_TTY") ?: "sshd"; // OpenSSH on Windows may not set SSH_TTY
129    }
130
131    // Windows Terminal
132    if (!term && (getenv("WT_SESSION") != nullptr || getenv("WT_PROFILE_ID") != nullptr)) {
133        term = "WindowsTerminal";
134    }
135
136    // Alacritty
137    if (!term && (getenv("ALACRITTY_SOCKET") != nullptr || getenv("ALACRITTY_LOG") != nullptr || getenv("ALACRITTY_WINDOW_ID") != nullptr)) {
138        term = "Alacritty";
139    }
140
141    if (!term) {
142        term = getenv("TERM_PROGRAM");
143    }
144
145    // Normal Terminal
146    if (!term) {
147        term = getenv("TERM");
148    }
149
150    if (term) {
151        ffStrbufSetS(&result->processName, term);
152        ffStrbufSetS(&result->prettyName, term);
153        ffStrbufSetS(&result->exe, term);
154        result->exeName = "";
155        return true;
156    }
157
158    return false;
159}
160
161static bool detectDefaultTerminal(FFTerminalResult* result) {
162    wchar_t regPath[128] = L"SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\";
163    wchar_t* uuid = regPath + strlen("SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\");
164    FF_AUTO_CLOSE_FD HANDLE hkcu = nullptr;
165    if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Console\\%%Startup", &hkcu, nullptr) &&
166        ffRegReadData(hkcu, L"DelegationTerminal", &(FFArgBuffer) {
167                                                       .data = uuid,
168                                                       .length = (uint32_t) (sizeof(regPath) - (size_t) (uuid - regPath) * sizeof(wchar_t)),
169                                                   },
170            nullptr)) {
171        if (wcscmp(uuid, L"{00000000-0000-0000-0000-000000000000}") == 0 || // Let Windows decide
172            wcscmp(uuid, L"{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}") == 0)   // Conhost
173        {
174            goto conhost;
175        }
176
177        FF_AUTO_CLOSE_FD HANDLE hklm = nullptr;
178        if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, regPath, &hklm, nullptr)) {
179            FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
180            if (ffRegGetSubKey(hklm, 0, &path, nullptr)) {
181                if (ffStrbufStartsWithS(&path, "Microsoft.WindowsTerminal")) {
182                    ffStrbufSetS(&result->processName, "WindowsTerminal.exe");
183                    ffStrbufSetS(&result->prettyName, "WindowsTerminal");
184
185                    if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &result->exe)) {
186                        ffStrbufAppendS(&result->exe, "\\WindowsApps\\");
187                        ffStrbufAppend(&result->exe, &path);
188                        ffStrbufAppendS(&result->exe, "\\WindowsTerminal.exe");
189
190                        if (ffPathExists(result->exe.chars, FF_PATHTYPE_FILE)) {
191                            result->exeName = result->exe.chars + ffStrbufLastIndexC(&result->exe, '\\') + 1;
192                            ffStrbufSet(&result->exePath, &result->exe);
193                        } else {
194                            ffStrbufDestroy(&result->exe);
195                            ffStrbufInitMove(&result->exe, &path);
196                            result->exeName = "";
197                        }
198                    }
199                    return true;
200                }
201            }
202        }
203    }
204
205conhost:;
206    ULONG_PTR conhostPid = 0;
207    ULONG size;
208    if (NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(), ProcessConsoleHostProcess, &conhostPid, sizeof(conhostPid), &size)) && conhostPid != 0) {
209        // For Windows Terminal, it reports the PID of OpenConsole
210        if (ffProcessGetInfoWindows((uint32_t) conhostPid, nullptr, &result->processName, &result->exe, &result->exeName, &result->exePath, nullptr)) {
211            ffStrbufSet(&result->prettyName, &result->processName);
212            if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) {
213                ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4);
214            }
215            return true;
216        }
217    }
218
219    ffStrbufClear(&result->exe);
220    return false;
221}
222
223static uint32_t getTerminalInfo(FFTerminalResult* result, uint32_t pid) {
224    if (getenv("MSYSTEM")) {
225        // Don't try to detect terminals in MSYS shell
226        // It won't work because MSYS doesn't follow process tree of native Windows programs
227        return 0;
228    }
229
230    uint32_t ppid = 0;
231    bool gui;
232
233    while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) {
234        if (!gui) {
235            // We are in nested shell
236            ffStrbufClear(&result->processName);
237            ffStrbufClear(&result->prettyName);
238            ffStrbufClear(&result->exe);
239            ffStrbufClear(&result->exePath);
240            result->exeName = "";
241            pid = ppid;
242            continue;
243        }
244
245        ffStrbufSet(&result->prettyName, &result->processName);
246        if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) {
247            ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4);
248        }
249
250        if (ffStrbufIgnCaseEqualS(&result->prettyName, "sihost") ||
251            ffStrbufIgnCaseEqualS(&result->prettyName, "explorer") ||
252            ffStrbufIgnCaseEqualS(&result->prettyName, "wininit") ||
253            ffStrbufIgnCaseEqualS(&result->prettyName, "services")) {
254            // A CUI program created by Windows Explorer will spawn a conhost as its child.
255            // However the conhost process is just a placeholder;
256            // The true terminal can be Windows Terminal or others.
257            ffStrbufClear(&result->processName);
258            ffStrbufClear(&result->prettyName);
259            ffStrbufClear(&result->exe);
260            ffStrbufClear(&result->exePath);
261            result->exeName = "";
262            return 0;
263        } else {
264            result->pid = pid;
265            result->ppid = ppid;
266        }
267
268        break;
269    }
270    return ppid;
271}
272
273static void setTerminalInfoDetails(FFTerminalResult* result) {
274    if (ffStrbufIgnCaseEqualS(&result->prettyName, "WindowsTerminal")) {
275        ffStrbufSetStatic(&result->prettyName, ffStrbufContainIgnCaseS(&result->exe, ".WindowsTerminalPreview_") ? "Windows Terminal Preview" : "Windows Terminal");
276    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "conhost")) {
277        ffStrbufSetStatic(&result->prettyName, "Windows Console");
278    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "Code")) {
279        ffStrbufSetStatic(&result->prettyName, "Visual Studio Code");
280    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) {
281        ffStrbufSetStatic(&result->prettyName, "Windows Explorer");
282    } else if (ffStrbufEqualS(&result->prettyName, "wezterm-gui")) {
283        ffStrbufSetStatic(&result->prettyName, "WezTerm");
284    } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "sshd") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "sshd-")) {
285        const char* tty = getenv("SSH_TTY");
286        if (tty) {
287            ffStrbufSetS(&result->prettyName, tty);
288        }
289    }
290}
291
292static FFShellResult shellResult;
293
294static void destroyShellResult(void* storage) {
295    FFShellResult* result = storage;
296
297    ffStrbufDestroy(&result->processName);
298    ffStrbufDestroy(&result->exe);
299    result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it
300    ffStrbufDestroy(&result->exePath);
301    ffStrbufDestroy(&result->prettyName);
302    ffStrbufDestroy(&result->version);
303    result->pid = 0;
304    result->ppid = 0;
305    result->tty = -1;
306}
307
308static FFcacheEntry ffCacheEntryShell = {
309    .name = "shell",
310    .storage = &shellResult,
311    .destroy = destroyShellResult,
312};
313
314const FFShellResult* ffDetectShell(void) {
315    FFShellResult* result = &shellResult;
316
317    if (!ffCacheBeginInit(&ffCacheEntryShell)) {
318        return result;
319    }
320
321    ffStrbufInit(&result->processName);
322    ffStrbufInitA(&result->exe, MAX_PATH);
323    result->exeName = "";
324    ffStrbufInit(&result->exePath);
325    ffStrbufInit(&result->prettyName);
326    ffStrbufInit(&result->version);
327    result->pid = 0;
328    result->ppid = 0;
329    result->tty = -1;
330
331    uint32_t ppid;
332    if (!ffProcessGetInfoWindows(0, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr)) {
333        return result;
334    }
335
336    const char* ignoreParent = getenv("FFTS_IGNORE_PARENT");
337    if (ignoreParent && ffStrEquals(ignoreParent, "1")) {
338        ffProcessGetInfoWindows(ppid, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr);
339    }
340
341    ppid = getShellInfo(result, ppid);
342
343    if (result->processName.length > 0) {
344        setShellInfoDetails(result);
345        char tmp[MAX_PATH];
346        char* end = ffStrCopy(tmp, result->exeName, sizeof(tmp));
347        char* ext = (char*) memrchr(tmp, '.', (size_t) (end - tmp));
348        if (ext) {
349            *ext = '\0';
350        }
351        if (instance.config.general.detectVersion) {
352            if (!fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, tmp, &result->version)) {
353                ffStrbufClear(&result->version);
354            }
355        }
356    }
357
358    return result;
359}
360
361static FFTerminalResult terminalResult;
362
363static void destroyTerminalResult(void* storage) {
364    FFTerminalResult* result = storage;
365
366    ffStrbufDestroy(&result->processName);
367    ffStrbufDestroy(&result->exe);
368    result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it
369    ffStrbufDestroy(&result->exePath);
370    ffStrbufDestroy(&result->prettyName);
371    ffStrbufDestroy(&result->version);
372    ffStrbufDestroy(&result->tty);
373    result->pid = 0;
374    result->ppid = 0;
375}
376
377static FFcacheEntry ffCacheEntryTerminal = {
378    .name = "terminal",
379    .storage = &terminalResult,
380    .destroy = destroyTerminalResult,
381};
382
383const FFTerminalResult* ffDetectTerminal(void) {
384    FFTerminalResult* result = &terminalResult;
385
386    if (!ffCacheBeginInit(&ffCacheEntryTerminal)) {
387        return result;
388    }
389
390    ffStrbufInit(&result->processName);
391    ffStrbufInitA(&result->exe, MAX_PATH);
392    result->exeName = "";
393    ffStrbufInit(&result->exePath);
394    ffStrbufInit(&result->prettyName);
395    ffStrbufInit(&result->version);
396    ffStrbufInit(&result->tty);
397    result->pid = 0;
398    result->ppid = 0;
399
400    uint32_t ppid = ffDetectShell()->ppid;
401    if (ppid) {
402        getTerminalInfo(result, ppid);
403    }
404
405    if (result->processName.length == 0) {
406        getTerminalFromEnv(result);
407    }
408    if (result->processName.length == 0) {
409        detectDefaultTerminal(result);
410    }
411
412    if (result->processName.length > 0) {
413        setTerminalInfoDetails(result);
414        if (instance.config.general.detectVersion) {
415            if (!fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version)) {
416                ffStrbufClear(&result->version);
417            }
418        }
419    }
420
421    return result;
422}