ohmu
·
2026-10-15
1#include "terminalshell.h"
2#include "common/FFcache.h"
3#include "common/debug.h"
4#include "common/io.h"
5#include "common/parsing.h"
6#include "common/processing.h"
7#include "common/thread.h"
8#include "common/strutil.h"
9
10#ifdef __ANDROID__
11 #include "common/android/applabel.h"
12#endif
13
14#include <string.h>
15#include <stdlib.h>
16#include <unistd.h>
17
18static void setExeName(FFstrbuf* exe, const char** exeName) {
19 assert(exe->length > 0);
20 uint32_t lastSlashIndex = ffStrbufLastIndexC(exe, '/');
21 if (lastSlashIndex < exe->length) {
22 *exeName = exe->chars + lastSlashIndex + 1;
23 }
24}
25
26static pid_t getShellInfo(FFShellResult* result, pid_t pid) {
27 pid_t ppid = 0;
28 int32_t tty = -1;
29
30 const char* userShellName = nullptr;
31 {
32 uint32_t index = ffStrbufLastIndexC(&instance.state.platform.userShell, '/');
33 if (index == instance.state.platform.userShell.length) {
34 userShellName = instance.state.platform.userShell.chars;
35 } else {
36 userShellName = instance.state.platform.userShell.chars + index + 1;
37 }
38 }
39
40 while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, &tty) == nullptr) {
41 if (!ffStrbufEqualS(&result->processName, userShellName)) {
42 // Common programs that are between terminal and own process, but are not the shell
43 if (
44 // tty < 0 || //A shell should connect to a tty
45 pid == 1 || // init/systemd
46 ffStrbufEqualS(&result->processName, "sh") || // This prevents us from detecting things like pipes and redirects, i hope nobody uses plain `sh` as shell
47 ffStrbufEqualS(&result->processName, "sudo") ||
48 ffStrbufEqualS(&result->processName, "su") ||
49 ffStrbufEqualS(&result->processName, "strace") ||
50 ffStrbufEqualS(&result->processName, "gdb") ||
51 ffStrbufEqualS(&result->processName, "lldb") ||
52 ffStrbufEqualS(&result->processName, "lldb-mi") ||
53 ffStrbufEqualS(&result->processName, "login") ||
54 ffStrbufEqualS(&result->processName, "ltrace") ||
55 ffStrbufEqualS(&result->processName, "perf") ||
56 ffStrbufEqualS(&result->processName, "guake-wrapped") ||
57 ffStrbufEqualS(&result->processName, "time") ||
58 ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289
59 ffStrbufEqualS(&result->processName, "valgrind") ||
60 ffStrbufEqualS(&result->processName, "fastfetch") || // #994
61 ffStrbufEqualS(&result->processName, "flashfetch") ||
62 ffStrbufEqualS(&result->processName, "proot") ||
63 ffStrbufEqualS(&result->processName, "script") ||
64#ifdef __linux__
65 ffStrbufEqualS(&result->processName, "run-parts") ||
66#endif
67 ffStrbufContainS(&result->processName, "debug") ||
68 ffStrbufContainS(&result->processName, "command-not-") ||
69 ffStrbufEndsWithS(&result->processName, ".sh")) {
70 pid = ppid;
71 ffStrbufClear(&result->processName);
72 continue;
73 }
74 }
75
76 result->pid = (uint32_t) pid;
77 result->ppid = (uint32_t) ppid;
78 result->tty = tty;
79 ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath);
80 break;
81 }
82 return pid > 1 ? ppid : 0;
83}
84
85static pid_t getTerminalInfo(FFTerminalResult* result, pid_t pid) {
86 pid_t ppid = 0;
87
88 while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, nullptr) == nullptr) {
89 // Known shells
90 if (
91 pid == 1 || // init/systemd
92 ffStrbufEqualS(&result->processName, "sudo") ||
93 ffStrbufEqualS(&result->processName, "su") ||
94 ffStrbufEqualS(&result->processName, "sh") ||
95 ffStrbufEqualS(&result->processName, "ash") ||
96 ffStrbufEqualS(&result->processName, "bash") ||
97 ffStrbufEqualS(&result->processName, "zsh") ||
98 ffStrbufEqualS(&result->processName, "ksh") ||
99 ffStrbufEqualS(&result->processName, "ksh93") ||
100 ffStrbufEqualS(&result->processName, "mksh") ||
101 ffStrbufEqualS(&result->processName, "pdksh") ||
102 ffStrbufEqualS(&result->processName, "oksh") ||
103 ffStrbufEqualS(&result->processName, "csh") ||
104 ffStrbufEqualS(&result->processName, "tcsh") ||
105 ffStrbufEqualS(&result->processName, "fish") ||
106 ffStrbufEqualS(&result->processName, "dash") ||
107 ffStrbufEqualS(&result->processName, "pwsh") ||
108 ffStrbufEqualS(&result->processName, "nu") ||
109 ffStrbufEqualS(&result->processName, "git-shell") ||
110 ffStrbufEqualS(&result->processName, "elvish") ||
111 ffStrbufEqualS(&result->processName, "oil.ovm") ||
112 ffStrbufEqualS(&result->processName, "xonsh") || // works in Linux but not in macOS because kernel returns `Python` in this case
113 ffStrbufEqualS(&result->processName, "login") ||
114 ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289
115 ffStrbufEqualS(&result->processName, "chezmoi") || // #762
116 ffStrbufEqualS(&result->processName, "proot") ||
117 ffStrbufEqualS(&result->processName, "script") ||
118#ifdef __linux__
119 ffStrbufStartsWithS(&result->processName, "Relay(") || // Unknown process in WSL2
120 ffStrbufStartsWithS(&result->processName, "flatpak-") || // #707
121 ffStrbufEqualS(&result->processName, "run-parts") || // #2048
122#endif
123 ffStrbufEndsWithS(&result->processName, ".sh")) {
124 pid = ppid;
125 ffStrbufClear(&result->processName);
126 continue;
127 }
128
129#ifdef __APPLE__
130 // https://github.com/fastfetch-cli/fastfetch/discussions/501
131 const char* pLeft = strstr(result->processName.chars, " (");
132 if (pLeft) {
133 pLeft += 2;
134 const char* pRight = strstr(pLeft, "term)");
135 if (pRight && pRight[5] == '\0') {
136 for (; pLeft < pRight; ++pLeft) {
137 if (*pLeft < 'a' || *pLeft > 'z') {
138 break;
139 }
140 }
141 if (pLeft == pRight && ffProcessGetBasicInfoLinux(ppid, &result->processName, &ppid, nullptr) != nullptr) {
142 return 0;
143 }
144 }
145 }
146#endif
147
148 result->pid = (uint32_t) pid;
149 result->ppid = (uint32_t) ppid;
150 ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath);
151 break;
152 }
153 return pid > 1 ? ppid : 0;
154}
155
156static bool getTerminalInfoByPidEnv(FFTerminalResult* result, const char* pidEnv) {
157 const char* envStr = getenv(pidEnv);
158 if (envStr == nullptr) {
159 return false;
160 }
161
162 pid_t pid = (pid_t) strtol(envStr, nullptr, 10);
163 result->pid = (uint32_t) pid;
164 if (ffProcessGetBasicInfoLinux(pid, &result->processName, (pid_t*) &result->ppid, nullptr) == nullptr) {
165 ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath);
166 return true;
167 }
168
169 return false;
170}
171
172static void getTerminalFromEnv(FFTerminalResult* result) {
173 if (result->processName.length > 0) {
174 if (!ffStrbufStartsWithS(&result->processName, "login") &&
175 !ffStrbufEqualS(&result->processName, "(login)") &&
176
177#ifdef __APPLE__
178 !ffStrbufEqualS(&result->processName, "launchd") &&
179#else
180 !ffStrbufEqualS(&result->processName, "systemd") &&
181 !ffStrbufEqualS(&result->processName, "init") &&
182 !ffStrbufEqualS(&result->processName, "(init)") &&
183 !ffStrbufEqualS(&result->processName, "SessionLeader") && // #750
184#endif
185
186 !ffStrbufEqualS(&result->processName, "0"))
187 return;
188
189 ffStrbufClear(&result->processName);
190 ffStrbufClear(&result->exe);
191 result->exeName = result->exe.chars;
192 ffStrbufClear(&result->exePath);
193 result->pid = result->ppid = 0;
194 }
195
196 const char* term = nullptr;
197
198 // SSH
199 if (
200 getenv("SSH_TTY") != nullptr) {
201 term = getenv("SSH_TTY");
202 } else if (
203 getenv("KITTY_PID") != nullptr ||
204 getenv("KITTY_INSTALLATION_DIR") != nullptr) {
205 if (getTerminalInfoByPidEnv(result, "KITTY_PID")) {
206 return;
207 }
208 term = "kitty";
209 }
210
211#ifdef __linux__ // WSL
212 // Windows Terminal
213 else if (
214 getenv("WT_SESSION") != nullptr ||
215 getenv("WT_PROFILE_ID") != nullptr)
216 term = "Windows Terminal";
217
218 // ConEmu
219 else if (
220 getenv("ConEmuPID") != nullptr) {
221 term = "ConEmu";
222 }
223#endif
224
225 // Alacritty
226 else if (
227 getenv("ALACRITTY_SOCKET") != nullptr ||
228 getenv("ALACRITTY_LOG") != nullptr ||
229 getenv("ALACRITTY_WINDOW_ID") != nullptr) {
230 term = "Alacritty";
231 }
232#ifdef __ANDROID__
233 // Termux
234 else if (
235 getenv("TERMUX_VERSION") != nullptr ||
236 getenv("TERMUX_MAIN_PACKAGE_FORMAT") != nullptr) {
237 if (getTerminalInfoByPidEnv(result, "TERMUX_APP__PID")) {
238 return;
239 }
240 term = "com.termux";
241 }
242#endif
243
244#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__)
245 // Konsole
246 else if (
247 getenv("KONSOLE_VERSION") != nullptr)
248 term = "konsole";
249
250 else if (
251 getenv("GNOME_TERMINAL_SCREEN") != nullptr ||
252 getenv("GNOME_TERMINAL_SERVICE") != nullptr) {
253 term = "gnome-terminal";
254 }
255#endif
256
257 // MacOS, mintty
258 else if (getenv("TERM_PROGRAM") != nullptr) {
259 term = getenv("TERM_PROGRAM");
260 }
261
262 else if (getenv("LC_TERMINAL") != nullptr) {
263 term = getenv("LC_TERMINAL");
264 }
265
266 // Normal Terminal
267 else {
268 term = getenv("TERM");
269 // TTY
270 if (!ffStrSet(term) || ffStrEquals(term, "linux")) {
271 term = ttyname(STDIN_FILENO);
272 }
273 }
274
275 if (ffStrSet(term)) {
276 ffStrbufSetS(&result->processName, term);
277 ffStrbufSetS(&result->exe, term);
278 setExeName(&result->exe, &result->exeName);
279 }
280}
281
282static void getUserShellFromEnv(FFShellResult* result) {
283 // If shell detection via processes failed
284 if (result->processName.length == 0 && instance.state.platform.userShell.length > 0) {
285 ffStrbufSet(&result->exe, &instance.state.platform.userShell);
286 setExeName(&result->exe, &result->exeName);
287 ffStrbufAppendS(&result->processName, result->exeName);
288 }
289}
290
291static void setShellInfoDetails(FFShellResult* result) {
292 if (ffStrbufEqualS(&result->processName, "pwsh")) {
293 ffStrbufInitStatic(&result->prettyName, "PowerShell");
294 } else if (ffStrbufEqualS(&result->processName, "nu")) {
295 ffStrbufInitStatic(&result->prettyName, "nushell");
296 } else if (ffStrbufEqualS(&result->processName, "oil.ovm")) {
297 ffStrbufInitStatic(&result->prettyName, "Oils");
298 } else if (ffStrbufEqualS(&result->processName, "busybox")) {
299 ffStrbufInitStatic(&result->prettyName, "ash");
300 } else {
301 // https://github.com/fastfetch-cli/fastfetch/discussions/280#discussioncomment-3831734
302 ffStrbufInitS(&result->prettyName, result->exeName);
303 }
304}
305
306static void setTerminalInfoDetails(FFTerminalResult* result) {
307 // For Nixpkgs. Ref: #510 and https://github.com/NixOS/nixpkgs/pull/249428
308 // We use processName when detecting version and font, overriding it for simplification
309 if (ffStrbufStartsWithC(&result->processName, '.') && ffStrbufStartsWithS(&result->exePath,"/nix/store")) {
310 ffStrbufSubstrAfter(&result->processName, 0);
311 if (strlen(result->exeName) < 15) {
312 ffStrbufSubstrBeforeLastC(&result->processName, '-');
313 }
314 }
315
316 if (ffStrbufEqualS(&result->processName, "wezterm-gui")) {
317 ffStrbufInitStatic(&result->prettyName, "WezTerm");
318 } else if (ffStrbufStartsWithS(&result->processName, "tmux:")) {
319 ffStrbufInitStatic(&result->prettyName, "tmux");
320 } else if (ffStrbufStartsWithS(&result->processName, "screen-")) {
321 ffStrbufInitStatic(&result->prettyName, "screen");
322 } else if (ffStrbufEqualS(&result->processName, "sshd") || ffStrbufStartsWithS(&result->processName, "sshd-")) {
323 if (result->tty.length) {
324 ffStrbufInitCopy(&result->prettyName, &result->tty);
325 } else {
326 ffStrbufSetStatic(&result->prettyName, "sshd");
327 }
328 }
329
330#if defined(__ANDROID__)
331
332 else if (ffStrbufEqualS(&result->processName, "com.termux")) {
333 // The app is installed under a label rather than under its package name, and the label is
334 // what every screen the user sees calls it -- "Termux:Launcher" on the test device, not
335 // "Termux". The package name is the fallback for a build whose label cannot be read, which
336 // includes an app whose label is a literal string in its manifest rather than a resource
337 // reference; see common/android/applabel.h for what that read costs and when it fails.
338 ffStrbufInitStatic(&result->prettyName, "Termux");
339 const char* labelError = ffAndroidGetAppLabel("com.termux", &result->prettyName);
340 if (labelError != nullptr) {
341 FF_DEBUG("The label of com.termux could not be resolved: %s", labelError);
342 }
343 }
344
345#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__)
346
347 else if (ffStrbufStartsWithS(&result->processName, "gnome-terminal"))
348 ffStrbufInitStatic(&result->prettyName, "GNOME Terminal");
349 else if (ffStrbufStartsWithS(&result->processName, "kgx")) {
350 ffStrbufInitStatic(&result->prettyName, "GNOME Console");
351 } else if (ffStrbufEqualS(&result->processName, "urxvt") ||
352 ffStrbufEqualS(&result->processName, "urxvtd") ||
353 ffStrbufEqualS(&result->processName, "rxvt")) {
354 ffStrbufInitStatic(&result->prettyName, "rxvt-unicode");
355 } else if (ffStrbufStartsWithS(&result->processName, "ptyxis-agent")) {
356 ffStrbufInitStatic(&result->prettyName, "Ptyxis");
357 }
358
359#elif defined(__APPLE__)
360
361 else if (ffStrbufEqualS(&result->processName, "iTerm.app") || ffStrbufStartsWithS(&result->processName, "iTermServer-"))
362 ffStrbufInitStatic(&result->prettyName, "iTerm");
363 else if (ffStrbufEndsWithS(&result->exePath, "Terminal.app/Contents/MacOS/Terminal")) {
364 ffStrbufSetStatic(&result->processName, "Apple_Terminal"); // $TERM_PROGRAM, for terminal font detection
365 ffStrbufInitStatic(&result->prettyName, "Apple Terminal");
366 } else if (ffStrbufEqualS(&result->processName, "Apple_Terminal")) {
367 ffStrbufInitStatic(&result->prettyName, "Apple Terminal");
368 } else if (ffStrbufEndsWithS(&result->exePath, "Warp.app/Contents/MacOS/stable")) {
369 ffStrbufSetStatic(&result->processName, "WarpTerminal"); // $TERM_PROGRAM, for terminal font detection
370 ffStrbufInitStatic(&result->prettyName, "Warp");
371 } else if (ffStrbufEqualS(&result->processName, "WarpTerminal")) {
372 ffStrbufInitStatic(&result->prettyName, "Warp");
373 }
374
375#elif defined(__HAIKU__)
376
377 else if (ffStrbufEqualS(&result->processName, "Terminal"))
378 ffStrbufInitStatic(&result->prettyName, "Haiku Terminal");
379
380#endif
381
382 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
383 ffStrbufInitS(&result->prettyName, result->exeName);
384 } else {
385 ffStrbufInitCopy(&result->prettyName, &result->processName);
386 }
387}
388
389#if defined(MAXPATH)
390 #define FF_EXE_PATH_LEN MAXPATH
391#elif defined(PATH_MAX)
392 #define FF_EXE_PATH_LEN PATH_MAX
393#else
394 #define FF_EXE_PATH_LEN 260
395#endif
396
397static FFShellResult shellResult;
398
399static void destroyShellResult(void* storage) {
400 FFShellResult* result = storage;
401
402 ffStrbufDestroy(&result->processName);
403 ffStrbufDestroy(&result->exe);
404 result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again
405 ffStrbufDestroy(&result->exePath);
406 ffStrbufDestroy(&result->prettyName);
407 ffStrbufDestroy(&result->version);
408 result->pid = 0;
409 result->ppid = 0;
410 result->tty = -1;
411}
412
413static FFcacheEntry ffCacheEntryShell = {
414 .name = "shell",
415 .storage = &shellResult,
416 .destroy = destroyShellResult,
417};
418
419const FFShellResult* ffDetectShell() {
420 FFShellResult* result = &shellResult;
421
422 if (!ffCacheBeginInit(&ffCacheEntryShell)) {
423 return result;
424 }
425
426 ffStrbufInit(&result->processName);
427 ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN);
428 result->exeName = result->exe.chars;
429 ffStrbufInit(&result->exePath);
430 ffStrbufInit(&result->version);
431 result->pid = 0;
432 result->ppid = 0;
433 result->tty = -1;
434
435 pid_t ppid = getppid();
436
437 const char* ignoreParent = getenv("FFTS_IGNORE_PARENT");
438 if (ignoreParent && ffStrEquals(ignoreParent, "1")) {
439 FF_STRBUF_AUTO_DESTROY _ = ffStrbufCreate();
440 ffProcessGetBasicInfoLinux(ppid, &_, &ppid, nullptr);
441 }
442
443 ppid = getShellInfo(result, ppid);
444 getUserShellFromEnv(result);
445
446 if (result->processName.length > 0) {
447 setShellInfoDetails(result);
448 if (instance.config.general.detectVersion) {
449 if (!fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, result->exeName, &result->version)) {
450 ffStrbufClear(&result->version);
451 }
452 }
453 }
454
455 return result;
456}
457
458static FFTerminalResult terminalResult;
459
460static void destroyTerminalResult(void* storage) {
461 FFTerminalResult* result = storage;
462
463 ffStrbufDestroy(&result->processName);
464 ffStrbufDestroy(&result->exe);
465 result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again
466 ffStrbufDestroy(&result->exePath);
467 ffStrbufDestroy(&result->prettyName);
468 ffStrbufDestroy(&result->version);
469 ffStrbufDestroy(&result->tty);
470 result->pid = 0;
471 result->ppid = 0;
472}
473
474static FFcacheEntry ffCacheEntryTerminal = {
475 .name = "terminal",
476 .storage = &terminalResult,
477 .destroy = destroyTerminalResult,
478};
479
480const FFTerminalResult* ffDetectTerminal() {
481 FFTerminalResult* result = &terminalResult;
482
483 if (!ffCacheBeginInit(&ffCacheEntryTerminal)) {
484 return result;
485 }
486
487 ffStrbufInit(&result->processName);
488 ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN);
489 result->exeName = result->exe.chars;
490 ffStrbufInit(&result->exePath);
491 ffStrbufInit(&result->version);
492 ffStrbufInitS(&result->tty, ttyname(STDOUT_FILENO));
493 result->pid = 0;
494 result->ppid = 0;
495
496 pid_t ppid = (pid_t) ffDetectShell()->ppid;
497
498 if (ppid) {
499 ppid = getTerminalInfo(result, ppid);
500 }
501 getTerminalFromEnv(result);
502
503 if (result->processName.length > 0) {
504 setTerminalInfoDetails(result);
505 if (instance.config.general.detectVersion) {
506 if (!fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version)) {
507 ffStrbufClear(&result->version);
508 }
509 }
510 }
511
512 return result;
513}