master ohmu/fastfetch-openbsd / src / modules / command / command.c
ohmu  ·  2026-10-15
  1#include "common/printing.h"
  2#include "common/jsonconfig.h"
  3#include "modules/command/command.h"
  4#include "detection/command/command.h"
  5
  6#if defined(_WIN32)
  7// `ffStrbufGetline()` splits on `\n` only, so a CRLF line keeps its trailing `\r`. That is what every
  8// `cmd.exe` builtin and most native Windows tools produce, and the leftover carriage return makes the
  9// terminal jump back to the start of the line. The `\r` is overwritten with a NUL, so `line` stays a
 10// usable C string; the length without the `\r` is returned for callers that need to copy the line.
 11//
 12// `len` itself must stay the distance from the line start to the delimiter: `ffStrbufGetdelim()`
 13// advances with `*lineptr += *n`, so shortening it would make the next call start inside this line.
 14static size_t trimCarriageReturn(char* line, size_t len) {
 15    if (len > 0 && line[len - 1] == '\r') {
 16        line[len - 1] = '\0';
 17        return len - 1;
 18    }
 19    return len;
 20}
 21#endif
 22
 23bool ffPrintCommand(FFCommandOptions* options) {
 24    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
 25    const char* error = ffDetectCommand(options, &result);
 26
 27    if (error) {
 28        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
 29        return false;
 30    }
 31
 32    if (!result.length) {
 33        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No result generated");
 34        return false;
 35    }
 36
 37    if (options->splitLines) {
 38        uint32_t index = 0;
 39        char* line = nullptr;
 40        size_t len = 0;
 41        while (ffStrbufGetline(&line, &len, &result)) {
 42#if defined(_WIN32)
 43            trimCarriageReturn(line, len);
 44#endif
 45            if (options->moduleArgs.outputFormat.length == 0) {
 46                ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Command), ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
 47                puts(line);
 48            } else {
 49                FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Command), ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(line, "result") }));
 50            }
 51        }
 52    } else {
 53        if (options->moduleArgs.outputFormat.length == 0) {
 54            ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
 55            ffStrbufPutTo(&result, stdout);
 56        } else {
 57            FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(result, "result") }));
 58        }
 59    }
 60
 61    return true;
 62}
 63
 64void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) {
 65    yyjson_val *key, *val;
 66    size_t idx, max;
 67    yyjson_obj_foreach (module, idx, max, key, val) {
 68        if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
 69            continue;
 70        }
 71
 72        if (unsafe_yyjson_equals_str(key, "shell")) {
 73            ffStrbufSetJsonVal(&options->shell, val);
 74            continue;
 75        }
 76
 77        if (unsafe_yyjson_equals_str(key, "param")) {
 78            ffStrbufSetJsonVal(&options->param, val);
 79            continue;
 80        }
 81
 82        if (unsafe_yyjson_equals_str(key, "text")) {
 83            ffStrbufSetJsonVal(&options->text, val);
 84            continue;
 85        }
 86
 87        if (unsafe_yyjson_equals_str(key, "useStdErr")) {
 88            options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT;
 89            ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead");
 90            continue;
 91        }
 92
 93        if (unsafe_yyjson_equals_str(key, "useOutput")) {
 94            int value;
 95            const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
 96                                                                       { "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT },
 97                                                                       { "stderr", FF_PROCESS_OUTPUT_STDERR_BIT },
 98                                                                       { "both", FF_PROCESS_OUTPUT_BOTH },
 99                                                                       {},
100                                                                   });
101            if (error) {
102                ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
103            } else {
104                options->useOutput = (FFProcessOutputType) value;
105            }
106            continue;
107        }
108
109        if (unsafe_yyjson_equals_str(key, "parallel")) {
110            options->parallel = yyjson_get_bool(val);
111            continue;
112        }
113
114        if (unsafe_yyjson_equals_str(key, "splitLines")) {
115            options->splitLines = yyjson_get_bool(val);
116            continue;
117        }
118
119        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
120    }
121}
122
123[[gnu::cold]]
124void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
125    ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
126
127    yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell);
128    yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param);
129    yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text);
130    switch (options->useOutput) {
131        case FF_PROCESS_OUTPUT_STDOUT_BIT:
132            yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout");
133            break;
134        case FF_PROCESS_OUTPUT_STDERR_BIT:
135            yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr");
136            break;
137        case FF_PROCESS_OUTPUT_BOTH:
138            yyjson_mut_obj_add_str(doc, module, "useOutput", "both");
139            break;
140        default:
141            break;
142    }
143    yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel);
144    yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines);
145}
146
147bool ffGenerateCommandJsonResult([[maybe_unused]] FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
148    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
149    const char* error = ffDetectCommand(options, &result);
150
151    if (error) {
152        yyjson_mut_obj_add_str(doc, module, "error", error);
153        return false;
154    }
155
156    if (!result.length) {
157        yyjson_mut_obj_add_str(doc, module, "error", "No result generated");
158        return false;
159    }
160
161    if (options->splitLines) {
162        yyjson_mut_val* jsonArray = yyjson_mut_obj_add_arr(doc, module, "result");
163        char* line = nullptr;
164        size_t len = 0;
165        while (ffStrbufGetline(&line, &len, &result)) {
166            // `len` has to stay the distance to the delimiter (see `trimCarriageReturn`), so the
167            // possibly shorter length of the line itself is tracked separately.
168#if defined(_WIN32)
169            size_t contentLen = trimCarriageReturn(line, len);
170#else
171            size_t contentLen = len;
172#endif
173            yyjson_mut_arr_add_strncpy(doc, jsonArray, line, contentLen);
174        }
175    } else {
176        yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
177    }
178
179    return true;
180}
181
182void ffInitCommandOptions(FFCommandOptions* options) {
183    ffOptionInitModuleArg(&options->moduleArgs, "");
184
185    ffStrbufInitStatic(&options->shell,
186#ifdef _WIN32
187        "cmd.exe"
188#else
189        "/bin/sh"
190#endif
191    );
192    ffStrbufInitStatic(&options->param,
193#ifdef _WIN32
194        "/d/c"
195#else
196        "-c"
197#endif
198    );
199    ffStrbufInit(&options->text);
200    // Capture both streams by default: most tools log to stdout or stderr without saying which, and
201    // the previous single-stream default silently dropped one of them.
202    options->useOutput = FF_PROCESS_OUTPUT_BOTH;
203    options->parallel = true;
204    options->splitLines = false;
205}
206
207void ffDestroyCommandOptions(FFCommandOptions* options) {
208    ffOptionDestroyModuleArg(&options->moduleArgs);
209    ffStrbufDestroy(&options->shell);
210    ffStrbufDestroy(&options->param);
211    ffStrbufDestroy(&options->text);
212}
213
214FFModuleBaseInfo ffCommandModuleInfo = {
215    .name = "Command",
216    .description = "Run custom shell scripts",
217    .displayName = {
218        .en = "Command",
219        .ar = "الأمر",
220        .cs = "Příkaz",
221        .de = "Befehl",
222        .es = "Comando",
223        .fi = "Komento",
224        .fr = "Commande",
225        .gl = "Comando",
226        .he = "פקודה",
227        .id = "Perintah",
228        .it = "Comando",
229        .ja = "コマンド",
230        .ko = "명령어",
231        .pl = "Polecenie",
232        .pt = "Comando",
233        .ru = "Команда",
234        .tr = "Komut",
235        .uk = "Команда",
236        .vi = "Lệnh",
237        .zh_CN = "命令",
238        .zh_TW = "命令",
239    },
240    .initOptions = (void*) ffInitCommandOptions,
241    .destroyOptions = (void*) ffDestroyCommandOptions,
242    .parseJsonObject = (void*) ffParseCommandJsonObject,
243    .printModule = (void*) ffPrintCommand,
244    .generateJsonResult = (void*) ffGenerateCommandJsonResult,
245    .generateJsonConfig = (void*) ffGenerateCommandJsonConfig,
246    .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
247        { "Command result", "result" },
248    })),
249};