master ohmu/fastfetch-openbsd / src / detection / editor / editor.c
ohmu  ·  2026-10-15
  1#include "editor.h"
  2#include "common/processing.h"
  3#include "common/library.h"
  4#include "common/strutil.h"
  5#include "common/path.h"
  6#include "common/binary.h"
  7
  8#include <stdlib.h>
  9
 10static bool extractNvimVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
 11    if (!ffStrStartsWith(str, "NVIM v")) {
 12        return true;
 13    }
 14    ffStrbufSetS((FFstrbuf*) userdata, str + strlen("NVIM v"));
 15    return false;
 16}
 17
 18static bool extractVimVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
 19    if (!ffStrStartsWith(str, "VIM - Vi IMproved ")) {
 20        return true;
 21    }
 22    ffStrbufSetS((FFstrbuf*) userdata, str + strlen("VIM - Vi IMproved "));
 23    ffStrbufSubstrBeforeFirstC(userdata, ' ');
 24    return false;
 25}
 26
 27static bool extractNanoVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
 28    if (!ffStrStartsWith(str, "GNU nano ")) {
 29        return true;
 30    }
 31    ffStrbufSetS((FFstrbuf*) userdata, str + strlen("GNU nano "));
 32    return false;
 33}
 34
 35// The message is printed to the user verbatim, so it has to name the variable that was wrong:
 36// `"Executable not found"` from `ffFindExecutableInPath()` doesn't say what was searched for.
 37static const char* notAnExecutable(bool isVisual) {
 38    return isVisual ? "$VISUAL does not point to an executable" : "$EDITOR does not point to an executable";
 39}
 40
 41const char* ffDetectEditor(FFEditorResult* result) {
 42    bool isVisual = true;
 43    ffStrbufSetS(&result->name, getenv("VISUAL"));
 44    if (!result->name.length) {
 45        isVisual = false;
 46        ffStrbufSetS(&result->name, getenv("EDITOR"));
 47    }
 48
 49    if (!result->name.length) {
 50        return "$VISUAL or $EDITOR not set";
 51    }
 52    result->type = isVisual ? "Visual" : "Editor";
 53
 54    // `$VISUAL`/`$EDITOR` hold a shell command line rather than a bare name, so they routinely
 55    // carry arguments: `EDITOR="code -w"`, `EDITOR="vim -f"`. Only the first word names the
 56    // program. The whole string used to be handed to `ffFindExecutableInPath()`, which could never
 57    // match it, and the module then printed the unparsed value as if it had been resolved.
 58    ffStrbufSubstrBefore(&result->name, (uint32_t) strcspn(result->name.chars, " \t\n\v\f\r"));
 59
 60    if (result->name.length == 0) {
 61        return notAnExecutable(isVisual);
 62    }
 63
 64    if (ffIsAbsolutePath(result->name.chars)) {
 65        ffStrbufSet(&result->path, &result->name);
 66    } else {
 67        const char* error = ffFindExecutableInPath(result->name.chars, &result->path);
 68        if (error) {
 69            // Reporting success here is what made `EDITOR="code -w"` print the raw value: the module
 70            // reads `nullptr` as "detection succeeded" and then falls back to printing `name`.
 71            return notAnExecutable(isVisual);
 72        }
 73    }
 74
 75    {
 76        char buf[PATH_MAX + 1];
 77        if (!realpath(result->path.chars, buf)) {
 78            // An absolute `$VISUAL`/`$EDITOR` is copied into `path` before it is verified, so a path
 79            // that does not exist has to be reported here instead of being handed to the caller as
 80            // if it had been checked.
 81            return notAnExecutable(isVisual);
 82        }
 83
 84        // WIN32: Should we handle scoop shim exe here?
 85
 86#ifdef __linux__
 87        if (!ffStrEndsWith(buf, "/snap"))
 88#endif
 89            ffStrbufSetS(&result->path, buf);
 90    }
 91
 92    {
 93        uint32_t index = ffStrbufLastIndexC(&result->path,
 94#ifndef _WIN32
 95            '/'
 96#else
 97            '\\'
 98#endif
 99        );
100        if (index == result->path.length) {
101            // `realpath()` always yields an absolute path, so neither of these two is reachable in
102            // practice. They used to `return nullptr`, which the module read as a success.
103            return "Failed to determine the executable name";
104        }
105        ffStrbufSetS(&result->exe, &result->path.chars[index + 1]);
106        if (!result->exe.length) {
107            return "Failed to determine the executable name";
108        }
109
110#ifdef _WIN32
111        if (ffStrbufEndsWithS(&result->exe, ".exe")) {
112            ffStrbufSubstrBefore(&result->exe, result->exe.length - 4);
113        }
114#endif
115    }
116
117    if (!instance.config.general.detectVersion) {
118        // Not an error: the editor was found, version probing was just turned off.
119        return nullptr;
120    }
121
122    if (ffStrbufEqualS(&result->exe, "nvim")) {
123        ffBinaryExtractStrings(result->path.chars, extractNvimVersionFromBinary, &result->version, (uint32_t) strlen("NVIM v0.0.0"));
124    } else if (ffStrbufEqualS(&result->exe, "vim") || ffStrbufStartsWithS(&result->exe, "vim.")) {
125        ffBinaryExtractStrings(result->path.chars, extractVimVersionFromBinary, &result->version, (uint32_t) strlen("VIM - Vi IMproved 0.0"));
126    } else if (ffStrbufEqualS(&result->exe, "nano")) {
127        ffBinaryExtractStrings(result->path.chars, extractNanoVersionFromBinary, &result->version, (uint32_t) strlen("GNU nano 0.0"));
128    }
129
130    if (result->version.length > 0) {
131        return nullptr;
132    }
133
134    const char* param = nullptr;
135    if (
136        ffStrbufEqualS(&result->exe, "nano") ||
137        ffStrbufEqualS(&result->exe, "vim") ||
138        ffStrbufStartsWithS(&result->exe, "vim.") || // vim.basic/vim.tiny
139        ffStrbufEqualS(&result->exe, "nvim") ||
140        ffStrbufEqualS(&result->exe, "micro") ||
141        ffStrbufEqualS(&result->exe, "emacs") ||
142        ffStrbufStartsWithS(&result->exe, "emacs-") || // emacs-29.3
143        ffStrbufEqualS(&result->exe, "hx") ||
144        ffStrbufEqualS(&result->exe, "code") ||
145        ffStrbufEqualS(&result->exe, "pluma") ||
146        ffStrbufEqualS(&result->exe, "sublime_text") ||
147        ffStrbufEqualS(&result->exe, "zeditor")) {
148        param = "--version";
149    } else if (
150        ffStrbufEqualS(&result->exe, "kak") ||
151        ffStrbufEqualS(&result->exe, "pico")) {
152        param = "-version";
153    } else if (
154        ffStrbufEqualS(&result->exe, "ne")) {
155        param = "-h";
156    } else {
157        return nullptr;
158    }
159
160    ffProcessAppendStdOut(&result->version, (char* const[]) {
161                                                result->path.chars,
162                                                (char*) param,
163                                                nullptr,
164                                            });
165
166    if (result->version.length == 0) {
167        return nullptr;
168    }
169
170    ffStrbufSubstrBeforeFirstC(&result->version, '\n');
171    const char* versionStart = strpbrk(result->version.chars, "0123456789");
172    if (versionStart != nullptr) {
173        const char* versionEnd = strpbrk(versionStart, " \t\v\f\r");
174        if (versionEnd != nullptr) {
175            ffStrbufSubstrBefore(&result->version, (uint32_t) (versionEnd - result->version.chars));
176        }
177
178        if (versionStart != result->version.chars) {
179            ffStrbufSubstrAfter(&result->version, (uint32_t) (versionStart - result->version.chars - 1));
180        }
181    }
182
183    return nullptr;
184}