master ohmu/fastfetch-openbsd / src / logo / logo.c
ohmu  ·  2026-10-15
  1#include "logo/logo.h"
  2#include "common/io.h"
  3#include "common/printing.h"
  4#include "common/processing.h"
  5#include "common/textModifier.h"
  6#include "common/strutil.h"
  7#include "detection/media/media.h"
  8#include "detection/os/os.h"
  9#include "detection/terminalshell/terminalshell.h"
 10#include "detection/terminalsize/terminalsize.h"
 11
 12#include <ctype.h>
 13#include <stdlib.h>
 14#include <string.h>
 15
 16typedef struct FFLogoCachedLine {
 17    FFstrbuf chars;
 18    uint32_t width;
 19} FFLogoCachedLine;
 20
 21FFLogoPosition ffLogoSelectPosition(FFLogoPosition configured, uint32_t terminalColumns, uint32_t logoWidth) {
 22    if (configured != FF_LOGO_POSITION_AUTO) {
 23        return configured;
 24    }
 25
 26    if (terminalColumns == 0) {
 27        return FF_LOGO_POSITION_LEFT;
 28    }
 29
 30    return logoWidth <= terminalColumns && terminalColumns - logoWidth >= FF_LOGO_AUTO_MIN_INFO_WIDTH
 31        ? FF_LOGO_POSITION_LEFT
 32        : FF_LOGO_POSITION_TOP;
 33}
 34
 35void ffLogoResolveAutoPosition(uint32_t logoWidth) {
 36    FFOptionsLogo* options = &instance.config.logo;
 37    if (options->position != FF_LOGO_POSITION_AUTO) {
 38        return;
 39    }
 40
 41    uint32_t terminalColumns = 0;
 42    if (ffIsTerminal(STDOUT_FILENO)) {
 43        FFTerminalSizeResult size = {};
 44        if (ffDetectTerminalSize(&size, true)) {
 45            terminalColumns = size.columns;
 46        }
 47    }
 48
 49    options->position = ffLogoSelectPosition(FF_LOGO_POSITION_AUTO, terminalColumns, logoWidth);
 50}
 51
 52static void logoLineCacheClear(FFLogoLineCacheState* cache) {
 53    FF_LIST_FOR_EACH (FFLogoCachedLine, line, cache->lines) {
 54        ffStrbufDestroy(&line->chars);
 55    }
 56    ffListDestroy(&cache->lines);
 57    cache->nextLine = 0;
 58    cache->rightOffset = 0;
 59}
 60
 61static void logoLineCachePush(const FFstrbuf* chars, uint32_t width, FFLogoLineCacheState* cache) {
 62    FFLogoCachedLine* line = FF_LIST_ADD(FFLogoCachedLine, cache->lines);
 63    if (width > 0) {
 64        ffStrbufInitCopy(&line->chars, chars);
 65        if (!instance.config.display.pipe) {
 66            ffStrbufAppendS(&line->chars, FASTFETCH_TEXT_MODIFIER_RESET);
 67        }
 68    } else {
 69        ffStrbufInit(&line->chars);
 70    }
 71    line->width = width;
 72}
 73
 74static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bool doColorReplacement) {
 75    FFOptionsLogo* options = &instance.config.logo;
 76    bool keepCarryColor = options->type != FF_LOGO_TYPE_IMAGE_CHAFA;
 77
 78    logoLineCacheClear(cache);
 79
 80    // Overrides the auto detected max-width with the configured width, if set.
 81    // In case we fail to get the actual width of the logo, as we don't use `wcwidth`
 82    uint32_t maxLineWidth = options->width;
 83    uint32_t parsedHeight = 0;
 84
 85    FF_STRBUF_AUTO_DESTROY carryColor = ffStrbufCreate();
 86    if (keepCarryColor && doColorReplacement && !instance.config.display.pipe) {
 87        ffStrbufSetF(&carryColor, "\e[%sm", options->colors[0].chars);
 88    }
 89
 90    for (uint32_t i = 0; i < options->paddingTop; ++i) {
 91        logoLineCachePush(nullptr, 0, cache);
 92    }
 93
 94    if (*data != '\0') {
 95        while (true) {
 96            FF_STRBUF_AUTO_DESTROY line = ffStrbufCreateA(256);
 97            uint32_t lineWidth = 0;
 98
 99            if (!instance.config.display.pipe && instance.config.display.brightColor) {
100                ffStrbufAppendS(&line, FASTFETCH_TEXT_MODIFIER_BOLT);
101            }
102
103            if (keepCarryColor && carryColor.length > 0) {
104                ffStrbufAppend(&line, &carryColor);
105            }
106
107            if ((options->position != FF_LOGO_POSITION_RIGHT) && options->paddingLeft > 0) {
108                ffStrbufAppendNC(&line, options->paddingLeft, ' ');
109                lineWidth += options->paddingLeft;
110            }
111
112            while (*data != '\0' && *data != '\n' && !(*data == '\r' && *(data + 1) == '\n')) {
113                if (*data == '\t') {
114                    ffStrbufAppendNC(&line, 4, ' ');
115                    lineWidth += 4;
116                    ++data;
117                    continue;
118                }
119
120                if (*data == '\e' && *(data + 1) == '[') {
121                    const char* start = data;
122                    data += 2;
123
124                    while (ffCharIsDigit(*data) || *data == ';') {
125                        ++data;
126                    }
127
128                    if (isascii(*data)) {
129                        ++data;
130
131                        uint32_t escLen = (uint32_t) (data - start);
132                        ffStrbufAppendNS(&line, escLen, start);
133
134                        if (keepCarryColor && start[escLen - 1] == 'm') {
135                            ffStrbufSetNS(&carryColor, escLen, start);
136                        }
137                        continue;
138                    }
139
140                    lineWidth += (uint32_t) (data - start - 1);
141                }
142
143                if (doColorReplacement && *data == '$') {
144                    ++data;
145
146                    if (*data == '$' || *data == '\0') {
147                        ffStrbufAppendC(&line, '$');
148                        ++lineWidth;
149                        ++data;
150                        continue;
151                    }
152
153                    if (!instance.config.display.pipe) {
154                        int index = *data - '1';
155                        if (index >= 0 && index < FASTFETCH_LOGO_MAX_COLORS) {
156                            if (keepCarryColor) {
157                                ffStrbufSetF(&carryColor, "\e[%sm", options->colors[index].chars);
158                                ffStrbufAppend(&line, &carryColor);
159                            } else {
160                                ffStrbufAppendF(&line, "\e[%sm", options->colors[index].chars);
161                            }
162                            ++data;
163                            continue;
164                        }
165
166                        ffStrbufAppendC(&line, '$');
167                        ++lineWidth;
168                    } else {
169                        ++data;
170                        continue;
171                    }
172                }
173
174                uint8_t charWidth;
175                uint8_t bytes = ffUtf8CharLenWidth(data, UINT32_MAX, &charWidth);
176                lineWidth += charWidth;
177
178                for (uint8_t i = 0; i < bytes; ++i) {
179                    if (*data == '\0') {
180                        break;
181                    }
182
183                    ffStrbufAppendC(&line, *data);
184                    ++data;
185                }
186            }
187
188            logoLineCachePush(&line, lineWidth, cache);
189            if (lineWidth > maxLineWidth) {
190                maxLineWidth = lineWidth;
191            }
192
193            if (*data == '\n' || (*data == '\r' && *(data + 1) == '\n')) {
194                if (*data == '\r') {
195                    ++data;
196                }
197                ++data;
198                ++parsedHeight;
199                continue;
200            }
201
202            break;
203        }
204    }
205
206    if (options->type != FF_LOGO_TYPE_IMAGE_CHAFA && options->height > parsedHeight) {
207        parsedHeight = options->height;
208    }
209
210    ffLogoResolveAutoPosition(maxLineWidth + options->paddingRight);
211
212    instance.state.logoHeight = options->paddingTop + parsedHeight;
213    if (options->position == FF_LOGO_POSITION_LEFT) {
214        instance.state.logoWidth = maxLineWidth + options->paddingRight;
215    } else {
216        instance.state.logoWidth = 0;
217    }
218
219    uint32_t totalLines = instance.state.logoHeight + 1;
220    while (cache->lines.length < totalLines) {
221        logoLineCachePush(nullptr, 0, cache);
222    }
223
224    cache->nextLine = 0;
225    cache->rightOffset = maxLineWidth + options->paddingRight - 1;
226}
227
228static bool ffLogoPrintCharsRaw(const char* data, size_t length, bool printError) {
229    FFOptionsLogo* options = &instance.config.logo;
230    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
231
232    ffLogoResolveAutoPosition(options->width + options->paddingLeft + options->paddingRight);
233
234    if (!options->width || !options->height) {
235        if (options->position == FF_LOGO_POSITION_LEFT) {
236            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
237        } else if (options->position == FF_LOGO_POSITION_TOP) {
238            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
239            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
240        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
241            if (!options->width) {
242                if (printError) {
243                    fputs("Logo (image-raw): Must set logo width when using position right\n", stderr);
244                }
245                return false;
246            }
247            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
248        }
249        ffStrbufAppendNS(&buf, (uint32_t) length, data);
250        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
251
252        if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
253            uint16_t X = 0, Y = 0;
254            // Windows Terminal doesn't report `\e` for some reason
255            const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X); // %*[^0-9]: ignore optional \e[
256            if (error) {
257                if (printError) {
258                    fprintf(stderr, "\nLogo (image-raw): fail to query cursor position: %s\n", error);
259                }
260                return true;
261            }
262            if (options->position == FF_LOGO_POSITION_LEFT) {
263                if (options->width + options->paddingLeft > X) {
264                    X = (uint16_t) (options->width + options->paddingLeft);
265                }
266                instance.state.logoWidth = X + instance.config.logo.paddingRight - 1;
267            }
268            instance.state.logoHeight = Y;
269            fputs("\e[H", stdout);
270        } else if (options->position == FF_LOGO_POSITION_TOP) {
271            instance.state.logoWidth = instance.state.logoHeight = 0;
272            ffPrintCharTimes('\n', options->paddingRight);
273        }
274    } else {
275        ffStrbufAppendNC(&buf, options->paddingTop, '\n');
276
277        if (options->position == FF_LOGO_POSITION_RIGHT) {
278            ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
279        } else if (options->paddingLeft) {
280            ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
281        }
282
283        ffStrbufAppendNS(&buf, (uint32_t) length, data);
284        ffStrbufAppendC(&buf, '\n');
285
286        if (options->position == FF_LOGO_POSITION_LEFT) {
287            instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
288            instance.state.logoHeight = options->paddingTop + options->height;
289            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
290        } else if (options->position == FF_LOGO_POSITION_TOP) {
291            instance.state.logoWidth = instance.state.logoHeight = 0;
292            ffStrbufAppendNC(&buf, options->paddingRight, '\n');
293        } else if (options->position == FF_LOGO_POSITION_RIGHT) {
294            instance.state.logoWidth = instance.state.logoHeight = 0;
295            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) options->height);
296        }
297        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
298    }
299
300    return true;
301}
302
303void ffLogoPrintChars(const char* data, bool doColorReplacement) {
304    FFOptionsLogo* options = &instance.config.logo;
305    FFLogoLineCacheState* cache = &instance.state.logoLineCache;
306
307    logoLineCacheBuild(cache, data, doColorReplacement);
308
309    if (options->position != FF_LOGO_POSITION_TOP) {
310        return;
311    }
312
313    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(4096);
314    FF_LIST_FOR_EACH (FFLogoCachedLine, line, cache->lines) {
315        ffStrbufAppend(&result, &line->chars);
316        ffStrbufAppendC(&result, '\n');
317    }
318    ffStrbufAppendNC(&result, options->paddingBottom, '\n');
319    ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &result);
320    instance.state.logoWidth = instance.state.logoHeight = 0;
321    logoLineCacheClear(cache);
322}
323
324static void logoApplyColors(const FFlogo* logo, bool replacement) {
325    if (instance.config.display.colorTitle.length == 0) {
326        ffStrbufSetStatic(&instance.config.display.colorTitle, logo->colorTitle ?: logo->colors[0]);
327    }
328
329    if (instance.config.display.colorKeys.length == 0) {
330        ffStrbufSetStatic(&instance.config.display.colorKeys, logo->colorKeys ?: logo->colors[1]);
331    }
332
333    if (replacement) {
334        FFOptionsLogo* options = &instance.config.logo;
335
336        const char* const* colors = logo->colors;
337        for (int i = 0; *colors != nullptr && i < FASTFETCH_LOGO_MAX_COLORS; i++, colors++) {
338            if (options->colors[i].length == 0) {
339                ffStrbufSetStatic(&options->colors[i], *colors);
340            }
341        }
342    }
343}
344
345static bool logoHasName(const FFlogo* logo, const FFstrbuf* name, bool small) {
346    for (
347        const char* const* logoName = logo->names;
348        *logoName != nullptr && logoName <= &logo->names[FASTFETCH_LOGO_MAX_NAMES];
349        ++logoName) {
350        if (small) {
351            uint32_t logoNameLength = (uint32_t) (strlen(*logoName) - strlen("_small"));
352            if (name->length == logoNameLength && strncasecmp(*logoName, name->chars, logoNameLength) == 0) {
353                return true;
354            }
355        }
356        if (ffStrbufIgnCaseEqualS(name, *logoName)) {
357            return true;
358        }
359    }
360
361    return false;
362}
363
364static const FFlogo* logoGetBuiltin(const FFstrbuf* name, FFLogoSize size) {
365    if (name->length == 0 || !isalpha(name->chars[0])) {
366        return nullptr;
367    }
368
369    for (const FFlogo* logo = ffLogoBuiltins[toupper(name->chars[0]) - 'A']; *logo->names; ++logo) {
370        switch (size) {
371            // Never use alternate logos
372            case FF_LOGO_SIZE_NORMAL:
373                if (logo->type != FF_LOGO_LINE_TYPE_NORMAL) {
374                    continue;
375                }
376                break;
377            case FF_LOGO_SIZE_SMALL:
378                if (logo->type != FF_LOGO_LINE_TYPE_SMALL_BIT) {
379                    continue;
380                }
381                break;
382            default:
383                break;
384        }
385
386        if (logoHasName(logo, name, size == FF_LOGO_SIZE_SMALL)) {
387            return logo;
388        }
389    }
390
391    return nullptr;
392}
393
394static const FFlogo* logoGetBuiltinDetected(FFLogoSize size) {
395    const FFOSResult* os = ffDetectOS();
396
397    const FFlogo* logo = logoGetBuiltin(&os->id, size);
398    if (logo != nullptr) {
399        return logo;
400    }
401
402    logo = logoGetBuiltin(&os->name, size);
403    if (logo != nullptr) {
404        return logo;
405    }
406
407    if (ffStrbufContainC(&os->idLike, ' ')) {
408        FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
409        for (
410            uint32_t start = 0, end = ffStrbufFirstIndexC(&os->idLike, ' ');
411            true;
412            start = end + 1, end = ffStrbufNextIndexC(&os->idLike, start, ' ')) {
413            ffStrbufSetNS(&buf, end - start, os->idLike.chars + start);
414            logo = logoGetBuiltin(&buf, size);
415            if (logo != nullptr) {
416                return logo;
417            }
418
419            if (end >= os->idLike.length) {
420                break;
421            }
422        }
423    } else {
424        logo = logoGetBuiltin(&os->idLike, size);
425        if (logo != nullptr) {
426            return logo;
427        }
428    }
429
430    logo = logoGetBuiltin(&instance.state.platform.sysinfo.name, size);
431    if (logo != nullptr) {
432        return logo;
433    }
434
435    return &ffLogoUnknown;
436}
437
438static void logoPrintStruct(const FFlogo* logo) {
439    logoApplyColors(logo, true);
440
441    ffLogoPrintChars(logo->lines, true);
442}
443
444static void logoPrintNone(void) {
445    if (!instance.config.display.pipe) {
446        logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
447    }
448    instance.state.logoHeight = 0;
449    instance.state.logoWidth = 0;
450}
451
452static bool logoPrintBuiltinIfExists(const FFstrbuf* name, FFLogoSize size) {
453    if (name->chars[0] == '~' || name->chars[0] == '.' || name->chars[0] == '/'
454#if _WIN32
455        || (ffCharIsEnglishAlphabet(name->chars[0]) && name->chars[1] == ':') // Windows drive letter
456#endif
457    )
458        return false; // Paths
459
460    if (ffStrbufIgnCaseEqualS(name, "none")) {
461        logoPrintNone();
462        return true;
463    }
464
465    const FFlogo* logo = ffLogoGetBuiltinForName(name, size);
466    if (logo == nullptr) {
467        return false;
468    }
469
470    logoPrintStruct(logo);
471
472    return true;
473}
474
475void ffLogoPrintDetected(FFLogoSize size) {
476    logoPrintStruct(logoGetBuiltinDetected(size));
477}
478
479static bool logoPrintData(bool doColorReplacement, FFstrbuf* source) {
480    if (source->length == 0) {
481        return false;
482    }
483
484    logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
485    ffLogoPrintChars(source->chars, doColorReplacement);
486    return true;
487}
488
489static bool updateLogoPath(void) {
490    FFOptionsLogo* options = &instance.config.logo;
491
492    if (ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
493        return true;
494    }
495
496    if (ffStrbufEqualS(&options->source, "-")) { // stdin
497        return true;
498    }
499
500#if !FF_MODULE_DISABLE_MEDIA
501    if (ffStrbufIgnCaseEqualS(&options->source, "media-cover")) {
502        const FFMediaResult* media = ffDetectMedia(true);
503        if (media->cover.length == 0) {
504            return false;
505        }
506        ffStrbufSet(&options->source, &media->cover);
507        return true;
508    }
509#endif
510
511    FF_STRBUF_AUTO_DESTROY fullPath = ffStrbufCreateA(128);
512    if (ffPathExpandEnv(options->source.chars, &fullPath) && ffPathExists(fullPath.chars, FF_PATHTYPE_FILE)) {
513        ffStrbufDestroy(&options->source);
514        ffStrbufInitMove(&options->source, &fullPath);
515        return true;
516    }
517
518    FF_LIST_FOR_EACH (FFstrbuf, dataDir, instance.state.platform.dataDirs) {
519        // We need to copy it, because multiple threads might be using dataDirs at the same time
520        ffStrbufSet(&fullPath, dataDir);
521        ffStrbufAppendS(&fullPath, "fastfetch/logos/");
522        ffStrbufAppend(&fullPath, &options->source);
523
524        if (ffPathExists(fullPath.chars, FF_PATHTYPE_FILE)) {
525            ffStrbufDestroy(&options->source);
526            ffStrbufInitMove(&options->source, &fullPath);
527            return true;
528        }
529    }
530
531    return false;
532}
533
534static bool logoPrintFileIfExists(bool doColorReplacement, bool raw) {
535    FFOptionsLogo* options = &instance.config.logo;
536
537    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
538
539    if (ffStrbufEqualS(&options->source, "-")
540            ? !ffAppendFDBuffer(FFUnixFD2NativeFD(STDIN_FILENO), &content)
541            : !ffAppendFileBuffer(options->source.chars, &content)) {
542        if (instance.config.display.showErrors) {
543            fprintf(stderr, "Logo: Failed to load file content from logo source: %s\n", options->source.chars);
544        }
545        return false;
546    }
547
548    logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
549    if (raw) {
550        return ffLogoPrintCharsRaw(content.chars, content.length, instance.config.display.showErrors);
551    }
552
553    ffLogoPrintChars(content.chars, doColorReplacement);
554    return true;
555}
556
557#if FF_HAVE_IMAGE_LOGO
558static bool logoPrintImageIfExists(FFLogoType logo, bool printError) {
559    if (!ffLogoPrintImageIfExists(logo, printError)) {
560        return false;
561    }
562
563    logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
564    return true;
565}
566#endif
567
568static bool logoTryKnownType(void) {
569    FFOptionsLogo* options = &instance.config.logo;
570
571    if (options->type == FF_LOGO_TYPE_NONE) {
572        logoPrintNone();
573        return true;
574    }
575
576    if (options->type == FF_LOGO_TYPE_BUILTIN) {
577        return logoPrintBuiltinIfExists(&options->source, FF_LOGO_SIZE_UNKNOWN);
578    }
579
580    if (options->type == FF_LOGO_TYPE_SMALL) {
581        return logoPrintBuiltinIfExists(&options->source, FF_LOGO_SIZE_SMALL);
582    }
583
584    if (options->type == FF_LOGO_TYPE_DATA) {
585        return logoPrintData(true, &options->source);
586    }
587
588    if (options->type == FF_LOGO_TYPE_DATA_RAW) {
589        return logoPrintData(false, &options->source);
590    }
591
592    if (options->type == FF_LOGO_TYPE_COMMAND_RAW) {
593        FF_STRBUF_AUTO_DESTROY source = ffStrbufCreate();
594
595        const char* error = ffProcessAppendStdOut(&source, (char* const[]){
596#ifdef _WIN32
597                                                               "cmd.exe", "/c",
598#else
599                                                               "/bin/sh", "-c",
600#endif
601                                                               options->source.chars,
602                                                               nullptr });
603
604        if (error) {
605            if (instance.config.display.showErrors) {
606                fprintf(stderr, "Logo: failed to execute command `%s`: %s\n", options->source.chars, error);
607            }
608            return false;
609        }
610
611        return logoPrintData(false, &source);
612    }
613
614    // We sure have a file, resolve relative paths
615    if (!updateLogoPath()) {
616        if (instance.config.display.showErrors) {
617            fprintf(stderr, "Logo: Failed to resolve logo source: %s\n", options->source.chars);
618        }
619        return false;
620    }
621
622    if (options->type == FF_LOGO_TYPE_FILE) {
623        return logoPrintFileIfExists(true, false);
624    }
625
626    if (options->type == FF_LOGO_TYPE_FILE_RAW) {
627        return logoPrintFileIfExists(false, false);
628    }
629
630    if (options->type == FF_LOGO_TYPE_IMAGE_RAW) {
631        return logoPrintFileIfExists(false, true);
632    }
633
634#if FF_HAVE_IMAGE_LOGO
635    return logoPrintImageIfExists(options->type, instance.config.display.showErrors);
636#else
637    // No image logo type can be selected in this build (see ffOptionsParseLogoCommandLine), and
638    // FF_LOGO_TYPE_IMAGE_RAW was handled above, so nothing printable is left for this source.
639    if (instance.config.display.showErrors) {
640        fprintf(stderr, "Logo: Unsupported logo type for source: %s\n", options->source.chars);
641    }
642    return false;
643#endif
644}
645
646void ffLogoPrint(void) {
647    const FFOptionsLogo* options = &instance.config.logo;
648
649    if (options->type == FF_LOGO_TYPE_NONE) {
650        logoPrintNone();
651        return;
652    }
653
654    // If the source is not set, we can directly print the detected logo.
655    if (options->source.length == 0) {
656        ffLogoPrintDetected(options->type == FF_LOGO_TYPE_SMALL ? FF_LOGO_SIZE_SMALL : FF_LOGO_SIZE_NORMAL);
657        return;
658    }
659
660    // If the source and source type is set to something else than auto, always print with the set type.
661    if (options->source.length > 0 && options->type != FF_LOGO_TYPE_AUTO) {
662        if (!logoTryKnownType()) {
663            if (instance.config.display.showErrors) {
664                // Image logo should have been handled
665                if (options->type == FF_LOGO_TYPE_BUILTIN || options->type == FF_LOGO_TYPE_SMALL) {
666                    fprintf(stderr, "Logo: Failed to load %s logo: %s\n", options->type == FF_LOGO_TYPE_BUILTIN ? "builtin" : "builtin small", options->source.chars);
667                }
668            }
669
670            ffLogoPrintDetected(FF_LOGO_SIZE_UNKNOWN);
671        }
672        return;
673    }
674
675    // If source matches the name of a builtin logo, print it and return.
676    if (logoPrintBuiltinIfExists(&options->source, FF_LOGO_SIZE_UNKNOWN)) {
677        return;
678    }
679
680    // Make sure the logo path is set correctly.
681    if (updateLogoPath()) {
682        if (ffStrbufEndsWithIgnCaseS(&options->source, ".raw")) {
683            if (logoPrintFileIfExists(false, true)) {
684                return;
685            }
686        }
687
688#if FF_HAVE_IMAGE_LOGO
689        // Try to load the logo as an image.
690        if (!ffStrbufEndsWithIgnCaseS(&options->source, ".txt")) {
691    #if !FF_MODULE_DISABLE_TERMINAL
692            const FFTerminalResult* terminal = ffDetectTerminal();
693
694            bool supportsIterm2 = ffStrbufEqualS(&terminal->prettyName, "iTerm");
695
696            if (supportsIterm2 && logoPrintImageIfExists(FF_LOGO_TYPE_IMAGE_ITERM, false)) {
697                return;
698            }
699
700            // Terminal emulators that support kitty graphics protocol.
701            bool supportsKitty =
702                ffStrbufIgnCaseEqualS(&terminal->processName, "kitty") ||
703                ffStrbufIgnCaseEqualS(&terminal->processName, "konsole") ||
704                ffStrbufIgnCaseEqualS(&terminal->processName, "wezterm") ||
705                ffStrbufIgnCaseEqualS(&terminal->processName, "wayst") ||
706                ffStrbufIgnCaseEqualS(&terminal->processName, "ghostty") ||
707        #ifdef __APPLE__
708                ffStrbufIgnCaseEqualS(&terminal->processName, "WarpTerminal") ||
709        #else
710                ffStrbufIgnCaseEqualS(&terminal->processName, "warp") ||
711        #endif
712                false;
713    #else
714            bool supportsKitty = false;
715    #endif
716
717            // Try to load the logo as an image. If it succeeds, print it and return.
718            if (logoPrintImageIfExists(supportsKitty ? FF_LOGO_TYPE_IMAGE_KITTY : FF_LOGO_TYPE_IMAGE_CHAFA, false)) {
719                return;
720            }
721        }
722#endif
723
724        // Try to load the logo as a file. If it succeeds, print it and return.
725        if (logoPrintFileIfExists(true, false)) {
726            return;
727        }
728    } else {
729        if (instance.config.display.showErrors) {
730            fprintf(stderr, "Logo: Failed to resolve logo source: %s\n", options->source.chars);
731        }
732    }
733
734    ffLogoPrintDetected(FF_LOGO_SIZE_UNKNOWN);
735}
736
737void ffLogoPrintLine(void) {
738    FFLogoLineCacheState* cache = &instance.state.logoLineCache;
739    FFOptionsLogo* logo = &instance.config.logo;
740
741    if (cache->lines.length > 0) {
742        // Line cache is enabled. Always move cursor with whitespaces to make lolcat happy
743        if (cache->nextLine < cache->lines.length) {
744            // Print logo line and move cursor
745            FFLogoCachedLine* line = FF_LIST_GET(FFLogoCachedLine, cache->lines, cache->nextLine);
746
747            if (logo->position == FF_LOGO_POSITION_RIGHT) {
748                printf("\033[9999999C\033[%uD", cache->rightOffset);
749                ffStrbufWriteTo(&line->chars, stdout);
750
751                fputs("\033[G", stdout);
752            } else {
753                ffStrbufWriteTo(&line->chars, stdout);
754
755                uint32_t remaining = instance.state.logoWidth;
756                remaining = line->width < remaining ? remaining - line->width : 0;
757                ffPrintCharTimes(' ', remaining);
758            }
759            ++cache->nextLine;
760        } else if (logo->position == FF_LOGO_POSITION_LEFT) {
761            // Move cursor to the start position
762            ffPrintCharTimes(' ', instance.state.logoWidth);
763        }
764    } else if (instance.state.logoWidth > 0) {
765        printf("\033[%uC", instance.state.logoWidth);
766    }
767
768    if (instance.state.dynamicInterval > 0 && logo->position == FF_LOGO_POSITION_LEFT) {
769        fputs("\033[K", stdout); // Clear to the end of the line
770        // Note that we don't clear the line when the logo is on the right, as it will also clear the logo itself
771    }
772
773    ++instance.state.keysHeight;
774}
775
776void ffLogoPrintRemaining(void) {
777    FFLogoLineCacheState* cache = &instance.state.logoLineCache;
778    FFOptionsLogo* logo = &instance.config.logo;
779
780    if (cache->lines.length > 0 && (logo->position == FF_LOGO_POSITION_LEFT || logo->position == FF_LOGO_POSITION_RIGHT)) {
781        while (cache->nextLine < cache->lines.length) {
782            FFLogoCachedLine* line = FF_LIST_GET(FFLogoCachedLine, cache->lines, cache->nextLine);
783
784            if (logo->position == FF_LOGO_POSITION_RIGHT) {
785                printf("\033[9999999C\033[%uD", cache->rightOffset);
786            }
787            ffStrbufPutTo(&line->chars, stdout);
788
789            ++cache->nextLine;
790        }
791
792        if (!instance.config.display.pipe) {
793            fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
794        }
795
796        instance.state.keysHeight = instance.state.logoHeight + 1;
797        logoLineCacheClear(cache);
798        return;
799    }
800
801    if (instance.state.keysHeight <= instance.state.logoHeight) {
802        ffPrintCharTimes('\n', instance.state.logoHeight - instance.state.keysHeight + 1);
803    }
804    instance.state.keysHeight = instance.state.logoHeight + 1;
805}
806
807void ffLogoBuiltinPrint(void) {
808    FFOptionsLogo* options = &instance.config.logo;
809    options->position = FF_LOGO_POSITION_TOP;
810    options->paddingRight = 2; // empty line after logo printing
811    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
812
813    for (uint8_t ch = 0; ch < 26; ++ch) {
814        for (const FFlogo* logo = ffLogoBuiltins[ch]; *logo->names; ++logo) {
815            if (instance.config.display.pipe) {
816                ffStrbufSetF(&buf, "%s:\n", logo->names[0]);
817            } else {
818                ffStrbufSetF(&buf, "\e[%sm%s:\e[0m\n", logo->colors[0], logo->names[0]);
819            }
820            ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
821            logoPrintStruct(logo);
822
823            for (uint8_t i = 0; i < FASTFETCH_LOGO_MAX_COLORS; i++) {
824                ffStrbufClear(&options->colors[i]);
825            }
826        }
827    }
828}
829
830void ffLogoBuiltinList(void) {
831    uint32_t counter = 0;
832    for (uint8_t ch = 0; ch < 26; ++ch) {
833        for (const FFlogo* logo = ffLogoBuiltins[ch]; *logo->names; ++logo) {
834            ++counter;
835            printf("%u)%s ", counter, counter < 10 ? " " : "");
836
837            for (
838                const char* const* names = logo->names;
839                *names != nullptr && names <= &logo->names[FASTFETCH_LOGO_MAX_NAMES];
840                ++names) {
841                printf("\"%s\" ", *names);
842            }
843
844            putchar('\n');
845        }
846    }
847}
848
849void ffLogoBuiltinListAutocompletion(void) {
850    for (uint8_t ch = 0; ch < 26; ++ch) {
851        for (const FFlogo* logo = ffLogoBuiltins[ch]; *logo->names; ++logo) {
852            printf("%s\n", logo->names[0]);
853        }
854    }
855}
856
857const FFlogo* ffLogoGetBuiltinForName(const FFstrbuf* name, FFLogoSize size) {
858    return ffStrbufEqualS(name, "?") ? &ffLogoUnknown : logoGetBuiltin(name, size);
859}
860
861const FFlogo* ffLogoGetBuiltinDetected(FFLogoSize size) {
862    return logoGetBuiltinDetected(size);
863}