mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-09-13 02:14:37 +02:00
1005 lines
32 KiB
C
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;
|
|
}
|