From c857d7bd81c4d80ced9128c682cc232f78574e3b Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 19:15:55 -0700 Subject: [PATCH 1/3] feat: implement dynamic shifting gradients for messages and scrolling content, and adjust rendering parameters --- mainline.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/mainline.py b/mainline.py index 212f002..9eca86d 100755 --- a/mainline.py +++ b/mainline.py @@ -147,7 +147,7 @@ KATA = "ハミヒーウシナモニサワツオリアホテマケメエカキム _FONT_PATH = "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.otf" _FONT_OBJ = None _FONT_SZ = 60 -_RENDER_H = 8 # terminal rows per rendered text line +_RENDER_H = 16 # terminal rows per rendered text line # Non-Latin scripts → macOS system fonts _SCRIPT_FONTS = { @@ -502,9 +502,10 @@ def _save_cache(items): # ─── STREAM ─────────────────────────────────────────────── -_SCROLL_DUR = 3.75 # seconds per headline -_FRAME_DT = 0.05 # 50ms base frame rate (20 FPS) -FIREHOSE_H = 12 # firehose zone height (terminal rows) +_SCROLL_DUR = 5.625 # seconds per headline (2/3 original speed) +_FRAME_DT = 0.05 # 50ms base frame rate (20 FPS) +FIREHOSE_H = 12 # firehose zone height (terminal rows) +GRAD_SPEED = 0.08 # gradient traversal speed (cycles/sec, ~12s full sweep) _mic_db = -99.0 # current mic level, written by background thread _mic_stream = None @@ -636,8 +637,8 @@ def _big_wrap(text, max_w, font=None): return out -def _lr_gradient(rows): - """Color each non-space block character with a left-to-right gradient.""" +def _lr_gradient(rows, offset=0.0): + """Color each non-space block character with a shifting left-to-right gradient.""" n = len(_GRAD_COLS) max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1) out = [] @@ -650,7 +651,8 @@ def _lr_gradient(rows): if ch == ' ': buf.append(' ') else: - idx = min(round(x / max(max_x - 1, 1) * (n - 1)), n - 1) + shifted = (x / max(max_x - 1, 1) + offset) % 1.0 + idx = min(round(shifted * (n - 1)), n - 1) buf.append(f"{_GRAD_COLS[idx]}{ch}\033[0m") out.append("".join(buf)) return out @@ -730,7 +732,6 @@ def _make_block(title, src, ts, w): ("\u201d",'"'), ("\u2013","-"), ("\u2014","-")]: title_up = title_up.replace(old, new) big_rows = _big_wrap(title_up, w - 4, lang_font) - big_rows = _lr_gradient(big_rows) hc = random.choice([ "\033[38;5;46m", # matrix green "\033[38;5;34m", # dark green @@ -861,10 +862,10 @@ def stream(items): cache_key = (display_text, w) if _msg_cache[0] != cache_key: msg_rows = _big_wrap(display_text, w - 4) - msg_rows = _lr_gradient(msg_rows) _msg_cache = (cache_key, msg_rows) else: msg_rows = _msg_cache[1] + msg_rows = _lr_gradient(msg_rows, (time.monotonic() * GRAD_SPEED) % 1.0) # Center vertically in scroll zone total_h = len(msg_rows) + 4 # +4 for border + meta + padding y_off = max(0, (sh - total_h) // 2) @@ -933,6 +934,7 @@ def stream(items): # Draw scroll zone top_zone = max(1, int(sh * 0.25)) bot_zone = max(1, int(sh * 0.10)) + grad_offset = (time.monotonic() * GRAD_SPEED) % 1.0 buf = [] for r in range(sh): cy = cam + r @@ -943,13 +945,18 @@ def stream(items): for content, hc, by, midx in active: cr = cy - by if 0 <= cr < len(content): - ln = _vis_trunc(content[cr], w) + raw = content[cr] + if cr != midx: + colored = _lr_gradient([raw], grad_offset)[0] + else: + colored = raw + ln = _vis_trunc(colored, w) if row_fade < 1.0: ln = _fade_line(ln, row_fade) if cr == midx: buf.append(f"\033[{r+1};1H{W_COOL}{ln}{RST}\033[K") elif ln.strip(): - buf.append(f"\033[{r+1};1H{hc}{ln}{RST}\033[K") + buf.append(f"\033[{r+1};1H{ln}{RST}\033[K") else: buf.append(f"\033[{r+1};1H\033[K") drawn = True From 7274f57bbbb353ff728c00a2c317f207bdfd0b2f Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 19:21:24 -0700 Subject: [PATCH 2/3] feat: Implement super-sampling for text rendering and adjust `_RENDER_H` from 16 to 8. --- mainline.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mainline.py b/mainline.py index 9eca86d..8bd8fc0 100755 --- a/mainline.py +++ b/mainline.py @@ -147,7 +147,8 @@ KATA = "ハミヒーウシナモニサワツオリアホテマケメエカキム _FONT_PATH = "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.otf" _FONT_OBJ = None _FONT_SZ = 60 -_RENDER_H = 16 # terminal rows per rendered text line +_RENDER_H = 8 # terminal rows per rendered text line +_SSAA = 4 # super-sampling factor: render at _SSAA× then downsample # Non-Latin scripts → macOS system fonts _SCRIPT_FONTS = { @@ -579,8 +580,11 @@ def _render_line(text, font=None): draw = ImageDraw.Draw(img) draw.text((-bbox[0] + pad, -bbox[1] + pad), text, fill=255, font=font) pix_h = _RENDER_H * 2 - scale = pix_h / max(img_h, 1) - new_w = max(1, int(img_w * scale)) + hi_h = pix_h * _SSAA + scale = hi_h / max(img_h, 1) + new_w_hi = max(1, int(img_w * scale)) + img = img.resize((new_w_hi, hi_h), Image.Resampling.LANCZOS) + new_w = max(1, int(new_w_hi / _SSAA)) img = img.resize((new_w, pix_h), Image.Resampling.LANCZOS) data = img.tobytes() thr = 80 From 424332e065d806db1bfc9b50e4ef8d53032a4e0a Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 22:02:28 -0700 Subject: [PATCH 3/3] feat: Implement a top-pinned ntfy message banner that reduces scrollable area instead of freezing the display. --- mainline.py | 111 +++++++++++++++++++++++----------------------------- 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/mainline.py b/mainline.py index 8bd8fc0..b862d95 100755 --- a/mainline.py +++ b/mainline.py @@ -848,6 +848,7 @@ def stream(items): sh = h - fh # ── Check for ntfy message ──────────────────────── + msg_h = 0 # rows consumed by message zone at top msg_active = False with _ntfy_lock: if _ntfy_message is not None: @@ -857,10 +858,9 @@ def stream(items): else: _ntfy_message = None # expired + buf = [] if msg_active: - # ── MESSAGE state: freeze scroll, render message ── - buf = [] - # Render message text with OTF font (cached across frames) + # ── Message zone: pinned to top, scroll continues below ── display_text = m_body or m_title or "(empty)" display_text = re.sub(r"\s+", " ", display_text.upper()) cache_key = (display_text, w) @@ -870,50 +870,33 @@ def stream(items): else: msg_rows = _msg_cache[1] msg_rows = _lr_gradient(msg_rows, (time.monotonic() * GRAD_SPEED) % 1.0) - # Center vertically in scroll zone - total_h = len(msg_rows) + 4 # +4 for border + meta + padding - y_off = max(0, (sh - total_h) // 2) - for r in range(sh): - ri = r - y_off - if ri == 0 or ri == total_h - 1: - # Border lines - bar = "─" * (w - 4) - buf.append(f"\033[{r+1};1H {MSG_BORDER}{bar}{RST}\033[K") - elif 1 <= ri <= len(msg_rows): - ln = _vis_trunc(msg_rows[ri - 1], w) - buf.append(f"\033[{r+1};1H {ln}{RST}\033[K") - elif ri == len(msg_rows) + 1: - # Title line (if present and different from body) - if m_title and m_title != m_body: - meta = f" {MSG_META}\u2591 {m_title}{RST}" - else: - meta = "" - buf.append(f"\033[{r+1};1H{meta}\033[K") - elif ri == len(msg_rows) + 2: - # Source + timestamp - elapsed_s = int(time.monotonic() - m_ts) - remaining = max(0, MESSAGE_DISPLAY_SECS - elapsed_s) - ts_str = datetime.now().strftime("%H:%M:%S") - meta = f" {MSG_META}\u2591 ntfy \u00b7 {ts_str} \u00b7 {remaining}s{RST}" - buf.append(f"\033[{r+1};1H{meta}\033[K") - else: - # Sparse noise outside the message - if random.random() < 0.06: - buf.append(f"\033[{r+1};1H{noise(w)}") - else: - buf.append(f"\033[{r+1};1H\033[K") - # Firehose keeps running during messages - if FIREHOSE and fh > 0: - for fr in range(fh): - fline = _firehose_line(items, w) - buf.append(f"\033[{sh + fr + 1};1H{fline}\033[K") - sys.stdout.buffer.write("".join(buf).encode()) - sys.stdout.flush() - elapsed = time.monotonic() - t0 - time.sleep(max(0, _FRAME_DT - elapsed)) - continue + # Layout: rendered text + meta + border + elapsed_s = int(time.monotonic() - m_ts) + remaining = max(0, MESSAGE_DISPLAY_SECS - elapsed_s) + ts_str = datetime.now().strftime("%H:%M:%S") + row_idx = 0 + for mr in msg_rows: + ln = _vis_trunc(mr, w) + buf.append(f"\033[{row_idx+1};1H {ln}{RST}\033[K") + row_idx += 1 + # Meta line: title (if distinct) + source + countdown + meta_parts = [] + if m_title and m_title != m_body: + meta_parts.append(m_title) + meta_parts.append(f"ntfy \u00b7 {ts_str} \u00b7 {remaining}s") + meta = " " + " \u00b7 ".join(meta_parts) if len(meta_parts) > 1 else " " + meta_parts[0] + buf.append(f"\033[{row_idx+1};1H{MSG_META}{meta}{RST}\033[K") + row_idx += 1 + # Border — constant boundary between message and scroll + bar = "\u2500" * (w - 4) + buf.append(f"\033[{row_idx+1};1H {MSG_BORDER}{bar}{RST}\033[K") + row_idx += 1 + msg_h = row_idx - # ── SCROLL state: normal headline rendering ─────── + # Effective scroll zone: below message, above firehose + scroll_h = sh - msg_h + + # ── Scroll: headline rendering (always runs) ────── # Advance scroll on schedule scroll_accum += _FRAME_DT while scroll_accum >= scroll_interval: @@ -935,15 +918,16 @@ def stream(items): if k < cam: del noise_cache[k] - # Draw scroll zone - top_zone = max(1, int(sh * 0.25)) - bot_zone = max(1, int(sh * 0.10)) + # Draw scroll zone (below message zone, above firehose) + top_zone = max(1, int(scroll_h * 0.25)) + bot_zone = max(1, int(scroll_h * 0.10)) grad_offset = (time.monotonic() * GRAD_SPEED) % 1.0 - buf = [] - for r in range(sh): + scroll_buf_start = len(buf) # track where scroll rows start in buf + for r in range(scroll_h): + scr_row = msg_h + r + 1 # 1-indexed ANSI screen row cy = cam + r - top_f = min(1.0, r / top_zone) - bot_f = min(1.0, (sh - 1 - r) / bot_zone) + top_f = min(1.0, r / top_zone) if top_zone > 0 else 1.0 + bot_f = min(1.0, (scroll_h - 1 - r) / bot_zone) if bot_zone > 0 else 1.0 row_fade = min(top_f, bot_f) drawn = False for content, hc, by, midx in active: @@ -958,11 +942,11 @@ def stream(items): if row_fade < 1.0: ln = _fade_line(ln, row_fade) if cr == midx: - buf.append(f"\033[{r+1};1H{W_COOL}{ln}{RST}\033[K") + buf.append(f"\033[{scr_row};1H{W_COOL}{ln}{RST}\033[K") elif ln.strip(): - buf.append(f"\033[{r+1};1H{ln}{RST}\033[K") + buf.append(f"\033[{scr_row};1H{ln}{RST}\033[K") else: - buf.append(f"\033[{r+1};1H\033[K") + buf.append(f"\033[{scr_row};1H\033[K") drawn = True break if not drawn: @@ -970,9 +954,9 @@ def stream(items): if row_fade < 1.0 and n: n = _fade_line(n, row_fade) if n: - buf.append(f"\033[{r+1};1H{n}") + buf.append(f"\033[{scr_row};1H{n}") else: - buf.append(f"\033[{r+1};1H\033[K") + buf.append(f"\033[{scr_row};1H\033[K") # Draw firehose zone if FIREHOSE and fh > 0: @@ -984,11 +968,12 @@ def stream(items): mic_excess = max(0.0, _mic_db - MIC_THRESHOLD_DB) glitch_prob = 0.32 + min(0.9, mic_excess * 0.16) n_hits = 4 + int(mic_excess / 2) - g_limit = sh if FIREHOSE else len(buf) - if random.random() < glitch_prob and g_limit > 0: - for _ in range(min(n_hits, g_limit)): - gi = random.randint(0, g_limit - 1) - buf[gi] = f"\033[{gi+1};1H{glitch_bar(w)}" + scroll_buf_len = len(buf) - scroll_buf_start + if random.random() < glitch_prob and scroll_buf_len > 0: + for _ in range(min(n_hits, scroll_buf_len)): + gi = random.randint(0, scroll_buf_len - 1) + scr_row = msg_h + gi + 1 + buf[scroll_buf_start + gi] = f"\033[{scr_row};1H{glitch_bar(w)}" sys.stdout.buffer.write("".join(buf).encode()) sys.stdout.flush()