diff --git a/include/cst_version.h b/include/cst_version.h index 981ca65..0c6e982 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -3,7 +3,7 @@ #define CST_VERSION_MAJOR 0 #define CST_VERSION_MINOR 9 -#define CST_VERSION_PATCH 25 +#define CST_VERSION_PATCH 26 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/src/tui.c b/src/tui.c index 798d111..b0b82a0 100644 --- a/src/tui.c +++ b/src/tui.c @@ -961,6 +961,102 @@ static long ms_since(const struct timespec *a) { (long)(now.tv_nsec - a->tv_nsec) / 1000000; } +/* Stream frontier: trailing glyphs briefly scramble then settle to plaintext. + * Returns a malloc'd ANSI copy, or NULL to use the source as-is. */ +#define GLITCH_FRONT 28 + +static unsigned glitch_mix(unsigned x) { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return x; +} + +static char *glitch_stream_frontier(const char *src, int spin) { + if (!src || !src[0]) + return NULL; + size_t n = strlen(src); + char *out = malloc(n * 3 + 64); /* room if we swap ASCII for UTF-8 glyphs */ + if (!out) + return NULL; + /* Collect starts of the last GLITCH_FRONT visible codepoints (skip SGR). */ + size_t starts[GLITCH_FRONT]; + size_t lens[GLITCH_FRONT]; + int nv = 0; + size_t i = 0; + while (i < n) { + if (src[i] == '\033' && i + 1 < n && src[i + 1] == '[') { + size_t j = i + 2; + while (j < n && src[j] != 'm') + j++; + i = j < n ? j + 1 : n; + continue; + } + wchar_t wc; + size_t eaten; + utf8_decode(src + i, n - i, &wc, &eaten); + if (eaten == 0) + break; + if (wc != L'\n' && wc != L'\r') { + if (nv < GLITCH_FRONT) { + starts[nv] = i; + lens[nv] = eaten; + nv++; + } else { + memmove(starts, starts + 1, (GLITCH_FRONT - 1) * sizeof(starts[0])); + memmove(lens, lens + 1, (GLITCH_FRONT - 1) * sizeof(lens[0])); + starts[GLITCH_FRONT - 1] = i; + lens[GLITCH_FRONT - 1] = eaten; + } + } + i += eaten; + } + if (nv < 4) { + free(out); + return NULL; + } + static const char *const uni_g[] = { + "░", "▒", "▓", "█", "▄", "▀", "▌", "▐", "╳", "╬", "═", "║", + "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "#", "*", "+", "/"}; + static const char *const asc_g[] = { + "#", "$", "%", "&", "*", "+", "=", "?", "/", "\\", "|", ":", + ".", ",", ";", "~", "^", "x", "o", "0", "1", "Z", "X", "/"}; + const char *const *glyphs = g_ascii ? asc_g : uni_g; + const int ng = g_ascii ? (int)(sizeof(asc_g) / sizeof(asc_g[0])) + : (int)(sizeof(uni_g) / sizeof(uni_g[0])); + + size_t o = 0; + size_t cursor = 0; + for (int v = 0; v < nv; v++) { + size_t st = starts[v]; + if (st > cursor) { + memcpy(out + o, src + cursor, st - cursor); + o += st - cursor; + } + /* Intensity: tip of stream most scrambled; older glyphs settle. */ + int age = nv - 1 - v; /* 0 = newest */ + unsigned h = glitch_mix((unsigned)spin * 131u + (unsigned)v * 17u + + (unsigned)st); + int scramble = age < 10 && (int)(h % 10) < (10 - age); + if (scramble && age < 6) { + const char *g = glyphs[h % (unsigned)ng]; + size_t gl = strlen(g); + memcpy(out + o, g, gl); + o += gl; + } else { + memcpy(out + o, src + st, lens[v]); + o += lens[v]; + } + cursor = st + lens[v]; + } + if (cursor < n) { + memcpy(out + o, src + cursor, n - cursor); + o += n - cursor; + } + out[o] = 0; + return out; +} + static void live_begin(tui_t *t) { live_end_cleanup(t); t->md_cap = MD_INIT; @@ -1835,8 +1931,19 @@ static void build_display(tui_t *t, dlines_t *dl, int width) { block_to_lines_indented(&t->hist.items[i], width, t->details_all, dl); } g_dl_blk = -1; - if (t->as_live) - block_to_lines_indented(&t->live, width, t->details_all, dl); + if (t->as_live) { + block_t live = t->live; + char *glitch = NULL; + if (t->motion_on && t->busy) { + const char *src = + t->live.render ? t->live.render : (t->live.body ? t->live.body : ""); + glitch = glitch_stream_frontier(src, t->spin); + if (glitch) + live.render = glitch; + } + block_to_lines_indented(&live, width, t->details_all, dl); + free(glitch); + } } /* Draws s at (y, x0) within maxw columns; returns the columns used. */ @@ -3279,6 +3386,15 @@ static void layout(tui_t *t) { if (t->side) wbkgdset(t->side, COLOR_PAIR(CP_SIDE)); } + /* Side/status refreshes must not steal the terminal cursor from input. */ + if (t->side) + leaveok(t->side, TRUE); + if (t->status) + leaveok(t->status, TRUE); + if (t->header) + leaveok(t->header, TRUE); + if (t->todow) + leaveok(t->todow, TRUE); scrollok(t->logw, FALSE); keypad(t->input, TRUE); wtimeout(t->input, 80); @@ -7436,9 +7552,11 @@ int cst_tui_run(cst_agent_t *agent) { if (busy) { t.spin++; draw_status(&t); - draw_input(&t); + if (t.motion_on && t.as_live) + draw_log(&t); /* animate stream frontier glitch */ if (t.motion_on && t.side) draw_side(&t); + draw_input(&t); /* always last — park cursor in the composer */ } if (turn_done) { @@ -7472,6 +7590,7 @@ int cst_tui_run(cst_agent_t *agent) { if (!busy && t.motion_on && t.side && !t.home) { t.spin++; draw_side(&t); + draw_input(&t); /* restore cursor after side refresh */ } continue; }