master ohmu/fastfetch-openbsd / src / common / impl / genconfig.c
ohmu  ·  2026-10-15
   1#include "common/genconfig.h"
   2
   3#include "fastfetch.h"
   4
   5#include "common/color.h"
   6#include "common/io.h"
   7#include "common/strutil.h"
   8#include "common/textModifier.h"
   9#include "detection/terminalsize/terminalsize.h"
  10#include "modules/modules.h"
  11
  12#include <stdio.h>
  13#include <stdlib.h>
  14#include <string.h>
  15
  16#ifndef _WIN32
  17    #include <termios.h>
  18    #include <poll.h>
  19    #include <signal.h>
  20    #include <unistd.h>
  21#else
  22    #include <windows.h>
  23#endif
  24
  25// Layout (rows):
  26//  0: title
  27//  1: blank
  28//  2: logo type row
  29//  3: logo position row
  30//  4: output mode row
  31//  5: blank
  32//  6: modules title row
  33//  7: blank
  34//  8..8+listRows-1: module grid
  35//  8+listRows: description row
  36//  9+listRows: help line 1
  37// 10+listRows: help line 2
  38#define FF_GEN_CONFIG_LIST_TOP 8
  39#define FF_GEN_CONFIG_BOTTOM_CHROME 3
  40
  41// Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen
  42// redraws (flicker); terminal size changes redraw immediately via SIGWINCH /
  43// console window-size events.
  44#define FF_GEN_CONFIG_POLL_TIMEOUT 1000
  45
  46typedef enum : uint8_t {
  47    FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED = false,
  48    FF_GEN_CONFIG_ITEM_STATUS_SELECTED = true,
  49    FF_GEN_CONFIG_ITEM_STATUS_SPECIAL = 2,
  50} FFGenConfigItemStatus;
  51
  52typedef struct FFGenConfigItem {
  53    FFModuleBaseInfo* baseInfo;
  54    FFGenConfigItemStatus status;
  55} FFGenConfigItem;
  56
  57typedef struct FFGenConfigLayout {
  58    uint16_t listLeft;
  59    uint16_t listRows;
  60    uint32_t colWidth;
  61    uint32_t columns;
  62    uint32_t itemsPerColumn;
  63} FFGenConfigLayout;
  64
  65typedef struct FFGenConfigUI {
  66    FFlist items; // FFGenConfigItem
  67    uint32_t cursor;
  68    uint32_t viewOffset;
  69    FFLogoType logoType;
  70    FFLogoPosition logoPosition;
  71    bool fullConfig;
  72    bool confirmingOverwrite;
  73    uint16_t rows;
  74    uint16_t cols;
  75    FFGenConfigLayout layout;
  76} FFGenConfigUI;
  77
  78typedef enum : uint8_t {
  79    FF_GEN_KEY_UP,
  80    FF_GEN_KEY_DOWN,
  81    FF_GEN_KEY_LEFT,
  82    FF_GEN_KEY_RIGHT,
  83    FF_GEN_KEY_ENTER,
  84    FF_GEN_KEY_ESCAPE,
  85    FF_GEN_KEY_CHAR,
  86    FF_GEN_KEY_RESIZE,
  87    FF_GEN_KEY_UNKNOWN,
  88} FFGenConfigKey;
  89
  90#ifndef _WIN32
  91static struct termios gOriginalTermios;
  92static bool gRawModeActive = false;
  93static volatile sig_atomic_t gWindowResized = 0;
  94
  95static void onWindowResize(int sig) {
  96    (void) sig;
  97    gWindowResized = 1;
  98}
  99
 100static void restoreRawMode(void) {
 101    if (gRawModeActive) {
 102        tcsetattr(STDIN_FILENO, TCSAFLUSH, &gOriginalTermios);
 103        gRawModeActive = false;
 104    }
 105}
 106
 107static void enterRawMode(void) {
 108    if (gRawModeActive) {
 109        return;
 110    }
 111    if (tcgetattr(STDIN_FILENO, &gOriginalTermios) != 0) {
 112        return;
 113    }
 114    struct termios raw = gOriginalTermios;
 115    raw.c_lflag &= (tcflag_t) ~(ICANON | ECHO | ISIG | IEXTEN);
 116    raw.c_iflag &= (tcflag_t) ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
 117    raw.c_cflag |= CS8;
 118    raw.c_oflag &= (tcflag_t) ~OPOST;
 119    raw.c_cc[VMIN] = 0;
 120    raw.c_cc[VTIME] = 1;
 121    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) {
 122        return;
 123    }
 124    gRawModeActive = true;
 125
 126    struct sigaction sa = { .sa_handler = onWindowResize };
 127    sigemptyset(&sa.sa_mask);
 128    sigaction(SIGWINCH, &sa, nullptr);
 129}
 130#else
 131static DWORD gOriginalInputMode;
 132static DWORD gOriginalOutputMode;
 133static bool gRawModeActive = false;
 134
 135static void restoreRawMode(void) {
 136    if (gRawModeActive) {
 137        SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), gOriginalInputMode);
 138        SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), gOriginalOutputMode);
 139        gRawModeActive = false;
 140    }
 141}
 142
 143static void enterRawMode(void) {
 144    if (gRawModeActive) {
 145        return;
 146    }
 147    HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
 148    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 149
 150    if (GetConsoleMode(hInput, &gOriginalInputMode)) {
 151        DWORD newMode = gOriginalInputMode;
 152        newMode &= ~(DWORD) (ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
 153        newMode |= ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT;
 154        SetConsoleMode(hInput, newMode);
 155    }
 156
 157    if (GetConsoleMode(hOutput, &gOriginalOutputMode)) {
 158        DWORD newMode = gOriginalOutputMode;
 159        newMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
 160        SetConsoleMode(hOutput, newMode);
 161    }
 162
 163    FlushConsoleInputBuffer(hInput);
 164    gRawModeActive = true;
 165}
 166#endif
 167
 168static void getTerminalSize(uint16_t* rows, uint16_t* cols) {
 169    FFTerminalSizeResult size;
 170    if (ffDetectTerminalSize(&size, true) && size.rows > 0 && size.columns > 0) {
 171        *rows = size.rows;
 172        *cols = size.columns;
 173    } else {
 174        *rows = 24;
 175        *cols = 80;
 176    }
 177}
 178
 179static int compareModuleInfo(const void* a, const void* b) {
 180    const FFModuleBaseInfo* const* ma = (const FFModuleBaseInfo* const*) a;
 181    const FFModuleBaseInfo* const* mb = (const FFModuleBaseInfo* const*) b;
 182    if ((*ma)->defaultOrder != (*mb)->defaultOrder) {
 183        return (int) (*ma)->defaultOrder - (int) (*mb)->defaultOrder;
 184    }
 185    return strcmp((*ma)->name, (*mb)->name);
 186}
 187
 188static void collectModuleInfos(FFlist* modules) {
 189    ffListInitA(modules, sizeof(FFModuleBaseInfo*), 64);
 190    for (uint32_t i = 0; i <= 'Z' - 'A'; ++i) {
 191        for (FFModuleBaseInfo** it = ffModuleInfos[i]; *it; ++it) {
 192            if ((*it)->defaultOrder == 0) {
 193                continue;
 194            }
 195            *(FFModuleBaseInfo**) ffListAdd(modules, sizeof(FFModuleBaseInfo*)) = *it;
 196        }
 197    }
 198    ffListSort(modules, sizeof(FFModuleBaseInfo*), compareModuleInfo);
 199}
 200
 201static void initItems(FFlist* items, const FFlist* modules, FFdata* data) {
 202    ffListInitA(items, sizeof(FFGenConfigItem), modules->length + 8);
 203    FF_LIST_FOR_EACH (FFModuleBaseInfo*, info, *modules) {
 204        FFGenConfigItem* item = FF_LIST_ADD(FFGenConfigItem, *items);
 205        item->baseInfo = *info;
 206        if (*info == &ffBreakModuleInfo || *info == &ffSeparatorModuleInfo) {
 207            item->status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL;
 208        } else {
 209            item->status = ffStrbufSeparatedContainIgnCaseS(&data->structure, (*info)->name, ':') &&
 210                    !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, (*info)->name, ':')
 211                ? FF_GEN_CONFIG_ITEM_STATUS_SELECTED
 212                : FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED;
 213        }
 214    }
 215}
 216
 217static void moveItemDown(FFGenConfigUI* ui, uint32_t idx) {
 218    if (idx + 1 >= ui->items.length) {
 219        return;
 220    }
 221    FFGenConfigItem* items = (FFGenConfigItem*) ui->items.data;
 222    FFGenConfigItem tmp = items[idx];
 223    items[idx] = items[idx + 1];
 224    items[idx + 1] = tmp;
 225    ui->cursor = idx + 1;
 226}
 227
 228static void moveItemUp(FFGenConfigUI* ui, uint32_t idx) {
 229    if (idx == 0) {
 230        return;
 231    }
 232    FFGenConfigItem* items = (FFGenConfigItem*) ui->items.data;
 233    FFGenConfigItem tmp = items[idx];
 234    items[idx] = items[idx - 1];
 235    items[idx - 1] = tmp;
 236    ui->cursor = idx - 1;
 237}
 238
 239static void removeAllBreaksSeparators(FFGenConfigUI* ui) {
 240    for (uint32_t i = ui->items.length; i-- > 0;) {
 241        FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, i);
 242        if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 243            FF_LIST_REMOVE_AT(FFGenConfigItem, ui->items, i);
 244        }
 245    }
 246    if (ui->cursor >= ui->items.length && ui->items.length > 0) {
 247        ui->cursor = ui->items.length - 1;
 248    }
 249}
 250
 251static void computeLayout(FFGenConfigUI* ui) {
 252    FFGenConfigLayout* layout = &ui->layout;
 253    if (ui->rows < 11) {
 254        ui->rows = 11;
 255    }
 256    int32_t listRows = (int32_t) ui->rows - FF_GEN_CONFIG_LIST_TOP - FF_GEN_CONFIG_BOTTOM_CHROME;
 257    layout->listRows = listRows < 0 ? 0 : (uint16_t) listRows;
 258    layout->listLeft = 2;
 259
 260    uint32_t maxContentWidth = 0;
 261    FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) {
 262        uint32_t w = (uint32_t) strlen(item->baseInfo->name) + 4; // "[x] " / "[─] " prefix
 263        if (w > maxContentWidth) {
 264            maxContentWidth = w;
 265        }
 266    }
 267    layout->colWidth = maxContentWidth + 2;
 268    if (layout->colWidth < 1) {
 269        layout->colWidth = 1;
 270    }
 271
 272    uint32_t gridWidth = ui->cols > 4 ? (uint32_t) ui->cols - 4 : 1;
 273    uint32_t maxColumns = gridWidth / layout->colWidth;
 274    if (maxColumns < 1) {
 275        maxColumns = 1;
 276    }
 277
 278    uint32_t length = ui->items.length;
 279    if (length == 0 || layout->listRows == 0) {
 280        layout->columns = 1;
 281        layout->itemsPerColumn = length > 0 ? length : 1;
 282        return;
 283    }
 284    uint32_t neededColumns = (length + layout->listRows - 1) / layout->listRows;
 285    if (neededColumns < 1) {
 286        neededColumns = 1;
 287    }
 288    layout->columns = neededColumns < maxColumns ? neededColumns : maxColumns;
 289    layout->itemsPerColumn = (length + layout->columns - 1) / layout->columns;
 290}
 291
 292static void recomputeView(FFGenConfigUI* ui) {
 293    const FFGenConfigLayout* layout = &ui->layout;
 294    uint32_t length = ui->items.length;
 295    if (length == 0) {
 296        ui->cursor = 0;
 297        ui->viewOffset = 0;
 298        return;
 299    }
 300    if (ui->cursor >= length) {
 301        ui->cursor = length - 1;
 302    }
 303
 304    uint32_t rowsPerColumn = layout->itemsPerColumn;
 305    if (rowsPerColumn <= layout->listRows) {
 306        ui->viewOffset = 0;
 307        return;
 308    }
 309
 310    uint32_t cursorRow = ui->cursor % rowsPerColumn;
 311    uint32_t pageTop = ui->viewOffset;
 312    if (cursorRow < pageTop) {
 313        pageTop = cursorRow;
 314    } else if (cursorRow >= pageTop + layout->listRows) {
 315        pageTop = cursorRow - layout->listRows + 1;
 316    }
 317    uint32_t maxTop = rowsPerColumn - layout->listRows;
 318    if (pageTop > maxTop) {
 319        pageTop = maxTop;
 320    }
 321    ui->viewOffset = pageTop;
 322}
 323
 324static void toggleItem(FFGenConfigUI* ui, uint32_t idx) {
 325    FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, idx);
 326    if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 327        return;
 328    }
 329    item->status = !item->status;
 330}
 331
 332static void selectAllModules(FFGenConfigUI* ui) {
 333    FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) {
 334        if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 335            item->status = FF_GEN_CONFIG_ITEM_STATUS_SELECTED;
 336        }
 337    }
 338}
 339
 340static void invertAllModules(FFGenConfigUI* ui) {
 341    FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) {
 342        if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 343            item->status = !item->status;
 344        }
 345    }
 346}
 347
 348static void addBreakBelow(FFGenConfigUI* ui, uint32_t idx) {
 349    FFGenConfigItem br = { .baseInfo = &ffBreakModuleInfo, .status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL };
 350    FF_LIST_INSERT_AT(FFGenConfigItem, ui->items, idx + 1, &br);
 351}
 352
 353static void addSeparatorBelow(FFGenConfigUI* ui, uint32_t idx) {
 354    FFGenConfigItem sep = { .baseInfo = &ffSeparatorModuleInfo, .status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL };
 355    FF_LIST_INSERT_AT(FFGenConfigItem, ui->items, idx + 1, &sep);
 356}
 357
 358static void cycleLogoType(FFGenConfigUI* ui, int delta) {
 359    switch (ui->logoType) {
 360        case FF_LOGO_TYPE_AUTO:
 361            ui->logoType = delta > 0 ? FF_LOGO_TYPE_SMALL : FF_LOGO_TYPE_NONE;
 362            break;
 363        case FF_LOGO_TYPE_SMALL:
 364            ui->logoType = delta > 0 ? FF_LOGO_TYPE_NONE : FF_LOGO_TYPE_AUTO;
 365            break;
 366        default:
 367            ui->logoType = delta > 0 ? FF_LOGO_TYPE_AUTO : FF_LOGO_TYPE_SMALL;
 368            break;
 369    }
 370}
 371
 372static void cycleLogoPosition(FFGenConfigUI* ui, int delta) {
 373    static const FFLogoPosition positions[] = {
 374        FF_LOGO_POSITION_AUTO,
 375        FF_LOGO_POSITION_LEFT,
 376        FF_LOGO_POSITION_RIGHT,
 377        FF_LOGO_POSITION_TOP,
 378    };
 379
 380    uint32_t index = 0;
 381    for (uint32_t i = 0; i < ARRAY_SIZE(positions); ++i) {
 382        if (positions[i] == ui->logoPosition) {
 383            index = i;
 384            break;
 385        }
 386    }
 387
 388    index = (index + (delta > 0 ? 1 : ARRAY_SIZE(positions) - 1)) % ARRAY_SIZE(positions);
 389    ui->logoPosition = positions[index];
 390}
 391
 392static uint32_t countSelectedModules(const FFlist* items) {
 393    uint32_t count = 0;
 394    FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
 395        if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SELECTED) {
 396            ++count;
 397        }
 398    }
 399    return count;
 400}
 401
 402static uint32_t countModules(const FFlist* items) {
 403    uint32_t count = 0;
 404    FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
 405        if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 406            ++count;
 407        }
 408    }
 409    return count;
 410}
 411
 412// ---- Row builder -----------------------------------------------------------
 413
 414typedef struct FFRow {
 415    FFstrbuf buf;
 416    uint32_t visualCol;
 417    uint16_t cols;
 418} FFRow;
 419
 420static void rowInit(FFRow* row, uint16_t cols) {
 421    ffStrbufInitA(&row->buf, (uint32_t) cols + 16);
 422    row->visualCol = 0;
 423    row->cols = cols;
 424}
 425
 426static void rowAppendRaw(FFRow* row, const char* s) {
 427    ffStrbufAppendS(&row->buf, s);
 428}
 429
 430static void rowAppendVisual(FFRow* row, const char* s) {
 431    uint32_t len = (uint32_t) strlen(s);
 432    ffStrbufAppendNS(&row->buf, len, s);
 433    row->visualCol += ffUtf8StrWidth(s, len);
 434}
 435
 436static void rowPadVisual(FFRow* row, uint32_t col) {
 437    if (row->visualCol < col) {
 438        ffStrbufAppendNC(&row->buf, col - row->visualCol, ' ');
 439        row->visualCol = col;
 440    }
 441}
 442
 443static void finishRow(FFRow* row, FFstrbuf* out, bool isLastRow) {
 444    if (row->visualCol < row->cols) {
 445        rowPadVisual(row, row->cols);
 446    }
 447    ffStrbufAppend(out, &row->buf);
 448    ffStrbufAppendS(out, "\e[K");
 449    if (!isLastRow) {
 450        ffStrbufAppendS(out, "\r\n");
 451    }
 452    ffStrbufDestroy(&row->buf);
 453}
 454
 455static void drawModuleItem(FFRow* row, const FFGenConfigItem* item, bool highlight) {
 456    if (item->baseInfo == &ffBreakModuleInfo) {
 457        rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_MAGENTA "m" : "\e[" FF_COLOR_FG_MAGENTA "m");
 458        ffStrbufAppendS(&row->buf, "[-]");
 459        row->visualCol += 3;
 460        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 461        rowAppendVisual(row, " ");
 462        rowAppendRaw(row, "\e[" FF_COLOR_FG_MAGENTA "m");
 463        rowAppendVisual(row, item->baseInfo->name);
 464        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 465        return;
 466    }
 467    if (item->baseInfo == &ffSeparatorModuleInfo) {
 468        rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_CYAN "m" : "\e[" FF_COLOR_FG_CYAN "m");
 469        ffStrbufAppendS(&row->buf, "[-]");
 470        row->visualCol += 3;
 471        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 472        rowAppendVisual(row, " ");
 473        rowAppendRaw(row, "\e[" FF_COLOR_FG_CYAN "m");
 474        rowAppendVisual(row, item->baseInfo->name);
 475        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 476        return;
 477    }
 478
 479    if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SELECTED) {
 480        rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_GREEN "m" : "\e[" FF_COLOR_FG_GREEN "m");
 481        ffStrbufAppendS(&row->buf, "[x]");
 482        row->visualCol += 3;
 483        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 484        rowAppendVisual(row, " ");
 485        rowAppendRaw(row, "\e[" FF_COLOR_FG_GREEN "m");
 486        rowAppendVisual(row, item->baseInfo->name);
 487        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 488    } else {
 489        if (highlight) {
 490            rowAppendRaw(row, "\e[" FF_COLOR_MODE_INVERSE_ "m");
 491        }
 492        ffStrbufAppendS(&row->buf, "[ ]");
 493        row->visualCol += 3;
 494        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 495        rowAppendVisual(row, " ");
 496        rowAppendVisual(row, item->baseInfo->name);
 497        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 498    }
 499}
 500
 501static void drawItemCell(FFRow* row, uint32_t startCol, uint32_t cellWidth, const FFGenConfigItem* item, bool highlight) {
 502    rowPadVisual(row, startCol);
 503    drawModuleItem(row, item, highlight);
 504    rowPadVisual(row, startCol + cellWidth);
 505}
 506
 507static void drawLogoOption(FFRow* row, const char* name, bool active) {
 508    if (active) {
 509        rowAppendRaw(row, "\e[" FF_COLOR_FG_GREEN "m");
 510        rowAppendVisual(row, "[x] ");
 511        rowAppendVisual(row, name);
 512        rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET);
 513    } else {
 514        rowAppendVisual(row, "[ ] ");
 515        rowAppendVisual(row, name);
 516    }
 517}
 518
 519static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
 520    getTerminalSize(&ui->rows, &ui->cols);
 521    computeLayout(ui);
 522    recomputeView(ui);
 523
 524    const FFGenConfigLayout* layout = &ui->layout;
 525    const uint16_t cols = ui->cols;
 526    const uint32_t selectedCount = countSelectedModules(&ui->items);
 527    const uint32_t moduleCount = countModules(&ui->items);
 528
 529    ffStrbufAppendS(out, "\e[?25l\e[H");
 530    uint32_t rowCount = 0;
 531
 532    FFRow row;
 533
 534    // Row 0: title
 535    rowInit(&row, cols);
 536    rowAppendVisual(&row, "  ");
 537    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
 538    rowAppendVisual(&row, "fastfetch");
 539    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 540    rowAppendVisual(&row, " configuration");
 541    if (cols >= 48) {
 542        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
 543        rowAppendVisual(&row, "    interactive config generator");
 544        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 545    }
 546    finishRow(&row, out, ++rowCount == ui->rows);
 547
 548    // Row 1: blank
 549    rowInit(&row, cols);
 550    finishRow(&row, out, ++rowCount == ui->rows);
 551
 552    // Row 2: logo type
 553    rowInit(&row, cols);
 554    rowAppendVisual(&row, "  ");
 555    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
 556    rowAppendVisual(&row, "L");
 557    rowAppendRaw(&row, "\e[24m"); // Reset underline
 558    rowAppendVisual(&row, "ogo type:");
 559    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 560    rowAppendVisual(&row, "  ");
 561    drawLogoOption(&row, "default", ui->logoType == FF_LOGO_TYPE_AUTO);
 562    rowAppendVisual(&row, "  ");
 563    drawLogoOption(&row, "small", ui->logoType == FF_LOGO_TYPE_SMALL);
 564    rowAppendVisual(&row, "  ");
 565    drawLogoOption(&row, "none", ui->logoType == FF_LOGO_TYPE_NONE);
 566    if (cols >= 72) {
 567        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
 568        switch (ui->logoType) {
 569            case FF_LOGO_TYPE_AUTO:
 570                rowAppendVisual(&row, " - Built-in ASCII art");
 571                break;
 572            case FF_LOGO_TYPE_SMALL:
 573                rowAppendVisual(&row, " - Built-in ASCII art, small version");
 574                break;
 575            default:
 576                rowAppendVisual(&row, " - Disable logo printing");
 577                break;
 578        }
 579        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 580    }
 581    finishRow(&row, out, ++rowCount == ui->rows);
 582
 583    // Row 3: logo position
 584    rowInit(&row, cols);
 585    rowAppendVisual(&row, "  ");
 586    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD "m");
 587    rowAppendVisual(&row, "Logo ");
 588    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_UNDERLINE_ "m");
 589    rowAppendVisual(&row, "p");
 590    rowAppendRaw(&row, "\e[24m"); // Reset underline
 591    rowAppendVisual(&row, "osition:");
 592    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 593    rowAppendVisual(&row, "  ");
 594    drawLogoOption(&row, "auto", ui->logoPosition == FF_LOGO_POSITION_AUTO);
 595    rowAppendVisual(&row, "  ");
 596    drawLogoOption(&row, "left", ui->logoPosition == FF_LOGO_POSITION_LEFT);
 597    rowAppendVisual(&row, "  ");
 598    drawLogoOption(&row, "right", ui->logoPosition == FF_LOGO_POSITION_RIGHT);
 599    rowAppendVisual(&row, "  ");
 600    drawLogoOption(&row, "top", ui->logoPosition == FF_LOGO_POSITION_TOP);
 601    if (cols >= 120) {
 602        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
 603        switch (ui->logoPosition) {
 604            case FF_LOGO_POSITION_AUTO:
 605                rowAppendVisual(&row, " - Choose left or top from terminal width");
 606                break;
 607            case FF_LOGO_POSITION_RIGHT:
 608                rowAppendVisual(&row, " - Place the logo to the right");
 609                break;
 610            case FF_LOGO_POSITION_TOP:
 611                rowAppendVisual(&row, " - Place the logo above module output");
 612                break;
 613            case FF_LOGO_POSITION_LEFT:
 614            default:
 615                rowAppendVisual(&row, " - Place the logo to the left");
 616                break;
 617        }
 618        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 619    }
 620    finishRow(&row, out, ++rowCount == ui->rows);
 621
 622    // Row 4: output mode
 623    rowInit(&row, cols);
 624    rowAppendVisual(&row, "  ");
 625    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
 626    rowAppendVisual(&row, "O");
 627    rowAppendRaw(&row, "\e[24m"); // Reset underline
 628    rowAppendVisual(&row, "utput:");
 629    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 630    rowAppendVisual(&row, "  ");
 631    drawLogoOption(&row, "minimal", !ui->fullConfig);
 632    rowAppendVisual(&row, "  ");
 633    drawLogoOption(&row, "full", ui->fullConfig);
 634    if (cols >= 64) {
 635        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
 636        if (ui->fullConfig) {
 637            rowAppendVisual(&row, " - Full JSON config with all module options");
 638        } else {
 639            rowAppendVisual(&row, " - Minimal JSON config");
 640        }
 641        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 642    }
 643    finishRow(&row, out, ++rowCount == ui->rows);
 644
 645    // Row 5: blank
 646    rowInit(&row, cols);
 647    finishRow(&row, out, ++rowCount == ui->rows);
 648
 649    // Row 6: modules title
 650    rowInit(&row, cols);
 651    rowAppendVisual(&row, "  ");
 652    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
 653    rowAppendVisual(&row, "Modules:");
 654    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET "\e[" FF_COLOR_FG_GREEN "m");
 655    FF_STRBUF_AUTO_DESTROY counter = ffStrbufCreateF("  [%u/%u selected]", selectedCount, moduleCount);
 656    rowAppendVisual(&row, counter.chars);
 657    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 658    finishRow(&row, out, ++rowCount == ui->rows);
 659
 660    // Row 7: blank
 661    rowInit(&row, cols);
 662    finishRow(&row, out, ++rowCount == ui->rows);
 663
 664    // Grid rows
 665    const uint32_t rowsPerColumn = layout->itemsPerColumn;
 666    const uint32_t length = ui->items.length;
 667    for (uint16_t screenRow = 0; screenRow < layout->listRows; ++screenRow) {
 668        rowInit(&row, cols);
 669        for (uint32_t c = 0; c < layout->columns; ++c) {
 670            uint32_t startCol = layout->listLeft + c * layout->colWidth;
 671            uint32_t index = (ui->viewOffset + screenRow) + c * rowsPerColumn;
 672            if (screenRow < rowsPerColumn && index < length) {
 673                const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, index);
 674                drawItemCell(&row, startCol, layout->colWidth, item, ui->cursor == index);
 675            } else {
 676                rowPadVisual(&row, startCol + layout->colWidth);
 677            }
 678        }
 679        finishRow(&row, out, ++rowCount == ui->rows);
 680    }
 681
 682    // Row 9+listRows: description
 683    rowInit(&row, cols);
 684    if (ui->cursor < ui->items.length) {
 685        const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
 686        if (item->baseInfo && item->baseInfo->description) {
 687            rowAppendVisual(&row, "  ");
 688            rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
 689            rowAppendVisual(&row, item->baseInfo->description);
 690            rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 691        }
 692    }
 693    finishRow(&row, out, ++rowCount == ui->rows);
 694
 695    // Help lines
 696    rowInit(&row, cols);
 697    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
 698    rowAppendVisual(&row, "  "
 699#if _WIN32 || __APPLE__ || __linux__ || (__FreeBSD__ && !__DragonFly__)
 700                          "↑/↓ k/j move  ←/→ col"
 701#else
 702                          "^/v k/j move  </> col"
 703#endif
 704                          "  Space toggle  f/F all/invert  K/J reorder  b/B break/sep  d/D del");
 705    if (ui->confirmingOverwrite) {
 706        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET "\e[" FF_COLOR_MODE_BOLD FF_COLOR_FG_YELLOW "m");
 707        rowAppendVisual(&row, "  File exists. Overwrite? (y/N)");
 708        rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 709    }
 710    finishRow(&row, out, ++rowCount == ui->rows);
 711
 712    rowInit(&row, cols);
 713    rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
 714    rowAppendVisual(&row, "  l/L logo  p/P position  o minimal/full  s/Enter save  q/Esc quit  g/G top/bottom");
 715    rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
 716    finishRow(&row, out, ++rowCount == ui->rows);
 717
 718    ffStrbufAppendS(out, "\e[J");
 719}
 720
 721// ---- Key input -------------------------------------------------------------
 722
 723static FFGenConfigKey readKey(char* outChar) {
 724#ifndef _WIN32
 725    if (gWindowResized) {
 726        gWindowResized = 0;
 727        return FF_GEN_KEY_RESIZE;
 728    }
 729    struct pollfd pfd = {
 730        .fd = STDIN_FILENO,
 731        .events = POLLIN,
 732    };
 733    if (poll(&pfd, 1, FF_GEN_CONFIG_POLL_TIMEOUT) <= 0) {
 734        if (gWindowResized) {
 735            gWindowResized = 0;
 736            return FF_GEN_KEY_RESIZE;
 737        }
 738        return FF_GEN_KEY_UNKNOWN;
 739    }
 740#else
 741    HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
 742    if (WaitForSingleObject(hInput, FF_GEN_CONFIG_POLL_TIMEOUT) != WAIT_OBJECT_0) {
 743        return FF_GEN_KEY_UNKNOWN;
 744    }
 745    DWORD numEvents = 0;
 746    if (GetNumberOfConsoleInputEvents(hInput, &numEvents) && numEvents > 0) {
 747        bool consumed = false;
 748        INPUT_RECORD record;
 749        while (numEvents > 0) {
 750            DWORD n = 0;
 751            if (!PeekConsoleInputW(hInput, &record, 1, &n) || n == 0) {
 752                break;
 753            }
 754            if (record.EventType == KEY_EVENT) {
 755                break; // keep key events for ffReadFDData
 756            }
 757            if (!ReadConsoleInputW(hInput, &record, 1, &n) || n == 0) {
 758                break;
 759            }
 760            consumed = true;
 761            --numEvents;
 762        }
 763        if (consumed) {
 764            return FF_GEN_KEY_RESIZE;
 765        }
 766    }
 767#endif
 768
 769    char buf[16];
 770    ssize_t n = ffReadFDData(FFUnixFD2NativeFD(STDIN_FILENO), sizeof(buf), buf);
 771    if (n <= 0) {
 772        return FF_GEN_KEY_UNKNOWN;
 773    }
 774    if (buf[0] == '\r' || buf[0] == '\n') {
 775        return FF_GEN_KEY_ENTER;
 776    }
 777    if (buf[0] != '\x1b') {
 778        *outChar = buf[0];
 779        return FF_GEN_KEY_CHAR;
 780    }
 781    if (n == 1) {
 782        return FF_GEN_KEY_ESCAPE;
 783    }
 784
 785    if (buf[1] == '[' || buf[1] == 'O') {
 786        size_t i = 2;
 787        if (i < (size_t) n && buf[i] >= 'A' && buf[i] <= 'D') {
 788            switch (buf[i]) {
 789                case 'A':
 790                    return FF_GEN_KEY_UP;
 791                case 'B':
 792                    return FF_GEN_KEY_DOWN;
 793                case 'C':
 794                    return FF_GEN_KEY_RIGHT;
 795                case 'D':
 796                    return FF_GEN_KEY_LEFT;
 797            }
 798        }
 799        uint32_t param = 0;
 800        while (i < (size_t) n && buf[i] >= '0' && buf[i] <= '9') {
 801            param = param * 10 + (uint32_t) (buf[i] - '0');
 802            ++i;
 803        }
 804        while (i < (size_t) n && buf[i] == ';') {
 805            ++i;
 806            while (i < (size_t) n && buf[i] >= '0' && buf[i] <= '9') {
 807                ++i;
 808            }
 809        }
 810        if (i < (size_t) n) {
 811            switch (buf[i]) {
 812                case 'A':
 813                    return FF_GEN_KEY_UP;
 814                case 'B':
 815                    return FF_GEN_KEY_DOWN;
 816                case 'C':
 817                    return FF_GEN_KEY_RIGHT;
 818                case 'D':
 819                    return FF_GEN_KEY_LEFT;
 820            }
 821        }
 822        return FF_GEN_KEY_UNKNOWN;
 823    }
 824
 825    return FF_GEN_KEY_ESCAPE;
 826}
 827
 828static void moveCursorLeft(FFGenConfigUI* ui) {
 829    uint32_t rowsPerColumn = ui->layout.itemsPerColumn;
 830    if (rowsPerColumn == 0) {
 831        return;
 832    }
 833    if (ui->cursor >= rowsPerColumn) {
 834        ui->cursor -= rowsPerColumn;
 835    }
 836}
 837
 838static void moveCursorRight(FFGenConfigUI* ui) {
 839    uint32_t rowsPerColumn = ui->layout.itemsPerColumn;
 840    if (rowsPerColumn == 0) {
 841        return;
 842    }
 843    uint32_t newCursor = ui->cursor + rowsPerColumn;
 844    if (newCursor < ui->items.length) {
 845        ui->cursor = newCursor;
 846    }
 847}
 848
 849static void moveCursorUp(FFGenConfigUI* ui) {
 850    if (ui->cursor > 0) {
 851        --ui->cursor;
 852    } else {
 853        ui->cursor = ui->items.length > 0 ? ui->items.length - 1 : 0;
 854    }
 855}
 856
 857static void moveCursorDown(FFGenConfigUI* ui) {
 858    if (ui->items.length == 0) {
 859        return;
 860    }
 861    if (ui->cursor + 1 < ui->items.length) {
 862        ++ui->cursor;
 863    } else {
 864        ui->cursor = 0;
 865    }
 866}
 867
 868// Returns 1 on save, 0 on quit, -1 on continue
 869static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileExists) {
 870    if (key == FF_GEN_KEY_UNKNOWN) {
 871        return -1;
 872    }
 873    if (key == FF_GEN_KEY_RESIZE) {
 874        return -1; // Terminal resized: redraw with new size
 875    }
 876
 877    if (ui->confirmingOverwrite) {
 878        if (key == FF_GEN_KEY_CHAR && (ch == 'y' || ch == 'Y')) {
 879            return 1;
 880        }
 881        if ((key == FF_GEN_KEY_CHAR && (ch == 'n' || ch == 'N')) || key == FF_GEN_KEY_ESCAPE || key == FF_GEN_KEY_ENTER) {
 882            ui->confirmingOverwrite = false;
 883        }
 884        return -1;
 885    }
 886
 887    switch (key) {
 888        case FF_GEN_KEY_UP:
 889            moveCursorUp(ui);
 890            break;
 891        case FF_GEN_KEY_DOWN:
 892            moveCursorDown(ui);
 893            break;
 894        case FF_GEN_KEY_LEFT:
 895            moveCursorLeft(ui);
 896            break;
 897        case FF_GEN_KEY_RIGHT:
 898            moveCursorRight(ui);
 899            break;
 900        case FF_GEN_KEY_ENTER:
 901            if (fileExists) {
 902                ui->confirmingOverwrite = true;
 903            } else {
 904                return 1;
 905            }
 906            break;
 907        case FF_GEN_KEY_ESCAPE:
 908            return 0;
 909        case FF_GEN_KEY_CHAR:
 910            if (ch == 'q') {
 911                return 0;
 912            } else if (ch == 's') {
 913                if (fileExists) {
 914                    ui->confirmingOverwrite = true;
 915                } else {
 916                    return 1;
 917                }
 918            } else if (ch == ' ') {
 919                if (ui->cursor < ui->items.length) {
 920                    toggleItem(ui, ui->cursor);
 921                }
 922            } else if (ch == 'f') {
 923                selectAllModules(ui);
 924            } else if (ch == 'F') {
 925                invertAllModules(ui);
 926            } else if (ch == 'o') {
 927                ui->fullConfig = !ui->fullConfig;
 928            } else if (ch == 'j') {
 929                moveCursorDown(ui);
 930            } else if (ch == 'k') {
 931                moveCursorUp(ui);
 932            } else if (ch == 'J') {
 933                if (ui->cursor < ui->items.length) {
 934                    moveItemDown(ui, ui->cursor);
 935                }
 936            } else if (ch == 'K') {
 937                if (ui->cursor < ui->items.length) {
 938                    moveItemUp(ui, ui->cursor);
 939                }
 940            } else if (ch == 'b') {
 941                if (ui->cursor < ui->items.length) {
 942                    addBreakBelow(ui, ui->cursor);
 943                }
 944            } else if (ch == 'B') {
 945                if (ui->cursor < ui->items.length) {
 946                    addSeparatorBelow(ui, ui->cursor);
 947                }
 948            } else if (ch == 'd') {
 949                if (ui->cursor < ui->items.length) {
 950                    FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
 951                    if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) {
 952                        FF_LIST_REMOVE_AT(FFGenConfigItem, ui->items, ui->cursor);
 953                        if (ui->cursor >= ui->items.length && ui->items.length > 0) {
 954                            ui->cursor = ui->items.length - 1;
 955                        }
 956                    }
 957                }
 958            } else if (ch == 'D') {
 959                removeAllBreaksSeparators(ui);
 960            } else if (ch == 'g') {
 961                ui->cursor = 0;
 962            } else if (ch == 'G') {
 963                ui->cursor = ui->items.length > 0 ? ui->items.length - 1 : 0;
 964            } else if (ch == 'l') {
 965                cycleLogoType(ui, 1);
 966            } else if (ch == 'L') {
 967                cycleLogoType(ui, -1);
 968            } else if (ch == 'p') {
 969                cycleLogoPosition(ui, 1);
 970            } else if (ch == 'P') {
 971                cycleLogoPosition(ui, -1);
 972            } else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') {
 973                return 0;
 974            }
 975            break;
 976        default:
 977            break;
 978    }
 979    return -1;
 980}
 981
 982static int runCui(FFGenConfigUI* ui, bool fileExists) {
 983    while (true) {
 984        FF_STRBUF_AUTO_DESTROY frame = ffStrbufCreateA((uint32_t) ui->cols * ((uint32_t) ui->rows + 4));
 985        renderFrame(ui, &frame);
 986        ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), frame.length, frame.chars);
 987
 988        char ch = 0;
 989        FFGenConfigKey key = readKey(&ch);
 990        int result = handleKey(ui, key, ch, fileExists);
 991        if (result != -1) {
 992            return result;
 993        }
 994    }
 995}
 996
 997static FFLogoType initialLogoType(void) {
 998    switch (instance.config.logo.type) {
 999        case FF_LOGO_TYPE_SMALL:
1000            return FF_LOGO_TYPE_SMALL;
1001        case FF_LOGO_TYPE_NONE:
1002            return FF_LOGO_TYPE_NONE;
1003        default:
1004            return FF_LOGO_TYPE_AUTO;
1005    }
1006}
1007
1008static FFLogoPosition initialLogoPosition(void) {
1009    switch (instance.config.logo.position) {
1010        case FF_LOGO_POSITION_TOP:
1011        case FF_LOGO_POSITION_RIGHT:
1012        case FF_LOGO_POSITION_AUTO:
1013            return instance.config.logo.position;
1014        case FF_LOGO_POSITION_LEFT:
1015        default:
1016            return FF_LOGO_POSITION_LEFT;
1017    }
1018}
1019
1020static bool applyConfig(FFdata* data, const FFlist* items, bool fullConfig) {
1021    if (data->resultDoc) {
1022        fputs("Error: duplicated `--gen-config` or `--format json` flags found\n", stderr);
1023        return false;
1024    }
1025
1026    FF_STRBUF_AUTO_DESTROY structure = ffStrbufCreateA(256);
1027    FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
1028        if (item->status == FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED) {
1029            continue;
1030        }
1031        ffStrbufAppendS(&structure, item->baseInfo->name);
1032        ffStrbufAppendC(&structure, ':');
1033    }
1034    ffStrbufTrimRight(&structure, ':');
1035    ffStrbufSet(&data->structure, &structure);
1036
1037    data->docType = fullConfig ? FF_RESULT_DOC_TYPE_CONFIG_FULL : FF_RESULT_DOC_TYPE_CONFIG;
1038    data->resultDoc = yyjson_mut_doc_new(nullptr);
1039    return true;
1040}
1041
1042[[gnu::cold]]
1043bool ffGenConfigInteractive(FFdata* data) {
1044    FF_LIST_AUTO_DESTROY modules;
1045    collectModuleInfos(&modules);
1046
1047    FFGenConfigUI ui = {
1048        .items = ffListCreate(),
1049        .cursor = 0,
1050        .viewOffset = 0,
1051        .logoType = initialLogoType(),
1052        .logoPosition = initialLogoPosition(),
1053        .fullConfig = false,
1054        .confirmingOverwrite = false,
1055        .rows = 24,
1056        .cols = 80,
1057    };
1058    initItems(&ui.items, &modules, data);
1059
1060    getTerminalSize(&ui.rows, &ui.cols);
1061    enterRawMode();
1062    ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), strlen("\e[?1049h\e[?25l\e[?7l"), "\e[?1049h\e[?25l\e[?7l");
1063
1064    const bool fileExists = ffPathExists(data->genConfigPath.chars, FF_PATHTYPE_ANY);
1065    int result = runCui(&ui, fileExists);
1066
1067    restoreRawMode();
1068    ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), strlen("\e[?1049l\e[?25h\e[?7h" FASTFETCH_TEXT_MODIFIER_RESET), "\e[?1049l\e[?25h\e[?7h" FASTFETCH_TEXT_MODIFIER_RESET);
1069
1070    bool success = false;
1071    if (result == 1) {
1072        if (applyConfig(data, &ui.items, ui.fullConfig)) {
1073            instance.config.logo.type = ui.logoType;
1074            instance.config.logo.position = ui.logoPosition;
1075            success = true;
1076        }
1077    } else {
1078        fputs("\nConfig generation canceled.\n", stderr);
1079    }
1080
1081    ffListDestroy(&ui.items);
1082    return success;
1083}