master ohmu/fastfetch-openbsd / src / detection / terminalshell / terminalshell_linux.c
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}