Files
fastfetch/src/common/impl/genconfig.c
T
2026-08-05 15:47:42 +08:00

1005 lines
32 KiB
C

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <termios.h>
#include <poll.h>
#include <signal.h>
#include <unistd.h>
#else
#include <windows.h>
#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 cancelled.\n", stderr);
}
ffListDestroy(&ui.items);
return success;
}