#include "common/genconfig.h" #include "fastfetch.h" #include "common/color.h" #include "common/io.h" #include "common/strutil.h" #include "common/textModifier.h" #include "detection/terminalsize/terminalsize.h" #include "modules/modules.h" #include #include #include #ifndef _WIN32 #include #include #include #include #else #include #endif // Layout (rows): // 0: title // 1: blank // 2: logo type row // 3: output mode row // 4: blank // 5: modules title row // 6: blank // 7..7+listRows-1: module grid // 7+listRows: description row // 8+listRows: help line 1 // 9+listRows: help line 2 #define FF_GEN_CONFIG_LIST_TOP 7 #define FF_GEN_CONFIG_BOTTOM_CHROME 3 // Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen // redraws (flicker); terminal size changes redraw immediately via SIGWINCH / // console window-size events. #define FF_GEN_CONFIG_POLL_TIMEOUT 1000 typedef enum : uint8_t { FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED = false, FF_GEN_CONFIG_ITEM_STATUS_SELECTED = true, FF_GEN_CONFIG_ITEM_STATUS_SPECIAL = 2, } FFGenConfigItemStatus; typedef struct FFGenConfigItem { FFModuleBaseInfo* baseInfo; FFGenConfigItemStatus status; } FFGenConfigItem; typedef struct FFGenConfigLayout { uint16_t listLeft; uint16_t listRows; uint32_t colWidth; uint32_t columns; uint32_t itemsPerColumn; } FFGenConfigLayout; typedef struct FFGenConfigUI { FFlist items; // FFGenConfigItem uint32_t cursor; uint32_t viewOffset; FFLogoType logoType; bool fullConfig; bool confirmingOverwrite; uint16_t rows; uint16_t cols; FFGenConfigLayout layout; } FFGenConfigUI; typedef enum : uint8_t { FF_GEN_KEY_UP, FF_GEN_KEY_DOWN, FF_GEN_KEY_LEFT, FF_GEN_KEY_RIGHT, FF_GEN_KEY_ENTER, FF_GEN_KEY_ESCAPE, FF_GEN_KEY_CHAR, FF_GEN_KEY_RESIZE, FF_GEN_KEY_UNKNOWN, } FFGenConfigKey; #ifndef _WIN32 static struct termios gOriginalTermios; static bool gRawModeActive = false; static volatile sig_atomic_t gWindowResized = 0; static void onWindowResize(int sig) { (void) sig; gWindowResized = 1; } static void restoreRawMode(void) { if (gRawModeActive) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &gOriginalTermios); gRawModeActive = false; } } static void enterRawMode(void) { if (gRawModeActive) { return; } if (tcgetattr(STDIN_FILENO, &gOriginalTermios) != 0) { return; } struct termios raw = gOriginalTermios; raw.c_lflag &= (tcflag_t) ~(ICANON | ECHO | ISIG | IEXTEN); raw.c_iflag &= (tcflag_t) ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); raw.c_cflag |= CS8; raw.c_oflag &= (tcflag_t) ~OPOST; raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 1; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) { return; } gRawModeActive = true; struct sigaction sa = { .sa_handler = onWindowResize }; sigemptyset(&sa.sa_mask); sigaction(SIGWINCH, &sa, nullptr); } #else static DWORD gOriginalInputMode; static DWORD gOriginalOutputMode; static bool gRawModeActive = false; static void restoreRawMode(void) { if (gRawModeActive) { SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), gOriginalInputMode); SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), gOriginalOutputMode); gRawModeActive = false; } } static void enterRawMode(void) { if (gRawModeActive) { return; } HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); if (GetConsoleMode(hInput, &gOriginalInputMode)) { DWORD newMode = gOriginalInputMode; newMode &= ~(DWORD) (ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); newMode |= ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT; SetConsoleMode(hInput, newMode); } if (GetConsoleMode(hOutput, &gOriginalOutputMode)) { DWORD newMode = gOriginalOutputMode; newMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; SetConsoleMode(hOutput, newMode); } FlushConsoleInputBuffer(hInput); gRawModeActive = true; } #endif static void getTerminalSize(uint16_t* rows, uint16_t* cols) { FFTerminalSizeResult size; if (ffDetectTerminalSize(&size) && size.rows > 0 && size.columns > 0) { *rows = size.rows; *cols = size.columns; } else { *rows = 24; *cols = 80; } } static int compareModuleInfo(const void* a, const void* b) { const FFModuleBaseInfo* const* ma = (const FFModuleBaseInfo* const*) a; const FFModuleBaseInfo* const* mb = (const FFModuleBaseInfo* const*) b; if ((*ma)->defaultOrder != (*mb)->defaultOrder) { return (int) (*ma)->defaultOrder - (int) (*mb)->defaultOrder; } return strcmp((*ma)->name, (*mb)->name); } static void collectModuleInfos(FFlist* modules) { ffListInitA(modules, sizeof(FFModuleBaseInfo*), 64); for (uint32_t i = 0; i <= 'Z' - 'A'; ++i) { for (FFModuleBaseInfo** it = ffModuleInfos[i]; *it; ++it) { if ((*it)->defaultOrder == 0) { continue; } *(FFModuleBaseInfo**) ffListAdd(modules, sizeof(FFModuleBaseInfo*)) = *it; } } ffListSort(modules, sizeof(FFModuleBaseInfo*), compareModuleInfo); } static void initItems(FFlist* items, const FFlist* modules, FFdata* data) { ffListInitA(items, sizeof(FFGenConfigItem), modules->length + 8); FF_LIST_FOR_EACH (FFModuleBaseInfo*, info, *modules) { FFGenConfigItem* item = FF_LIST_ADD(FFGenConfigItem, *items); item->baseInfo = *info; if (*info == &ffBreakModuleInfo || *info == &ffSeparatorModuleInfo) { item->status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL; } else { item->status = ffStrbufSeparatedContainIgnCaseS(&data->structure, (*info)->name, ':') && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, (*info)->name, ':') ? FF_GEN_CONFIG_ITEM_STATUS_SELECTED : FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED; } } } static void moveItemDown(FFGenConfigUI* ui, uint32_t idx) { if (idx + 1 >= ui->items.length) { return; } FFGenConfigItem* items = (FFGenConfigItem*) ui->items.data; FFGenConfigItem tmp = items[idx]; items[idx] = items[idx + 1]; items[idx + 1] = tmp; ui->cursor = idx + 1; } static void moveItemUp(FFGenConfigUI* ui, uint32_t idx) { if (idx == 0) { return; } FFGenConfigItem* items = (FFGenConfigItem*) ui->items.data; FFGenConfigItem tmp = items[idx]; items[idx] = items[idx - 1]; items[idx - 1] = tmp; ui->cursor = idx - 1; } static void removeAllBreaksSeparators(FFGenConfigUI* ui) { for (uint32_t i = ui->items.length; i-- > 0;) { FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, i); if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { FF_LIST_REMOVE_AT(FFGenConfigItem, ui->items, i); } } if (ui->cursor >= ui->items.length && ui->items.length > 0) { ui->cursor = ui->items.length - 1; } } static void computeLayout(FFGenConfigUI* ui) { FFGenConfigLayout* layout = &ui->layout; if (ui->rows < 10) { ui->rows = 10; } int32_t listRows = (int32_t) ui->rows - FF_GEN_CONFIG_LIST_TOP - FF_GEN_CONFIG_BOTTOM_CHROME; layout->listRows = listRows < 0 ? 0 : (uint16_t) listRows; layout->listLeft = 2; uint32_t maxContentWidth = 0; FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) { uint32_t w = (uint32_t) strlen(item->baseInfo->name) + 4; // "[x] " / "[─] " prefix if (w > maxContentWidth) { maxContentWidth = w; } } layout->colWidth = maxContentWidth + 2; if (layout->colWidth < 1) { layout->colWidth = 1; } uint32_t gridWidth = ui->cols > 4 ? (uint32_t) ui->cols - 4 : 1; uint32_t maxColumns = gridWidth / layout->colWidth; if (maxColumns < 1) { maxColumns = 1; } uint32_t length = ui->items.length; if (length == 0 || layout->listRows == 0) { layout->columns = 1; layout->itemsPerColumn = length > 0 ? length : 1; return; } uint32_t neededColumns = (length + layout->listRows - 1) / layout->listRows; if (neededColumns < 1) { neededColumns = 1; } layout->columns = neededColumns < maxColumns ? neededColumns : maxColumns; layout->itemsPerColumn = (length + layout->columns - 1) / layout->columns; } static void recomputeView(FFGenConfigUI* ui) { const FFGenConfigLayout* layout = &ui->layout; uint32_t length = ui->items.length; if (length == 0) { ui->cursor = 0; ui->viewOffset = 0; return; } if (ui->cursor >= length) { ui->cursor = length - 1; } uint32_t rowsPerColumn = layout->itemsPerColumn; if (rowsPerColumn <= layout->listRows) { ui->viewOffset = 0; return; } uint32_t cursorRow = ui->cursor % rowsPerColumn; uint32_t pageTop = ui->viewOffset; if (cursorRow < pageTop) { pageTop = cursorRow; } else if (cursorRow >= pageTop + layout->listRows) { pageTop = cursorRow - layout->listRows + 1; } uint32_t maxTop = rowsPerColumn - layout->listRows; if (pageTop > maxTop) { pageTop = maxTop; } ui->viewOffset = pageTop; } static void toggleItem(FFGenConfigUI* ui, uint32_t idx) { FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, idx); if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { return; } item->status = !item->status; } static void selectAllModules(FFGenConfigUI* ui) { FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) { if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { item->status = FF_GEN_CONFIG_ITEM_STATUS_SELECTED; } } } static void invertAllModules(FFGenConfigUI* ui) { FF_LIST_FOR_EACH (FFGenConfigItem, item, ui->items) { if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { item->status = !item->status; } } } static void addBreakBelow(FFGenConfigUI* ui, uint32_t idx) { FFGenConfigItem br = { .baseInfo = &ffBreakModuleInfo, .status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL }; FF_LIST_INSERT_AT(FFGenConfigItem, ui->items, idx + 1, &br); } static void addSeparatorBelow(FFGenConfigUI* ui, uint32_t idx) { FFGenConfigItem sep = { .baseInfo = &ffSeparatorModuleInfo, .status = FF_GEN_CONFIG_ITEM_STATUS_SPECIAL }; FF_LIST_INSERT_AT(FFGenConfigItem, ui->items, idx + 1, &sep); } static void cycleLogoType(FFGenConfigUI* ui, int delta) { switch (ui->logoType) { case FF_LOGO_TYPE_AUTO: ui->logoType = delta > 0 ? FF_LOGO_TYPE_SMALL : FF_LOGO_TYPE_NONE; break; case FF_LOGO_TYPE_SMALL: ui->logoType = delta > 0 ? FF_LOGO_TYPE_NONE : FF_LOGO_TYPE_AUTO; break; default: ui->logoType = delta > 0 ? FF_LOGO_TYPE_AUTO : FF_LOGO_TYPE_SMALL; break; } } static uint32_t countSelectedModules(const FFlist* items) { uint32_t count = 0; FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) { if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SELECTED) { ++count; } } return count; } static uint32_t countModules(const FFlist* items) { uint32_t count = 0; FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) { if (item->status != FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { ++count; } } return count; } // ---- Row builder ----------------------------------------------------------- typedef struct FFRow { FFstrbuf buf; uint32_t visualCol; uint16_t cols; } FFRow; static void rowInit(FFRow* row, uint16_t cols) { ffStrbufInitA(&row->buf, (uint32_t) cols + 16); row->visualCol = 0; row->cols = cols; } static void rowAppendRaw(FFRow* row, const char* s) { ffStrbufAppendS(&row->buf, s); } static void rowAppendVisual(FFRow* row, const char* s) { uint32_t len = (uint32_t) strlen(s); ffStrbufAppendNS(&row->buf, len, s); row->visualCol += ffUtf8StrWidth(s, len); } static void rowPadVisual(FFRow* row, uint32_t col) { if (row->visualCol < col) { ffStrbufAppendNC(&row->buf, col - row->visualCol, ' '); row->visualCol = col; } } static void finishRow(FFRow* row, FFstrbuf* out, bool isLastRow) { if (row->visualCol < row->cols) { rowPadVisual(row, row->cols); } ffStrbufAppend(out, &row->buf); ffStrbufAppendS(out, "\e[K"); if (!isLastRow) { ffStrbufAppendS(out, "\r\n"); } ffStrbufDestroy(&row->buf); } static void drawModuleItem(FFRow* row, const FFGenConfigItem* item, bool highlight) { if (item->baseInfo == &ffBreakModuleInfo) { rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_MAGENTA "m" : "\e[" FF_COLOR_FG_MAGENTA "m"); ffStrbufAppendS(&row->buf, "[-]"); row->visualCol += 3; rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(row, " "); rowAppendRaw(row, "\e[" FF_COLOR_FG_MAGENTA "m"); rowAppendVisual(row, item->baseInfo->name); rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); return; } if (item->baseInfo == &ffSeparatorModuleInfo) { rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_CYAN "m" : "\e[" FF_COLOR_FG_CYAN "m"); ffStrbufAppendS(&row->buf, "[-]"); row->visualCol += 3; rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(row, " "); rowAppendRaw(row, "\e[" FF_COLOR_FG_CYAN "m"); rowAppendVisual(row, item->baseInfo->name); rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); return; } if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SELECTED) { rowAppendRaw(row, highlight ? "\e[" FF_COLOR_MODE_INVERSE FF_COLOR_FG_GREEN "m" : "\e[" FF_COLOR_FG_GREEN "m"); ffStrbufAppendS(&row->buf, "[x]"); row->visualCol += 3; rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(row, " "); rowAppendRaw(row, "\e[" FF_COLOR_FG_GREEN "m"); rowAppendVisual(row, item->baseInfo->name); rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); } else { if (highlight) { rowAppendRaw(row, "\e[" FF_COLOR_MODE_INVERSE_ "m"); } ffStrbufAppendS(&row->buf, "[ ]"); row->visualCol += 3; rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(row, " "); rowAppendVisual(row, item->baseInfo->name); rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); } } static void drawItemCell(FFRow* row, uint32_t startCol, uint32_t cellWidth, const FFGenConfigItem* item, bool highlight) { rowPadVisual(row, startCol); drawModuleItem(row, item, highlight); rowPadVisual(row, startCol + cellWidth); } static void drawLogoOption(FFRow* row, const char* name, bool active) { if (active) { rowAppendRaw(row, "\e[" FF_COLOR_FG_GREEN "m"); rowAppendVisual(row, "[x] "); rowAppendVisual(row, name); rowAppendRaw(row, FASTFETCH_TEXT_MODIFIER_RESET); } else { rowAppendVisual(row, "[ ] "); rowAppendVisual(row, name); } } static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) { getTerminalSize(&ui->rows, &ui->cols); computeLayout(ui); recomputeView(ui); const FFGenConfigLayout* layout = &ui->layout; const uint16_t cols = ui->cols; const uint32_t selectedCount = countSelectedModules(&ui->items); const uint32_t moduleCount = countModules(&ui->items); ffStrbufAppendS(out, "\e[?25l\e[H"); uint32_t rowCount = 0; FFRow row; // Row 0: title rowInit(&row, cols); rowAppendVisual(&row, " "); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT); rowAppendVisual(&row, "fastfetch"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(&row, " configuration"); if (cols >= 48) { rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT); rowAppendVisual(&row, " interactive config generator"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); } finishRow(&row, out, ++rowCount == ui->rows); // Row 1: blank rowInit(&row, cols); finishRow(&row, out, ++rowCount == ui->rows); // Row 2: logo type rowInit(&row, cols); rowAppendVisual(&row, " "); rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m"); rowAppendVisual(&row, "L"); rowAppendRaw(&row, "\e[24m"); // Reset underline rowAppendVisual(&row, "ogo type:"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(&row, " "); drawLogoOption(&row, "default", ui->logoType == FF_LOGO_TYPE_AUTO); rowAppendVisual(&row, " "); drawLogoOption(&row, "small", ui->logoType == FF_LOGO_TYPE_SMALL); rowAppendVisual(&row, " "); drawLogoOption(&row, "none", ui->logoType == FF_LOGO_TYPE_NONE); if (cols >= 72) { rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT); switch (ui->logoType) { case FF_LOGO_TYPE_AUTO: rowAppendVisual(&row, " - Built-in ASCII art"); break; case FF_LOGO_TYPE_SMALL: rowAppendVisual(&row, " - Built-in ASCII art, small version"); break; default: rowAppendVisual(&row, " - Disable logo printing"); break; } rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); } finishRow(&row, out, ++rowCount == ui->rows); // Row 3: output mode rowInit(&row, cols); rowAppendVisual(&row, " "); rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m"); rowAppendVisual(&row, "O"); rowAppendRaw(&row, "\e[24m"); // Reset underline rowAppendVisual(&row, "utput:"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); rowAppendVisual(&row, " "); drawLogoOption(&row, "minimal", !ui->fullConfig); rowAppendVisual(&row, " "); drawLogoOption(&row, "full", ui->fullConfig); if (cols >= 64) { rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT); if (ui->fullConfig) { rowAppendVisual(&row, " - Full JSON config with all module options"); } else { rowAppendVisual(&row, " - Minimal JSON config"); } rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); } finishRow(&row, out, ++rowCount == ui->rows); // Row 4: blank rowInit(&row, cols); finishRow(&row, out, ++rowCount == ui->rows); // Row 5: modules title rowInit(&row, cols); rowAppendVisual(&row, " "); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT); rowAppendVisual(&row, "Modules:"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET "\e[" FF_COLOR_FG_GREEN "m"); FF_STRBUF_AUTO_DESTROY counter = ffStrbufCreateF(" [%u/%u selected]", selectedCount, moduleCount); rowAppendVisual(&row, counter.chars); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); finishRow(&row, out, ++rowCount == ui->rows); // Row 6: blank rowInit(&row, cols); finishRow(&row, out, ++rowCount == ui->rows); // Grid rows const uint32_t rowsPerColumn = layout->itemsPerColumn; const uint32_t length = ui->items.length; for (uint16_t screenRow = 0; screenRow < layout->listRows; ++screenRow) { rowInit(&row, cols); for (uint32_t c = 0; c < layout->columns; ++c) { uint32_t startCol = layout->listLeft + c * layout->colWidth; uint32_t index = (ui->viewOffset + screenRow) + c * rowsPerColumn; if (screenRow < rowsPerColumn && index < length) { const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, index); drawItemCell(&row, startCol, layout->colWidth, item, ui->cursor == index); } else { rowPadVisual(&row, startCol + layout->colWidth); } } finishRow(&row, out, ++rowCount == ui->rows); } // Row after grid: description rowInit(&row, cols); if (ui->cursor < ui->items.length) { const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor); if (item->baseInfo && item->baseInfo->description) { rowAppendVisual(&row, " "); rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m"); rowAppendVisual(&row, item->baseInfo->description); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); } } finishRow(&row, out, ++rowCount == ui->rows); // Help lines rowInit(&row, cols); rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m"); rowAppendVisual(&row, " " #if _WIN32 || __APPLE__ || __linux__ || (__FreeBSD__ && !__DragonFly__) "↑/↓ k/j move ←/→ col" #else "^/v k/j move col" #endif " Space toggle f/F all/invert K/J reorder b/B break/sep d/D del"); if (ui->confirmingOverwrite) { rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET "\e[" FF_COLOR_MODE_BOLD FF_COLOR_FG_YELLOW "m"); rowAppendVisual(&row, " File exists. Overwrite? (y/N)"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); } finishRow(&row, out, ++rowCount == ui->rows); rowInit(&row, cols); rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m"); rowAppendVisual(&row, " l/L logo o minimal/full s/Enter save q/Esc quit g/G top/bottom"); rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET); finishRow(&row, out, ++rowCount == ui->rows); ffStrbufAppendS(out, "\e[J"); } // ---- Key input ------------------------------------------------------------- static FFGenConfigKey readKey(char* outChar) { #ifndef _WIN32 if (gWindowResized) { gWindowResized = 0; return FF_GEN_KEY_RESIZE; } struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN, }; if (poll(&pfd, 1, FF_GEN_CONFIG_POLL_TIMEOUT) <= 0) { if (gWindowResized) { gWindowResized = 0; return FF_GEN_KEY_RESIZE; } return FF_GEN_KEY_UNKNOWN; } #else HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); if (WaitForSingleObject(hInput, FF_GEN_CONFIG_POLL_TIMEOUT) != WAIT_OBJECT_0) { return FF_GEN_KEY_UNKNOWN; } DWORD numEvents = 0; if (GetNumberOfConsoleInputEvents(hInput, &numEvents) && numEvents > 0) { bool consumed = false; INPUT_RECORD record; while (numEvents > 0) { DWORD n = 0; if (!PeekConsoleInputW(hInput, &record, 1, &n) || n == 0) { break; } if (record.EventType == KEY_EVENT) { break; // keep key events for ffReadFDData } if (!ReadConsoleInputW(hInput, &record, 1, &n) || n == 0) { break; } consumed = true; --numEvents; } if (consumed) { return FF_GEN_KEY_RESIZE; } } #endif char buf[16]; ssize_t n = ffReadFDData(FFUnixFD2NativeFD(STDIN_FILENO), sizeof(buf), buf); if (n <= 0) { return FF_GEN_KEY_UNKNOWN; } if (buf[0] == '\r' || buf[0] == '\n') { return FF_GEN_KEY_ENTER; } if (buf[0] != '\x1b') { *outChar = buf[0]; return FF_GEN_KEY_CHAR; } if (n == 1) { return FF_GEN_KEY_ESCAPE; } if (buf[1] == '[' || buf[1] == 'O') { size_t i = 2; if (i < (size_t) n && buf[i] >= 'A' && buf[i] <= 'D') { switch (buf[i]) { case 'A': return FF_GEN_KEY_UP; case 'B': return FF_GEN_KEY_DOWN; case 'C': return FF_GEN_KEY_RIGHT; case 'D': return FF_GEN_KEY_LEFT; } } uint32_t param = 0; while (i < (size_t) n && buf[i] >= '0' && buf[i] <= '9') { param = param * 10 + (uint32_t) (buf[i] - '0'); ++i; } while (i < (size_t) n && buf[i] == ';') { ++i; while (i < (size_t) n && buf[i] >= '0' && buf[i] <= '9') { ++i; } } if (i < (size_t) n) { switch (buf[i]) { case 'A': return FF_GEN_KEY_UP; case 'B': return FF_GEN_KEY_DOWN; case 'C': return FF_GEN_KEY_RIGHT; case 'D': return FF_GEN_KEY_LEFT; } } return FF_GEN_KEY_UNKNOWN; } return FF_GEN_KEY_ESCAPE; } static void moveCursorLeft(FFGenConfigUI* ui) { uint32_t rowsPerColumn = ui->layout.itemsPerColumn; if (rowsPerColumn == 0) { return; } if (ui->cursor >= rowsPerColumn) { ui->cursor -= rowsPerColumn; } } static void moveCursorRight(FFGenConfigUI* ui) { uint32_t rowsPerColumn = ui->layout.itemsPerColumn; if (rowsPerColumn == 0) { return; } uint32_t newCursor = ui->cursor + rowsPerColumn; if (newCursor < ui->items.length) { ui->cursor = newCursor; } } static void moveCursorUp(FFGenConfigUI* ui) { if (ui->cursor > 0) { --ui->cursor; } else { ui->cursor = ui->items.length > 0 ? ui->items.length - 1 : 0; } } static void moveCursorDown(FFGenConfigUI* ui) { if (ui->items.length == 0) { return; } if (ui->cursor + 1 < ui->items.length) { ++ui->cursor; } else { ui->cursor = 0; } } // Returns 1 on save, 0 on quit, -1 on continue static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileExists) { if (key == FF_GEN_KEY_UNKNOWN) { return -1; } if (key == FF_GEN_KEY_RESIZE) { return -1; // Terminal resized: redraw with new size } if (ui->confirmingOverwrite) { if (key == FF_GEN_KEY_CHAR && (ch == 'y' || ch == 'Y')) { return 1; } if ((key == FF_GEN_KEY_CHAR && (ch == 'n' || ch == 'N')) || key == FF_GEN_KEY_ESCAPE || key == FF_GEN_KEY_ENTER) { ui->confirmingOverwrite = false; } return -1; } switch (key) { case FF_GEN_KEY_UP: moveCursorUp(ui); break; case FF_GEN_KEY_DOWN: moveCursorDown(ui); break; case FF_GEN_KEY_LEFT: moveCursorLeft(ui); break; case FF_GEN_KEY_RIGHT: moveCursorRight(ui); break; case FF_GEN_KEY_ENTER: if (fileExists) { ui->confirmingOverwrite = true; } else { return 1; } break; case FF_GEN_KEY_ESCAPE: return 0; case FF_GEN_KEY_CHAR: if (ch == 'q') { return 0; } else if (ch == 's') { if (fileExists) { ui->confirmingOverwrite = true; } else { return 1; } } else if (ch == ' ') { if (ui->cursor < ui->items.length) { toggleItem(ui, ui->cursor); } } else if (ch == 'f') { selectAllModules(ui); } else if (ch == 'F') { invertAllModules(ui); } else if (ch == 'o') { ui->fullConfig = !ui->fullConfig; } else if (ch == 'j') { moveCursorDown(ui); } else if (ch == 'k') { moveCursorUp(ui); } else if (ch == 'J') { if (ui->cursor < ui->items.length) { moveItemDown(ui, ui->cursor); } } else if (ch == 'K') { if (ui->cursor < ui->items.length) { moveItemUp(ui, ui->cursor); } } else if (ch == 'b') { if (ui->cursor < ui->items.length) { addBreakBelow(ui, ui->cursor); } } else if (ch == 'B') { if (ui->cursor < ui->items.length) { addSeparatorBelow(ui, ui->cursor); } } else if (ch == 'd') { if (ui->cursor < ui->items.length) { FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor); if (item->status == FF_GEN_CONFIG_ITEM_STATUS_SPECIAL) { FF_LIST_REMOVE_AT(FFGenConfigItem, ui->items, ui->cursor); if (ui->cursor >= ui->items.length && ui->items.length > 0) { ui->cursor = ui->items.length - 1; } } } } else if (ch == 'D') { removeAllBreaksSeparators(ui); } else if (ch == 'g') { ui->cursor = 0; } else if (ch == 'G') { ui->cursor = ui->items.length > 0 ? ui->items.length - 1 : 0; } else if (ch == 'l') { cycleLogoType(ui, 1); } else if (ch == 'L') { cycleLogoType(ui, -1); } else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') { return 0; } break; default: break; } return -1; } static int runCui(FFGenConfigUI* ui, bool fileExists) { while (true) { FF_STRBUF_AUTO_DESTROY frame = ffStrbufCreateA((uint32_t) ui->cols * ((uint32_t) ui->rows + 4)); renderFrame(ui, &frame); ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), frame.length, frame.chars); char ch = 0; FFGenConfigKey key = readKey(&ch); int result = handleKey(ui, key, ch, fileExists); if (result != -1) { return result; } } } static FFLogoType initialLogoType(void) { switch (instance.config.logo.type) { case FF_LOGO_TYPE_SMALL: return FF_LOGO_TYPE_SMALL; case FF_LOGO_TYPE_NONE: return FF_LOGO_TYPE_NONE; default: return FF_LOGO_TYPE_AUTO; } } static bool applyConfig(FFdata* data, const FFlist* items, bool fullConfig) { if (data->resultDoc) { fputs("Error: duplicated `--gen-config` or `--format json` flags found\n", stderr); return false; } FF_STRBUF_AUTO_DESTROY structure = ffStrbufCreateA(256); FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) { if (item->status == FF_GEN_CONFIG_ITEM_STATUS_UNSELECTED) { continue; } ffStrbufAppendS(&structure, item->baseInfo->name); ffStrbufAppendC(&structure, ':'); } ffStrbufTrimRight(&structure, ':'); ffStrbufSet(&data->structure, &structure); data->docType = fullConfig ? FF_RESULT_DOC_TYPE_CONFIG_FULL : FF_RESULT_DOC_TYPE_CONFIG; data->resultDoc = yyjson_mut_doc_new(nullptr); return true; } [[gnu::cold]] bool ffGenConfigInteractive(FFdata* data) { FF_LIST_AUTO_DESTROY modules; collectModuleInfos(&modules); FFGenConfigUI ui = { .items = ffListCreate(), .cursor = 0, .viewOffset = 0, .logoType = initialLogoType(), .fullConfig = false, .confirmingOverwrite = false, .rows = 24, .cols = 80, }; initItems(&ui.items, &modules, data); getTerminalSize(&ui.rows, &ui.cols); enterRawMode(); ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), strlen("\e[?1049h\e[?25l\e[?7l"), "\e[?1049h\e[?25l\e[?7l"); const bool fileExists = ffPathExists(data->genConfigPath.chars, FF_PATHTYPE_ANY); int result = runCui(&ui, fileExists); restoreRawMode(); 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); bool success = false; if (result == 1) { if (applyConfig(data, &ui.items, ui.fullConfig)) { instance.config.logo.type = ui.logoType; success = true; } } else { fputs("\nConfig generation canceled.\n", stderr); } ffListDestroy(&ui.items); return success; }