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}