mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-11-04 22:14:20 +01:00
56f4dc00a2
The builtin logo, or a text file in logo syntax, drawn as an extruded slab: the front is the art as it is, the back is the art mirrored, and the side walls are shaded with block glyphs. Rendering is pure: no terminal, no input, no timer. `--logo-type spin` hands every frame to the normal logo pipeline, so the modules are laid out beside it exactly as they are for any other type, and the first frame without --watch is byte-identical to the same art printed as a builtin logo. The rotation is driven by the `--watch` loop: `ffSpinTick()` runs once per refresh and `ffLogoPrint()` is called only when it reports a step, so the logo and the modules can never drift apart, and a build that never uses the type costs one comparison per refresh. Speed is set either as degrees per frame (--logo-spin-speed-per-frame, default 10) or as seconds per revolution (--logo-spin-speed-per-round, read from the clock and winning over the former when both are given). Both take negative values to turn the other way, and --logo-spin-angle picks the frame to start from.
168 lines
6.4 KiB
C
168 lines
6.4 KiB
C
#include "logo/spin/spin.h"
|
|
|
|
#include "common/io.h"
|
|
#include "common/strutil.h"
|
|
#include "common/time.h"
|
|
#include "logo/logo.h"
|
|
#include "logo/spin/render.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
// Degrees per frame when neither speed option is set.
|
|
#define SPIN_DEFAULT_DEGREES_PER_FRAME 10.0f
|
|
|
|
// One step of the turn. `logo.spin.speedPerRound` wins over
|
|
// `logo.spin.speedPerFrame` when both are set; 0 means unset for either.
|
|
static float spinStepDegrees(void) {
|
|
const FFOptionsLogoSpin* spin = &instance.config.logo.spin;
|
|
if (spin->speedPerRound != 0.0f) {
|
|
// Seconds per revolution: follow the clock instead of counting frames,
|
|
// so the turn keeps its pace even when a refresh takes longer than the
|
|
// interval asked for.
|
|
FFLogoSpinState* state = &instance.state.logoSpin;
|
|
const double now = ffTimeGetTick();
|
|
const double elapsedMs = now - state->lastDrawMs;
|
|
state->lastDrawMs = now;
|
|
return (float) (360.0 / (double) spin->speedPerRound * elapsedMs / 1000.0);
|
|
}
|
|
if (spin->speedPerFrame != 0.0f) {
|
|
return spin->speedPerFrame;
|
|
}
|
|
return SPIN_DEFAULT_DEGREES_PER_FRAME;
|
|
}
|
|
|
|
static bool spinIsAuto(const FFstrbuf* source) {
|
|
return source->length == 0 || ffStrbufIgnCaseEqualS(source, "auto");
|
|
}
|
|
|
|
static const FFlogo* spinBuiltin(const FFstrbuf* source, FFLogoSize size) {
|
|
return spinIsAuto(source) ? ffLogoGetBuiltinDetected(size) : ffLogoGetBuiltinForName(source, size);
|
|
}
|
|
|
|
static bool spinLoadFile(const FFstrbuf* source, FFstrbuf* text) {
|
|
FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
|
|
if (ffPathExpandEnv(source->chars, &path) && ffAppendFileBuffer(path.chars, text)) {
|
|
return text->length > 0;
|
|
}
|
|
return ffAppendFileBuffer(source->chars, text) && text->length > 0;
|
|
}
|
|
|
|
void ffSpinLogoPrint(void) {
|
|
FFOptionsLogo* options = &instance.config.logo;
|
|
FFLogoSpinState* state = &instance.state.logoSpin;
|
|
instance.state.logoWidth = 0;
|
|
instance.state.logoHeight = 0;
|
|
|
|
if (!state->started) {
|
|
// The first frame starts where `logo.spin.angle` says, and every
|
|
// `--watch` refresh turns it by one step.
|
|
state->started = true;
|
|
state->degrees = fmodf(options->spin.angle, 360.0f);
|
|
if (state->degrees < 0.0f) {
|
|
state->degrees += 360.0f;
|
|
}
|
|
}
|
|
const float spinAngle = state->degrees * (float) M_PI / 180.0f;
|
|
|
|
// The source is the detected OS (`auto`), a builtin logo name, or a file
|
|
// in fastfetch's logo syntax, like the other text logo types.
|
|
FF_STRBUF_AUTO_DESTROY fileText = ffStrbufCreate();
|
|
const FFlogo* logo = spinBuiltin(&options->source, FF_LOGO_SIZE_NORMAL);
|
|
if (!logo) {
|
|
logo = spinBuiltin(&options->source, FF_LOGO_SIZE_UNKNOWN);
|
|
}
|
|
if (!logo && !spinLoadFile(&options->source, &fileText)) {
|
|
if (instance.config.display.showErrors) {
|
|
fprintf(stderr, "Logo (spin): no builtin logo or readable file named %s\n", options->source.chars);
|
|
}
|
|
}
|
|
|
|
// `logo.color.N` on top of the logo's own colors; a file logo borrows the
|
|
// detected logo's palette, as the other text logo types do.
|
|
ffLogoApplyColors(logo ? logo : ffLogoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), true);
|
|
|
|
FFSpinArt art = {};
|
|
if (!ffSpinArtParse(&art, logo ? logo->lines : fileText.chars, options->colors, FASTFETCH_LOGO_MAX_COLORS)) {
|
|
ffSpinArtDestroy(&art); // parse may have allocated before it gave up
|
|
return;
|
|
}
|
|
|
|
const float tilt = options->spin.tilt * (float) M_PI / 180.0f;
|
|
uint32_t width = art.width;
|
|
uint32_t height = art.height;
|
|
// With `--watch` the frame has to stay the same size from the first draw on,
|
|
// or the information column would move when the logo starts turning.
|
|
const bool animated = instance.state.dynamicInterval > 0;
|
|
if (animated || spinAngle != 0.0f || tilt != 0.0f) {
|
|
// A turned slab sticks out of the art's box, so the frame has to make
|
|
// room for its widest angle or the logo would be clipped.
|
|
ffSpinFrameSize(&art, tilt, &width, &height);
|
|
}
|
|
|
|
FFSpinFrame frame = {};
|
|
if (!ffSpinFrameInit(&frame, width, height)) {
|
|
ffSpinArtDestroy(&art);
|
|
return;
|
|
}
|
|
|
|
if (spinAngle == 0.0f && tilt == 0.0f) {
|
|
ffSpinFramePutArt(&frame, &art); // face on: exactly the original art
|
|
} else {
|
|
ffSpinRenderArt(&art, spinAngle, tilt, &frame);
|
|
}
|
|
|
|
// Hand the frame to the upstream logo pipeline: it pads, positions and
|
|
// measures the art, and its line cache is what puts the modules next to
|
|
// the logo. `false`: the frame already carries its own SGR sequences.
|
|
FF_STRBUF_AUTO_DESTROY ansi = ffStrbufCreateA(width * height * 4);
|
|
ffSpinFrameToAnsi(&frame, &art, !instance.config.display.pipe, instance.config.display.brightColor, !animated, &ansi);
|
|
// The serialiser ends every row with a newline; the last one would add an
|
|
// empty line to the logo and change the output from the other logo types.
|
|
if (ansi.length > 0 && ansi.chars[ansi.length - 1] == '\n') {
|
|
ansi.chars[--ansi.length] = '\0';
|
|
}
|
|
|
|
// A resize changes the frame size, and the cells outside the new frame
|
|
// would keep showing the previous one.
|
|
if (state->printed && (width != state->printedWidth || height != state->printedHeight)) {
|
|
fputs("\e[2J\e[H", stdout);
|
|
}
|
|
// `ffLogoPrintChars` writes the art with write(2) for `--logo-position top`,
|
|
// while the caller's cursor moves go through stdio; flush so the art lands
|
|
// where the cursor was just moved to.
|
|
fflush(stdout);
|
|
ffLogoPrintChars(ansi.chars, false);
|
|
|
|
ffSpinFrameDestroy(&frame);
|
|
ffSpinArtDestroy(&art);
|
|
|
|
state->printed = true;
|
|
state->printedWidth = width;
|
|
state->printedHeight = height;
|
|
state->lastDrawMs = ffTimeGetTick(); // start measuring the next step from now
|
|
}
|
|
|
|
bool ffSpinTick(void) {
|
|
FFLogoSpinState* state = &instance.state.logoSpin;
|
|
if (!state->printed) {
|
|
return false;
|
|
}
|
|
|
|
const float step = spinStepDegrees();
|
|
if (step == 0.0f) {
|
|
return false; // a logo that does not turn does not have to be redrawn
|
|
}
|
|
|
|
// The angle is wrapped instead of growing, or a long `--watch` session
|
|
// would lose precision in the sine and cosine.
|
|
float degrees = state->degrees + step;
|
|
state->degrees = degrees - floorf(degrees / 360.0f) * 360.0f;
|
|
return true;
|
|
}
|