Logo: adds option logo.position: auto

This commit is contained in:
Carter Li
2026-09-25 09:51:34 +08:00
parent c68bedd9bf
commit 537635cd9c
16 changed files with 277 additions and 45 deletions
+97 -18
View File
@@ -26,15 +26,16 @@
// 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
// 3: logo position row
// 4: output mode row
// 5: blank
// 6: modules title row
// 7: blank
// 8..8+listRows-1: module grid
// 8+listRows: description row
// 9+listRows: help line 1
// 10+listRows: help line 2
#define FF_GEN_CONFIG_LIST_TOP 8
#define FF_GEN_CONFIG_BOTTOM_CHROME 3
// Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen
@@ -66,6 +67,7 @@ typedef struct FFGenConfigUI {
uint32_t cursor;
uint32_t viewOffset;
FFLogoType logoType;
FFLogoPosition logoPosition;
bool fullConfig;
bool confirmingOverwrite;
uint16_t rows;
@@ -165,7 +167,7 @@ static void enterRawMode(void) {
static void getTerminalSize(uint16_t* rows, uint16_t* cols) {
FFTerminalSizeResult size;
if (ffDetectTerminalSize(&size) && size.rows > 0 && size.columns > 0) {
if (ffDetectTerminalSize(&size, true) && size.rows > 0 && size.columns > 0) {
*rows = size.rows;
*cols = size.columns;
} else {
@@ -248,8 +250,8 @@ static void removeAllBreaksSeparators(FFGenConfigUI* ui) {
static void computeLayout(FFGenConfigUI* ui) {
FFGenConfigLayout* layout = &ui->layout;
if (ui->rows < 10) {
ui->rows = 10;
if (ui->rows < 11) {
ui->rows = 11;
}
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;
@@ -367,6 +369,26 @@ static void cycleLogoType(FFGenConfigUI* ui, int delta) {
}
}
static void cycleLogoPosition(FFGenConfigUI* ui, int delta) {
static const FFLogoPosition positions[] = {
FF_LOGO_POSITION_AUTO,
FF_LOGO_POSITION_LEFT,
FF_LOGO_POSITION_RIGHT,
FF_LOGO_POSITION_TOP,
};
uint32_t index = 0;
for (uint32_t i = 0; i < ARRAY_SIZE(positions); ++i) {
if (positions[i] == ui->logoPosition) {
index = i;
break;
}
}
index = (index + (delta > 0 ? 1 : ARRAY_SIZE(positions) - 1)) % ARRAY_SIZE(positions);
ui->logoPosition = positions[index];
}
static uint32_t countSelectedModules(const FFlist* items) {
uint32_t count = 0;
FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
@@ -558,7 +580,46 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 3: output mode
// Row 3: logo position
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD "m");
rowAppendVisual(&row, "Logo ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_UNDERLINE_ "m");
rowAppendVisual(&row, "p");
rowAppendRaw(&row, "\e[24m"); // Reset underline
rowAppendVisual(&row, "osition:");
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "auto", ui->logoPosition == FF_LOGO_POSITION_AUTO);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "left", ui->logoPosition == FF_LOGO_POSITION_LEFT);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "right", ui->logoPosition == FF_LOGO_POSITION_RIGHT);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "top", ui->logoPosition == FF_LOGO_POSITION_TOP);
if (cols >= 120) {
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
switch (ui->logoPosition) {
case FF_LOGO_POSITION_AUTO:
rowAppendVisual(&row, " - Choose left or top from terminal width");
break;
case FF_LOGO_POSITION_RIGHT:
rowAppendVisual(&row, " - Place the logo to the right");
break;
case FF_LOGO_POSITION_TOP:
rowAppendVisual(&row, " - Place the logo above module output");
break;
case FF_LOGO_POSITION_LEFT:
default:
rowAppendVisual(&row, " - Place the logo to the left");
break;
}
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 4: output mode
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
@@ -581,11 +642,11 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 4: blank
// Row 5: blank
rowInit(&row, cols);
finishRow(&row, out, ++rowCount == ui->rows);
// Row 5: modules title
// Row 6: modules title
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
@@ -596,7 +657,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
finishRow(&row, out, ++rowCount == ui->rows);
// Row 6: blank
// Row 7: blank
rowInit(&row, cols);
finishRow(&row, out, ++rowCount == ui->rows);
@@ -618,7 +679,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
finishRow(&row, out, ++rowCount == ui->rows);
}
// Row after grid: description
// Row 9+listRows: description
rowInit(&row, cols);
if (ui->cursor < ui->items.length) {
const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
@@ -650,7 +711,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
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");
rowAppendVisual(&row, " l/L logo p/P position 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);
@@ -904,6 +965,10 @@ static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileEx
cycleLogoType(ui, 1);
} else if (ch == 'L') {
cycleLogoType(ui, -1);
} else if (ch == 'p') {
cycleLogoPosition(ui, 1);
} else if (ch == 'P') {
cycleLogoPosition(ui, -1);
} else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') {
return 0;
}
@@ -940,6 +1005,18 @@ static FFLogoType initialLogoType(void) {
}
}
static FFLogoPosition initialLogoPosition(void) {
switch (instance.config.logo.position) {
case FF_LOGO_POSITION_TOP:
case FF_LOGO_POSITION_RIGHT:
case FF_LOGO_POSITION_AUTO:
return instance.config.logo.position;
case FF_LOGO_POSITION_LEFT:
default:
return FF_LOGO_POSITION_LEFT;
}
}
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);
@@ -972,6 +1049,7 @@ bool ffGenConfigInteractive(FFdata* data) {
.cursor = 0,
.viewOffset = 0,
.logoType = initialLogoType(),
.logoPosition = initialLogoPosition(),
.fullConfig = false,
.confirmingOverwrite = false,
.rows = 24,
@@ -993,6 +1071,7 @@ bool ffGenConfigInteractive(FFdata* data) {
if (result == 1) {
if (applyConfig(data, &ui.items, ui.fullConfig)) {
instance.config.logo.type = ui.logoType;
instance.config.logo.position = ui.logoPosition;
success = true;
}
} else {
+2 -1
View File
@@ -10,4 +10,5 @@ typedef struct FFTerminalSizeResult {
uint16_t height;
} FFTerminalSizeResult;
bool ffDetectTerminalSize(FFTerminalSizeResult* result);
// `fastOnly` avoids terminal query/response sequences and is intended for startup layout decisions.
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly);
+12 -11
View File
@@ -9,7 +9,9 @@
#include <sys/termios.h>
#endif
bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly) {
*result = (FFTerminalSizeResult){};
struct winsize winsize = {};
static int ttyfd = STDOUT_FILENO;
if (!isatty(ttyfd)) {
@@ -18,21 +20,20 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
ioctl(ttyfd, TIOCGWINSZ, &winsize);
if (winsize.ws_row == 0 || winsize.ws_col == 0) {
ffGetTerminalResponse("\e[18t", 2, "\e[8;%hu;%hut", &winsize.ws_row, &winsize.ws_col);
}
if (!fastOnly) {
if (winsize.ws_row == 0 || winsize.ws_col == 0) {
ffGetTerminalResponse("\e[18t", 2, "\e[8;%hu;%hut", &winsize.ws_row, &winsize.ws_col);
}
if (winsize.ws_ypixel == 0 || winsize.ws_xpixel == 0) {
ffGetTerminalResponse("\e[14t", 2, "\e[4;%hu;%hut", &winsize.ws_ypixel, &winsize.ws_xpixel);
}
if (winsize.ws_row == 0 && winsize.ws_col == 0) {
return false;
if (winsize.ws_ypixel == 0 || winsize.ws_xpixel == 0) {
ffGetTerminalResponse("\e[14t", 2, "\e[4;%hu;%hut", &winsize.ws_ypixel, &winsize.ws_xpixel);
}
}
result->rows = winsize.ws_row;
result->columns = winsize.ws_col;
result->width = winsize.ws_xpixel;
result->height = winsize.ws_ypixel;
return true;
return result->rows > 0 && result->columns > 0;
}
@@ -3,7 +3,9 @@
#include <windows.h>
bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly) {
*result = (FFTerminalSizeResult){};
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE;
{
@@ -18,16 +20,20 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
if (GetConsoleScreenBufferInfo(hOutput, &csbi)) {
result->columns = (uint16_t) (csbi.srWindow.Right - csbi.srWindow.Left + 1);
result->rows = (uint16_t) (csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
} else {
} else if (!fastOnly) {
// Windows Terminal doesn't report `\e` for some reason
ffGetTerminalResponse("\e[18t", 2, "%*[^;];%hu;%hut", &result->rows, &result->columns);
}
}
if (result->columns == 0 && result->rows == 0) {
if (result->columns == 0 || result->rows == 0) {
return false;
}
if (fastOnly) {
return true;
}
{
CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) };
if (GetCurrentConsoleFontEx(hOutput, FALSE, &cfi)) // Only works for ConHost
@@ -41,5 +47,5 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
}
}
return result->columns > 0 && result->rows > 0;
return true;
}
+9 -2
View File
@@ -854,9 +854,16 @@ static void writeConfigFile(FFdata* data) {
} else if (data->genConfigInteractive) {
if (instance.config.logo.type == FF_LOGO_TYPE_NONE) {
yyjson_mut_obj_add_null(doc, root, "logo");
} else if (instance.config.logo.type == FF_LOGO_TYPE_SMALL) {
} else if (instance.config.logo.type == FF_LOGO_TYPE_SMALL && instance.config.logo.position == FF_LOGO_POSITION_LEFT) {
yyjson_mut_obj_add_str(doc, root, "logo", "small");
} else if (instance.config.logo.position != FF_LOGO_POSITION_LEFT || instance.config.logo.type == FF_LOGO_TYPE_SMALL) {
yyjson_mut_val* logo = yyjson_mut_obj(doc);
yyjson_mut_obj_add_str(doc, logo, "type", "small");
if (instance.config.logo.type == FF_LOGO_TYPE_SMALL) {
yyjson_mut_obj_add_str(doc, logo, "type", "small");
}
if (instance.config.logo.position != FF_LOGO_POSITION_LEFT) {
yyjson_mut_obj_add_str(doc, logo, "position", ffLogoPositionToString(instance.config.logo.position));
}
yyjson_mut_obj_add_val(doc, root, "logo", logo);
}
}
+12 -1
View File
@@ -20,6 +20,11 @@
#include <sys/termios.h>
#endif
static void resolveImageAutoPosition(uint32_t logoWidth) {
const FFOptionsLogo* options = &instance.config.logo;
ffLogoResolveAutoPosition(logoWidth + options->paddingLeft + options->paddingRight);
}
static bool printImageIterm(bool printError) {
const FFOptionsLogo* options = &instance.config.logo;
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
@@ -633,6 +638,7 @@ static void printImagePixelsNoCache(FFLogoRequestData* requestData, const FFstrb
static void fillCharacterDimensions(FFLogoRequestData* requestData) {
requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth);
requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight);
resolveImageAutoPosition(requestData->logoCharacterWidth);
}
static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) {
@@ -1515,6 +1521,8 @@ static bool printCachedPixel(FFLogoRequestData* requestData) {
}
}
resolveImageAutoPosition(requestData->logoCharacterWidth);
if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY &&
options->animationFrame == FF_LOGO_ANIMATION_FRAME_ANIMATE) {
// An animation entry holds payloads, not an envelope, so it can not be streamed out like
@@ -1610,7 +1618,7 @@ static bool getCharacterPixelDimensions(FFLogoRequestData* requestData) {
#endif
FFTerminalSizeResult termSize = {};
if (ffDetectTerminalSize(&termSize)) {
if (ffDetectTerminalSize(&termSize, false)) {
requestData->characterPixelWidth = termSize.width / (double) termSize.columns;
requestData->characterPixelHeight = termSize.height / (double) termSize.rows;
}
@@ -1773,14 +1781,17 @@ bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) {
}
if (type == FF_LOGO_TYPE_IMAGE_ITERM) {
resolveImageAutoPosition(instance.config.logo.width);
return printImageIterm(printError);
}
if (type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT) {
resolveImageAutoPosition(instance.config.logo.width);
return printImageKittyDirect(printError);
}
if (type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT) {
resolveImageAutoPosition(instance.config.logo.width);
return printImageKittyIcat(printError);
}
+36
View File
@@ -7,6 +7,7 @@
#include "detection/media/media.h"
#include "detection/os/os.h"
#include "detection/terminalshell/terminalshell.h"
#include "detection/terminalsize/terminalsize.h"
#include <ctype.h>
#include <stdlib.h>
@@ -17,6 +18,37 @@ typedef struct FFLogoCachedLine {
uint32_t width;
} FFLogoCachedLine;
FFLogoPosition ffLogoSelectPosition(FFLogoPosition configured, uint32_t terminalColumns, uint32_t logoWidth) {
if (configured != FF_LOGO_POSITION_AUTO) {
return configured;
}
if (terminalColumns == 0) {
return FF_LOGO_POSITION_LEFT;
}
return logoWidth <= terminalColumns && terminalColumns - logoWidth >= FF_LOGO_AUTO_MIN_INFO_WIDTH
? FF_LOGO_POSITION_LEFT
: FF_LOGO_POSITION_TOP;
}
void ffLogoResolveAutoPosition(uint32_t logoWidth) {
FFOptionsLogo* options = &instance.config.logo;
if (options->position != FF_LOGO_POSITION_AUTO) {
return;
}
uint32_t terminalColumns = 0;
if (ffIsTerminal(STDOUT_FILENO)) {
FFTerminalSizeResult size = {};
if (ffDetectTerminalSize(&size, true)) {
terminalColumns = size.columns;
}
}
options->position = ffLogoSelectPosition(FF_LOGO_POSITION_AUTO, terminalColumns, logoWidth);
}
static void logoLineCacheClear(FFLogoLineCacheState* cache) {
FF_LIST_FOR_EACH (FFLogoCachedLine, line, cache->lines) {
ffStrbufDestroy(&line->chars);
@@ -175,6 +207,8 @@ static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bo
parsedHeight = options->height;
}
ffLogoResolveAutoPosition(maxLineWidth + options->paddingRight);
instance.state.logoHeight = options->paddingTop + parsedHeight;
if (options->position == FF_LOGO_POSITION_LEFT) {
instance.state.logoWidth = maxLineWidth + options->paddingRight;
@@ -195,6 +229,8 @@ static bool ffLogoPrintCharsRaw(const char* data, size_t length, bool printError
FFOptionsLogo* options = &instance.config.logo;
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
ffLogoResolveAutoPosition(options->width + options->paddingLeft + options->paddingRight);
if (!options->width || !options->height) {
if (options->position == FF_LOGO_POSITION_LEFT) {
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
+4
View File
@@ -24,6 +24,10 @@ typedef struct FFlogo {
} FFlogo;
// logo.c
enum { FF_LOGO_AUTO_MIN_INFO_WIDTH = 32 };
FFLogoPosition ffLogoSelectPosition(FFLogoPosition configured, uint32_t terminalColumns, uint32_t logoWidth);
void ffLogoResolveAutoPosition(uint32_t logoWidth);
void ffLogoPrint(void);
void ffLogoPrintChars(const char* data, bool doColorReplacement);
void ffLogoPrintLine(void);
+2 -2
View File
@@ -7,7 +7,7 @@
bool ffPrintTerminalSize(FFTerminalSizeOptions* options) {
FFTerminalSizeResult result = {};
if (!ffDetectTerminalSize(&result)) {
if (!ffDetectTerminalSize(&result, false)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(TerminalSize), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Failed to detect terminal size");
return false;
}
@@ -51,7 +51,7 @@ void ffGenerateTerminalSizeJsonConfig(FFTerminalSizeOptions* options, yyjson_mut
bool ffGenerateTerminalSizeJsonResult([[maybe_unused]] FFTerminalSizeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
FFTerminalSizeResult result;
if (!ffDetectTerminalSize(&result)) {
if (!ffDetectTerminalSize(&result, false)) {
yyjson_mut_obj_add_str(doc, module, "error", "Failed to detect terminal size");
return false;
}
+17 -5
View File
@@ -39,6 +39,20 @@ static void logoParseImageFlag(FFOptionsLogo* options, const char* key, const ch
#endif
}
const char* ffLogoPositionToString(FFLogoPosition position) {
switch (position) {
case FF_LOGO_POSITION_TOP:
return "top";
case FF_LOGO_POSITION_RIGHT:
return "right";
case FF_LOGO_POSITION_AUTO:
return "auto";
case FF_LOGO_POSITION_LEFT:
default:
return "left";
}
}
void ffOptionsInitLogo(FFOptionsLogo* options) {
ffStrbufInit(&options->source);
options->type = FF_LOGO_TYPE_AUTO;
@@ -162,6 +176,7 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons
exit(477);
} else if (ffStrEqualsIgnCase(subKey, "position")) {
options->position = (FFLogoPosition) ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
{ "auto", FF_LOGO_POSITION_AUTO },
{ "left", FF_LOGO_POSITION_LEFT },
{ "right", FF_LOGO_POSITION_RIGHT },
{ "top", FF_LOGO_POSITION_TOP },
@@ -403,6 +418,7 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo
} else if (unsafe_yyjson_equals_str(key, "position")) {
int value;
const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
{ "auto", FF_LOGO_POSITION_AUTO },
{ "left", FF_LOGO_POSITION_LEFT },
{ "top", FF_LOGO_POSITION_TOP },
{ "right", FF_LOGO_POSITION_RIGHT },
@@ -604,11 +620,7 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) {
break;
}
yyjson_mut_obj_add_str(doc, obj, "position", ((const char*[]) {
"left",
"top",
"right",
})[options->position]);
yyjson_mut_obj_add_str(doc, obj, "position", ffLogoPositionToString(options->position));
yyjson_mut_obj_add_int(doc, obj, "animationFrame", options->animationFrame);
+2
View File
@@ -28,6 +28,7 @@ typedef enum FFLogoPosition: uint8_t {
FF_LOGO_POSITION_LEFT,
FF_LOGO_POSITION_TOP,
FF_LOGO_POSITION_RIGHT,
FF_LOGO_POSITION_AUTO,
} FFLogoPosition;
typedef enum FFLogoCacheStrategy: uint8_t {
@@ -70,6 +71,7 @@ typedef struct FFOptionsLogo {
#endif
} FFOptionsLogo;
const char* ffLogoPositionToString(FFLogoPosition position);
void ffOptionsInitLogo(FFOptionsLogo* options);
bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, const char* value);
void ffOptionsDestroyLogo(FFOptionsLogo* options);