master ohmu/fastfetch-openbsd / src / common / impl / format.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/format.h"
  3#include "common/parsing.h"
  4#include "common/textModifier.h"
  5#include "common/strutil.h"
  6#include "common/library.h"
  7
  8#include <inttypes.h>
  9
 10void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg) {
 11    switch (formatarg->type) {
 12        case FF_ARG_TYPE_INT:
 13            ffStrbufAppendSInt(buffer, *(int32_t*) formatarg->value);
 14            break;
 15        case FF_ARG_TYPE_UINT:
 16            ffStrbufAppendUInt(buffer, *(uint32_t*) formatarg->value);
 17            break;
 18        case FF_ARG_TYPE_UINT64:
 19            ffStrbufAppendUInt(buffer, *(uint64_t*) formatarg->value);
 20            break;
 21        case FF_ARG_TYPE_UINT16:
 22            ffStrbufAppendUInt(buffer, *(uint16_t*) formatarg->value);
 23            break;
 24        case FF_ARG_TYPE_UINT8:
 25            ffStrbufAppendUInt(buffer, *(uint8_t*) formatarg->value);
 26            break;
 27        case FF_ARG_TYPE_STRING:
 28            ffStrbufAppendS(buffer, (const char*) formatarg->value);
 29            break;
 30        case FF_ARG_TYPE_STRBUF:
 31            ffStrbufAppend(buffer, (const FFstrbuf*) formatarg->value);
 32            break;
 33        case FF_ARG_TYPE_FLOAT:
 34            ffStrbufAppendDouble(buffer, *(float*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER);
 35            break;
 36        case FF_ARG_TYPE_DOUBLE:
 37            ffStrbufAppendDouble(buffer, *(double*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER);
 38            break;
 39        case FF_ARG_TYPE_BOOL:
 40            ffStrbufAppendS(buffer, *(bool*) formatarg->value ? "true" : "false");
 41            break;
 42        case FF_ARG_TYPE_LIST: {
 43            const FFlist* list = (const FFlist*) formatarg->value;
 44            for (uint32_t i = 0; i < list->length; i++) {
 45                ffStrbufAppend(buffer, FF_LIST_GET(FFstrbuf, *list, i));
 46                if (i < list->length - 1) {
 47                    ffStrbufAppendS(buffer, ", ");
 48                }
 49            }
 50            break;
 51        }
 52        case FF_ARG_TYPE_BUFFER: {
 53            // Placeholder for binary data, just print the size for now
 54            const FFArgBuffer* argBuffer = (const FFArgBuffer*) formatarg->value;
 55            ffStrbufAppendF(buffer, "buffer(%u bytes)", argBuffer->length);
 56            break;
 57        }
 58        default:
 59            if (formatarg->type != FF_ARG_TYPE_NULL) {
 60                fprintf(stderr, "Error: format string \"%s\": argument is not implemented: %i\n", buffer->chars, formatarg->type);
 61            }
 62            break;
 63    }
 64}
 65
 66/**
 67 * @brief parses a string to a uint32_t
 68 *
 69 * If the string can't be parsed, or is < 1, uint32_t max is returned.
 70 *
 71 * @param placeholderValue the string to parse
 72 * @return uint32_t the parsed value
 73 */
 74static uint32_t getArgumentIndex(const char* placeholderValue, uint32_t numArgs, const FFformatarg* arguments) {
 75    char firstChar = placeholderValue[0];
 76    if (firstChar == '\0') {
 77        return 0; // use arg counter
 78    }
 79
 80    if (firstChar >= '0' && firstChar <= '9') {
 81        char* pEnd = nullptr;
 82        uint32_t result = (uint32_t) strtoul(placeholderValue, &pEnd, 10);
 83        if (result > numArgs) {
 84            return UINT32_MAX;
 85        }
 86        if (*pEnd != '\0') {
 87            return UINT32_MAX;
 88        }
 89        return result;
 90    } else if (ffCharIsEnglishAlphabet(firstChar)) {
 91        for (uint32_t i = 0; i < numArgs; ++i) {
 92            const FFformatarg* arg = &arguments[i];
 93            if (arg->name && ffStrEqualsIgnCase(placeholderValue, arg->name)) {
 94                return i + 1;
 95            }
 96        }
 97    }
 98
 99    return UINT32_MAX;
100}
101
102static inline void appendInvalidPlaceholder(FFstrbuf* buffer, const char* start, const FFstrbuf* placeholderValue, uint32_t index, uint32_t formatStringLength) {
103    ffStrbufAppendS(buffer, start);
104    ffStrbufAppend(buffer, placeholderValue);
105
106    if (index < formatStringLength) {
107        ffStrbufAppendC(buffer, '}');
108    }
109}
110
111static inline bool formatArgSet(const FFformatarg* arg) {
112    // clang-format off
113    return arg->value != nullptr && (
114        (arg->type == FF_ARG_TYPE_DOUBLE && *(double*) arg->value > 0.0) ||
115        (arg->type == FF_ARG_TYPE_FLOAT && *(float*) arg->value > 0.0) ||
116        (arg->type == FF_ARG_TYPE_INT && *(int32_t*) arg->value > 0) ||
117        (arg->type == FF_ARG_TYPE_STRBUF && ((FFstrbuf*) arg->value)->length > 0) ||
118        (arg->type == FF_ARG_TYPE_STRING && ffStrSet((char*) arg->value)) ||
119        (arg->type == FF_ARG_TYPE_UINT8 && *(uint8_t*) arg->value > 0) ||
120        (arg->type == FF_ARG_TYPE_UINT16 && *(uint16_t*) arg->value > 0) ||
121        (arg->type == FF_ARG_TYPE_UINT && *(uint32_t*) arg->value > 0) ||
122        (arg->type == FF_ARG_TYPE_UINT64 && *(uint64_t*) arg->value > 0) ||
123        (arg->type == FF_ARG_TYPE_BOOL && *(bool*) arg->value) ||
124        (arg->type == FF_ARG_TYPE_LIST && ((FFlist*) arg->value)->length > 0)
125    );
126    // clang-format on
127}
128
129[[maybe_unused]] static inline void normalizeArgName(FFstrbuf* dst, const char* src) {
130    ffStrbufClear(dst);
131    bool flag = false;
132    for (const char* p = src; *p; ++p) {
133        if (*p == '-') {
134            flag = true;
135        } else if (flag) {
136            ffStrbufAppendC(dst, (char) toupper((unsigned char) *p));
137            flag = false;
138        } else {
139            ffStrbufAppendC(dst, *p);
140        }
141    }
142}
143
144#if FF_HAVE_LUA
145    #include "common/lua.h"
146
147static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) {
148    const char* err = lua_tolstring(L, -1, nullptr);
149    if (err) {
150        // Drop the position Lua puts in front of an error and keep the message only. Both places
151        // that build one -- `luaG_addinfo` and `luaL_where` -- format it as `%s:%d: %s`, so a
152        // position is a colon, a line number, a colon and a space:
153        //   general.preload.lua:1: <name> expected near '='   -> <name> expected near '='
154        //   [string ""]:1: x                                  -> x
155        // Not every error has one, and cutting at the first colon mangled those. `luaL_loadfilex`
156        // reports `cannot open <path>: <strerror>`, where that colon is the one after the path, so
157        // the path -- the only part naming the problem -- was dropped and `No such file or
158        // directory` was left on its own. A colon followed by a line number does not identify a
159        // position either, because a path can contain one: `cannot open /tmp/x:12:y/nope.lua: ...`
160        // has to survive whole. The trailing space is what separates the two, and a real position
161        // always has it.
162        for (const char* colon = strchr(err, ':'); colon; colon = strchr(colon + 1, ':')) {
163            const char* line = colon + 1;
164            while (*line >= '0' && *line <= '9') {
165                ++line;
166            }
167            if (line > colon + 1 && line[0] == ':' && line[1] == ' ') {
168                err = line + 2;
169                break;
170            }
171        }
172    }
173    ffStrbufAppendF(buffer, "%s: %s", prefix, err ? err : "unknown");
174}
175
176static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) {
177    const char* err = ffLuaLoadState();
178    if (err) {
179        ffStrbufAppendF(buffer, "Lua init error: %s", err);
180        return false;
181    }
182
183    FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate();
184    bool ret = false;
185
186    lua_State* L = luaData.L;
187    // Clear stack and load chunk
188    lua_settop(L, 0);
189    if (luaL_loadbuffer(L, script, scriptLen, "") != LUA_OK) {
190        appendLuaError(buffer, "Lua load error", L);
191    } else {
192        // Build args table for name lookup only.
193        lua_createtable(L, 0, 0);
194
195        for (uint32_t i = 0; i < numArgs; ++i) {
196            const FFformatarg* arg = &arguments[i];
197            switch (arg->type) {
198                case FF_ARG_TYPE_INT:
199                    lua_pushinteger(L, (lua_Integer) * (int32_t*) arg->value);
200                    break;
201                case FF_ARG_TYPE_UINT:
202                    lua_pushinteger(L, (lua_Integer) * (uint32_t*) arg->value);
203                    break;
204                case FF_ARG_TYPE_UINT64:
205                    lua_pushinteger(L, (lua_Integer) * (uint64_t*) arg->value);
206                    break;
207                case FF_ARG_TYPE_UINT16:
208                    lua_pushinteger(L, (lua_Integer) * (uint16_t*) arg->value);
209                    break;
210                case FF_ARG_TYPE_UINT8:
211                    lua_pushinteger(L, (lua_Integer) * (uint8_t*) arg->value);
212                    break;
213                case FF_ARG_TYPE_FLOAT:
214                    lua_pushnumber(L, (lua_Number) * (float*) arg->value);
215                    break;
216                case FF_ARG_TYPE_DOUBLE:
217                    lua_pushnumber(L, (lua_Number) * (double*) arg->value);
218                    break;
219                case FF_ARG_TYPE_BOOL:
220                    lua_pushboolean(L, *(bool*) arg->value);
221                    break;
222                case FF_ARG_TYPE_STRING:
223                    if (!arg->value) {
224                        lua_pushlstring(L, "", 0);
225                    } else {
226                        lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
227                    }
228                    break;
229                case FF_ARG_TYPE_STRBUF: {
230                    const FFstrbuf* sb = (const FFstrbuf*) arg->value;
231                    lua_pushlstring(L, sb->chars, sb->length);
232                    break;
233                }
234                case FF_ARG_TYPE_LIST: {
235                    const FFlist* list = (const FFlist*) arg->value;
236                    lua_createtable(L, 0, 0);
237                    for (uint32_t li = 0; li < list->length; ++li) {
238                        const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li);
239                        lua_pushlstring(L, item->chars, item->length);
240                        lua_seti(L, -2, (lua_Integer) (li + 1));
241                    }
242                    break;
243                }
244                default:
245                    lua_pushnil(L);
246                    break;
247            }
248            if (arg->name && arg->name[0]) {
249                normalizeArgName(&argNameBuf, arg->name);
250            } else {
251                ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1);
252            }
253            lua_setfield(L, -2, argNameBuf.chars);
254        }
255
256        if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) {
257            appendLuaError(buffer, "Lua runtime error", L);
258        } else {
259            int nresults = lua_gettop(L);
260            if (nresults == 0) {
261                ffStrbufAppendS(buffer, "Lua result error: no result");
262            } else {
263                // Convert first result to string
264                const char* res = lua_tolstring(L, 1, nullptr);
265                if (res) {
266                    ffStrbufAppendS(buffer, res);
267                } else {
268                    luaL_tolstring(L, 1, nullptr);
269                    const char* sval = lua_tolstring(L, -1, nullptr);
270                    if (sval) {
271                        ffStrbufAppendS(buffer, sval);
272                    }
273                }
274                ret = true;
275            }
276        }
277    }
278    lua_settop(L, 0);
279    return ret;
280}
281#endif
282
283// Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none.
284// A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
285static uint32_t ansiEscapeLength(const char* str) {
286    if (str[0] != '\e' || str[1] != '[') {
287        return 0;
288    }
289
290    const char* p = str + 2;
291    while (*p >= '0' && *p <= '?') {
292        ++p;
293    }
294    while (*p >= ' ' && *p <= '/') {
295        ++p;
296    }
297    if (*p < '@' || *p > '~') {
298        return 0; // not a complete CSI sequence
299    }
300    return (uint32_t) (p - str) + 1;
301}
302
303static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
304    // Skip all ANSI escape codes at the start of the string, so that they don't count
305    // towards the truncation / padding length. A value may carry more than one of them,
306    // e.g. `\e[31m\e[1m`.
307    uint32_t prefixLen = 0;
308    while (in->length - prefixLen >= 2) {
309        uint32_t len = ansiEscapeLength(in->chars + prefixLen);
310        if (len == 0) {
311            break;
312        }
313        prefixLen += len;
314    }
315
316    if (prefixLen == 0) {
317        return false;
318    }
319
320    ffStrbufAppendNS(out, prefixLen, in->chars);
321    ffStrbufSubstrAfter(in, prefixLen - 1);
322
323    if (trailingEscape) {
324        // likely have one or more `CSI m` resets at the end of the string
325        uint32_t end = in->length;
326        while (end >= 3) {
327            uint32_t start = end - 1;
328            while (start > 0 && in->chars[start] != '\e') {
329                --start;
330            }
331            uint32_t len = ansiEscapeLength(in->chars + start);
332            if (len == 0 || start + len != end) {
333                break;
334            }
335            end = start;
336        }
337
338        if (end < in->length) {
339            ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end);
340            ffStrbufSubstrBefore(in, end);
341        }
342    }
343    return true;
344}
345
346static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
347    uint32_t argCounter = 0;
348
349    uint32_t numOpenIfs = 0;
350    uint32_t numOpenNotIfs = 0;
351
352    FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate();
353
354    for (uint32_t i = 0; i < formatstr->length; ++i) {
355        // if we don't have a placeholder start just copy the chars over to output buffer
356        if (formatstr->chars[i] != '{') {
357            ffStrbufAppendC(buffer, formatstr->chars[i]);
358            continue;
359        }
360
361        // jump to next char, the start of the placeholder value
362        ++i;
363
364        // unmatched trailing '{'
365        if (i >= formatstr->length) {
366            ffStrbufAppendC(buffer, '{');
367            break;
368        }
369
370        // double {{ elvaluates to a single { and doesn't count as start
371        if (formatstr->chars[i] == '{') {
372            ffStrbufAppendC(buffer, '{');
373            continue;
374        }
375
376        ffStrbufClear(&placeholderValue);
377
378        {
379            uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}');
380            ffStrbufAppendNS(&placeholderValue, iEnd - i, &formatstr->chars[i]);
381            i = iEnd;
382        }
383
384        char firstChar = placeholderValue.chars[0];
385
386        if (placeholderValue.length == 1) {
387            // test if for stop, if so break the loop
388            if (firstChar == '-') {
389                break;
390            }
391
392            // test for end of an if, if so do nothing
393            if (firstChar == '?') {
394                if (numOpenIfs == 0) {
395                    appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
396                } else {
397                    --numOpenIfs;
398                }
399
400                continue;
401            }
402
403            // test for end of a not if, if so do nothing
404            if (firstChar == '/') {
405                if (numOpenNotIfs == 0) {
406                    appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
407                } else {
408                    --numOpenNotIfs;
409                }
410
411                continue;
412            }
413
414            // test for end of a color, if so do nothing
415            if (firstChar == '#') {
416                if (!instance.config.display.pipe) {
417                    ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
418                }
419
420                continue;
421            }
422        }
423
424        // test for if, if so evaluate it
425        if (firstChar == '?') {
426            ffStrbufSubstrAfter(&placeholderValue, 0);
427
428            uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
429
430            // testing for an invalid index
431            if (index > numArgs || index < 1) {
432                appendInvalidPlaceholder(buffer, "{?", &placeholderValue, i, formatstr->length);
433                continue;
434            }
435
436            // continue normally if an format arg is set and the value is > 0
437            if (formatArgSet(&arguments[index - 1])) {
438                ++numOpenIfs;
439                continue;
440            }
441
442            // fastforward to the end of the if without printing the in between
443            i = ffStrbufNextIndexS(formatstr, i, "{?}") + 2; // 2 is the length of "{?}" - 1 because the loop will increment it again directly after continue
444            continue;
445        }
446
447        // test for not if, if so evaluate it
448        if (firstChar == '/') {
449            ffStrbufSubstrAfter(&placeholderValue, 0);
450
451            uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
452
453            // testing for an invalid index
454            if (index > numArgs || index < 1) {
455                appendInvalidPlaceholder(buffer, "{/", &placeholderValue, i, formatstr->length);
456                continue;
457            }
458
459            // continue normally if an format arg is not set or the value is 0
460            if (!formatArgSet(&arguments[index - 1])) {
461                ++numOpenNotIfs;
462                continue;
463            }
464
465            // fastforward to the end of the if without printing the in between
466            i = ffStrbufNextIndexS(formatstr, i, "{/}") + 2; // 2 is the length of "{/}" - 1 because the loop will increment it again directly after continue
467            continue;
468        }
469
470        // test for color, if so evaluate it
471        if (firstChar == '#') {
472            if (!instance.config.display.pipe) {
473                ffStrbufAppendS(buffer, "\e[");
474                ffOptionParseColorNoClear(placeholderValue.chars + 1, buffer);
475                ffStrbufAppendC(buffer, 'm');
476            }
477            continue;
478        }
479
480        // test for constant or env var, if so evaluate it
481        if (firstChar == '$') {
482            char* pend = nullptr;
483            int32_t indexSigned = (int32_t) strtol(placeholderValue.chars + 1, &pend, 10);
484            if (pend == placeholderValue.chars + 1) {
485                // treat placeholder as an environment variable
486                char* envValue = getenv(placeholderValue.chars + 1);
487                if (envValue) {
488                    ffStrbufAppendS(buffer, envValue);
489                } else {
490                    appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
491                }
492            } else {
493                // treat placeholder as a constant
494                uint32_t index = (uint32_t) (indexSigned < 0 ? (int32_t) instance.config.display.constants.length + indexSigned : indexSigned - 1);
495
496                if (*pend != '\0' || instance.config.display.constants.length <= index) {
497                    appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
498                } else {
499                    FFstrbuf* item = FF_LIST_GET(FFstrbuf, instance.config.display.constants, index);
500                    ffStrbufAppend(buffer, item);
501                }
502            }
503            continue;
504        }
505
506        char* pSep = placeholderValue.chars;
507        char cSep = '\0';
508        while (*pSep && *pSep != ':' && *pSep != '<' && *pSep != '>' && *pSep != '|' && *pSep != '~') {
509            ++pSep;
510        }
511        if (*pSep) {
512            cSep = *pSep;
513            *pSep = '\0';
514        } else {
515            pSep = nullptr;
516        }
517
518        uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
519
520        // test for invalid index
521        if (index == 0) {
522            index = ++argCounter;
523        }
524
525        if (index > numArgs) {
526            if (pSep) {
527                *pSep = cSep;
528            }
529            appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
530            continue;
531        }
532
533        if (!cSep) {
534            ffFormatAppendFormatArg(buffer, &arguments[index - 1]);
535        } else if (cSep == '~') {
536            FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate();
537            ffFormatAppendFormatArg(&tempString, &arguments[index - 1]);
538            FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate();
539            skipAnsiEscape(&tempString, buffer, &trailingEscape);
540
541            char* pEnd = nullptr;
542            int32_t start = (int32_t) strtol(pSep + 1, &pEnd, 10);
543            if (start < 0) {
544                start = (int32_t) tempString.length + start;
545            }
546            if (start >= 0 && (uint32_t) start < tempString.length) {
547                if (*pEnd == '\0') {
548                    ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]);
549                } else if (*pEnd == ',') {
550                    int32_t end = (int32_t) strtol(pEnd + 1, &pEnd, 10);
551                    if (!*pEnd) {
552                        if (end < 0) {
553                            end = (int32_t) tempString.length + end;
554                        }
555                        if ((uint32_t) end > tempString.length) {
556                            end = (int32_t) tempString.length;
557                        }
558                        if (end > start) {
559                            ffStrbufAppendNS(buffer, (uint32_t) (end - start), &tempString.chars[start]);
560                        }
561                    }
562                }
563            }
564
565            if (trailingEscape.length > 0) {
566                ffStrbufAppend(buffer, &trailingEscape);
567            }
568
569            if (*pEnd) {
570                *pSep = cSep;
571                appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
572                continue;
573            }
574        } else {
575            char* pEnd = nullptr;
576            int32_t truncLength = (int32_t) strtol(pSep + 1, &pEnd, 10);
577            if (*pEnd != '\0') {
578                *pSep = cSep;
579                appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
580                continue;
581            }
582
583            bool ellipsis = false;
584            if (truncLength < 0) {
585                ellipsis = true;
586                truncLength = -truncLength;
587            }
588
589            FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate();
590            ffFormatAppendFormatArg(&tempString, &arguments[index - 1]);
591            FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate();
592            skipAnsiEscape(&tempString, buffer, &trailingEscape);
593
594            if (tempString.length == (uint32_t) truncLength) {
595                ffStrbufAppend(buffer, &tempString);
596            } else if (tempString.length > (uint32_t) truncLength) {
597                if (cSep == ':') {
598                    ffStrbufSubstrBefore(&tempString, (uint32_t) truncLength);
599                    ffStrbufTrimRightSpace(&tempString);
600                } else {
601                    ffStrbufSubstrBefore(&tempString, (uint32_t) (!ellipsis ? truncLength : truncLength - 1));
602                }
603                ffStrbufAppend(buffer, &tempString);
604
605                if (ellipsis) {
606                    ffStrbufAppendS(buffer, "…");
607                }
608            } else if (cSep == ':') {
609                ffStrbufAppend(buffer, &tempString);
610            } else {
611                if (cSep == '<') { // left align
612                    ffStrbufAppend(buffer, &tempString);
613                    ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' ');
614                } else if (cSep == '>') { // right align
615                    ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' ');
616                    ffStrbufAppend(buffer, &tempString);
617                } else if (cSep == '|') { // center align
618                    uint32_t padding = ((uint32_t) truncLength - tempString.length) / 2;
619                    ffStrbufAppendNC(buffer, padding, ' ');
620                    ffStrbufAppend(buffer, &tempString);
621                    ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length - padding, ' ');
622                }
623            }
624
625            if (trailingEscape.length > 0) {
626                ffStrbufAppend(buffer, &trailingEscape);
627            }
628        }
629    }
630
631    if (!instance.config.display.pipe) {
632        ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
633    }
634
635    return true;
636}
637
638bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
639#if FF_HAVE_LUA
640    if (ffStrbufStartsWithS(formatstr, "lua:")) {
641        // If outputFormat starts with "lua:", treat the rest as a Lua script
642        return parseLuaString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments);
643    }
644#endif
645
646    return parseFormatString(buffer, formatstr, numArgs, arguments);
647}