aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-19 14:10:52 +0200
committeruser <user@clank>2026-08-19 14:10:52 +0200
commit1f5b55bc323e89399ec82eca392612ce2c0167a4 (patch)
tree84cffc149d0329117013debf04df8ac9c984f144 /src
parentslosh: split app.c into five units, mechanically (diff)
downloadslosh-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.c12
-rw-r--r--src/app_draw.c98
-rw-r--r--src/app_ui.c1
-rw-r--r--src/cmd.c6
-rw-r--r--src/config.c8
-rw-r--r--src/server.c3
6 files changed, 128 insertions, 0 deletions
diff --git a/src/app.c b/src/app.c
index 76da001..ce1fae6 100644
--- a/src/app.c
+++ b/src/app.c
@@ -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 */
}
diff --git a/src/cmd.c b/src/cmd.c
index eb3a09d..734fe8b 100644
--- a/src/cmd.c
+++ b/src/cmd.c
@@ -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]);