master ohmu/fastfetch-openbsd / src / common / impl / percent.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/debug.h"
  3#include "common/percent.h"
  4#include "common/color.h"
  5#include "common/option.h"
  6#include "common/jsonconfig.h"
  7#include "common/textModifier.h"
  8#include "common/strutil.h"
  9
 10static void appendOutputColor(FFstrbuf* buffer, const FFModuleArgs* module) {
 11    if (module->outputColor.length) {
 12        ffStrbufAppendF(buffer, "\e[%sm", module->outputColor.chars);
 13    } else if (instance.config.display.colorOutput.length) {
 14        ffStrbufAppendF(buffer, "\e[%sm", instance.config.display.colorOutput.chars);
 15    }
 16}
 17
 18const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFlags* result) {
 19    if (yyjson_is_uint(jsonVal)) {
 20        *result = (FFPercentageTypeFlags) yyjson_get_uint(jsonVal);
 21        return nullptr;
 22    }
 23    if (yyjson_is_arr(jsonVal)) {
 24        FFPercentageTypeFlags flags = 0;
 25
 26        yyjson_val* item;
 27        size_t idx, max;
 28        yyjson_arr_foreach (jsonVal, idx, max, item) {
 29            const char* flag = yyjson_get_str(item);
 30            if (!flag) {
 31                FF_DEBUG("A percent.type array entry is of JSON type %s, not a string",
 32                    yyjson_get_type_desc(item));
 33                return "Error: percent.type: invalid flag string";
 34            }
 35            if (ffStrEqualsIgnCase(flag, "num")) {
 36                flags |= FF_PERCENTAGE_TYPE_NUM_BIT;
 37            } else if (ffStrEqualsIgnCase(flag, "bar")) {
 38                flags |= FF_PERCENTAGE_TYPE_BAR_BIT;
 39            } else if (ffStrEqualsIgnCase(flag, "hide-others")) {
 40                flags |= FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT;
 41            } else if (ffStrEqualsIgnCase(flag, "num-color")) {
 42                flags |= FF_PERCENTAGE_TYPE_NUM_COLOR_BIT;
 43            } else if (ffStrEqualsIgnCase(flag, "bar-monochrome")) {
 44                flags |= FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT;
 45            } else {
 46                FF_DEBUG("Unknown percent.type flag \"%s\"", flag);
 47                return "Error: percent.type: unknown flag string";
 48            }
 49        }
 50
 51        *result = flags;
 52        return nullptr;
 53    }
 54
 55    FF_DEBUG("percent.type is of JSON type %s", yyjson_get_type_desc(jsonVal));
 56    return "Error: usage: percent.type must be a number or an array of strings";
 57}
 58
 59void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module) {
 60    uint8_t green = config.green, yellow = config.yellow;
 61    assert(green <= 100 && yellow <= 100);
 62
 63    const FFOptionsDisplay* options = &instance.config.display;
 64
 65    const bool borderAsValue = options->barBorderLeftElapsed.length && options->barBorderRightElapsed.length;
 66
 67    if (!borderAsValue && options->barBorderLeft.length) {
 68        if (!options->pipe && options->barColorBorder.length > 0) {
 69            ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars);
 70        }
 71        ffStrbufAppend(buffer, &options->barBorderLeft);
 72    }
 73
 74    if (percent == -DBL_MAX) {
 75        // Use total color for simplification
 76        if (!options->pipe && options->barColorTotal.length > 0) {
 77            ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m");
 78        }
 79
 80        for (uint8_t i = 0; i < options->barWidth; ++i) {
 81            ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight
 82                                                                                                                                   : &options->barCharTotal);
 83        }
 84    } else {
 85        const char* colorGreen = options->percentColorGreen.chars;
 86        const char* colorYellow = options->percentColorYellow.chars;
 87        const char* colorRed = options->percentColorRed.chars;
 88
 89        FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
 90
 91        uint8_t blocksPercent = 0;
 92        if (percent > 0) {
 93            double blocks = percent / 100.0 * options->barWidth + 0.5;
 94            blocksPercent = (uint8_t) (blocks >= options->barWidth ? options->barWidth : blocks);
 95        }
 96
 97        bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
 98
 99        bool monochrome = (percentType & FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT) || !autoColorElapsed;
100        if (!options->pipe && options->barColorElapsed.length > 0 && monochrome) {
101            const char* color = nullptr;
102            if (!autoColorElapsed) {
103                color = options->barColorElapsed.chars;
104            } else if (green <= yellow) {
105                if (percent < green) {
106                    color = colorGreen;
107                } else if (percent < yellow) {
108                    color = colorYellow;
109                } else {
110                    color = colorRed;
111                }
112            } else {
113                if (percent < yellow) {
114                    color = colorRed;
115                } else if (percent < green) {
116                    color = colorYellow;
117                } else {
118                    color = colorGreen;
119                }
120            }
121            ffStrbufAppendF(buffer, "\e[%sm", color);
122        }
123        for (uint8_t i = 0; i < blocksPercent; ++i) {
124            if (!options->pipe && options->barColorElapsed.length > 0 && !monochrome) {
125                uint32_t section1Begin = (uint32_t) ((green <= yellow ? green : yellow) / 100.0 * options->barWidth + 0.5);
126                uint32_t section2Begin = (uint32_t) ((green > yellow ? green : yellow) / 100.0 * options->barWidth + 0.5);
127                if (i == section2Begin) {
128                    ffStrbufAppendF(buffer, "\e[%sm", (green > yellow ? colorGreen : colorRed));
129                } else if (i == section1Begin) {
130                    ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
131                } else if (i == 0) {
132                    ffStrbufAppendF(buffer, "\e[%sm", (green <= yellow ? colorGreen : colorRed));
133                }
134            }
135            ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeftElapsed : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRightElapsed
136                                                                                                                                          : &options->barCharElapsed);
137        }
138
139        if (blocksPercent < options->barWidth) {
140            if (!options->pipe && options->barColorTotal.length > 0) {
141                ffStrbufAppendF(buffer, "\e[%sm", options->barColorTotal.chars);
142            }
143            for (uint8_t i = blocksPercent; i < options->barWidth; ++i) {
144                ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight
145                                                                                                                                       : &options->barCharTotal);
146            }
147        }
148    }
149
150    if (!borderAsValue && options->barBorderRight.length) {
151        if (!options->pipe && options->barColorBorder.length > 0) {
152            ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars);
153        }
154        ffStrbufAppend(buffer, &options->barBorderRight);
155    }
156
157    if (!options->pipe && (options->barColorElapsed.length > 0 || options->barColorTotal.length > 0 || options->barColorBorder.length > 0)) {
158        ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
159        appendOutputColor(buffer, module);
160    }
161}
162
163void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module) {
164    uint8_t green = config.green, yellow = config.yellow;
165    assert(green <= 100 && yellow <= 100);
166
167    const FFOptionsDisplay* options = &instance.config.display;
168    FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
169
170    bool colored = !!(percentType & FF_PERCENTAGE_TYPE_NUM_COLOR_BIT);
171
172    if (parentheses) {
173        ffStrbufAppendC(buffer, '(');
174    }
175
176    if (colored && !options->pipe) {
177        const char* colorGreen = options->percentColorGreen.chars;
178        const char* colorYellow = options->percentColorYellow.chars;
179        const char* colorRed = options->percentColorRed.chars;
180
181        if (percent == -DBL_MAX) {
182            ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m");
183        } else if (green <= yellow) {
184            if (percent > yellow) {
185                ffStrbufAppendF(buffer, "\e[%sm", colorRed);
186            } else if (percent > green) {
187                ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
188            } else {
189                ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
190            }
191        } else {
192            if (percent < yellow) {
193                ffStrbufAppendF(buffer, "\e[%sm", colorRed);
194            } else if (percent < green) {
195                ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
196            } else {
197                ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
198            }
199        }
200    }
201    if (percent == -DBL_MAX) {
202        ffStrbufAppendS(buffer, "-");
203    } else {
204        ffStrbufAppendF(buffer, "%*.*f%s%%", options->percentWidth, options->percentNdigits, percent, options->percentSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "");
205    }
206
207    if (colored && !options->pipe) {
208        ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
209        appendOutputColor(buffer, module);
210    }
211
212    if (parentheses) {
213        ffStrbufAppendC(buffer, ')');
214    }
215}
216
217bool ffPercentParseCommandOptions(const char* key, const char* subkey, const char* value, FFPercentageModuleConfig* config) {
218    if (!ffStrStartsWithIgnCase(subkey, "percent-")) {
219        return false;
220    }
221
222    subkey += strlen("percent-");
223
224    if (ffStrEqualsIgnCase(subkey, "green")) {
225        uint32_t num = ffOptionParseUInt32(key, value);
226        if (num > 100) {
227            fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
228            exit(480);
229        }
230        config->green = (uint8_t) num;
231        return true;
232    }
233
234    if (ffStrEqualsIgnCase(subkey, "yellow")) {
235        uint32_t num = ffOptionParseUInt32(key, value);
236        if (num > 100) {
237            fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
238            exit(480);
239        }
240        config->yellow = (uint8_t) num;
241        return true;
242    }
243
244    if (ffStrEqualsIgnCase(subkey, "type")) {
245        config->type = (FFPercentageTypeFlags) ffOptionParseUInt32(key, value);
246        return true;
247    }
248
249    return false;
250}
251
252bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageModuleConfig* config) {
253    assert(key);
254
255    if (!unsafe_yyjson_equals_str(key, "percent")) {
256        return false;
257    }
258
259    if (!yyjson_is_obj(value)) {
260        fprintf(stderr, "Error: usage: %s must be an object\n", unsafe_yyjson_get_str(key));
261        exit(480);
262    }
263
264    yyjson_val* greenVal = yyjson_obj_get(value, "green");
265    if (greenVal) {
266        int num = yyjson_get_int(greenVal);
267        if (num < 0 || num > 100) {
268            fputs("Error: usage: percent.green must be between 0 and 100\n", stderr);
269            exit(480);
270        }
271        config->green = (uint8_t) num;
272    }
273
274    yyjson_val* yellowVal = yyjson_obj_get(value, "yellow");
275    if (yellowVal) {
276        int num = yyjson_get_int(yellowVal);
277        if (num < 0 || num > 100) {
278            fputs("Error: usage: percent.yellow must be between 0 and 100\n", stderr);
279            exit(480);
280        }
281        config->yellow = (uint8_t) num;
282    }
283
284    yyjson_val* typeVal = yyjson_obj_get(value, "type");
285    if (typeVal) {
286        const char* error = ffPercentParseTypeJsonConfig(typeVal, &config->type);
287        if (error) {
288            fputs(error, stderr);
289            exit(480);
290        }
291    }
292
293    return true;
294}
295
296[[gnu::cold]]
297void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config) {
298    yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, module, "percent");
299    yyjson_mut_obj_add_uint(doc, percent, "green", config.green);
300    yyjson_mut_obj_add_uint(doc, percent, "yellow", config.yellow);
301    yyjson_mut_obj_add_uint(doc, percent, "type", config.type);
302}