master ohmu/fastfetch-openbsd / src / options / display.c
ohmu  ·  2026-10-15
   1#include "fastfetch.h"
   2#include "common/color.h"
   3#include "common/io.h"
   4#include "common/jsonconfig.h"
   5#include "common/percent.h"
   6#include "common/strutil.h"
   7#include "options/display.h"
   8
   9#if !FF_MODULE_DISABLE_TERMINALTHEME
  10    #include "detection/terminaltheme/terminaltheme.h"
  11#endif
  12#if !FF_MODULE_DISABLE_LOCALE
  13    #include "detection/locale/locale.h"
  14#endif
  15
  16#include <unistd.h>
  17
  18// @returns offsetof(FFModuleDisplayName, <language>)
  19// @see src/common/option.h
  20static uint32_t optionParseLanguageString(FFstrbuf* language) {
  21    const bool detected = language->length == 0;
  22
  23    if (detected) {
  24#if !FF_MODULE_DISABLE_LOCALE
  25        const char* error = ffDetectLocale(language);
  26        if (__builtin_expect(error != nullptr, false)) {
  27            fprintf(stderr, "ffDetectLocale: %s\n", error);
  28            exit(477);
  29        }
  30#else
  31        fputs("Error: --key-language requires a value\n", stderr);
  32        exit(477);
  33#endif
  34    }
  35
  36    ffStrbufSubstrBeforeFirstC(language, '.'); // Remove the encoding part of the locale
  37    #if __linux__
  38        ffStrbufSubstrBeforeFirstC(language, '@'); // Remove the modifier part of the locale (Linux only)
  39    #endif
  40    ffStrbufLowerCase(language);
  41
  42    if (language->length <= 1) {
  43        if (language->chars[0] == 'c') {
  44            return offsetof(FFModuleDisplayName, en);
  45        } else {
  46            goto error;
  47        }
  48    } else if (language->length > 2) {
  49        if (language->chars[2] == '-') {
  50            language->chars[2] = '_';
  51        } else if (__builtin_expect(language->chars[2] != '_', false)) {
  52            goto error;
  53        }
  54    }
  55
  56    // The language code is expected to be in the format of "en", "en_US", "de", "de_DE", etc.
  57    // First try to match the full language code, then try to match just the first two characters (the language part).
  58    // If no match is found, report a language not supported error, unless the language was auto-detected.
  59    // en_US -> offsetof(FFModuleDisplayName, en)
  60    // en -> offsetof(FFModuleDisplayName, en)
  61    // zh_CN -> offsetof(FFModuleDisplayName, zh_CN)
  62    // zh -> Error: Language 'zh' is not supported. This is an intentional design decision, as the language code 'zh' could refer to either Simplified or Traditional Chinese.
  63
  64#pragma GCC diagnostic push
  65#pragma GCC diagnostic ignored "-Wmultichar"
  66    switch (language->chars[0] << 8 | language->chars[1]) {
  67        case 'en': return offsetof(FFModuleDisplayName, en);
  68        case 'ar': return offsetof(FFModuleDisplayName, ar);
  69        case 'cs': return offsetof(FFModuleDisplayName, cs);
  70        case 'de': return offsetof(FFModuleDisplayName, de);
  71        case 'es': return offsetof(FFModuleDisplayName, es);
  72        case 'fi': return offsetof(FFModuleDisplayName, fi);
  73        case 'fr': return offsetof(FFModuleDisplayName, fr);
  74        case 'gl': return offsetof(FFModuleDisplayName, gl);
  75        case 'he': return offsetof(FFModuleDisplayName, he);
  76        case 'id': return offsetof(FFModuleDisplayName, id);
  77        case 'it': return offsetof(FFModuleDisplayName, it);
  78        case 'ja': return offsetof(FFModuleDisplayName, ja);
  79        case 'ko': return offsetof(FFModuleDisplayName, ko);
  80        case 'pl': return offsetof(FFModuleDisplayName, pl);
  81        case 'pt': return offsetof(FFModuleDisplayName, pt);
  82        case 'ru': return offsetof(FFModuleDisplayName, ru);
  83        case 'tr': return offsetof(FFModuleDisplayName, tr);
  84        case 'uk': return offsetof(FFModuleDisplayName, uk);
  85        case 'vi': return offsetof(FFModuleDisplayName, vi);
  86        case 'zh': {
  87            if (ffStrbufEqualS(language, "zh_cn")) {
  88                return offsetof(FFModuleDisplayName, zh_CN);
  89            } else if (ffStrbufEqualS(language, "zh_tw")) {
  90                return offsetof(FFModuleDisplayName, zh_TW);
  91            }
  92            goto error;
  93        }
  94    }
  95#pragma GCC diagnostic pop
  96
  97error:
  98    // The user asked for the system locale, not for this specific language. Most locales have no
  99    // translation, so falling back to English is friendlier than aborting fastfetch entirely.
 100    if (detected) return offsetof(FFModuleDisplayName, en);
 101
 102    fprintf(stderr, "Error: Language '%s' is not supported\n", language->chars);
 103    exit(477);
 104}
 105
 106const char* ffOptionsParseDisplayJsonConfig(FFOptionsDisplay* options, yyjson_val* root, yyjson_val** pkey) {
 107    yyjson_val* object = yyjson_obj_get(root, "display");
 108    if (!object) {
 109        return nullptr;
 110    }
 111    if (!yyjson_is_obj(object)) {
 112        return "Property 'display' must be an object";
 113    }
 114
 115    yyjson_val *key, *val;
 116    size_t idx, max;
 117    yyjson_obj_foreach (object, idx, max, key, val) {
 118        *pkey = key;
 119        if (unsafe_yyjson_equals_str(key, "stat")) {
 120            if (yyjson_is_bool(val)) {
 121                if (yyjson_get_bool(val)) {
 122                    options->stat = 0;
 123                    options->showErrors = true;
 124                } else {
 125                    options->stat = -1;
 126                }
 127            } else if (yyjson_is_uint(val)) {
 128                options->stat = (int) yyjson_get_uint(val);
 129                options->showErrors = true;
 130            } else {
 131                return "display.stat must be a boolean or a positive integer";
 132            }
 133        } else if (unsafe_yyjson_equals_str(key, "pipe")) {
 134            options->pipe = yyjson_get_bool(val);
 135        } else if (unsafe_yyjson_equals_str(key, "showErrors")) {
 136            options->showErrors = yyjson_get_bool(val);
 137        } else if (unsafe_yyjson_equals_str(key, "disableLinewrap")) {
 138            options->disableLinewrap = yyjson_get_bool(val);
 139        } else if (unsafe_yyjson_equals_str(key, "hideCursor")) {
 140            options->hideCursor = yyjson_get_bool(val);
 141        } else if (unsafe_yyjson_equals_str(key, "separator")) {
 142            ffStrbufSetJsonVal(&options->keyValueSeparator, val);
 143        } else if (unsafe_yyjson_equals_str(key, "color")) {
 144            if (yyjson_is_str(val)) {
 145                ffOptionParseColor(unsafe_yyjson_get_str(val), &options->colorKeys);
 146                ffStrbufSet(&options->colorTitle, &options->colorKeys);
 147            } else if (yyjson_is_obj(val)) {
 148                yyjson_val* colorKeys = yyjson_obj_get(val, "keys");
 149                if (colorKeys) {
 150                    ffOptionParseColor(yyjson_get_str(colorKeys), &options->colorKeys);
 151                }
 152                yyjson_val* colorTitle = yyjson_obj_get(val, "title");
 153                if (colorTitle) {
 154                    ffOptionParseColor(yyjson_get_str(colorTitle), &options->colorTitle);
 155                }
 156                yyjson_val* colorOutput = yyjson_obj_get(val, "output");
 157                if (colorOutput) {
 158                    ffOptionParseColor(yyjson_get_str(colorOutput), &options->colorOutput);
 159                }
 160                yyjson_val* colorSeparator = yyjson_obj_get(val, "separator");
 161                if (colorSeparator) {
 162                    ffOptionParseColor(yyjson_get_str(colorSeparator), &options->colorSeparator);
 163                }
 164            } else {
 165                return "display.color must be either a string or an object";
 166            }
 167        } else if (unsafe_yyjson_equals_str(key, "brightColor")) {
 168            options->brightColor = yyjson_get_bool(val);
 169        } else if (unsafe_yyjson_equals_str(key, "duration")) {
 170            if (!yyjson_is_obj(val)) {
 171                return "display.duration must be an object";
 172            }
 173
 174            yyjson_val* abbreviation = yyjson_obj_get(val, "abbreviation");
 175            if (abbreviation) {
 176                options->durationAbbreviation = yyjson_get_bool(abbreviation);
 177            }
 178
 179            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 180            if (spaceBeforeUnit) {
 181                int value;
 182                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 183                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 184                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 185                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 186                                                                                       {},
 187                                                                                   });
 188                if (error) {
 189                    return error;
 190                }
 191                options->durationSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 192            }
 193        } else if (unsafe_yyjson_equals_str(key, "size")) {
 194            if (!yyjson_is_obj(val)) {
 195                return "display.size must be an object";
 196            }
 197
 198            yyjson_val* maxPrefix = yyjson_obj_get(val, "maxPrefix");
 199            if (maxPrefix) {
 200                int value;
 201                const char* error = ffJsonConfigParseEnum(
 202                    maxPrefix,
 203                    &value,
 204                    (FFKeyValuePair[]) {
 205                        { "B", 0 },
 206                        { "kB", 1 },
 207                        { "MB", 2 },
 208                        { "GB", 3 },
 209                        { "TB", 4 },
 210                        { "PB", 5 },
 211                        { "EB", 6 },
 212                        { "ZB", 7 },
 213                        { "YB", 8 },
 214                        {},
 215                    });
 216                if (error) {
 217                    return error;
 218                }
 219                options->sizeMaxPrefix = (uint8_t) value;
 220            }
 221
 222            yyjson_val* binaryPrefix = yyjson_obj_get(val, "binaryPrefix");
 223            if (binaryPrefix) {
 224                int value;
 225                const char* error = ffJsonConfigParseEnum(binaryPrefix, &value, (FFKeyValuePair[]) {
 226                                                                                    { "iec", FF_SIZE_BINARY_PREFIX_TYPE_IEC },
 227                                                                                    { "si", FF_SIZE_BINARY_PREFIX_TYPE_SI },
 228                                                                                    { "jedec", FF_SIZE_BINARY_PREFIX_TYPE_JEDEC },
 229                                                                                    {},
 230                                                                                });
 231                if (error) {
 232                    return error;
 233                }
 234                options->sizeBinaryPrefix = (FFSizeBinaryPrefixType) value;
 235            }
 236
 237            yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 238            if (ndigits) {
 239                if (!yyjson_is_uint(ndigits)) {
 240                    return "display.size.ndigits must be an unsigned integer";
 241                }
 242                uint64_t val = yyjson_get_uint(ndigits);
 243                if (val > 9) {
 244                    return "display.size.ndigits must be between 0 and 9";
 245                }
 246                options->sizeNdigits = (uint8_t) val;
 247            }
 248
 249            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 250            if (spaceBeforeUnit) {
 251                int value;
 252                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 253                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 254                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 255                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 256                                                                                       {},
 257                                                                                   });
 258                if (error) {
 259                    return error;
 260                }
 261                options->sizeSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 262            }
 263        } else if (unsafe_yyjson_equals_str(key, "temp")) {
 264            if (!yyjson_is_obj(val)) {
 265                return "display.temp must be an object";
 266            }
 267
 268            yyjson_val* unit = yyjson_obj_get(val, "unit");
 269            if (unit) {
 270                int value;
 271                const char* error = ffJsonConfigParseEnum(unit, &value, (FFKeyValuePair[]) {
 272                                                                            { "DEFAULT", FF_TEMPERATURE_UNIT_DEFAULT },
 273                                                                            { "D", FF_TEMPERATURE_UNIT_DEFAULT },
 274                                                                            { "CELSIUS", FF_TEMPERATURE_UNIT_CELSIUS },
 275                                                                            { "C", FF_TEMPERATURE_UNIT_CELSIUS },
 276                                                                            { "FAHRENHEIT", FF_TEMPERATURE_UNIT_FAHRENHEIT },
 277                                                                            { "F", FF_TEMPERATURE_UNIT_FAHRENHEIT },
 278                                                                            { "KELVIN", FF_TEMPERATURE_UNIT_KELVIN },
 279                                                                            { "K", FF_TEMPERATURE_UNIT_KELVIN },
 280                                                                            {},
 281                                                                        });
 282                if (error) {
 283                    return error;
 284                }
 285                options->tempUnit = (FFTemperatureUnit) value;
 286            }
 287
 288            yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 289            if (ndigits) {
 290                if (!yyjson_is_uint(ndigits)) {
 291                    return "display.temp.ndigits must be an unsigned integer";
 292                }
 293                uint64_t val = yyjson_get_uint(ndigits);
 294                if (val > 9) {
 295                    return "display.temp.ndigits must be between 0 and 9";
 296                }
 297                options->tempNdigits = (uint8_t) val;
 298            }
 299
 300            yyjson_val* color = yyjson_obj_get(val, "color");
 301            if (color) {
 302                if (!yyjson_is_obj(color)) {
 303                    return "display.temp.color must be an object";
 304                }
 305
 306                yyjson_val* green = yyjson_obj_get(color, "green");
 307                if (green) {
 308                    ffOptionParseColor(yyjson_get_str(green), &options->tempColorGreen);
 309                }
 310
 311                yyjson_val* yellow = yyjson_obj_get(color, "yellow");
 312                if (yellow) {
 313                    ffOptionParseColor(yyjson_get_str(yellow), &options->tempColorYellow);
 314                }
 315
 316                yyjson_val* red = yyjson_obj_get(color, "red");
 317                if (red) {
 318                    ffOptionParseColor(yyjson_get_str(red), &options->tempColorRed);
 319                }
 320            }
 321
 322            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 323            if (spaceBeforeUnit) {
 324                int value;
 325                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 326                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 327                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 328                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 329                                                                                       {},
 330                                                                                   });
 331                if (error) {
 332                    return error;
 333                }
 334                options->tempSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 335            }
 336        } else if (unsafe_yyjson_equals_str(key, "percent")) {
 337            if (!yyjson_is_obj(val)) {
 338                return "display.percent must be an object";
 339            }
 340
 341            yyjson_val* type = yyjson_obj_get(val, "type");
 342            if (type) {
 343                const char* error = ffPercentParseTypeJsonConfig(type, &options->percentType);
 344                if (error) {
 345                    return error;
 346                }
 347            }
 348
 349            yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 350            if (ndigits) {
 351                if (!yyjson_is_uint(ndigits)) {
 352                    return "display.percent.ndigits must be an unsigned integer";
 353                }
 354                uint64_t val = yyjson_get_uint(ndigits);
 355                if (val > 9) {
 356                    return "display.percent.ndigits must be between 0 and 9";
 357                }
 358                options->percentNdigits = (uint8_t) val;
 359            }
 360
 361            yyjson_val* color = yyjson_obj_get(val, "color");
 362            if (color) {
 363                if (!yyjson_is_obj(color)) {
 364                    return "display.percent.color must be an object";
 365                }
 366
 367                yyjson_val* green = yyjson_obj_get(color, "green");
 368                if (green) {
 369                    ffOptionParseColor(yyjson_get_str(green), &options->percentColorGreen);
 370                }
 371
 372                yyjson_val* yellow = yyjson_obj_get(color, "yellow");
 373                if (yellow) {
 374                    ffOptionParseColor(yyjson_get_str(yellow), &options->percentColorYellow);
 375                }
 376
 377                yyjson_val* red = yyjson_obj_get(color, "red");
 378                if (red) {
 379                    ffOptionParseColor(yyjson_get_str(red), &options->percentColorRed);
 380                }
 381            }
 382
 383            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 384            if (spaceBeforeUnit) {
 385                int value;
 386                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 387                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 388                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 389                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 390                                                                                       {},
 391                                                                                   });
 392                if (error) {
 393                    return error;
 394                }
 395                options->percentSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 396            }
 397
 398            yyjson_val* width = yyjson_obj_get(val, "width");
 399            if (width) {
 400                options->percentWidth = (uint8_t) yyjson_get_uint(width);
 401            }
 402        } else if (unsafe_yyjson_equals_str(key, "bar")) {
 403            if (yyjson_is_obj(val)) {
 404                yyjson_val* char_ = yyjson_obj_get(val, "char");
 405                if (char_) {
 406                    if (!yyjson_is_obj(char_)) {
 407                        return "display.bar.char must be an object";
 408                    }
 409
 410                    yyjson_val* charElapsed = yyjson_obj_get(char_, "elapsed");
 411                    if (charElapsed) {
 412                        ffStrbufSetJsonVal(&options->barCharElapsed, charElapsed);
 413                    }
 414
 415                    yyjson_val* charTotal = yyjson_obj_get(char_, "total");
 416                    if (charTotal) {
 417                        ffStrbufSetJsonVal(&options->barCharTotal, charTotal);
 418                    }
 419                } else {
 420                    yyjson_val* charElapsed = yyjson_obj_get(val, "charElapsed");
 421                    if (charElapsed) {
 422                        return "display.bar.charElapsed has been renamed to display.bar.char.elapsed.";
 423                    }
 424
 425                    yyjson_val* charTotal = yyjson_obj_get(val, "charTotal");
 426                    if (charTotal) {
 427                        return "display.bar.charTotal has been renamed to display.bar.char.total.";
 428                    }
 429                }
 430
 431                yyjson_val* border = yyjson_obj_get(val, "border");
 432                if (border) {
 433                    if (yyjson_is_null(border)) {
 434                        ffStrbufClear(&options->barBorderLeft);
 435                        ffStrbufClear(&options->barBorderRight);
 436                        ffStrbufClear(&options->barBorderLeftElapsed);
 437                        ffStrbufClear(&options->barBorderRightElapsed);
 438                    } else {
 439                        if (!yyjson_is_obj(border)) {
 440                            return "display.bar.border must be an object";
 441                        }
 442
 443                        yyjson_val* borderLeft = yyjson_obj_get(border, "left");
 444                        if (borderLeft) {
 445                            ffStrbufSetJsonVal(&options->barBorderLeft, borderLeft);
 446                        }
 447
 448                        yyjson_val* borderRight = yyjson_obj_get(border, "right");
 449                        if (borderRight) {
 450                            ffStrbufSetJsonVal(&options->barBorderRight, borderRight);
 451                        }
 452
 453                        yyjson_val* borderLeftElapsed = yyjson_obj_get(border, "leftElapsed");
 454                        if (borderLeftElapsed) {
 455                            ffStrbufSetJsonVal(&options->barBorderLeftElapsed, borderLeftElapsed);
 456                        }
 457
 458                        yyjson_val* borderRightElapsed = yyjson_obj_get(border, "rightElapsed");
 459                        if (borderRightElapsed) {
 460                            ffStrbufSetJsonVal(&options->barBorderRightElapsed, borderRightElapsed);
 461                        }
 462                    }
 463                } else {
 464                    yyjson_val* borderLeft = yyjson_obj_get(val, "borderLeft");
 465                    if (borderLeft) {
 466                        return "display.bar.borderLeft has been renamed to display.bar.border.left.";
 467                    }
 468
 469                    yyjson_val* borderRight = yyjson_obj_get(val, "borderRight");
 470                    if (borderRight) {
 471                        return "display.bar.borderRight has been renamed to display.bar.border.right.";
 472                    }
 473                }
 474
 475                yyjson_val* color = yyjson_obj_get(val, "color");
 476                if (color) {
 477                    if (yyjson_is_null(color)) {
 478                        ffStrbufClear(&options->barColorElapsed);
 479                        ffStrbufClear(&options->barColorTotal);
 480                        ffStrbufClear(&options->barColorBorder);
 481                    } else {
 482                        if (!yyjson_is_obj(color)) {
 483                            return "display.bar.color must be an object";
 484                        }
 485
 486                        yyjson_val* colorElapsed = yyjson_obj_get(color, "elapsed");
 487                        if (colorElapsed) {
 488                            const char* value = yyjson_get_str(colorElapsed);
 489                            if (!value) {
 490                                ffStrbufClear(&options->barColorElapsed);
 491                            } else if (ffStrEqualsIgnCase(value, "auto")) {
 492                                ffStrbufSetStatic(&options->barColorElapsed, "auto");
 493                            } else {
 494                                ffOptionParseColor(value, &options->barColorElapsed);
 495                            }
 496                        }
 497
 498                        yyjson_val* colorTotal = yyjson_obj_get(color, "total");
 499                        if (colorTotal) {
 500                            ffOptionParseColor(yyjson_get_str(colorTotal), &options->barColorTotal);
 501                        }
 502
 503                        yyjson_val* colorBorder = yyjson_obj_get(color, "border");
 504                        if (colorBorder) {
 505                            ffOptionParseColor(yyjson_get_str(colorBorder), &options->barColorBorder);
 506                        }
 507                    }
 508                }
 509
 510                yyjson_val* width = yyjson_obj_get(val, "width");
 511                if (width) {
 512                    options->barWidth = (uint8_t) yyjson_get_uint(width);
 513                }
 514            } else {
 515                return "display.bar must be an object";
 516            }
 517        } else if (unsafe_yyjson_equals_str(key, "fraction")) {
 518            if (yyjson_is_obj(val)) {
 519                yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 520                if (ndigits) {
 521                    if (yyjson_is_null(ndigits)) {
 522                        options->fractionNdigits = -1;
 523                    } else {
 524                        if (!yyjson_is_int(ndigits)) {
 525                            return "display.fraction.ndigits must be an integer";
 526                        }
 527                        int64_t val = yyjson_get_int(ndigits);
 528                        if (val < -1 || val > 9) {
 529                            return "display.fraction.ndigits must be between -1 and 9";
 530                        }
 531                        options->fractionNdigits = (int8_t) val;
 532                    }
 533                }
 534                yyjson_val* trailingZeros = yyjson_obj_get(val, "trailingZeros");
 535                if (trailingZeros) {
 536                    if (yyjson_is_null(trailingZeros)) {
 537                        options->fractionTrailingZeros = FF_FRACTION_TRAILING_ZEROS_TYPE_DEFAULT;
 538                    } else {
 539                        int value;
 540                        const char* error = ffJsonConfigParseEnum(trailingZeros, &value, (FFKeyValuePair[]) {
 541                                                                                             { "default", FF_FRACTION_TRAILING_ZEROS_TYPE_DEFAULT },
 542                                                                                             { "always", FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS },
 543                                                                                             { "never", FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER },
 544                                                                                             {},
 545                                                                                         });
 546                        if (error) {
 547                            return error;
 548                        }
 549                        options->fractionTrailingZeros = (FFFractionTrailingZerosType) value;
 550                    }
 551                }
 552            } else {
 553                return "display.fraction must be an object";
 554            }
 555        } else if (unsafe_yyjson_equals_str(key, "noBuffer")) {
 556            options->noBuffer = yyjson_get_bool(val);
 557        } else if (unsafe_yyjson_equals_str(key, "key")) {
 558            if (yyjson_is_obj(val)) {
 559                yyjson_val* width = yyjson_obj_get(val, "width");
 560                if (width) {
 561                    options->keyWidth = (uint16_t) yyjson_get_uint(width);
 562                }
 563
 564                yyjson_val* type = yyjson_obj_get(val, "type");
 565                if (type) {
 566                    int value;
 567                    const char* error = ffJsonConfigParseEnum(type, &value, (FFKeyValuePair[]) { { "none", FF_MODULE_KEY_TYPE_NONE }, { "string", FF_MODULE_KEY_TYPE_STRING }, { "icon", FF_MODULE_KEY_TYPE_ICON }, { "both", FF_MODULE_KEY_TYPE_BOTH }, { "both-0", FF_MODULE_KEY_TYPE_BOTH_0 }, { "both-1", FF_MODULE_KEY_TYPE_BOTH_1 }, { "both-2", FF_MODULE_KEY_TYPE_BOTH_2 }, { "both-3", FF_MODULE_KEY_TYPE_BOTH_3 }, { "both-4", FF_MODULE_KEY_TYPE_BOTH_4 }, {} });
 568                    if (error) {
 569                        return error;
 570                    }
 571                    options->keyType = (uint8_t) value;
 572                }
 573
 574                yyjson_val* paddingLeft = yyjson_obj_get(val, "paddingLeft");
 575                if (paddingLeft) {
 576                    options->keyPaddingLeft = (uint16_t) yyjson_get_uint(paddingLeft);
 577                }
 578
 579                yyjson_val* language = yyjson_obj_get(val, "language");
 580                if (language) {
 581                    FF_STRBUF_AUTO_DESTROY languageBuffer = ffStrbufCreate();
 582                    if (yyjson_is_str(language)) {
 583                        ffStrbufSetJsonVal(&languageBuffer, language);
 584                    } else if (!yyjson_is_null(language)) {
 585                        return "display.key.language must be a string or null";
 586                    }
 587                    options->keyLanguage = optionParseLanguageString(&languageBuffer);
 588                }
 589            } else {
 590                return "display.key must be an object";
 591            }
 592        } else if (unsafe_yyjson_equals_str(key, "constants")) {
 593            if (!yyjson_is_arr(val)) {
 594                return "display.constants must be an array";
 595            }
 596            yyjson_val* item;
 597            size_t idx, max;
 598            yyjson_arr_foreach (val, idx, max, item) {
 599                FFstrbuf* buffer = FF_LIST_ADD(FFstrbuf, options->constants);
 600                ffStrbufInitJsonVal(buffer, item);
 601            }
 602        } else if (unsafe_yyjson_equals_str(key, "freq")) {
 603            if (!yyjson_is_obj(val)) {
 604                return "display.freq must be an object";
 605            }
 606
 607            yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 608            if (ndigits) {
 609                if (yyjson_is_null(ndigits)) {
 610                    options->freqNdigits = -1;
 611                } else {
 612                    if (!yyjson_is_int(ndigits)) {
 613                        return "display.freq.ndigits must be an integer";
 614                    }
 615                    int64_t val = yyjson_get_int(ndigits);
 616                    if (val < -1 || val > 9) {
 617                        return "display.freq.ndigits must be between -1 and 9";
 618                    }
 619                    options->freqNdigits = (int8_t) val;
 620                }
 621            }
 622
 623            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 624            if (spaceBeforeUnit) {
 625                int value;
 626                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 627                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 628                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 629                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 630                                                                                       {},
 631                                                                                   });
 632                if (error) {
 633                    return error;
 634                }
 635                options->freqSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 636            }
 637        } else if (unsafe_yyjson_equals_str(key, "common")) {
 638            if (!yyjson_is_obj(val)) {
 639                return "display.common must be an object";
 640            }
 641
 642            yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
 643            if (ndigits) {
 644                if (!yyjson_is_uint(ndigits)) {
 645                    return "display.common.ndigits must be an unsigned integer";
 646                }
 647                uint64_t val = yyjson_get_uint(ndigits);
 648                if (val > 9) {
 649                    return "display.common.ndigits must be between 0 and 9";
 650                }
 651                options->fractionNdigits = (int8_t) val;
 652                options->freqNdigits = (int8_t) val;
 653                options->percentNdigits = (uint8_t) val;
 654                options->sizeNdigits = (uint8_t) val;
 655                options->tempNdigits = (uint8_t) val;
 656            }
 657
 658            yyjson_val* spaceBeforeUnit = yyjson_obj_get(val, "spaceBeforeUnit");
 659            if (spaceBeforeUnit) {
 660                int value;
 661                const char* error = ffJsonConfigParseEnum(spaceBeforeUnit, &value, (FFKeyValuePair[]) {
 662                                                                                       { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 663                                                                                       { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 664                                                                                       { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 665                                                                                       {},
 666                                                                                   });
 667                if (error) {
 668                    return error;
 669                }
 670                options->durationSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 671                options->freqSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 672                options->percentSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 673                options->sizeSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 674                options->tempSpaceBeforeUnit = (FFSpaceBeforeUnitType) value;
 675            }
 676        } else {
 677            return "Unknown display property";
 678        }
 679    }
 680
 681    return nullptr;
 682}
 683
 684static inline void optionCheckString(const char* key, const char* value, FFstrbuf* buffer) {
 685    if (value == nullptr) {
 686        fprintf(stderr, "Error: usage: %s <str>\n", key);
 687        exit(477);
 688    }
 689    ffStrbufEnsureFree(buffer, 63); // This is not needed, as ffStrbufSetS will resize capacity if needed, but giving a higher start should improve performance
 690}
 691
 692bool ffOptionsParseDisplayCommandLine(FFOptionsDisplay* options, const char* key, const char* value) {
 693    if (ffStrEqualsIgnCase(key, "--stat")) {
 694        if (ffOptionParseBoolean(value)) {
 695            options->stat = 0;
 696            options->showErrors = true;
 697        } else if (value) {
 698            char* end;
 699            uint32_t num = (uint32_t) strtoul(value, &end, 10);
 700            if (*end == '\0') {
 701                options->stat = (int32_t) num;
 702                options->showErrors = true;
 703            } else {
 704                options->stat = -1;
 705            }
 706        } else {
 707            options->stat = -1;
 708        }
 709    } else if (ffStrEqualsIgnCase(key, "--pipe")) {
 710        options->pipe = ffOptionParseBoolean(value);
 711    } else if (ffStrEqualsIgnCase(key, "--show-errors")) {
 712        options->showErrors = ffOptionParseBoolean(value);
 713    } else if (ffStrEqualsIgnCase(key, "--debug"))
 714#ifndef NDEBUG
 715        options->debugMode = ffOptionParseBoolean(value);
 716#else
 717    {
 718        fprintf(stderr, "--debug is only available in debug builds\n");
 719        exit(477);
 720    }
 721#endif
 722    else if (ffStrEqualsIgnCase(key, "--disable-linewrap")) {
 723        options->disableLinewrap = ffOptionParseBoolean(value);
 724    } else if (ffStrEqualsIgnCase(key, "--hide-cursor")) {
 725        options->hideCursor = ffOptionParseBoolean(value);
 726    } else if (ffStrEqualsIgnCase(key, "--separator")) {
 727        ffOptionParseString(key, value, &options->keyValueSeparator);
 728    } else if (ffStrEqualsIgnCase(key, "--color")) {
 729        optionCheckString(key, value, &options->colorKeys);
 730        ffOptionParseColor(value, &options->colorKeys);
 731        ffStrbufSet(&options->colorTitle, &options->colorKeys);
 732    } else if (ffStrStartsWithIgnCase(key, "--color-")) {
 733        const char* subkey = key + strlen("--color-");
 734        if (ffStrEqualsIgnCase(subkey, "keys")) {
 735            optionCheckString(key, value, &options->colorKeys);
 736            ffOptionParseColor(value, &options->colorKeys);
 737        } else if (ffStrEqualsIgnCase(subkey, "title")) {
 738            optionCheckString(key, value, &options->colorTitle);
 739            ffOptionParseColor(value, &options->colorTitle);
 740        } else if (ffStrEqualsIgnCase(subkey, "output")) {
 741            optionCheckString(key, value, &options->colorOutput);
 742            ffOptionParseColor(value, &options->colorOutput);
 743        } else if (ffStrEqualsIgnCase(subkey, "separator")) {
 744            optionCheckString(key, value, &options->colorSeparator);
 745            ffOptionParseColor(value, &options->colorSeparator);
 746        } else {
 747            return false;
 748        }
 749    } else if (ffStrStartsWithIgnCase(key, "--key-")) {
 750        const char* subkey = key + strlen("--key-");
 751        if (ffStrEqualsIgnCase(subkey, "width")) {
 752            options->keyWidth = (uint16_t) ffOptionParseUInt32(key, value);
 753        } else if (ffStrEqualsIgnCase(subkey, "type")) {
 754            options->keyType = (FFModuleKeyType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) { { "none", FF_MODULE_KEY_TYPE_NONE }, { "string", FF_MODULE_KEY_TYPE_STRING }, { "icon", FF_MODULE_KEY_TYPE_ICON }, { "both", FF_MODULE_KEY_TYPE_BOTH }, { "both-0", FF_MODULE_KEY_TYPE_BOTH_0 }, { "both-1", FF_MODULE_KEY_TYPE_BOTH_1 }, { "both-2", FF_MODULE_KEY_TYPE_BOTH_2 }, { "both-3", FF_MODULE_KEY_TYPE_BOTH_3 }, { "both-4", FF_MODULE_KEY_TYPE_BOTH_4 }, {} });
 755        } else if (ffStrEqualsIgnCase(subkey, "padding-left")) {
 756            options->keyPaddingLeft = (uint16_t) ffOptionParseUInt32(key, value);
 757        } else if (ffStrEqualsIgnCase(subkey, "language")) {
 758            FF_STRBUF_AUTO_DESTROY language = ffStrbufCreateS(value);
 759            options->keyLanguage = optionParseLanguageString(&language);
 760        } else {
 761            return false;
 762        }
 763    } else if (ffStrEqualsIgnCase(key, "--bright-color")) {
 764        options->brightColor = ffOptionParseBoolean(value);
 765    } else if (ffStrEqualsIgnCase(key, "--binary-prefix")) {
 766        fprintf(stderr, "--binary-prefix has been renamed to --size-binary-prefix\n");
 767        exit(477);
 768    } else if (ffStrStartsWithIgnCase(key, "--duration-")) {
 769        const char* subkey = key + strlen("--duration-");
 770        if (ffStrEqualsIgnCase(subkey, "abbreviation")) {
 771            options->durationAbbreviation = ffOptionParseBoolean(value);
 772        } else if (ffStrEqualsIgnCase(subkey, "space-before-unit")) {
 773            options->durationSpaceBeforeUnit = (FFSpaceBeforeUnitType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 774                                                                                                         { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 775                                                                                                         { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 776                                                                                                         { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 777                                                                                                         {},
 778                                                                                                     });
 779        } else {
 780            return false;
 781        }
 782    } else if (ffStrStartsWithIgnCase(key, "--size-")) {
 783        const char* subkey = key + strlen("--size-");
 784        if (ffStrEqualsIgnCase(subkey, "binary-prefix")) {
 785            options->sizeBinaryPrefix = (FFSizeBinaryPrefixType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) { { "iec", FF_SIZE_BINARY_PREFIX_TYPE_IEC }, { "si", FF_SIZE_BINARY_PREFIX_TYPE_SI }, { "jedec", FF_SIZE_BINARY_PREFIX_TYPE_JEDEC }, {} });
 786        } else if (ffStrEqualsIgnCase(subkey, "ndigits")) {
 787            uint32_t num = ffOptionParseUInt32(key, value);
 788            if (num > 9) {
 789                fprintf(stderr, "Error: %s must be between 0 and 9\n", key);
 790                exit(479);
 791            }
 792            options->sizeNdigits = (uint8_t) num;
 793        } else if (ffStrEqualsIgnCase(subkey, "max-prefix")) {
 794            options->sizeMaxPrefix = (uint8_t) ffOptionParseEnum(
 795                key,
 796                value,
 797                (FFKeyValuePair[]) {
 798                    { "B", 0 },
 799                    { "kB", 1 },
 800                    { "MB", 2 },
 801                    { "GB", 3 },
 802                    { "TB", 4 },
 803                    { "PB", 5 },
 804                    { "EB", 6 },
 805                    { "ZB", 7 },
 806                    { "YB", 8 },
 807                    {},
 808                });
 809        } else if (ffStrEqualsIgnCase(subkey, "space-before-unit")) {
 810            options->sizeSpaceBeforeUnit = (FFSpaceBeforeUnitType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 811                                                                                                     { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 812                                                                                                     { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 813                                                                                                     { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 814                                                                                                     {},
 815                                                                                                 });
 816        } else {
 817            return false;
 818        }
 819    } else if (ffStrStartsWithIgnCase(key, "--temp-")) {
 820        const char* subkey = key + strlen("--temp-");
 821        if (ffStrEqualsIgnCase(subkey, "unit")) {
 822            options->tempUnit = (FFTemperatureUnit) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 823                                                                                      { "DEFAULT", FF_TEMPERATURE_UNIT_DEFAULT },
 824                                                                                      { "D", FF_TEMPERATURE_UNIT_DEFAULT },
 825                                                                                      { "CELSIUS", FF_TEMPERATURE_UNIT_CELSIUS },
 826                                                                                      { "C", FF_TEMPERATURE_UNIT_CELSIUS },
 827                                                                                      { "FAHRENHEIT", FF_TEMPERATURE_UNIT_FAHRENHEIT },
 828                                                                                      { "F", FF_TEMPERATURE_UNIT_FAHRENHEIT },
 829                                                                                      { "KELVIN", FF_TEMPERATURE_UNIT_KELVIN },
 830                                                                                      { "K", FF_TEMPERATURE_UNIT_KELVIN },
 831                                                                                      {},
 832                                                                                  });
 833        } else if (ffStrEqualsIgnCase(subkey, "ndigits")) {
 834            options->tempNdigits = (uint8_t) ffOptionParseUInt32(key, value);
 835        } else if (ffStrEqualsIgnCase(subkey, "color-green")) {
 836            ffOptionParseColor(value, &options->tempColorGreen);
 837        } else if (ffStrEqualsIgnCase(subkey, "color-yellow")) {
 838            ffOptionParseColor(value, &options->tempColorYellow);
 839        } else if (ffStrEqualsIgnCase(subkey, "color-red")) {
 840            ffOptionParseColor(value, &options->tempColorRed);
 841        } else if (ffStrEqualsIgnCase(subkey, "space-before-unit")) {
 842            options->tempSpaceBeforeUnit = (FFSpaceBeforeUnitType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 843                                                                                                     { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 844                                                                                                     { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 845                                                                                                     { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 846                                                                                                     {},
 847                                                                                                 });
 848        } else {
 849            return false;
 850        }
 851    } else if (ffStrStartsWithIgnCase(key, "--percent-")) {
 852        const char* subkey = key + strlen("--percent-");
 853        if (ffStrEqualsIgnCase(subkey, "type")) {
 854            options->percentType = (uint8_t) ffOptionParseUInt32(key, value);
 855        } else if (ffStrEqualsIgnCase(subkey, "ndigits")) {
 856            options->percentNdigits = (uint8_t) ffOptionParseUInt32(key, value);
 857        } else if (ffStrEqualsIgnCase(subkey, "color-green")) {
 858            ffOptionParseColor(value, &options->percentColorGreen);
 859        } else if (ffStrEqualsIgnCase(subkey, "color-yellow")) {
 860            ffOptionParseColor(value, &options->percentColorYellow);
 861        } else if (ffStrEqualsIgnCase(subkey, "color-red")) {
 862            ffOptionParseColor(value, &options->percentColorRed);
 863        } else if (ffStrEqualsIgnCase(subkey, "space-before-unit")) {
 864            options->percentSpaceBeforeUnit = (FFSpaceBeforeUnitType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 865                                                                                                        { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 866                                                                                                        { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 867                                                                                                        { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 868                                                                                                        {},
 869                                                                                                    });
 870        } else if (ffStrEqualsIgnCase(subkey, "width")) {
 871            options->percentWidth = (uint8_t) ffOptionParseUInt32(key, value);
 872        } else {
 873            return false;
 874        }
 875    } else if (ffStrEqualsIgnCase(key, "--fraction-ndigits")) {
 876        options->fractionNdigits = (int8_t) ffOptionParseInt32(key, value);
 877    } else if (ffStrEqualsIgnCase(key, "--fraction-trailing-zeros")) {
 878        options->fractionTrailingZeros = (FFFractionTrailingZerosType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 879                                                                                                         { "default", FF_FRACTION_TRAILING_ZEROS_TYPE_DEFAULT },
 880                                                                                                         { "always", FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS },
 881                                                                                                         { "never", FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER },
 882                                                                                                         {},
 883                                                                                                     });
 884    } else if (ffStrEqualsIgnCase(key, "--no-buffer")) {
 885        options->noBuffer = ffOptionParseBoolean(value);
 886    } else if (ffStrStartsWithIgnCase(key, "--bar-")) {
 887        const char* subkey = key + strlen("--bar-");
 888        if (ffStrEqualsIgnCase(subkey, "char-elapsed")) {
 889            ffOptionParseString(key, value, &options->barCharElapsed);
 890        } else if (ffStrEqualsIgnCase(subkey, "char-total")) {
 891            ffOptionParseString(key, value, &options->barCharTotal);
 892        } else if (ffStrEqualsIgnCase(subkey, "width")) {
 893            options->barWidth = (uint8_t) ffOptionParseUInt32(key, value);
 894        } else if (ffStrEqualsIgnCase(subkey, "border-left")) {
 895            ffOptionParseString(key, value, &options->barBorderLeft);
 896        } else if (ffStrEqualsIgnCase(subkey, "border-right")) {
 897            ffOptionParseString(key, value, &options->barBorderRight);
 898        } else if (ffStrEqualsIgnCase(subkey, "border-left-elapsed")) {
 899            ffOptionParseString(key, value, &options->barBorderLeftElapsed);
 900        } else if (ffStrEqualsIgnCase(subkey, "border-right-elapsed")) {
 901            ffOptionParseString(key, value, &options->barBorderRightElapsed);
 902        } else if (ffStrEqualsIgnCase(subkey, "color-elapsed")) {
 903            if (!value) {
 904                ffStrbufClear(&options->barColorElapsed);
 905            } else if (ffStrEqualsIgnCase(value, "auto")) {
 906                ffStrbufSetStatic(&options->barColorElapsed, "auto");
 907            } else {
 908                ffOptionParseColor(value, &options->barColorElapsed);
 909            }
 910        } else if (ffStrEqualsIgnCase(subkey, "color-total")) {
 911            ffOptionParseColor(value, &options->barColorTotal);
 912        } else if (ffStrEqualsIgnCase(subkey, "color-border")) {
 913            ffOptionParseColor(value, &options->barColorBorder);
 914        } else {
 915            return false;
 916        }
 917    } else if (ffStrStartsWithIgnCase(key, "--freq-")) {
 918        const char* subkey = key + strlen("--freq-");
 919        if (ffStrEqualsIgnCase(subkey, "ndigits")) {
 920            options->freqNdigits = (int8_t) ffOptionParseInt32(key, value);
 921        } else if (ffStrEqualsIgnCase(subkey, "space-before-unit")) {
 922            options->freqSpaceBeforeUnit = (FFSpaceBeforeUnitType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
 923                                                                                                     { "default", FF_SPACE_BEFORE_UNIT_DEFAULT },
 924                                                                                                     { "always", FF_SPACE_BEFORE_UNIT_ALWAYS },
 925                                                                                                     { "never", FF_SPACE_BEFORE_UNIT_NEVER },
 926                                                                                                     {},
 927                                                                                                 });
 928        } else {
 929            return false;
 930        }
 931    } else {
 932        return false;
 933    }
 934    return true;
 935}
 936
 937void ffOptionsInitDisplay(FFOptionsDisplay* options) {
 938    bool terminalLightTheme = false;
 939    #if !FF_MODULE_DISABLE_TERMINALTHEME
 940    {
 941        // don't enable bright color if the terminal is in light mode
 942        FFTerminalThemeResult result;
 943        if (ffDetectTerminalTheme(&result, true /* forceEnv for performance */) && !result.bg.dark) {
 944            terminalLightTheme = true;
 945        }
 946    }
 947    #endif
 948
 949    ffStrbufInit(&options->colorKeys);
 950    ffStrbufInit(&options->colorTitle);
 951    ffStrbufInit(&options->colorOutput);
 952    ffStrbufInit(&options->colorSeparator);
 953    options->brightColor = !terminalLightTheme;
 954    ffStrbufInitStatic(&options->keyValueSeparator, ": ");
 955
 956    options->showErrors = false;
 957    options->pipe = !ffIsTerminal(STDOUT_FILENO) || !!getenv("NO_COLOR");
 958    options->disableLinewrap = false;
 959
 960#ifndef NDEBUG
 961    options->debugMode = !!getenv("FF_DEBUG");
 962#endif
 963
 964    options->durationSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
 965    options->hideCursor = false;
 966    options->sizeBinaryPrefix = FF_SIZE_BINARY_PREFIX_TYPE_IEC;
 967    options->sizeNdigits = 2;
 968    options->sizeMaxPrefix = 8; // YB
 969    options->sizeSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
 970
 971    options->stat = -1;
 972    options->noBuffer = false;
 973    options->keyWidth = 0;
 974    options->keyPaddingLeft = 0;
 975    options->keyType = FF_MODULE_KEY_TYPE_STRING;
 976    options->keyLanguage = offsetof(FFModuleDisplayName, en);
 977
 978    options->tempUnit = FF_TEMPERATURE_UNIT_DEFAULT;
 979    options->tempNdigits = 1;
 980    ffStrbufInitStatic(&options->tempColorGreen, FF_COLOR_FG_GREEN);
 981    ffStrbufInitStatic(&options->tempColorYellow, terminalLightTheme ? FF_COLOR_FG_YELLOW : FF_COLOR_FG_LIGHT_YELLOW);
 982    ffStrbufInitStatic(&options->tempColorRed, terminalLightTheme ? FF_COLOR_FG_RED : FF_COLOR_FG_LIGHT_RED);
 983    options->tempSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
 984
 985    ffStrbufInitStatic(&options->barCharElapsed, "â– ");
 986    ffStrbufInitStatic(&options->barCharTotal, "-");
 987    ffStrbufInitStatic(&options->barBorderLeft, "[ ");
 988    ffStrbufInitStatic(&options->barBorderRight, " ]");
 989    ffStrbufInit(&options->barBorderLeftElapsed);
 990    ffStrbufInit(&options->barBorderRightElapsed);
 991    ffStrbufInitStatic(&options->barColorElapsed, "auto");
 992    ffStrbufInitStatic(&options->barColorTotal, terminalLightTheme ? FF_COLOR_FG_WHITE : FF_COLOR_FG_LIGHT_WHITE);
 993    ffStrbufInitStatic(&options->barColorBorder, terminalLightTheme ? FF_COLOR_FG_WHITE : FF_COLOR_FG_LIGHT_WHITE);
 994    options->barWidth = 10;
 995
 996    options->durationAbbreviation = false;
 997    options->durationSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
 998    options->percentType = FF_PERCENTAGE_TYPE_NUM_BIT | FF_PERCENTAGE_TYPE_NUM_COLOR_BIT;
 999    options->percentNdigits = 0;
1000    ffStrbufInitStatic(&options->percentColorGreen, FF_COLOR_FG_GREEN);
1001    ffStrbufInitStatic(&options->percentColorYellow, terminalLightTheme ? FF_COLOR_FG_YELLOW : FF_COLOR_FG_LIGHT_YELLOW);
1002    ffStrbufInitStatic(&options->percentColorRed, terminalLightTheme ? FF_COLOR_FG_RED : FF_COLOR_FG_LIGHT_RED);
1003    options->percentSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
1004    options->percentWidth = 0;
1005
1006    options->freqNdigits = 2;
1007    options->freqSpaceBeforeUnit = FF_SPACE_BEFORE_UNIT_DEFAULT;
1008    options->fractionNdigits = 2;
1009    options->fractionTrailingZeros = FF_FRACTION_TRAILING_ZEROS_TYPE_DEFAULT;
1010
1011    ffListInit(&options->constants);
1012}
1013
1014void ffOptionsDestroyDisplay(FFOptionsDisplay* options) {
1015    ffStrbufDestroy(&options->colorKeys);
1016    ffStrbufDestroy(&options->colorTitle);
1017    ffStrbufDestroy(&options->colorOutput);
1018    ffStrbufDestroy(&options->colorSeparator);
1019    ffStrbufDestroy(&options->keyValueSeparator);
1020    ffStrbufDestroy(&options->barCharElapsed);
1021    ffStrbufDestroy(&options->barCharTotal);
1022    ffStrbufDestroy(&options->barBorderLeft);
1023    ffStrbufDestroy(&options->barBorderRight);
1024    ffStrbufDestroy(&options->barBorderLeftElapsed);
1025    ffStrbufDestroy(&options->barBorderRightElapsed);
1026    ffStrbufDestroy(&options->barColorElapsed);
1027    ffStrbufDestroy(&options->barColorTotal);
1028    ffStrbufDestroy(&options->barColorBorder);
1029    ffStrbufDestroy(&options->tempColorGreen);
1030    ffStrbufDestroy(&options->tempColorYellow);
1031    ffStrbufDestroy(&options->tempColorRed);
1032    ffStrbufDestroy(&options->percentColorGreen);
1033    ffStrbufDestroy(&options->percentColorYellow);
1034    ffStrbufDestroy(&options->percentColorRed);
1035    FF_LIST_FOR_EACH (FFstrbuf, item, options->constants) {
1036        ffStrbufDestroy(item);
1037    }
1038    ffListDestroy(&options->constants);
1039}
1040
1041// `names` is indexed by the enum value, so its order must match the enum declaration. A value outside
1042// the array adds no key at all, which is what the exhaustive switches this replaces did.
1043static void optionAddEnumString(yyjson_mut_doc* doc, yyjson_mut_val* obj, const char* key, const char* const names[], size_t count, uint32_t value) {
1044    if (value < count) {
1045        yyjson_mut_obj_add_str(doc, obj, key, names[value]);
1046    }
1047}
1048
1049static void optionAddSpaceBeforeUnit(yyjson_mut_doc* doc, yyjson_mut_val* obj, FFSpaceBeforeUnitType type) {
1050    static const char* const names[] = {"default", "always", "never"};
1051    optionAddEnumString(doc, obj, "spaceBeforeUnit", names, sizeof(names) / sizeof(*names), type);
1052}
1053
1054// @returns the language code for offsetof(FFModuleDisplayName, <language>), or nullptr if unknown
1055// @see optionParseLanguageString
1056static const char* optionGetLanguageString(uint32_t offset) {
1057    switch (offset) {
1058        case offsetof(FFModuleDisplayName, en): return "en";
1059        case offsetof(FFModuleDisplayName, ar): return "ar";
1060        case offsetof(FFModuleDisplayName, cs): return "cs";
1061        case offsetof(FFModuleDisplayName, de): return "de";
1062        case offsetof(FFModuleDisplayName, es): return "es";
1063        case offsetof(FFModuleDisplayName, fi): return "fi";
1064        case offsetof(FFModuleDisplayName, fr): return "fr";
1065        case offsetof(FFModuleDisplayName, gl): return "gl";
1066        case offsetof(FFModuleDisplayName, he): return "he";
1067        case offsetof(FFModuleDisplayName, id): return "id";
1068        case offsetof(FFModuleDisplayName, it): return "it";
1069        case offsetof(FFModuleDisplayName, ja): return "ja";
1070        case offsetof(FFModuleDisplayName, ko): return "ko";
1071        case offsetof(FFModuleDisplayName, pl): return "pl";
1072        case offsetof(FFModuleDisplayName, pt): return "pt";
1073        case offsetof(FFModuleDisplayName, ru): return "ru";
1074        case offsetof(FFModuleDisplayName, tr): return "tr";
1075        case offsetof(FFModuleDisplayName, uk): return "uk";
1076        case offsetof(FFModuleDisplayName, vi): return "vi";
1077        case offsetof(FFModuleDisplayName, zh_CN): return "zh_CN";
1078        case offsetof(FFModuleDisplayName, zh_TW): return "zh_TW";
1079    }
1080    return nullptr;
1081}
1082
1083[[gnu::cold]]
1084void ffOptionsGenerateDisplayJsonConfig(FFdata* data, FFOptionsDisplay* options) {
1085    yyjson_mut_doc* doc = data->resultDoc;
1086    yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, doc->root, "display");
1087
1088    if (options->stat <= 0) {
1089        yyjson_mut_obj_add_bool(doc, obj, "stat", options->stat == 0);
1090    } else {
1091        yyjson_mut_obj_add_int(doc, obj, "stat", options->stat);
1092    }
1093
1094    yyjson_mut_obj_add_bool(doc, obj, "pipe", options->pipe);
1095
1096    yyjson_mut_obj_add_bool(doc, obj, "showErrors", options->showErrors);
1097
1098    yyjson_mut_obj_add_bool(doc, obj, "disableLinewrap", options->disableLinewrap);
1099
1100    yyjson_mut_obj_add_bool(doc, obj, "hideCursor", options->hideCursor);
1101
1102    yyjson_mut_obj_add_strbuf(doc, obj, "separator", &options->keyValueSeparator);
1103
1104    {
1105        yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, obj, "color");
1106        yyjson_mut_obj_add_strbuf(doc, color, "keys", &options->colorKeys);
1107        yyjson_mut_obj_add_strbuf(doc, color, "title", &options->colorTitle);
1108        yyjson_mut_obj_add_strbuf(doc, color, "output", &options->colorOutput);
1109        yyjson_mut_obj_add_strbuf(doc, color, "separator", &options->colorSeparator);
1110    }
1111
1112    yyjson_mut_obj_add_bool(doc, obj, "brightColor", options->brightColor);
1113
1114    {
1115        yyjson_mut_val* duration = yyjson_mut_obj_add_obj(doc, obj, "duration");
1116        yyjson_mut_obj_add_bool(doc, duration, "abbreviation", options->durationAbbreviation);
1117        optionAddSpaceBeforeUnit(doc, duration, options->durationSpaceBeforeUnit);
1118    }
1119
1120    {
1121        static const char* const binaryPrefixNames[] = {"iec", "si", "jedec"};
1122        yyjson_mut_val* size = yyjson_mut_obj_add_obj(doc, obj, "size");
1123        yyjson_mut_obj_add_str(doc, size, "maxPrefix", ((const char*[]) {
1124                                                           "B",
1125                                                           "kB",
1126                                                           "MB",
1127                                                           "GB",
1128                                                           "TB",
1129                                                           "PB",
1130                                                           "EB",
1131                                                           "ZB",
1132                                                           "YB",
1133                                                       })[options->sizeMaxPrefix]);
1134        optionAddEnumString(doc, size, "binaryPrefix", binaryPrefixNames, sizeof(binaryPrefixNames) / sizeof(*binaryPrefixNames), options->sizeBinaryPrefix);
1135        yyjson_mut_obj_add_uint(doc, size, "ndigits", options->sizeNdigits);
1136        optionAddSpaceBeforeUnit(doc, size, options->sizeSpaceBeforeUnit);
1137    }
1138
1139    {
1140        static const char* const tempUnitNames[] = {"D", "C", "F", "K"};
1141        yyjson_mut_val* temperature = yyjson_mut_obj_add_obj(doc, obj, "temp");
1142        optionAddEnumString(doc, temperature, "unit", tempUnitNames, sizeof(tempUnitNames) / sizeof(*tempUnitNames), options->tempUnit);
1143        yyjson_mut_obj_add_uint(doc, temperature, "ndigits", options->tempNdigits);
1144        {
1145            yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, temperature, "color");
1146            yyjson_mut_obj_add_strbuf(doc, color, "green", &options->tempColorGreen);
1147            yyjson_mut_obj_add_strbuf(doc, color, "yellow", &options->tempColorYellow);
1148            yyjson_mut_obj_add_strbuf(doc, color, "red", &options->tempColorRed);
1149        }
1150        optionAddSpaceBeforeUnit(doc, temperature, options->tempSpaceBeforeUnit);
1151    }
1152
1153    {
1154        yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, obj, "percent");
1155        {
1156            yyjson_mut_val* type = yyjson_mut_obj_add_arr(doc, percent, "type");
1157            if (options->percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
1158                yyjson_mut_arr_add_str(doc, type, "num");
1159            }
1160            if (options->percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
1161                yyjson_mut_arr_add_str(doc, type, "bar");
1162            }
1163            if (options->percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT) {
1164                yyjson_mut_arr_add_str(doc, type, "hide-others");
1165            }
1166            if (options->percentType & FF_PERCENTAGE_TYPE_NUM_COLOR_BIT) {
1167                yyjson_mut_arr_add_str(doc, type, "num-color");
1168            }
1169            if (options->percentType & FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT) {
1170                yyjson_mut_arr_add_str(doc, type, "bar-monochrome");
1171            }
1172        }
1173        yyjson_mut_obj_add_uint(doc, percent, "ndigits", options->percentNdigits);
1174        {
1175            yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, percent, "color");
1176            yyjson_mut_obj_add_strbuf(doc, color, "green", &options->percentColorGreen);
1177            yyjson_mut_obj_add_strbuf(doc, color, "yellow", &options->percentColorYellow);
1178            yyjson_mut_obj_add_strbuf(doc, color, "red", &options->percentColorRed);
1179        }
1180        optionAddSpaceBeforeUnit(doc, percent, options->percentSpaceBeforeUnit);
1181        yyjson_mut_obj_add_uint(doc, percent, "width", options->percentWidth);
1182    }
1183
1184    {
1185        yyjson_mut_val* bar = yyjson_mut_obj_add_obj(doc, obj, "bar");
1186
1187        yyjson_mut_val* char_ = yyjson_mut_obj_add_obj(doc, bar, "char");
1188        yyjson_mut_obj_add_strbuf(doc, char_, "elapsed", &options->barCharElapsed);
1189        yyjson_mut_obj_add_strbuf(doc, char_, "total", &options->barCharTotal);
1190
1191        yyjson_mut_val* border = yyjson_mut_obj_add_obj(doc, bar, "border");
1192        yyjson_mut_obj_add_strbuf(doc, border, "left", &options->barBorderLeft);
1193        yyjson_mut_obj_add_strbuf(doc, border, "right", &options->barBorderRight);
1194        yyjson_mut_obj_add_strbuf(doc, border, "leftElapsed", &options->barBorderLeftElapsed);
1195        yyjson_mut_obj_add_strbuf(doc, border, "rightElapsed", &options->barBorderRightElapsed);
1196
1197        yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, bar, "color");
1198        yyjson_mut_obj_add_strbuf(doc, color, "elapsed", &options->barColorElapsed);
1199        yyjson_mut_obj_add_strbuf(doc, color, "total", &options->barColorTotal);
1200        yyjson_mut_obj_add_strbuf(doc, color, "border", &options->barColorBorder);
1201
1202        yyjson_mut_obj_add_uint(doc, bar, "width", options->barWidth);
1203    }
1204
1205    {
1206        static const char* const trailingZerosNames[] = {"default", "always", "never"};
1207        yyjson_mut_val* fraction = yyjson_mut_obj_add_obj(doc, obj, "fraction");
1208
1209        if (options->fractionNdigits < 0) {
1210            yyjson_mut_obj_add_null(doc, fraction, "ndigits");
1211        } else {
1212            yyjson_mut_obj_add_uint(doc, fraction, "ndigits", (uint8_t) options->fractionNdigits);
1213        }
1214
1215        optionAddEnumString(doc, fraction, "trailingZeros", trailingZerosNames, sizeof(trailingZerosNames) / sizeof(*trailingZerosNames), options->fractionTrailingZeros);
1216    }
1217
1218    yyjson_mut_obj_add_bool(doc, obj, "noBuffer", options->noBuffer);
1219
1220    {
1221        yyjson_mut_val* key = yyjson_mut_obj_add_obj(doc, obj, "key");
1222        yyjson_mut_obj_add_uint(doc, key, "width", options->keyWidth);
1223        switch ((uint8_t) options->keyType) {
1224            case FF_MODULE_KEY_TYPE_NONE:
1225                yyjson_mut_obj_add_str(doc, key, "type", "none");
1226                break;
1227            case FF_MODULE_KEY_TYPE_STRING:
1228                yyjson_mut_obj_add_str(doc, key, "type", "string");
1229                break;
1230            case FF_MODULE_KEY_TYPE_ICON:
1231                yyjson_mut_obj_add_str(doc, key, "type", "icon");
1232                break;
1233            case FF_MODULE_KEY_TYPE_BOTH_0:
1234                yyjson_mut_obj_add_str(doc, key, "type", "both-0");
1235                break;
1236            case FF_MODULE_KEY_TYPE_BOTH_1: // alias: both
1237                yyjson_mut_obj_add_str(doc, key, "type", "both");
1238                break;
1239            case FF_MODULE_KEY_TYPE_BOTH_2:
1240                yyjson_mut_obj_add_str(doc, key, "type", "both-2");
1241                break;
1242            case FF_MODULE_KEY_TYPE_BOTH_3:
1243                yyjson_mut_obj_add_str(doc, key, "type", "both-3");
1244                break;
1245            case FF_MODULE_KEY_TYPE_BOTH_4:
1246                yyjson_mut_obj_add_str(doc, key, "type", "both-4");
1247                break;
1248        }
1249
1250        yyjson_mut_obj_add_uint(doc, key, "paddingLeft", options->keyPaddingLeft);
1251
1252        const char* language = optionGetLanguageString(options->keyLanguage);
1253        if (language != nullptr) {
1254            yyjson_mut_obj_add_str(doc, key, "language", language);
1255        }
1256    }
1257
1258    {
1259        yyjson_mut_val* freq = yyjson_mut_obj_add_obj(doc, obj, "freq");
1260        yyjson_mut_obj_add_int(doc, freq, "ndigits", options->freqNdigits);
1261        optionAddSpaceBeforeUnit(doc, freq, options->freqSpaceBeforeUnit);
1262    }
1263
1264    {
1265        yyjson_mut_val* constants = yyjson_mut_obj_add_arr(doc, obj, "constants");
1266        FF_LIST_FOR_EACH (FFstrbuf, item, options->constants) {
1267            yyjson_mut_arr_add_strbuf(doc, constants, item);
1268        }
1269    }
1270}