feat: TUI chrome refresh — steel theme, home card, session topbar (0.9.24)

Cooler brand palette, compact CST-agent home with tagline and framed
composer, session topbar (model + chair chip), sidebar off by default.
Screenshot pipeline in tools/tui_shot.py; docs/screenshots for before/after.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 18:27:06 +08:00
co-authored by Cursor
parent c329db504d
commit 00288108cd
9 changed files with 325 additions and 58 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0 #define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9 #define CST_VERSION_MINOR 9
#define CST_VERSION_PATCH 23 #define CST_VERSION_PATCH 24
#define CST_VERSION_STR_(x) #x #define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x) #define CST_VERSION_STR(x) CST_VERSION_STR_(x)
+169 -55
View File
@@ -260,7 +260,10 @@ enum {
CP_ACCENT, /* blue on panel */ CP_ACCENT, /* blue on panel */
CP_BRIGHT, /* near-white on panel */ CP_BRIGHT, /* near-white on panel */
CP_LOGO_DIM, CP_LOGO_DIM,
CP_SEL /* selected list row */ CP_SEL, /* selected list row */
CP_TOPBAR, /* session chrome strip */
CP_TOPBAR_MUTED,
CP_TOPBAR_ACCENT
}; };
typedef enum { typedef enum {
@@ -487,7 +490,7 @@ static char *xstrndup(const char *s, size_t n) {
/* --- theme ---------------------------------------------------------------- */ /* --- theme ---------------------------------------------------------------- */
/* OpenCode dark panel — never inherit terminal default bg. */ /* Dark panel chrome — cool steel accents (not peach OpenCode clone). */
static void theme_init(void) { static void theme_init(void) {
const char *nc = getenv("NO_COLOR"); const char *nc = getenv("NO_COLOR");
g_nocolor = nc && nc[0]; g_nocolor = nc && nc[0];
@@ -504,7 +507,7 @@ static void theme_init(void) {
start_color(); start_color();
/* Do NOT call use_default_colors(): -1 would leak the terminal theme. */ /* Do NOT call use_default_colors(): -1 would leak the terminal theme. */
short primary = COLOR_YELLOW; short primary = COLOR_CYAN;
short panel = COLOR_BLACK; short panel = COLOR_BLACK;
short muted = COLOR_CYAN; short muted = COLOR_CYAN;
short success = COLOR_GREEN; short success = COLOR_GREEN;
@@ -517,42 +520,39 @@ static void theme_init(void) {
if (COLORS >= 256) { if (COLORS >= 256) {
box = 235; box = 235;
side = 234; side = 234;
accent = 75; accent = 75; /* #5c9cf5 steel blue */
bright = 255; bright = 255;
logo_dim = 242; logo_dim = 242;
/* Prefer xterm-256 slots so we don't bleach the 8 ANSI base colors. primary = 75; /* brand/user accent: blue, not peach */
* panel #121212→233, primary #fab283→216, muted #808080→244 */ panel = 233; /* #121212 */
primary = 216;
panel = 233; /* #121212 — darker than 234 to avoid washed look */
muted = 244; muted = 244;
success = 114; success = 114;
err = 167; err = 167;
assist = 74; /* soft cyan, not neon */ assist = 74; /* soft cyan assistant */
text = 250; /* #bcbcbc — not near-white */ text = 252; /* brighter body for readability */
if (can_change_color()) { if (can_change_color()) {
/* Tune those slots toward exact OpenCode hex */ init_color(75, 430, 660, 996); /* #6ea8fe brand */
init_color(216, 980, 698, 514); /* #fab283 */ init_color(233, 55, 58, 70); /* #0e0f12 deeper panel */
init_color(233, 70, 70, 70); /* #121212 */ init_color(244, 502, 520, 560); /* cool muted */
init_color(244, 502, 502, 502); /* #808080 */ init_color(252, 820, 830, 850); /* body */
init_color(250, 720, 720, 720); /* body text */
init_color(114, 400, 750, 450); init_color(114, 400, 750, 450);
init_color(167, 850, 320, 320); init_color(167, 850, 320, 320);
init_color(74, 350, 700, 750); init_color(74, 380, 720, 780);
init_color(235, 118, 118, 118); /* #1e1e1e raised box */ init_color(235, 100, 105, 120); /* raised box */
init_color(234, 90, 90, 90); /* #171717 sidebar */ init_color(234, 78, 82, 96); /* sidebar */
init_color(75, 361, 612, 961); /* #5c9cf5 accent */ init_color(216, 980, 720, 420); /* warm tip only */
} }
} else if (can_change_color() && COLORS >= 8) { } else if (can_change_color() && COLORS >= 8) {
init_color(COLOR_BLACK, 70, 70, 70); /* panel #121212 */ init_color(COLOR_BLACK, 55, 58, 70);
init_color(COLOR_YELLOW, 980, 698, 514); /* primary */ init_color(COLOR_CYAN, 430, 660, 996);
init_color(COLOR_CYAN, 502, 502, 502); /* muted (reuse cyan slot) */ init_color(COLOR_WHITE, 820, 830, 850);
init_color(COLOR_WHITE, 720, 720, 720); /* body text */
init_color(COLOR_GREEN, 400, 750, 450); init_color(COLOR_GREEN, 400, 750, 450);
init_color(COLOR_RED, 850, 320, 320); init_color(COLOR_RED, 850, 320, 320);
init_color(COLOR_BLUE, 350, 700, 750); /* assist */ init_color(COLOR_BLUE, 380, 720, 780);
init_color(COLOR_YELLOW, 980, 720, 420);
panel = COLOR_BLACK; panel = COLOR_BLACK;
primary = COLOR_YELLOW; primary = COLOR_CYAN;
muted = COLOR_CYAN; muted = COLOR_WHITE;
text = COLOR_WHITE; text = COLOR_WHITE;
success = COLOR_GREEN; success = COLOR_GREEN;
err = COLOR_RED; err = COLOR_RED;
@@ -608,6 +608,9 @@ static void theme_init(void) {
init_pair(CP_BRIGHT, bright, panel); init_pair(CP_BRIGHT, bright, panel);
init_pair(CP_LOGO_DIM, logo_dim, panel); init_pair(CP_LOGO_DIM, logo_dim, panel);
init_pair(CP_SEL, panel, accent); init_pair(CP_SEL, panel, accent);
init_pair(CP_TOPBAR, bright, side);
init_pair(CP_TOPBAR_MUTED, muted, side);
init_pair(CP_TOPBAR_ACCENT, accent, side);
} }
/* Blank the window into the virtual screen before deleting it: refreshing /* Blank the window into the virtual screen before deleting it: refreshing
@@ -2196,8 +2199,22 @@ static void draw_input(tui_t *t) {
if (pad < 0) if (pad < 0)
pad = 0; pad = 0;
int bar = HAS_COLOR() ? (int)COLOR_PAIR(CP_BOX_ACCENT) : 0; int bar = HAS_COLOR() ? (int)COLOR_PAIR(CP_BOX_ACCENT) : 0;
for (int r = 0; r < bh; r++) int edge = HAS_COLOR() ? (int)COLOR_PAIR(CP_BOX_MUTED) : 0;
/* Composer card frame. */
if (bh >= 2 && bw >= 4) {
put_wc(w, 0, 0, g_ascii ? L'+' : L'┌', bar);
for (int x = 1; x < bw - 1; x++)
put_wc(w, 0, x, g_ascii ? L'-' : L'─', edge);
put_wc(w, 0, bw - 1, g_ascii ? L'+' : L'┐', edge);
put_wc(w, bh - 1, 0, g_ascii ? L'+' : L'└', edge);
for (int x = 1; x < bw - 1; x++)
put_wc(w, bh - 1, x, g_ascii ? L'-' : L'─', edge);
put_wc(w, bh - 1, bw - 1, g_ascii ? L'+' : L'┘', edge);
}
for (int r = 1; r < bh - 1; r++) {
put_wc(w, r, 0, L'┃', bar); put_wc(w, r, 0, L'┃', bar);
put_wc(w, r, bw - 1, g_ascii ? L'|' : L'│', edge);
}
draw_mode_line(t, w, pad + h, bw); draw_mode_line(t, w, pad + h, bw);
int cr, cc; int cr, cc;
input_layout(t, NULL, NULL, &cr, &cc); input_layout(t, NULL, NULL, &cr, &cc);
@@ -2370,43 +2387,79 @@ static void draw_home(tui_t *t) {
getmaxyx(t->logw, h, w); getmaxyx(t->logw, h, w);
int box_top = t->in_y - t->log_y; int box_top = t->in_y - t->log_y;
int box_bot = box_top + t->in_h; int box_bot = box_top + t->in_h;
if (g_ascii || !g_utf8 || g_logo_text) { /* Tight home stack: wordmark (default) or CST_LOGO=block, tagline, chip. */
int x = (w - 8) / 2, y = box_top - 2; int brand_y = box_top - 2;
if (y >= 0) { if (brand_y < 0)
draw_ansi_at(t->logw, y, x, 3, "cst", CP_LOGO_DIM, 1, 0); brand_y = 0;
draw_ansi_at(t->logw, y, x + 3, 5, "agent", CP_BRIGHT, 1, 0); int want_block = 0;
{
const char *lb = getenv("CST_LOGO");
want_block = lb && !strcmp(lb, "block");
} }
} else if (box_top - LOGO_H - 1 >= 0 && w >= LOGO_W) { if (want_block && !(g_ascii || !g_utf8 || g_logo_text) &&
int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 1; box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) {
int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 2;
for (int g = 0; g < 8; g++) for (int g = 0; g < 8; g++)
for (int r = 0; r < LOGO_H; r++) for (int r = 0; r < LOGO_H; r++)
draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r],
g < 3 ? CP_LOGO_DIM : CP_BRIGHT, 0, 0); g < 3 ? CP_ACCENT : CP_BRIGHT, 0, 0);
brand_y = y0 + LOGO_H;
} else {
const char *mark = "CST-agent";
int mw = (int)strlen(mark);
int x = (w - mw) / 2;
if (x < 0)
x = 0;
if (brand_y >= 1)
brand_y -= 1;
draw_ansi_at(t->logw, brand_y, x, 3, "CST", CP_ACCENT, 1, 0);
draw_ansi_at(t->logw, brand_y, x + 3, mw - 3, "-agent", CP_BRIGHT, 1, 0);
}
int ty = brand_y + 1;
if (ty < box_top) {
const char *tag =
g_ascii ? "code . plan . parliament" : "code · plan · parliament";
int tw = (int)strlen(tag);
int x = (w - tw) / 2;
if (x < 0)
x = 0;
draw_ansi_at(t->logw, ty, x, tw, tag, CP_MUTED, 0, 0);
}
if (cst_parliament_is_active() && ty + 1 < box_top) {
char *st = cst_parliament_status_text();
if (st && st[0]) {
char chip[160];
snprintf(chip, sizeof(chip), "%s %s", g_ascii ? "*" : "◆", st);
int cw = vis_cols(chip);
if (cw > w - 2)
cw = w - 2;
draw_ansi_at(t->logw, ty + 1, (w - cw) / 2, cw, chip, CP_ACCENT, 0, 0);
}
free(st);
} }
int right = t->in_x - t->log_x + t->in_w; int right = t->in_x - t->log_x + t->in_w;
if (box_bot + 1 < h) { if (box_bot < h) {
char hint[96]; char hint[96];
snprintf(hint, sizeof(hint), snprintf(hint, sizeof(hint),
"\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands"); "\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands");
int hw = vis_cols(hint); int hw = vis_cols(hint);
if (right - hw >= 0) if (right - hw >= 0)
draw_ansi_at(t->logw, box_bot + 1, right - hw, hw, hint, CP_MUTED, 0, 0); draw_ansi_at(t->logw, box_bot, right - hw, hw, hint, CP_MUTED, 0, 0);
} }
if (t->tip && box_bot + 3 < h) { if (t->tip && box_bot + 2 < h) {
char tip[256]; char tip[256];
snprintf(tip, sizeof(tip), "\033[33m%s Tip\033[39m %s", snprintf(tip, sizeof(tip), "%s %s", g_ascii ? "*" : "●", t->tip);
g_ascii ? "*" : "●", t->tip);
int tw = vis_cols(tip); int tw = vis_cols(tip);
if (tw > w - 2) if (tw > w - 4)
tw = w - 2; tw = w - 4;
draw_ansi_at(t->logw, box_bot + 3, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0); draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0);
} }
const char *ext = cst_extensions_report(); const char *ext = cst_extensions_report();
if (ext[0] && box_bot + 5 < h) { if (ext[0] && box_bot + 3 < h) {
int ew = vis_cols(ext); int ew = vis_cols(ext);
if (ew > w - 2) if (ew > w - 2)
ew = w - 2; ew = w - 2;
draw_ansi_at(t->logw, box_bot + 5, (w - ew) / 2, ew, ext, CP_MUTED, 0, 0); draw_ansi_at(t->logw, box_bot + 3, (w - ew) / 2, ew, ext, CP_MUTED, 0, 0);
} }
int room = h - box_bot; int room = h - box_bot;
draw_completion(t, box_bot, t->in_x - t->log_x, t->in_w, room, 0); draw_completion(t, box_bot, t->in_x - t->log_x, t->in_w, room, 0);
@@ -2704,10 +2757,61 @@ static int is_home(const tui_t *t) {
static void layout(tui_t *t); static void layout(tui_t *t);
static void draw_topbar(tui_t *t) {
if (!t->header)
return;
WINDOW *w = t->header;
if (HAS_COLOR())
wbkgd(w, COLOR_PAIR(CP_TOPBAR));
werase(w);
int bw = getmaxx(w);
const cst_agent_config_t *cfg = cst_agent_config(t->agent);
char left[256];
const char *title = t->title[0] ? t->title : "New session";
/* Session store ids look like hex prefixes — keep chrome human. */
{
int hexish = 1;
size_t n = strlen(title);
if (n >= 8 && n <= 40) {
for (size_t i = 0; i < n; i++) {
char c = title[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F') || c == '-')) {
hexish = 0;
break;
}
}
} else {
hexish = 0;
}
if (hexish)
title = "New session";
}
snprintf(left, sizeof(left), " %s", title);
int x = draw_ansi_at(w, 0, 0, bw, left, CP_TOPBAR, 1, 0);
if (cfg && x + 8 < bw) {
char mid[160];
snprintf(mid, sizeof(mid), " %.48s%s%.24s", cfg->model,
cfg->provider[0] ? " · " : "", cfg->provider);
x += draw_ansi_at(w, 0, x, bw - x, mid, CP_TOPBAR_MUTED, 0, 0);
}
if (cst_parliament_is_active() && bw > 24) {
char chip[64];
if (cst_parliament_is_chair())
snprintf(chip, sizeof(chip), " chair ");
else
snprintf(chip, sizeof(chip), " seat ");
int cw = (int)strlen(chip);
draw_ansi_at(w, 0, bw - cw - 1, cw, chip, CP_TOPBAR_ACCENT, 1, 0);
}
wrefresh(w);
}
static void redraw_all(tui_t *t) { static void redraw_all(tui_t *t) {
if (is_home(t) != t->home) if (is_home(t) != t->home)
layout(t); layout(t);
theme_paint_root(); theme_paint_root();
draw_topbar(t);
draw_log(t); draw_log(t);
draw_todo(t); draw_todo(t);
draw_side(t); draw_side(t);
@@ -2988,8 +3092,10 @@ static int clampi(int v, int lo, int hi) {
return v < lo ? lo : v > hi ? hi : v; return v < lo ? lo : v > hi ? hi : v;
} }
/* Start screen: centered logo + box. Session: transcript column, input box static void draw_topbar(tui_t *t);
* and status row at the bottom, optional sidebar on the right. */
/* Start screen: centered brand + composer. Session: top chrome, transcript,
* input card, status; optional sidebar. */
static void layout(tui_t *t) { static void layout(tui_t *t) {
getmaxyx(stdscr, t->rows, t->cols); getmaxyx(stdscr, t->rows, t->cols);
if (t->rows < 4) if (t->rows < 4)
@@ -3002,6 +3108,7 @@ static void layout(tui_t *t) {
t->side_w = 0; t->side_w = 0;
t->todo_rows = 0; t->todo_rows = 0;
int pad = t->rows >= 20 ? 1 : 0; int pad = t->rows >= 20 ? 1 : 0;
int topbar = (!t->home && t->rows >= 12) ? 1 : 0;
if (t->home) { if (t->home) {
t->in_w = t->cols - 8 < 72 ? t->cols - 8 : 72; t->in_w = t->cols - 8 < 72 ? t->cols - 8 : 72;
@@ -3009,23 +3116,26 @@ static void layout(tui_t *t) {
t->in_w = t->cols - 2; t->in_w = t->cols - 2;
t->in_x = (t->cols - t->in_w) / 2; t->in_x = (t->cols - t->in_w) / 2;
} else { } else {
if (t->show_side && t->cols >= 120) if (t->show_side && t->cols >= 110)
t->side_w = clampi(t->cols / 4, 32, 42); t->side_w = clampi(t->cols / 4, 30, 40);
int mw = t->cols - t->side_w; int mw = t->cols - t->side_w;
t->in_x = 1; t->in_x = 1;
t->in_w = mw - 2; t->in_w = mw - 2;
} }
t->input_rows = input_visual_rows(t); t->input_rows = input_visual_rows(t);
t->in_h = t->input_rows + 1 + 2 * pad; t->in_h = t->input_rows + 1 + 2 * pad;
if (t->in_h > t->rows - 2) { if (t->in_h > t->rows - 2 - topbar) {
t->input_rows = clampi(t->rows - 3, 1, INPUT_MAX_ROWS); t->input_rows = clampi(t->rows - 3 - topbar, 1, INPUT_MAX_ROWS);
t->in_h = t->input_rows + 1; t->in_h = t->input_rows + 1;
} }
if (t->home) { if (t->home) {
int block = LOGO_H + 1 + t->in_h + 4; /* Compact vertical rhythm: brand + tagline + composer, not a huge void. */
int block = 3 + t->in_h + 3;
int top = (t->rows - 1 - block) / 2; int top = (t->rows - 1 - block) / 2;
t->in_y = clampi(top + LOGO_H + 1, 0, t->rows - 1 - t->in_h); if (top < 1)
top = 1;
t->in_y = clampi(top + 3, 0, t->rows - 1 - t->in_h);
t->log_x = t->log_y = 0; t->log_x = t->log_y = 0;
t->logw = tui_newwin(t->rows - 1, t->cols, 0, 0); t->logw = tui_newwin(t->rows - 1, t->cols, 0, 0);
t->status = tui_newwin(1, t->cols - 2, t->rows - 1, 1); t->status = tui_newwin(1, t->cols - 2, t->rows - 1, 1);
@@ -3037,7 +3147,9 @@ static void layout(tui_t *t) {
if (open && t->rows >= 16) if (open && t->rows >= 16)
t->todo_rows = 1 + (total < TODO_PANEL_MAX ? total : TODO_PANEL_MAX); t->todo_rows = 1 + (total < TODO_PANEL_MAX ? total : TODO_PANEL_MAX);
t->log_x = 1; t->log_x = 1;
t->log_y = t->rows >= 12 ? 1 : 0; t->log_y = topbar;
if (topbar)
t->header = tui_newwin(1, mw, 0, 0);
int log_h = t->in_y - 1 - t->todo_rows - t->log_y; int log_h = t->in_y - 1 - t->todo_rows - t->log_y;
if (log_h < 1) { if (log_h < 1) {
t->todo_rows = 0; t->todo_rows = 0;
@@ -3055,6 +3167,8 @@ static void layout(tui_t *t) {
wbkgdset(t->logw, COLOR_PAIR(CP_PANEL)); wbkgdset(t->logw, COLOR_PAIR(CP_PANEL));
wbkgdset(t->status, COLOR_PAIR(CP_PANEL)); wbkgdset(t->status, COLOR_PAIR(CP_PANEL));
wbkgdset(t->input, COLOR_PAIR(CP_BOX)); wbkgdset(t->input, COLOR_PAIR(CP_BOX));
if (t->header)
wbkgdset(t->header, COLOR_PAIR(CP_TOPBAR));
if (t->side) if (t->side)
wbkgdset(t->side, COLOR_PAIR(CP_SIDE)); wbkgdset(t->side, COLOR_PAIR(CP_SIDE));
} }
@@ -7088,7 +7202,7 @@ int cst_tui_run(cst_agent_t *agent) {
t.focus_think_idx = -1; t.focus_think_idx = -1;
t.hist_browse = -1; t.hist_browse = -1;
t.show_todo = 1; t.show_todo = 1;
t.show_side = 1; t.show_side = 0; /* /sidebar to open; keeps the main column calm by default */
{ {
cst_settings_t st; cst_settings_t st;
cst_settings_load(&st); cst_settings_load(&st);
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""ANSI pane dump → HTML → PNG (chrome headless). Usage: tui_shot.py in.ansi out.png"""
from __future__ import annotations
import html
import subprocess
import sys
from pathlib import Path
SPECIAL = {
75: "#6ea8fe",
74: "#61b8c7",
216: "#faa06b",
233: "#0e0f12",
234: "#14161e",
235: "#1a1c24",
244: "#80848f",
250: "#bcbcbc",
252: "#d1d4d9",
255: "#ffffff",
242: "#6a6a6a",
114: "#66bf73",
167: "#d95252",
157: "#afff87",
22: "#005f00",
203: "#ff5f5f",
52: "#5f0000",
}
def ansi_to_html(text: str) -> str:
cmap = {
30: "#3c3c46",
31: "#e65a5a",
32: "#5ad278",
33: "#faa06b",
34: "#6ea8fe",
35: "#d282d2",
36: "#5ad2e6",
37: "#bcbcbc",
90: "#80848f",
91: "#ff8282",
92: "#82ff96",
93: "#ffeb82",
94: "#96beff",
95: "#eba0eb",
96: "#82ebff",
97: "#ffffff",
}
out: list[str] = []
i = 0
bold = dim = False
fg = "#d1d4d9"
open_span = False
def close() -> None:
nonlocal open_span
if open_span:
out.append("</span>")
open_span = False
def reopen() -> None:
nonlocal open_span
close()
styles = [f"color:{fg}"]
if bold:
styles.append("font-weight:700")
if dim:
styles.append("opacity:.65")
out.append(f'<span style="{";".join(styles)}">')
open_span = True
reopen()
while i < len(text):
if text[i] == "\x1b" and i + 1 < len(text) and text[i + 1] == "[":
j = i + 2
while j < len(text) and text[j] != "m":
j += 1
seq = text[i + 2 : j]
i = j + 1 if j < len(text) else len(text)
parts = [int(p) for p in seq.split(";") if p.isdigit()] if seq else [0]
k = 0
while k < len(parts):
c = parts[k]
if c == 0:
bold = dim = False
fg = "#d1d4d9"
elif c == 1:
bold = True
elif c == 2:
dim = True
elif c == 22:
bold = dim = False
elif c == 38 and k + 2 < len(parts) and parts[k + 1] == 5:
fg = SPECIAL.get(parts[k + 2], fg)
k += 2
elif c in cmap:
fg = cmap[c]
elif c == 39:
fg = "#d1d4d9"
k += 1
reopen()
continue
if text[i] == "\n":
out.append("\n")
i += 1
continue
if text[i] == "\r":
i += 1
continue
out.append(html.escape(text[i]))
i += 1
close()
body = "".join(out)
return f"""<!doctype html><meta charset=utf-8>
<style>
html,body{{margin:0;background:#07080a}}
pre{{margin:0;padding:18px 22px;background:#0e0f12;color:#d1d4d9;
font:14px/1.35 "JetBrains Mono","Cascadia Mono",Menlo,Consolas,monospace;
white-space:pre; min-height:100vh; letter-spacing:0.01em}}
</style><pre>{body}</pre>"""
def main() -> int:
if len(sys.argv) < 3:
print("usage: tui_shot.py in.ansi out.png", file=sys.stderr)
return 2
ansi = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
html_path = Path(sys.argv[2]).with_suffix(".html")
html_path.write_text(ansi_to_html(ansi), encoding="utf-8")
png = Path(sys.argv[2])
chrome = "google-chrome"
subprocess.run(
[
chrome,
"--headless=new",
"--disable-gpu",
"--no-sandbox",
"--hide-scrollbars",
"--window-size=1280,800",
f"--screenshot={png}",
html_path.resolve().as_uri(),
],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
print(png, png.stat().st_size if png.exists() else 0)
return 0 if png.exists() else 1
if __name__ == "__main__":
raise SystemExit(main())