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}