diff options
| author | user <user@clank> | 2026-08-19 14:10:52 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-19 14:10:52 +0200 |
| commit | 1f5b55bc323e89399ec82eca392612ce2c0167a4 (patch) | |
| tree | 84cffc149d0329117013debf04df8ac9c984f144 /src | |
| parent | slosh: split app.c into five units, mechanically (diff) | |
| download | slosh-1f5b55bc323e89399ec82eca392612ce2c0167a4.tar.gz slosh-1f5b55bc323e89399ec82eca392612ce2c0167a4.tar.xz slosh-1f5b55bc323e89399ec82eca392612ce2c0167a4.zip | |
slosh: greet an attach with the logo, wearing a random shader
logo.txt is the source of truth; the Makefile bakes it into
build/logo.h the way version.h is baked. When a client attaches
(MSG_HELLO turning a connection into the display), the logo appears
centered over everything for splash_ms (900, 0 for never), on a
cleared backdrop, in the accent -- and one of six effects runs over
its rect through the ordinary shader pass: a diagonal sheen, plasma,
rings, sparkle, a left-to-right reveal, a breathe. Which one is picked
by the splash's own timestamp, so every attach looks a little
different without anybody keeping a counter.
Every effect reads the clock, so the frame clock runs only while the
splash is up (note_animation, same as any animated chain), and `since`
in a splash context is milliseconds since it appeared, so one-shot
effects sweep exactly once. Any key or click ends it and still lands
where it was aimed: a greeting is not a modal. A screen the box does
not fit on gets no greeting rather than a cropped one.
Headless and scripted sessions never attach, so the whole fast suite
never sees it; {"cmd":"splash"} triggers it on demand, which is what
makes it testable from a scripted session -- and lets you replay the
greeting whenever you like.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 12 | ||||
| -rw-r--r-- | src/app_draw.c | 98 | ||||
| -rw-r--r-- | src/app_ui.c | 1 | ||||
| -rw-r--r-- | src/cmd.c | 6 | ||||
| -rw-r--r-- | src/config.c | 8 | ||||
| -rw-r--r-- | src/server.c | 3 |
6 files changed, 128 insertions, 0 deletions
@@ -1312,7 +1312,19 @@ bool run_action(app_t *a, action_t act) { } } +void app_splash(app_t *a) { + if (!CFG.splash_ms) return; + a->splash_until = now_ms_() + CFG.splash_ms; +} + void app_event(app_t *a, const input_event_t *ev) { + /* The splash is a greeting, not a modal: the first thing you do ends it, + * and that thing still happens -- swallowing a keystroke someone aimed at + * their shell would make the greeting a nuisance. */ + if (a->splash_until && (ev->kind == EV_KEY || + (ev->kind == EV_MOUSE && ev->maction == MOUSE_PRESS))) + a->splash_until = 0; + if (!a->ntabs || !cur(a)->focus) return; /* A rename whose subject went away must not keep the keyboard. */ diff --git a/src/app_draw.c b/src/app_draw.c index be3b854..303ee8f 100644 --- a/src/app_draw.c +++ b/src/app_draw.c @@ -1740,3 +1740,101 @@ char *app_graphics_json(app_t *a) { json_arr_close(&j); return j.buf; } + +/* ---- the splash ---------------------------------------------------------- * + * + * The logo, briefly, centered over everything, when a client attaches: a + * greeting, and a two-second advertisement for what the shader pass can do. + * Which effect it wears is picked by the splash's own timestamp, so every + * attach looks a little different without anybody keeping a counter. Any key + * or click ends it (app_event), and the pass machinery is exactly the one + * panes use -- same expressions, same clock, same cache. */ + +#include "logo.h" /* generated from logo.txt; see the Makefile */ + +typedef struct { + const char *kind; /* tint or dim: colour it, or carve it */ + const char *amount; /* the whole effect, as an expression over the rect */ + bool on_fg; /* tint towards default_fg rather than the accent */ +} splash_fx_t; + +/* Every entry reads `t` or `since`, deliberately: an animated splash keeps + * the frame clock only while it is on screen (note_animation), and a still + * one would just be a logo. `since` here is milliseconds since the splash + * began, so one-shot effects can sweep exactly once. */ +static const splash_fx_t SPLASH_FX[] = { + /* a sheen looping along the diagonal, the bell shimmer's big sibling */ + {"tint", + "max(0, 230 - abs(x + 2 * y - (since / 3) % (cols + 2 * rows + 24)) * 25)", + true}, + /* plasma: two sines that disagree */ + {"tint", "128 + sin(x * 24 + t / 4) / 4 + sin(y * 48 - t / 6) / 4", false}, + /* rings spreading from the middle */ + {"tint", "128 + sin(dist(x, y, cols / 2, rows / 2) * 55 - t / 2) / 2", + false}, + /* sparkle: a cheap hash of position and time */ + {"tint", "((x * 37 + y * 91 + (since / 90) * 53) % 89 < 7) * 230", true}, + /* a left-to-right reveal over the first half second */ + {"dim", "(x * 500 > since * cols) * 255", false}, + /* breathe */ + {"tint", "120 + sin(since / 2) / 3", false}, +}; +#define NSPLASH_FX (sizeof SPLASH_FX / sizeof *SPLASH_FX) + +void draw_splash(app_t *a, screen_t *s) { + if (!a->splash_until) return; + int64_t now = now_ms_(); + if (now >= a->splash_until) { + a->splash_until = 0; + return; + } + + size_t nlines = sizeof LOGO / sizeof *LOGO; + uint16_t w = 0; + for (size_t i = 0; i < nlines; i++) { + uint16_t c = cells(LOGO[i]); + if (c > w) w = c; + } + /* Two cells of air each side, a row above and below. A screen the box does + * not fit on gets no greeting rather than a cropped one. */ + uint16_t bw = (uint16_t)(w + 4), bh = (uint16_t)(nlines + 2); + if (bw > s->cols || bh > s->rows) return; + uint16_t x0 = (uint16_t)((s->cols - bw) / 2); + uint16_t y0 = (uint16_t)((s->rows - bh) / 2); + + /* A cleared backdrop, so the logo reads over whatever a pane put there. */ + char blank[512]; + size_t nb = bw < sizeof blank - 1 ? bw : sizeof blank - 1; + memset(blank, ' ', nb); + blank[nb] = 0; + for (uint16_t y = 0; y < bh; y++) + screen_text(s, x0, (uint16_t)(y0 + y), blank, NO_COLOR, NO_COLOR, 0); + for (size_t i = 0; i < nlines; i++) + screen_text(s, (uint16_t)(x0 + 2), (uint16_t)(y0 + 1 + i), LOGO[i], + FRAME_FOCUS, NO_COLOR, ATTR_BOLD); + + /* The effect. Compiled on first wear and kept for the life of the process: + * the sources are string literals above, and a splash that recompiled its + * expression every frame would be spending the one cost the map exists to + * avoid. */ + static expr_prog_t *progs[NSPLASH_FX]; + size_t idx = (size_t)a->splash_until % NSPLASH_FX; + if (!progs[idx]) { + char err[128] = {0}; + progs[idx] = expr_compile(SPLASH_FX[idx].amount, err, sizeof err); + } + if (!progs[idx]) return; /* cannot happen; the bare logo still greets */ + + shader_t sh; + shader_make(&sh, SPLASH_FX[idx].kind, + SPLASH_FX[idx].on_fg ? CFG.default_fg : CFG.frame_focus, 128); + sh.amount_expr = progs[idx]; + shade_ctx_t ctx = { + .now_ms = now, + .state_ms = now - (a->splash_until - CFG.splash_ms), + .default_fg = CFG.default_fg, + .default_bg = CFG.default_bg, + }; + note_animation(a, &sh, 1); + shade_apply(s, &sh, 1, (rect_t){x0, y0, bw, bh}, NULL, &ctx); +} diff --git a/src/app_ui.c b/src/app_ui.c index 021c282..ec4a6ef 100644 --- a/src/app_ui.c +++ b/src/app_ui.c @@ -1005,4 +1005,5 @@ void app_compose(app_t *a, screen_t *s) { if (a->picker) draw_picker(a, s); if (a->help) draw_help(a, s); draw_toasts(a, s); /* and above even that: it is transient */ + draw_splash(a, s); /* the greeting outranks everything, briefly */ } @@ -426,6 +426,12 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, json_obj_close(&j); return j.buf; } + if (strcmp(cmd, "splash") == 0) { + /* The attach greeting, on demand -- which is also what makes it testable + * from a scripted session, where nobody ever attaches. */ + app_splash(a); + return jok_int(NULL, 0); + } if (strcmp(cmd, "reload") == 0) { char err[256] = {0}; if (!app_reload_config(err, sizeof err)) { diff --git a/src/config.c b/src/config.c index 2d65998..10fe1e9 100644 --- a/src/config.c +++ b/src/config.c @@ -862,6 +862,9 @@ void config_defaults(config_t *c) { * checkout. No roots by default: the feature is dormant until you say where. */ c->project_depth = 2; c->toast_ms = 2500; + /* Long enough to read as a greeting, short enough never to be in the way + * -- and any key or click ends it early. */ + c->splash_ms = 900; c->hover_delay_ms = 250; c->double_click_ms = 400; /* Quotes, brackets, and the punctuation that ends a clause or a list. @@ -1545,6 +1548,8 @@ char *config_render(const config_t *c) { "scrollback_bytes %zu // the ceiling that count runs into; 0 for none\n", c->scrollback_bytes); cb_add(&b, "toast_ms %u\n", c->toast_ms); + cb_add(&b, "splash_ms %u // the logo, on attach; 0 for never\n", + c->splash_ms); cb_add(&b, "hover_delay_ms %u\n", c->hover_delay_ms); cb_add(&b, "double_click_ms %u\n", c->double_click_ms); cb_qstr(&b, "word_separators", c->word_separators, @@ -1791,6 +1796,7 @@ static const char *const KNOWN_TOP[] = { "shader_dir", "shaders", "shell", + "splash_ms", "states", "status_bar", "status_line", @@ -2095,6 +2101,8 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, } c->toast_ms = (uint16_t)kdl_arg_int(kdl_child(root, "toast_ms"), 0, c->toast_ms); + c->splash_ms = + (uint16_t)kdl_arg_int(kdl_child(root, "splash_ms"), 0, c->splash_ms); c->hover_delay_ms = (uint16_t)kdl_arg_int(kdl_child(root, "hover_delay_ms"), 0, c->hover_delay_ms); c->double_click_ms = (uint16_t)kdl_arg_int(kdl_child(root, "double_click_ms"), diff --git a/src/server.c b/src/server.c index 578ffff..4635e3b 100644 --- a/src/server.c +++ b/src/server.c @@ -428,6 +428,9 @@ int server_run(const char *name, const char *const argv[], uint16_t cols, break; } s.conns[ci].display = true; + /* Someone is now looking, which is the one moment a greeting + * makes sense: a headless or scripted session never gets here. */ + app_splash(s.app); } if (m.len >= 4 && s.conns[ci].display) { uint16_t c = (uint16_t)(m.data[0] << 8 | m.data[1]); |
