2022-04-13 23:04:42 +02:00
|
|
|
#include "image.h"
|
2026-01-06 09:48:38 +08:00
|
|
|
#include "common/io.h"
|
2026-09-13 22:22:06 +08:00
|
|
|
#include "common/mallocHelper.h"
|
2026-01-06 09:48:38 +08:00
|
|
|
#include "common/printing.h"
|
|
|
|
|
#include "common/processing.h"
|
2026-05-28 15:58:08 +08:00
|
|
|
#include "common/strutil.h"
|
2026-01-06 09:48:38 +08:00
|
|
|
#include "common/base64.h"
|
2024-09-03 21:31:43 +08:00
|
|
|
#include "detection/terminalsize/terminalsize.h"
|
2022-04-06 14:02:05 +02:00
|
|
|
|
2023-06-12 15:10:41 +02:00
|
|
|
#include <limits.h>
|
2023-08-22 15:31:10 +08:00
|
|
|
#include <math.h>
|
2023-06-12 15:10:41 +02:00
|
|
|
|
2023-01-09 13:42:58 +08:00
|
|
|
#ifdef __APPLE__
|
2026-04-15 16:16:46 +08:00
|
|
|
#include <sys/syslimits.h>
|
2023-04-02 20:40:37 +08:00
|
|
|
#elif _WIN32
|
2026-04-15 16:16:46 +08:00
|
|
|
#include <windows.h>
|
2024-06-28 10:23:38 +08:00
|
|
|
#elif __linux__
|
2026-04-15 16:16:46 +08:00
|
|
|
#include <sys/sendfile.h>
|
2024-07-03 15:12:48 +08:00
|
|
|
#elif __sun
|
2026-04-15 16:16:46 +08:00
|
|
|
#include <sys/termios.h>
|
2023-01-09 13:42:58 +08:00
|
|
|
#endif
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool printImageIterm(bool printError) {
|
2023-10-25 14:43:46 +08:00
|
|
|
const FFOptionsLogo* options = &instance.config.logo;
|
2023-04-15 15:29:54 +08:00
|
|
|
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffAppendFileBuffer(options->source.chars, &buf)) {
|
|
|
|
|
if (printError) {
|
2024-05-03 07:56:43 +08:00
|
|
|
fputs("Logo (iterm): Failed to load image file\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 21:37:16 +08:00
|
|
|
return false;
|
2023-01-01 12:29:08 +08:00
|
|
|
}
|
2022-12-22 21:37:16 +08:00
|
|
|
|
2023-09-02 00:35:18 -04:00
|
|
|
fflush(stdout);
|
2023-04-15 15:29:54 +08:00
|
|
|
|
2024-08-10 23:33:38 +08:00
|
|
|
bool inTmux = false;
|
|
|
|
|
{
|
|
|
|
|
const char* term = getenv("TERM");
|
|
|
|
|
inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-05 16:14:56 +08:00
|
|
|
FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&buf);
|
2023-09-02 00:35:18 -04:00
|
|
|
ffStrbufClear(&buf);
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
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);
|
|
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2023-09-15 10:53:09 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
|
|
|
|
|
ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
|
|
|
|
|
if (!options->width) {
|
|
|
|
|
if (printError) {
|
2024-07-24 09:59:23 +08:00
|
|
|
fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-07-24 09:59:23 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
2024-07-25 21:41:15 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
|
2024-06-27 20:39:22 +08:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\ePtmux;\e");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->width) {
|
2024-05-10 14:30:00 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u:%s\a", (unsigned) options->width, base64.chars);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2024-05-10 14:30:00 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e]1337;File=inline=1:%s\a", base64.chars);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\e\\");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-09-02 00:35:18 -04:00
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
|
2023-09-03 22:20:11 +08:00
|
|
|
uint16_t X = 0, Y = 0;
|
2024-09-10 14:48:47 +08:00
|
|
|
const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (error) {
|
2023-09-03 22:20:11 +08:00
|
|
|
fprintf(stderr, "\nLogo (iterm): fail to query cursor position: %s\n", error);
|
|
|
|
|
return true; // We already printed image logo, don't print ascii logo then
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (X < options->paddingLeft + options->width) {
|
2024-09-02 20:57:47 +08:00
|
|
|
X = (uint16_t) (options->paddingLeft + options->width);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2024-07-25 21:27:53 +08:00
|
|
|
instance.state.logoWidth = X + options->paddingRight - 1;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoHeight = Y;
|
|
|
|
|
fputs("\e[H", stdout);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2023-09-15 10:53:09 +08:00
|
|
|
ffPrintCharTimes('\n', options->paddingRight);
|
2023-09-02 00:35:18 -04:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2023-09-15 10:53:09 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-06-27 20:39:22 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->paddingLeft) {
|
2024-06-27 20:39:22 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\ePtmux;\e");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u;height=%u;preserveAspectRatio=%u:%s\a", (unsigned) options->width, (unsigned) options->height, (unsigned) options->preserveAspectRatio, base64.chars);
|
|
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\e\\");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendC(&buf, '\n');
|
2023-09-03 22:20:11 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2023-09-15 10:53:09 +08:00
|
|
|
instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
|
|
|
|
|
instance.state.logoHeight = options->paddingTop + options->height;
|
2023-09-03 22:20:11 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2023-09-15 10:53:09 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingRight, '\n');
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-06-27 20:39:22 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
|
|
|
|
ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
|
|
|
|
|
}
|
2023-09-02 00:35:18 -04:00
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
|
|
|
|
|
}
|
2022-12-22 21:37:16 +08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 00:44:02 +08:00
|
|
|
// `kitten icat` parses a cursor positioning sequence it may have written: `\e[<n>C` (relative) or
|
|
|
|
|
// `\e[<row>;<col>H` (absolute). Returns 1 for the first form, 2 for the second one, 0 for anything
|
|
|
|
|
// else, in which case nothing is written back.
|
|
|
|
|
static int parseIcatCsi(const char* data, uint32_t length, uint32_t pos, uint32_t* end, uint32_t* first, uint32_t* second) {
|
|
|
|
|
if (pos + 2 >= length || data[pos] != '\e' || data[pos + 1] != '[') {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t values[2] = { 0, 0 };
|
|
|
|
|
uint32_t count = 0;
|
|
|
|
|
uint32_t i = pos + 2;
|
|
|
|
|
while (i < length) {
|
|
|
|
|
char c = data[i];
|
|
|
|
|
if (c >= '0' && c <= '9') {
|
|
|
|
|
if (count == 0) {
|
|
|
|
|
count = 1;
|
|
|
|
|
}
|
|
|
|
|
values[count - 1] = values[count - 1] * 10 + (uint32_t) (c - '0');
|
|
|
|
|
++i;
|
|
|
|
|
} else if (c == ';' && count == 1) {
|
|
|
|
|
count = 2;
|
|
|
|
|
++i;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i >= length) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data[i] == 'C' && count == 1) {
|
|
|
|
|
*end = i + 1;
|
|
|
|
|
*first = values[0];
|
|
|
|
|
*second = 0;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data[i] == 'H' && count == 2) {
|
|
|
|
|
*end = i + 1;
|
|
|
|
|
*first = values[0];
|
|
|
|
|
*second = values[1];
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// `kitten icat` positions the image itself: it always writes a carriage return, followed by at most
|
|
|
|
|
// one positioning sequence, and then the graphics escape. The carriage return alone would pull the
|
|
|
|
|
// cursor back to column 1 and undo the padding, and the absolute cursor move `kitten` writes when
|
|
|
|
|
// `--place` is used points at screen coordinates it picked itself, which are not necessarily the ones
|
|
|
|
|
// the padding asks for. Both are dropped here and the position is left to the caller, which has
|
|
|
|
|
// already moved the cursor to where the padding asks for. The horizontal centering `kitten` applied
|
|
|
|
|
// inside the requested rectangle is kept, but re-applied relatively, so it still follows the caller's
|
|
|
|
|
// padding.
|
|
|
|
|
static void appendIcatOutput(FFstrbuf* buf, const FFstrbuf* output, const FFOptionsLogo* options) {
|
|
|
|
|
const char* data = output->chars;
|
|
|
|
|
uint32_t pos = 0; // Start of the graphics escape; 0 means "layout not recognised"
|
|
|
|
|
uint32_t offset = 0; // Column offset `kitten` applied inside the requested rectangle
|
|
|
|
|
|
|
|
|
|
if (output->length > 0 && data[0] == '\r') {
|
|
|
|
|
uint32_t cursor = 1;
|
|
|
|
|
uint32_t end = 0, first = 0, second = 0;
|
|
|
|
|
int kind = parseIcatCsi(data, output->length, cursor, &end, &first, &second);
|
|
|
|
|
if (kind == 1) { // Relative move, written instead of the absolute one when `--place` is not used
|
|
|
|
|
offset = first;
|
|
|
|
|
cursor = end;
|
|
|
|
|
} else if (kind == 2 && second >= options->paddingLeft + 1) { // Absolute move, produced by `--place`
|
|
|
|
|
// The row is dropped along with the move, so it does not matter which one `kitten` picked.
|
|
|
|
|
// The column is where the padding put it, plus the centering, which is all that is kept.
|
|
|
|
|
offset = second - options->paddingLeft - 1;
|
|
|
|
|
cursor = end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The graphics escape has to follow once the positioning is gone
|
|
|
|
|
if (cursor + 1 < output->length && data[cursor] == '\e' && data[cursor + 1] == '_') {
|
|
|
|
|
pos = cursor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pos == 0) {
|
|
|
|
|
// Unrecognised layout: keep it as is, positioning included
|
|
|
|
|
ffStrbufAppend(buf, output);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (offset > 0) {
|
|
|
|
|
// The cursor is already at the position the padding asks for
|
|
|
|
|
ffStrbufAppendF(buf, "\e[%uC", (unsigned) offset);
|
|
|
|
|
}
|
|
|
|
|
ffStrbufAppendNS(buf, output->length - pos, data + pos);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool printImageKittyIcat(bool printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
const FFOptionsLogo* options = &instance.config.logo;
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
|
|
|
|
|
if (printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fputs("Logo (kitty-icat): Failed to load image file\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fflush(stdout);
|
|
|
|
|
|
|
|
|
|
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
|
|
|
|
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
|
|
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
|
|
|
|
if (!options->width) {
|
2024-12-31 15:30:26 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
|
|
|
|
|
ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
|
|
|
|
if (printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fputs("Logo (kitty-icat): position top is not supported when logo width is set\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
|
|
|
|
|
if (printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fputs("Logo (kitty-icat): position right is not supported\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 00:44:02 +08:00
|
|
|
// `kitten icat` writes its own positioning, so its output is collected separately and only
|
|
|
|
|
// appended once that positioning has been dropped (see appendIcatOutput)
|
|
|
|
|
FF_STRBUF_AUTO_DESTROY icatOutput = ffStrbufCreate();
|
2024-12-31 15:30:26 +08:00
|
|
|
|
2026-07-10 18:08:04 +08:00
|
|
|
const char* error = nullptr;
|
2024-12-31 15:30:26 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->width) {
|
2026-09-15 00:44:02 +08:00
|
|
|
// `--place` measures `left` and `top` from the top left corner of the screen, so the padding is
|
|
|
|
|
// passed through unchanged. The move `kitten` writes for it is dropped again in appendIcatOutput,
|
|
|
|
|
// which leaves the position to the caller, so this is not what puts the image at the padding in
|
|
|
|
|
// the normal case. It is what keeps the fallback there working: when the output layout is not
|
|
|
|
|
// recognised - as under tmux, where `kitten` switches to unicode placeholders - its own
|
|
|
|
|
// positioning is kept, and this is what then places the image at the padding.
|
2024-12-31 15:30:26 +08:00
|
|
|
char place[64];
|
|
|
|
|
snprintf(place,
|
2024-12-31 15:43:01 +08:00
|
|
|
ARRAY_SIZE(place),
|
|
|
|
|
"--place=%ux%u@%ux%u",
|
|
|
|
|
options->width,
|
|
|
|
|
options->height == 0 ? 9999 : options->height,
|
2026-09-15 00:44:02 +08:00
|
|
|
options->paddingLeft,
|
|
|
|
|
options->paddingTop);
|
2024-12-31 15:30:26 +08:00
|
|
|
|
2026-09-15 00:44:02 +08:00
|
|
|
error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
|
2026-03-29 09:28:49 +08:00
|
|
|
"kitten",
|
|
|
|
|
"icat",
|
|
|
|
|
"-n",
|
2026-09-15 00:13:06 +08:00
|
|
|
"--stdin=no",
|
2026-03-29 09:28:49 +08:00
|
|
|
"--align=center",
|
|
|
|
|
place,
|
|
|
|
|
"--scale-up",
|
|
|
|
|
options->source.chars,
|
2026-07-10 18:08:04 +08:00
|
|
|
nullptr,
|
2026-03-29 09:28:49 +08:00
|
|
|
});
|
|
|
|
|
} else {
|
2026-09-15 00:44:02 +08:00
|
|
|
error = ffProcessAppendStdOut(&icatOutput, (char*[]) {
|
2026-03-29 09:28:49 +08:00
|
|
|
"kitten",
|
|
|
|
|
"icat",
|
|
|
|
|
"-n",
|
2026-09-15 00:13:06 +08:00
|
|
|
"--stdin=no",
|
2026-03-29 09:28:49 +08:00
|
|
|
"--align=left",
|
|
|
|
|
options->source.chars,
|
2026-07-10 18:08:04 +08:00
|
|
|
nullptr,
|
2026-03-29 09:28:49 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (error) {
|
|
|
|
|
if (printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fprintf(stderr, "Logo (kitty-icat): running `kitten icat` failed %s\n", error);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 00:44:02 +08:00
|
|
|
if (icatOutput.length == 0) {
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printError) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 00:44:02 +08:00
|
|
|
appendIcatOutput(&buf, &icatOutput, options);
|
|
|
|
|
|
2024-12-31 15:30:26 +08:00
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-12-31 15:30:26 +08:00
|
|
|
uint16_t X = 0, Y = 0;
|
|
|
|
|
const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (error) {
|
2024-12-31 15:30:26 +08:00
|
|
|
fprintf(stderr, "\nLogo (kitty-icat): fail to query cursor position: %s\n", error);
|
|
|
|
|
return true; // We already printed image logo, don't print ascii logo then
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (X < options->paddingLeft + options->width) {
|
2024-12-31 15:30:26 +08:00
|
|
|
X = (uint16_t) (options->paddingLeft + options->width);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2024-12-31 15:30:26 +08:00
|
|
|
instance.state.logoWidth = X + options->paddingRight - 1;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
instance.state.logoHeight = Y;
|
|
|
|
|
fputs("\e[H", stdout);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2024-12-31 15:30:26 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
|
|
|
|
ffPrintCharTimes('\n', options->paddingRight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool printImageKittyDirect(bool printError) {
|
2023-10-25 14:43:46 +08:00
|
|
|
const FFOptionsLogo* options = &instance.config.logo;
|
2024-05-03 07:56:43 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE)) {
|
|
|
|
|
if (printError) {
|
2024-05-03 07:56:43 +08:00
|
|
|
fputs("Logo (kitty-direct): Failed to load image file\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-05-03 07:56:43 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-10 23:33:38 +08:00
|
|
|
fflush(stdout);
|
|
|
|
|
|
|
|
|
|
bool inTmux = false;
|
|
|
|
|
{
|
|
|
|
|
const char* term = getenv("TERM");
|
|
|
|
|
inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-05 16:14:56 +08:00
|
|
|
FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&options->source);
|
2024-08-10 23:33:38 +08:00
|
|
|
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
2023-09-02 00:35:18 -04:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!options->width || !options->height) {
|
|
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2023-09-03 22:20:11 +08:00
|
|
|
// We must clear the entre screen to make sure that terminal buffer won't scroll up
|
2026-03-29 09:28:49 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH", (unsigned) options->paddingTop + 1, (unsigned) options->paddingLeft + 1);
|
|
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
|
|
|
|
|
ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
|
|
|
|
|
if (!options->width) {
|
|
|
|
|
if (printError) {
|
2024-07-24 09:59:23 +08:00
|
|
|
fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-07-24 09:59:23 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
|
2024-06-27 20:39:22 +08:00
|
|
|
}
|
2024-05-10 14:30:00 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\ePtmux;\e");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (options->width) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u;%s", (unsigned) options->width, base64.chars);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f;%s", base64.chars);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendC(&buf, '\e');
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\e\\");
|
2026-03-29 09:28:49 +08:00
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\e\\");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
|
|
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT) {
|
2023-09-03 22:20:11 +08:00
|
|
|
uint16_t X = 0, Y = 0;
|
2024-09-10 14:48:47 +08:00
|
|
|
const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (error) {
|
|
|
|
|
if (printError) {
|
2024-07-25 21:27:53 +08:00
|
|
|
fprintf(stderr, "\nLogo (kitty-direct): fail to query cursor position: %s\n", error);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-09-03 22:20:11 +08:00
|
|
|
return true; // We already printed image logo, don't print ascii logo then
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (X < options->paddingLeft + options->width) {
|
2024-09-02 20:57:47 +08:00
|
|
|
X = (uint16_t) (options->paddingLeft + options->width);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2024-07-25 21:27:53 +08:00
|
|
|
instance.state.logoWidth = X + options->paddingRight - 1;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoHeight = Y;
|
|
|
|
|
fputs("\e[H", stdout);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2023-09-15 10:53:09 +08:00
|
|
|
ffPrintCharTimes('\n', options->paddingRight);
|
2023-09-02 00:35:18 -04:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingTop, '\n');
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->paddingLeft) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-09-02 00:35:18 -04:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\ePtmux;\e");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u,r=%u;%s\e\\", (unsigned) options->width, (unsigned) options->height, base64.chars);
|
|
|
|
|
if (inTmux) {
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendS(&buf, "\e\\");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendC(&buf, '\n');
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_LEFT) {
|
2023-09-15 10:53:09 +08:00
|
|
|
instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
|
|
|
|
|
instance.state.logoHeight = options->paddingTop + options->height;
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_TOP) {
|
2023-09-03 22:20:11 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendNC(&buf, options->paddingRight, '\n');
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-06-27 20:39:22 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2024-08-10 23:33:38 +08:00
|
|
|
ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
|
2024-06-27 20:39:22 +08:00
|
|
|
}
|
2024-08-10 23:33:38 +08:00
|
|
|
|
|
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
|
2023-07-18 21:09:37 +08:00
|
|
|
}
|
2022-12-23 12:34:30 +08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 10:59:03 +08:00
|
|
|
#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6) || defined(_WIN32) || defined(__APPLE__) || defined(FF_HAVE_SIXEL)
|
2022-04-06 14:02:05 +02:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#define FF_KITTY_MAX_CHUNK_SIZE 4096
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#define FF_CACHE_FILE_HEIGHT "height"
|
|
|
|
|
#define FF_CACHE_FILE_WIDTH "width"
|
|
|
|
|
#define FF_CACHE_FILE_SIXEL "sixel"
|
|
|
|
|
#define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc"
|
|
|
|
|
#define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu"
|
|
|
|
|
#define FF_CACHE_FILE_CHAFA "chafa"
|
2026-09-14 22:43:19 +08:00
|
|
|
// Modification time of the image source the entry was produced from. Written last, so an
|
|
|
|
|
// entry that was interrupted mid-write is never mistaken for a complete one.
|
|
|
|
|
#define FF_CACHE_FILE_MTIME "mtime"
|
2022-04-19 11:48:08 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
#include <stdlib.h>
|
2026-04-15 16:16:46 +08:00
|
|
|
#include <string.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <fcntl.h>
|
2022-12-20 11:37:54 +08:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifndef _WIN32
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#else
|
|
|
|
|
#include <wincon.h>
|
|
|
|
|
#include "common/path.h"
|
|
|
|
|
#endif
|
2022-04-19 11:48:08 +02:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef FF_HAVE_ZLIB
|
|
|
|
|
#include "common/library.h"
|
|
|
|
|
#include <zlib.h>
|
2022-04-14 15:11:14 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool compressBlob(void** blob, size_t* length) {
|
2024-09-04 15:28:35 +08:00
|
|
|
FF_LIBRARY_LOAD(zlib, false, "libz" FF_LIBRARY_EXTENSION, 2)
|
2022-04-14 15:11:14 +02:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false)
|
|
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
#if _WIN32
|
|
|
|
|
// zlib's uLong is 32-bit on Windows (LLP64), so a >4 GiB source can't be
|
|
|
|
|
// compressed through this API; reject it instead of silently truncating
|
|
|
|
|
if (*length > (size_t) ULONG_MAX) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
uLong compressedLength = ffcompressBound((uLong) *length);
|
2022-04-14 15:11:14 +02:00
|
|
|
void* compressed = malloc(compressedLength);
|
2026-07-10 18:08:04 +08:00
|
|
|
if (compressed == nullptr) {
|
2022-04-14 15:11:14 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-14 15:11:14 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
if (ffcompress2(compressed, &compressedLength, *blob, (uLong) *length, Z_BEST_COMPRESSION) != Z_OK) {
|
2022-04-14 15:11:14 +02:00
|
|
|
free(compressed);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(*blob);
|
|
|
|
|
|
|
|
|
|
*length = (size_t) compressedLength;
|
|
|
|
|
*blob = compressed;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif // FF_HAVE_ZLIB
|
2022-04-14 15:11:14 +02:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
static void writeCacheData(FFLogoRequestData* requestData, const void* value, size_t len, const char* cacheFileName) {
|
2026-09-14 23:45:28 +08:00
|
|
|
// Every payload file goes through here, including the ones written from the printing helpers,
|
|
|
|
|
// so this is the single place that keeps `--logo-cache false` from writing anything at all.
|
|
|
|
|
if (instance.config.logo.cache == FF_LOGO_CACHE_OFF) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-23 15:32:10 +02:00
|
|
|
uint32_t cacheDirLength = requestData->cacheDir.length;
|
|
|
|
|
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
|
2026-09-14 22:43:19 +08:00
|
|
|
ffWriteFileData(requestData->cacheDir.chars, len, value);
|
2022-05-23 15:32:10 +02:00
|
|
|
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName) {
|
2023-10-25 14:43:46 +08:00
|
|
|
const FFOptionsLogo* options = &instance.config.logo;
|
2026-03-29 09:28:49 +08:00
|
|
|
// Calculate character dimensions
|
2023-09-15 10:53:09 +08:00
|
|
|
instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
|
|
|
|
|
instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;
|
2022-04-28 19:15:46 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
// Write cache files
|
2026-09-14 22:43:19 +08:00
|
|
|
writeCacheData(requestData, result->chars, result->length, cacheFileName);
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->width == 0) {
|
2026-09-14 22:43:19 +08:00
|
|
|
writeCacheData(requestData, &requestData->logoCharacterWidth, sizeof(requestData->logoCharacterWidth), FF_CACHE_FILE_WIDTH);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->height == 0) {
|
2026-09-14 22:43:19 +08:00
|
|
|
writeCacheData(requestData, &requestData->logoCharacterHeight, sizeof(requestData->logoCharacterHeight), FF_CACHE_FILE_HEIGHT);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
// Write result to stdout
|
2023-09-15 10:53:09 +08:00
|
|
|
ffPrintCharTimes('\n', options->paddingTop);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->paddingLeft) {
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[%uC", (unsigned) options->paddingLeft);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
fflush(stdout);
|
2023-01-28 22:05:23 +08:00
|
|
|
ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), result);
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position != FF_LOGO_POSITION_TOP) {
|
|
|
|
|
// Go to upper left corner
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[1G\e[%uA", instance.state.logoHeight);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position != FF_LOGO_POSITION_LEFT) {
|
2024-06-28 10:05:38 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-14 11:36:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
// The backends report the real pixel dimensions; the character dimensions are derived here
|
|
|
|
|
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);
|
|
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
static bool printImageSixel(FFLogoRequestData* requestData, const FFstrbuf* result) {
|
|
|
|
|
if (result->length == 0) {
|
2022-04-22 13:20:40 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
printImagePixels(requestData, result, FF_CACHE_FILE_SIXEL);
|
2022-04-22 13:20:40 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode) {
|
2022-04-22 13:20:40 +02:00
|
|
|
uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length;
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printEscapeCode) {
|
2022-04-22 13:20:40 +02:00
|
|
|
ffStrbufAppendS(result, "\033_G");
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2022-04-22 13:20:40 +02:00
|
|
|
ffStrbufAppendC(result, ',');
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
|
|
|
|
|
ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0");
|
|
|
|
|
ffStrbufAppendC(result, ';');
|
|
|
|
|
ffStrbufAppendNS(result, chunkSize, *blob);
|
|
|
|
|
ffStrbufAppendS(result, "\033\\");
|
|
|
|
|
*length -= chunkSize;
|
|
|
|
|
*blob += chunkSize;
|
2022-04-14 11:36:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
static bool printImageKitty(FFLogoRequestData* requestData, const FFImageBuffer* buffer) {
|
|
|
|
|
size_t length = (size_t) buffer->width * buffer->height * 4;
|
|
|
|
|
FF_AUTO_FREE void* blob = malloc(length);
|
|
|
|
|
if (blob == nullptr) {
|
2022-04-14 11:36:57 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
memcpy(blob, buffer->data, length);
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef FF_HAVE_ZLIB
|
2026-03-29 09:28:49 +08:00
|
|
|
bool isCompressed = compressBlob(&blob, &length);
|
2026-04-15 16:16:46 +08:00
|
|
|
#else
|
2026-03-29 09:28:49 +08:00
|
|
|
bool isCompressed = false;
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2022-04-14 15:11:14 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
// base64 output is 4 * ceil(length / 3) bytes, plus the terminating null byte
|
|
|
|
|
FF_STRBUF_AUTO_DESTROY base64 = ffStrbufCreateA((uint32_t) (10 + length * 4 / 3));
|
|
|
|
|
ffBase64EncodeRaw((uint32_t) length, (const char*) blob, &base64.length, base64.chars);
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(base64.length + 1024);
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
const char* currentPos = base64.chars;
|
|
|
|
|
size_t remainingLength = base64.length;
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", buffer->width, buffer->height);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (isCompressed) {
|
2022-04-22 13:20:40 +02:00
|
|
|
ffStrbufAppendS(&result, ",o=z");
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
appendKittyChunk(&result, ¤tPos, &remainingLength, false);
|
2026-03-29 09:28:49 +08:00
|
|
|
while (remainingLength > 0) {
|
2022-04-22 13:20:40 +02:00
|
|
|
appendKittyChunk(&result, ¤tPos, &remainingLength, true);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-14 11:36:57 +02:00
|
|
|
|
2023-06-24 10:59:53 +08:00
|
|
|
printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED);
|
2022-04-14 11:36:57 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef FF_HAVE_CHAFA
|
|
|
|
|
#include <chafa.h>
|
2026-09-13 22:22:06 +08:00
|
|
|
static bool printImageChafa(FFLogoRequestData* requestData, const FFImageBuffer* buffer) {
|
|
|
|
|
#if _WIN32
|
2026-06-30 18:42:51 +08:00
|
|
|
FF_LIBRARY_LOAD(chafa, false, "libchafa-0" FF_LIBRARY_EXTENSION, 0)
|
2026-09-13 22:22:06 +08:00
|
|
|
#else
|
2026-06-30 18:42:51 +08:00
|
|
|
FF_LIBRARY_LOAD(chafa, false, "libchafa" FF_LIBRARY_EXTENSION, 1)
|
2026-09-13 22:22:06 +08:00
|
|
|
#endif
|
2022-04-28 19:15:46 +02:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false)
|
2022-12-22 02:07:35 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false)
|
2022-04-28 19:15:46 +02:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_new, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_geometry, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_symbol_map, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_new, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_draw_all_pixels, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_print, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_unref, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_unref, false)
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_unref, false)
|
2022-12-22 02:07:35 +08:00
|
|
|
|
2022-04-28 19:15:46 +02:00
|
|
|
ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new();
|
2026-07-10 18:08:04 +08:00
|
|
|
GError* error = nullptr;
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error)) {
|
2022-12-22 02:07:35 +08:00
|
|
|
fputs(error->message, stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-28 19:15:46 +02:00
|
|
|
|
|
|
|
|
ChafaCanvasConfig* canvasConfig = ffchafa_canvas_config_new();
|
2022-05-23 15:32:10 +02:00
|
|
|
ffchafa_canvas_config_set_geometry(canvasConfig, (gint) requestData->logoCharacterWidth, (gint) requestData->logoCharacterHeight);
|
2022-04-28 19:15:46 +02:00
|
|
|
ffchafa_canvas_config_set_symbol_map(canvasConfig, symbolMap);
|
2022-12-22 11:36:13 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (instance.config.logo.chafaFgOnly) {
|
2022-12-22 11:36:13 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_fg_only_enabled);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffchafa_canvas_config_set_fg_only_enabled) {
|
2022-12-22 11:36:13 +08:00
|
|
|
ffchafa_canvas_config_set_fg_only_enabled(canvasConfig, true);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 11:36:13 +08:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (instance.config.logo.chafaCanvasMode < CHAFA_CANVAS_MODE_MAX) {
|
2022-12-22 11:36:13 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_canvas_mode);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffchafa_canvas_config_set_canvas_mode) {
|
2023-06-24 10:59:53 +08:00
|
|
|
ffchafa_canvas_config_set_canvas_mode(canvasConfig, (ChafaCanvasMode) instance.config.logo.chafaCanvasMode);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 11:36:13 +08:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (instance.config.logo.chafaColorSpace < CHAFA_COLOR_SPACE_MAX) {
|
2022-12-22 11:36:13 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_color_space)
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffchafa_canvas_config_set_color_space) {
|
2023-06-24 10:59:53 +08:00
|
|
|
ffchafa_canvas_config_set_color_space(canvasConfig, (ChafaColorSpace) instance.config.logo.chafaColorSpace);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 11:36:13 +08:00
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (instance.config.logo.chafaDitherMode < CHAFA_DITHER_MODE_MAX) {
|
2022-12-22 11:36:13 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_dither_mode)
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffchafa_canvas_config_set_dither_mode) {
|
2023-06-24 10:59:53 +08:00
|
|
|
ffchafa_canvas_config_set_dither_mode(canvasConfig, (ChafaDitherMode) instance.config.logo.chafaDitherMode);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 11:36:13 +08:00
|
|
|
}
|
2022-04-28 19:15:46 +02:00
|
|
|
|
|
|
|
|
ChafaCanvas* canvas = ffchafa_canvas_new(canvasConfig);
|
|
|
|
|
ffchafa_canvas_draw_all_pixels(
|
|
|
|
|
canvas,
|
|
|
|
|
CHAFA_PIXEL_RGBA8_UNASSOCIATED,
|
2026-09-13 22:22:06 +08:00
|
|
|
buffer->data,
|
|
|
|
|
(gint) buffer->width,
|
|
|
|
|
(gint) buffer->height,
|
|
|
|
|
(gint) buffer->width * 4);
|
2022-04-28 19:15:46 +02:00
|
|
|
|
2026-07-10 18:08:04 +08:00
|
|
|
GString* str = ffchafa_canvas_print(canvas, nullptr);
|
2022-04-28 19:15:46 +02:00
|
|
|
FFstrbuf result;
|
2022-05-23 15:32:10 +02:00
|
|
|
result.allocated = (uint32_t) str->allocated_len;
|
2022-04-28 19:15:46 +02:00
|
|
|
result.length = (uint32_t) str->len;
|
2022-05-23 15:32:10 +02:00
|
|
|
result.chars = str->str;
|
2022-12-20 22:44:45 +08:00
|
|
|
|
2023-06-24 10:59:53 +08:00
|
|
|
ffLogoPrintChars(result.chars, false);
|
2026-09-15 20:11:15 +08:00
|
|
|
writeCacheData(requestData, result.chars, result.length, FF_CACHE_FILE_CHAFA);
|
2022-04-28 19:15:46 +02:00
|
|
|
|
2022-12-22 11:36:13 +08:00
|
|
|
// FIXME: These functions must be imported from `libglib` dlls on Windows
|
|
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffg_string_free) {
|
2022-12-22 11:36:13 +08:00
|
|
|
ffg_string_free(str, TRUE);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
if (error) {
|
2022-12-22 11:36:13 +08:00
|
|
|
FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_error_free)
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffg_error_free) {
|
2022-12-22 11:36:13 +08:00
|
|
|
ffg_error_free(error);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-22 11:36:13 +08:00
|
|
|
}
|
2022-12-22 02:07:35 +08:00
|
|
|
|
2022-04-28 19:15:46 +02:00
|
|
|
ffchafa_canvas_unref(canvas);
|
|
|
|
|
ffchafa_canvas_config_unref(canvasConfig);
|
|
|
|
|
ffchafa_symbol_map_unref(symbolMap);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2022-04-28 19:15:46 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
bool ffImageCreate(FFLogoRequestData* requestData, FFImageBuffer* out, const char** error) {
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
return ffImageCreateWIC(requestData, out, error);
|
2026-09-14 10:59:03 +08:00
|
|
|
#elif defined(__APPLE__)
|
|
|
|
|
return ffImageCreateImageIO(requestData, out, error);
|
2026-09-13 22:22:06 +08:00
|
|
|
#else
|
|
|
|
|
#ifdef FF_HAVE_IMAGEMAGICK7
|
|
|
|
|
if (ffImageCreateIM7(requestData, out, error)) {
|
|
|
|
|
return true;
|
2022-12-20 11:01:13 +08:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
#endif
|
|
|
|
|
#ifdef FF_HAVE_IMAGEMAGICK6
|
|
|
|
|
if (ffImageCreateIM6(requestData, out, error)) {
|
|
|
|
|
return true;
|
2022-04-06 14:02:05 +02:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
#endif
|
|
|
|
|
return false;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
2022-04-06 14:02:05 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
void ffImageDestroy(FFImageBuffer* buffer) {
|
|
|
|
|
free(buffer->data);
|
|
|
|
|
buffer->data = nullptr;
|
|
|
|
|
buffer->width = 0;
|
|
|
|
|
buffer->height = 0;
|
|
|
|
|
}
|
2022-04-06 14:02:05 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
bool ffImageSixelEncode(FFLogoRequestData* requestData, FFstrbuf* out, const char** error) {
|
2026-09-14 10:59:03 +08:00
|
|
|
// Windows (WIC) and macOS (ImageIO) decode and resize to RGBA first, then the embedded
|
|
|
|
|
// libsixel encoder takes over. Other platforms let ImageMagick encode straight from the
|
|
|
|
|
// decoded image without an RGBA round trip.
|
|
|
|
|
#if defined(_WIN32) || defined(__APPLE__)
|
2026-09-13 22:22:06 +08:00
|
|
|
#ifdef FF_HAVE_SIXEL
|
|
|
|
|
FFImageBuffer buffer = {};
|
|
|
|
|
if (!ffImageCreate(requestData, &buffer, error)) {
|
|
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
bool ok = ffSixelEncode(&buffer, out, error);
|
|
|
|
|
ffImageDestroy(&buffer);
|
|
|
|
|
return ok;
|
|
|
|
|
#else
|
2026-09-14 10:59:03 +08:00
|
|
|
FF_UNUSED(requestData, out);
|
2026-09-13 22:22:06 +08:00
|
|
|
if (error) {
|
|
|
|
|
*error = "sixel support is not compiled in";
|
2022-04-28 19:15:46 +02:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
return false;
|
|
|
|
|
#endif
|
|
|
|
|
#else
|
|
|
|
|
#ifdef FF_HAVE_IMAGEMAGICK7
|
|
|
|
|
if (ffImageSixelEncodeIM7(requestData, out, error)) {
|
|
|
|
|
return true;
|
2022-04-06 14:02:05 +02:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
#endif
|
|
|
|
|
#ifdef FF_HAVE_IMAGEMAGICK6
|
|
|
|
|
if (ffImageSixelEncodeIM6(requestData, out, error)) {
|
|
|
|
|
return true;
|
2022-04-06 14:02:05 +02:00
|
|
|
}
|
2026-09-13 22:22:06 +08:00
|
|
|
#endif
|
|
|
|
|
return false;
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2022-04-06 14:02:05 +02:00
|
|
|
}
|
2022-04-13 23:04:42 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName) {
|
2022-04-21 15:50:45 +02:00
|
|
|
uint32_t cacheDirLength = requestData->cacheDir.length;
|
2022-04-22 13:20:40 +02:00
|
|
|
ffStrbufAppendS(&requestData->cacheDir, fileName);
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifndef _WIN32
|
2023-12-04 20:32:46 +08:00
|
|
|
int fd = open(requestData->cacheDir.chars, O_RDONLY
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef O_CLOEXEC
|
2023-12-04 20:32:46 +08:00
|
|
|
| O_CLOEXEC
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2023-12-04 20:32:46 +08:00
|
|
|
);
|
2026-04-15 16:16:46 +08:00
|
|
|
#else
|
2026-07-10 18:08:04 +08:00
|
|
|
HANDLE fd = CreateFileA(requestData->cacheDir.chars, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2022-04-21 15:50:45 +02:00
|
|
|
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
|
2022-04-22 13:20:40 +02:00
|
|
|
return fd;
|
2022-04-21 15:50:45 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
static bool readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName) {
|
2022-05-23 15:32:10 +02:00
|
|
|
uint32_t cacheDirLength = requestData->cacheDir.length;
|
|
|
|
|
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
|
2026-09-14 22:43:19 +08:00
|
|
|
bool res = ffAppendFileBuffer(requestData->cacheDir.chars, result);
|
2022-05-23 15:32:10 +02:00
|
|
|
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
|
2026-09-14 22:43:19 +08:00
|
|
|
return res;
|
2022-05-23 15:32:10 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
static bool readCachedData(FFLogoRequestData* requestData, void* buffer, size_t bufferSize, const char* cacheFileName) {
|
|
|
|
|
uint32_t cacheDirLength = requestData->cacheDir.length;
|
|
|
|
|
ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
|
|
|
|
|
bool res = ffReadFileData(requestData->cacheDir.chars, bufferSize, buffer) == (ssize_t) bufferSize;
|
|
|
|
|
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName) {
|
2022-05-23 15:32:10 +02:00
|
|
|
uint32_t result = 0;
|
2026-09-14 22:43:19 +08:00
|
|
|
if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
|
2022-05-23 15:32:10 +02:00
|
|
|
return 0;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint64_t readCachedUint64(FFLogoRequestData* requestData, const char* cacheFileName) {
|
|
|
|
|
uint64_t result = 0;
|
|
|
|
|
if (!readCachedData(requestData, &result, sizeof(result), cacheFileName)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2022-05-23 15:32:10 +02:00
|
|
|
return result;
|
|
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
// Drops everything a previous version of the source left in the entry directory.
|
|
|
|
|
// The directory is keyed on the source path and the pixel size only, so it is reused across
|
|
|
|
|
// edits; without this, a payload written for another logo type would be read back.
|
|
|
|
|
static void removeCachedFiles(FFLogoRequestData* requestData) {
|
|
|
|
|
static const char* const files[] = {
|
|
|
|
|
FF_CACHE_FILE_MTIME,
|
|
|
|
|
FF_CACHE_FILE_WIDTH,
|
|
|
|
|
FF_CACHE_FILE_HEIGHT,
|
|
|
|
|
FF_CACHE_FILE_SIXEL,
|
|
|
|
|
FF_CACHE_FILE_KITTY_COMPRESSED,
|
|
|
|
|
FF_CACHE_FILE_KITTY_UNCOMPRESSED,
|
|
|
|
|
FF_CACHE_FILE_CHAFA,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
uint32_t cacheDirLength = requestData->cacheDir.length;
|
|
|
|
|
for (uint32_t i = 0; i < ARRAY_SIZE(files); ++i) {
|
|
|
|
|
ffStrbufAppendS(&requestData->cacheDir, files[i]);
|
|
|
|
|
ffRemoveFile(requestData->cacheDir.chars);
|
|
|
|
|
ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 18:27:36 +08:00
|
|
|
static bool printCachedChars(FFLogoRequestData* requestData, const char* cacheFileName) {
|
|
|
|
|
FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
|
|
|
|
|
readCachedStrbuf(requestData, &content, cacheFileName);
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (content.length == 0) {
|
2022-05-23 15:32:10 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2023-06-24 10:59:53 +08:00
|
|
|
ffLogoPrintChars(content.chars, false);
|
2022-05-23 15:32:10 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool printCachedPixel(FFLogoRequestData* requestData) {
|
2024-06-28 10:05:38 +08:00
|
|
|
FFOptionsLogo* options = &instance.config.logo;
|
|
|
|
|
|
|
|
|
|
requestData->logoCharacterWidth = options->width;
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->logoCharacterWidth == 0) {
|
2022-05-23 15:32:10 +02:00
|
|
|
requestData->logoCharacterWidth = readCachedUint32(requestData, FF_CACHE_FILE_WIDTH);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->logoCharacterWidth == 0) {
|
2022-05-23 15:32:10 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-05-23 15:32:10 +02:00
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2024-06-28 10:05:38 +08:00
|
|
|
requestData->logoCharacterHeight = options->height;
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->logoCharacterHeight == 0) {
|
2022-05-23 15:32:10 +02:00
|
|
|
requestData->logoCharacterHeight = readCachedUint32(requestData, FF_CACHE_FILE_HEIGHT);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->logoCharacterHeight == 0) {
|
2022-04-30 11:22:51 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-30 11:22:51 +02:00
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2024-06-28 18:08:16 +08:00
|
|
|
FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD;
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->type == FF_LOGO_TYPE_IMAGE_KITTY) {
|
2022-04-22 13:20:40 +02:00
|
|
|
fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffIsValidNativeFD(fd)) {
|
2022-04-22 13:20:40 +02:00
|
|
|
fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
} else if (requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL) {
|
2022-04-22 13:20:40 +02:00
|
|
|
fd = getCacheFD(requestData, FF_CACHE_FILE_SIXEL);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffIsValidNativeFD(fd)) {
|
2022-04-22 13:20:40 +02:00
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
|
2024-06-28 10:05:38 +08:00
|
|
|
ffPrintCharTimes('\n', options->paddingTop);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position == FF_LOGO_POSITION_RIGHT) {
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
|
2026-03-29 09:28:49 +08:00
|
|
|
} else if (options->paddingLeft) {
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[%uC", (unsigned) options->paddingLeft);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
fflush(stdout);
|
|
|
|
|
|
2024-06-28 10:23:38 +08:00
|
|
|
bool sent = false;
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef __linux__
|
2024-06-28 10:23:38 +08:00
|
|
|
struct stat st;
|
2026-03-29 09:28:49 +08:00
|
|
|
if (fstat(fd, &st) >= 0) {
|
|
|
|
|
while (st.st_size > 0) {
|
2026-07-10 18:08:04 +08:00
|
|
|
ssize_t bytes = sendfile(STDOUT_FILENO, fd, nullptr, (size_t) st.st_size);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (bytes > 0) {
|
2024-06-28 10:23:38 +08:00
|
|
|
sent = true;
|
|
|
|
|
st.st_size -= bytes;
|
2026-03-29 09:28:49 +08:00
|
|
|
} else {
|
2024-06-28 10:23:38 +08:00
|
|
|
break;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-06-28 10:23:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2024-06-28 10:23:38 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!sent) {
|
2024-06-28 10:23:38 +08:00
|
|
|
char buffer[32768];
|
|
|
|
|
ssize_t readBytes;
|
2026-03-29 09:28:49 +08:00
|
|
|
while ((readBytes = ffReadFDData(fd, sizeof(buffer), buffer)) > 0) {
|
2024-06-28 10:23:38 +08:00
|
|
|
ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), (size_t) readBytes, buffer);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-06-28 10:23:38 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
|
2024-06-28 10:05:38 +08:00
|
|
|
instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
|
|
|
|
|
instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop;
|
2022-04-22 13:20:40 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position != FF_LOGO_POSITION_TOP) {
|
|
|
|
|
// Go to upper left corner
|
2024-06-28 10:05:38 +08:00
|
|
|
printf("\e[1G\e[%uA", instance.state.logoHeight);
|
|
|
|
|
}
|
2022-05-23 15:32:10 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (options->position != FF_LOGO_POSITION_LEFT) {
|
2024-06-28 10:05:38 +08:00
|
|
|
instance.state.logoWidth = instance.state.logoHeight = 0;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-22 13:20:40 +02:00
|
|
|
return true;
|
2022-04-21 15:50:45 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool getCharacterPixelDimensions(FFLogoRequestData* requestData) {
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef _WIN32
|
2022-12-20 11:37:54 +08:00
|
|
|
|
2026-03-30 10:27:23 +08:00
|
|
|
CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) };
|
2026-03-29 09:28:49 +08:00
|
|
|
if (GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) // Only works for ConHost
|
2024-09-03 21:31:43 +08:00
|
|
|
{
|
|
|
|
|
requestData->characterPixelWidth = cfi.dwFontSize.X;
|
|
|
|
|
requestData->characterPixelHeight = cfi.dwFontSize.Y;
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
if (requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0) {
|
2024-09-03 21:31:43 +08:00
|
|
|
return true;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2022-12-20 11:37:54 +08:00
|
|
|
|
2024-09-03 21:31:43 +08:00
|
|
|
FFTerminalSizeResult termSize = {};
|
2026-03-29 09:28:49 +08:00
|
|
|
if (ffDetectTerminalSize(&termSize)) {
|
2024-09-03 21:31:43 +08:00
|
|
|
requestData->characterPixelWidth = termSize.width / (double) termSize.columns;
|
|
|
|
|
requestData->characterPixelHeight = termSize.height / (double) termSize.rows;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-23 15:32:10 +02:00
|
|
|
return requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0;
|
2022-04-21 15:50:45 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) {
|
2022-04-21 15:50:45 +02:00
|
|
|
FFLogoRequestData requestData;
|
|
|
|
|
requestData.type = type;
|
2022-05-23 15:32:10 +02:00
|
|
|
requestData.characterPixelWidth = 1;
|
|
|
|
|
requestData.characterPixelHeight = 1;
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!getCharacterPixelDimensions(&requestData)) {
|
|
|
|
|
if (printError) {
|
2024-05-04 19:14:45 +08:00
|
|
|
fputs("Logo: getCharacterPixelDimensions() failed\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-06-08 10:59:33 +02:00
|
|
|
return false;
|
2022-12-23 12:44:19 +08:00
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2023-08-22 15:31:10 +08:00
|
|
|
requestData.logoPixelWidth = (uint32_t) ceil((double) instance.config.logo.width * requestData.characterPixelWidth);
|
|
|
|
|
requestData.logoPixelHeight = (uint32_t) ceil((double) instance.config.logo.height * requestData.characterPixelHeight);
|
2022-04-30 11:22:51 +02:00
|
|
|
|
2023-01-15 15:57:26 +01:00
|
|
|
ffStrbufInit(&requestData.cacheDir);
|
2023-06-24 10:59:53 +08:00
|
|
|
ffStrbufAppend(&requestData.cacheDir, &instance.state.platform.cacheDir);
|
2023-01-28 11:28:05 +01:00
|
|
|
ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images");
|
2022-04-21 15:50:45 +02:00
|
|
|
|
|
|
|
|
ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX);
|
2026-03-17 17:59:24 +08:00
|
|
|
char* filePath = requestData.cacheDir.chars + requestData.cacheDir.length;
|
2026-07-10 18:08:04 +08:00
|
|
|
if (realpath(instance.config.logo.source.chars, filePath) == nullptr) {
|
2026-03-29 09:28:49 +08:00
|
|
|
// We can safely return here, because if realpath failed, we surely won't be able to read the file
|
2022-04-21 15:50:45 +02:00
|
|
|
ffStrbufDestroy(&requestData.cacheDir);
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printError) {
|
2024-05-04 19:14:45 +08:00
|
|
|
fputs("Logo: Querying realpath of the image source failed\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-06-08 10:59:33 +02:00
|
|
|
return false;
|
2022-04-21 15:50:45 +02:00
|
|
|
}
|
2026-03-17 17:59:24 +08:00
|
|
|
|
2026-04-15 16:16:46 +08:00
|
|
|
#ifdef _WIN32
|
2026-03-17 17:59:24 +08:00
|
|
|
filePath[1] = filePath[0]; // Drive Name
|
|
|
|
|
filePath[0] = '/';
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2026-03-17 17:59:24 +08:00
|
|
|
|
2022-04-21 15:50:45 +02:00
|
|
|
ffStrbufRecalculateLength(&requestData.cacheDir);
|
2022-06-22 20:14:10 +02:00
|
|
|
ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/');
|
2026-09-13 22:22:06 +08:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
ffStrbufAppendF(&requestData.cacheDir, "%ux%u/", requestData.logoPixelWidth, requestData.logoPixelHeight);
|
2026-09-13 22:22:06 +08:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
// The cached payload is a rendering, not a bit-exact artefact: every backend produces a
|
|
|
|
|
// valid one for the same source and pixel size, so the backend is deliberately not part of
|
|
|
|
|
// the key. What the key does have to capture is the content of the source, which the path
|
|
|
|
|
// can not: the same file can be replaced in place. Hence the recorded mtime.
|
|
|
|
|
// 0 means the mtime could not be read, in which case the entry is never trusted.
|
|
|
|
|
const uint64_t sourceMtime = ffPathGetMtime(instance.config.logo.source.chars);
|
2022-04-21 15:50:45 +02:00
|
|
|
|
2026-09-14 23:45:28 +08:00
|
|
|
if (instance.config.logo.cache == FF_LOGO_CACHE_ON &&
|
2026-09-14 22:43:19 +08:00
|
|
|
sourceMtime != 0 &&
|
|
|
|
|
readCachedUint64(&requestData, FF_CACHE_FILE_MTIME) == sourceMtime) {
|
2026-06-04 18:27:36 +08:00
|
|
|
bool cacheValid = requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA
|
|
|
|
|
? printCachedChars(&requestData, FF_CACHE_FILE_CHAFA)
|
|
|
|
|
: printCachedPixel(&requestData);
|
|
|
|
|
if (cacheValid) {
|
|
|
|
|
ffStrbufDestroy(&requestData.cacheDir);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2022-04-21 15:50:45 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
// Cache miss. The entry directory is keyed on the source path and the pixel size only, so
|
|
|
|
|
// it is reused when the source is edited; drop what the previous version left behind.
|
2026-09-14 23:45:28 +08:00
|
|
|
// With the cache turned off the directory is left alone entirely.
|
|
|
|
|
if (instance.config.logo.cache != FF_LOGO_CACHE_OFF) {
|
|
|
|
|
removeCachedFiles(&requestData);
|
|
|
|
|
}
|
2026-09-14 22:43:19 +08:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
const char* error = nullptr;
|
|
|
|
|
bool printSuccessful = false;
|
2022-04-11 16:19:02 +02:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
if (requestData.type == FF_LOGO_TYPE_IMAGE_SIXEL) {
|
|
|
|
|
// The sixel encoder belongs to the backend, so it is not fed through ffImageCreate
|
|
|
|
|
FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
|
|
|
|
|
if (ffImageSixelEncode(&requestData, &result, &error)) {
|
|
|
|
|
fillCharacterDimensions(&requestData);
|
|
|
|
|
printSuccessful = printImageSixel(&requestData, &result);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
FFImageBuffer buffer = {};
|
|
|
|
|
if (ffImageCreate(&requestData, &buffer, &error)) {
|
|
|
|
|
fillCharacterDimensions(&requestData);
|
|
|
|
|
if (requestData.type == FF_LOGO_TYPE_IMAGE_KITTY) {
|
|
|
|
|
printSuccessful = printImageKitty(&requestData, &buffer);
|
|
|
|
|
}
|
|
|
|
|
#if FF_HAVE_CHAFA
|
|
|
|
|
else if (requestData.type == FF_LOGO_TYPE_IMAGE_CHAFA) {
|
|
|
|
|
printSuccessful = printImageChafa(&requestData, &buffer);
|
|
|
|
|
}
|
2026-04-15 16:16:46 +08:00
|
|
|
#endif
|
2026-09-13 22:22:06 +08:00
|
|
|
ffImageDestroy(&buffer);
|
|
|
|
|
}
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-04-10 15:26:18 +02:00
|
|
|
|
2026-09-14 22:43:19 +08:00
|
|
|
if (printSuccessful) {
|
|
|
|
|
// Written last: an entry only becomes usable once its payload is complete
|
|
|
|
|
writeCacheData(&requestData, &sourceMtime, sizeof(sourceMtime), FF_CACHE_FILE_MTIME);
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-21 15:50:45 +02:00
|
|
|
ffStrbufDestroy(&requestData.cacheDir);
|
2022-12-23 12:44:19 +08:00
|
|
|
|
2026-09-13 22:22:06 +08:00
|
|
|
if (printSuccessful) {
|
2023-01-28 11:14:16 +01:00
|
|
|
return true;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2023-01-28 11:14:16 +01:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printError) {
|
2026-09-13 22:22:06 +08:00
|
|
|
fprintf(stderr, "Logo: %s\n", error ? error : "Failed to load / convert the image source");
|
2022-12-23 12:44:19 +08:00
|
|
|
}
|
2023-01-28 11:14:16 +01:00
|
|
|
|
|
|
|
|
return false;
|
2022-04-06 14:54:39 +02:00
|
|
|
}
|
2022-04-22 21:29:14 +02:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
#endif // FF_HAVE_IMAGEMAGICK{6, 7}
|
2022-12-23 12:44:19 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
bool ffLogoPrintImageIfExists(FFLogoType type, bool printError) {
|
|
|
|
|
if (instance.config.display.pipe) {
|
|
|
|
|
if (printError) {
|
2024-05-05 20:01:12 +08:00
|
|
|
fputs("Logo: Image logo is not supported in pipe mode\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-05-05 20:01:12 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (!ffPathExists(instance.config.logo.source.chars, FF_PATHTYPE_FILE)) {
|
|
|
|
|
if (printError) {
|
2023-06-24 10:59:53 +08:00
|
|
|
fprintf(stderr, "Logo: Image source \"%s\" does not exist\n", instance.config.logo.source.chars);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-23 12:44:19 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-03 01:08:14 +08:00
|
|
|
const char* term = getenv("TERM");
|
2026-08-11 15:11:45 +05:30
|
|
|
if (term && ffStrEquals(term, "screen")) {
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printError) {
|
2024-05-03 01:08:14 +08:00
|
|
|
fputs("Logo: Image logo is not supported in terminal multiplexers\n", stderr);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-05-03 01:08:14 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (type == FF_LOGO_TYPE_IMAGE_ITERM) {
|
2024-05-03 07:56:43 +08:00
|
|
|
return printImageIterm(printError);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-23 12:44:19 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT) {
|
2024-05-03 07:56:43 +08:00
|
|
|
return printImageKittyDirect(printError);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2022-12-23 12:44:19 +08:00
|
|
|
|
2026-03-29 09:28:49 +08:00
|
|
|
if (type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT) {
|
2024-12-31 15:30:26 +08:00
|
|
|
return printImageKittyIcat(printError);
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
2024-12-31 15:30:26 +08:00
|
|
|
|
2026-06-04 18:27:36 +08:00
|
|
|
#if !FF_HAVE_CHAFA
|
2026-03-29 09:28:49 +08:00
|
|
|
if (type == FF_LOGO_TYPE_IMAGE_CHAFA) {
|
|
|
|
|
if (printError) {
|
|
|
|
|
fputs("Logo: Chafa support is not compiled in\n", stderr);
|
2022-12-23 12:44:19 +08:00
|
|
|
}
|
|
|
|
|
return false;
|
2026-03-29 09:28:49 +08:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-09-14 10:59:03 +08:00
|
|
|
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
|
2026-03-29 09:28:49 +08:00
|
|
|
if (printError) {
|
|
|
|
|
fputs("Logo: Image Magick support is not compiled in\n", stderr);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
#else
|
|
|
|
|
return printImageIfExistsSlowPath(type, printError);
|
|
|
|
|
#endif
|
2022-04-22 21:29:14 +02:00
|
|
|
}
|