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:
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 |
@@ -3,7 +3,7 @@
|
||||
|
||||
#define CST_VERSION_MAJOR 0
|
||||
#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) CST_VERSION_STR_(x)
|
||||
|
||||
@@ -260,7 +260,10 @@ enum {
|
||||
CP_ACCENT, /* blue on panel */
|
||||
CP_BRIGHT, /* near-white on panel */
|
||||
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 {
|
||||
@@ -487,7 +490,7 @@ static char *xstrndup(const char *s, size_t n) {
|
||||
|
||||
/* --- theme ---------------------------------------------------------------- */
|
||||
|
||||
/* OpenCode dark panel — never inherit terminal default bg. */
|
||||
/* Dark panel chrome — cool steel accents (not peach OpenCode clone). */
|
||||
static void theme_init(void) {
|
||||
const char *nc = getenv("NO_COLOR");
|
||||
g_nocolor = nc && nc[0];
|
||||
@@ -504,7 +507,7 @@ static void theme_init(void) {
|
||||
start_color();
|
||||
/* 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 muted = COLOR_CYAN;
|
||||
short success = COLOR_GREEN;
|
||||
@@ -517,42 +520,39 @@ static void theme_init(void) {
|
||||
if (COLORS >= 256) {
|
||||
box = 235;
|
||||
side = 234;
|
||||
accent = 75;
|
||||
accent = 75; /* #5c9cf5 steel blue */
|
||||
bright = 255;
|
||||
logo_dim = 242;
|
||||
/* Prefer xterm-256 slots so we don't bleach the 8 ANSI base colors.
|
||||
* panel #121212→233, primary #fab283→216, muted #808080→244 */
|
||||
primary = 216;
|
||||
panel = 233; /* #121212 — darker than 234 to avoid washed look */
|
||||
primary = 75; /* brand/user accent: blue, not peach */
|
||||
panel = 233; /* #121212 */
|
||||
muted = 244;
|
||||
success = 114;
|
||||
err = 167;
|
||||
assist = 74; /* soft cyan, not neon */
|
||||
text = 250; /* #bcbcbc — not near-white */
|
||||
assist = 74; /* soft cyan assistant */
|
||||
text = 252; /* brighter body for readability */
|
||||
if (can_change_color()) {
|
||||
/* Tune those slots toward exact OpenCode hex */
|
||||
init_color(216, 980, 698, 514); /* #fab283 */
|
||||
init_color(233, 70, 70, 70); /* #121212 */
|
||||
init_color(244, 502, 502, 502); /* #808080 */
|
||||
init_color(250, 720, 720, 720); /* body text */
|
||||
init_color(75, 430, 660, 996); /* #6ea8fe brand */
|
||||
init_color(233, 55, 58, 70); /* #0e0f12 deeper panel */
|
||||
init_color(244, 502, 520, 560); /* cool muted */
|
||||
init_color(252, 820, 830, 850); /* body */
|
||||
init_color(114, 400, 750, 450);
|
||||
init_color(167, 850, 320, 320);
|
||||
init_color(74, 350, 700, 750);
|
||||
init_color(235, 118, 118, 118); /* #1e1e1e raised box */
|
||||
init_color(234, 90, 90, 90); /* #171717 sidebar */
|
||||
init_color(75, 361, 612, 961); /* #5c9cf5 accent */
|
||||
init_color(74, 380, 720, 780);
|
||||
init_color(235, 100, 105, 120); /* raised box */
|
||||
init_color(234, 78, 82, 96); /* sidebar */
|
||||
init_color(216, 980, 720, 420); /* warm tip only */
|
||||
}
|
||||
} else if (can_change_color() && COLORS >= 8) {
|
||||
init_color(COLOR_BLACK, 70, 70, 70); /* panel #121212 */
|
||||
init_color(COLOR_YELLOW, 980, 698, 514); /* primary */
|
||||
init_color(COLOR_CYAN, 502, 502, 502); /* muted (reuse cyan slot) */
|
||||
init_color(COLOR_WHITE, 720, 720, 720); /* body text */
|
||||
init_color(COLOR_BLACK, 55, 58, 70);
|
||||
init_color(COLOR_CYAN, 430, 660, 996);
|
||||
init_color(COLOR_WHITE, 820, 830, 850);
|
||||
init_color(COLOR_GREEN, 400, 750, 450);
|
||||
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;
|
||||
primary = COLOR_YELLOW;
|
||||
muted = COLOR_CYAN;
|
||||
primary = COLOR_CYAN;
|
||||
muted = COLOR_WHITE;
|
||||
text = COLOR_WHITE;
|
||||
success = COLOR_GREEN;
|
||||
err = COLOR_RED;
|
||||
@@ -608,6 +608,9 @@ static void theme_init(void) {
|
||||
init_pair(CP_BRIGHT, bright, panel);
|
||||
init_pair(CP_LOGO_DIM, logo_dim, panel);
|
||||
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
|
||||
@@ -2196,8 +2199,22 @@ static void draw_input(tui_t *t) {
|
||||
if (pad < 0)
|
||||
pad = 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, bw - 1, g_ascii ? L'|' : L'│', edge);
|
||||
}
|
||||
draw_mode_line(t, w, pad + h, bw);
|
||||
int 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);
|
||||
int box_top = t->in_y - t->log_y;
|
||||
int box_bot = box_top + t->in_h;
|
||||
if (g_ascii || !g_utf8 || g_logo_text) {
|
||||
int x = (w - 8) / 2, y = box_top - 2;
|
||||
if (y >= 0) {
|
||||
draw_ansi_at(t->logw, y, x, 3, "cst", CP_LOGO_DIM, 1, 0);
|
||||
draw_ansi_at(t->logw, y, x + 3, 5, "agent", CP_BRIGHT, 1, 0);
|
||||
}
|
||||
} else if (box_top - LOGO_H - 1 >= 0 && w >= LOGO_W) {
|
||||
int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 1;
|
||||
/* Tight home stack: wordmark (default) or CST_LOGO=block, tagline, chip. */
|
||||
int brand_y = box_top - 2;
|
||||
if (brand_y < 0)
|
||||
brand_y = 0;
|
||||
int want_block = 0;
|
||||
{
|
||||
const char *lb = getenv("CST_LOGO");
|
||||
want_block = lb && !strcmp(lb, "block");
|
||||
}
|
||||
if (want_block && !(g_ascii || !g_utf8 || g_logo_text) &&
|
||||
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 r = 0; r < LOGO_H; 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;
|
||||
if (box_bot + 1 < h) {
|
||||
if (box_bot < h) {
|
||||
char hint[96];
|
||||
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);
|
||||
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];
|
||||
snprintf(tip, sizeof(tip), "\033[33m%s Tip\033[39m %s",
|
||||
g_ascii ? "*" : "●", t->tip);
|
||||
snprintf(tip, sizeof(tip), "%s %s", g_ascii ? "*" : "●", t->tip);
|
||||
int tw = vis_cols(tip);
|
||||
if (tw > w - 2)
|
||||
tw = w - 2;
|
||||
draw_ansi_at(t->logw, box_bot + 3, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0);
|
||||
if (tw > w - 4)
|
||||
tw = w - 4;
|
||||
draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0);
|
||||
}
|
||||
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);
|
||||
if (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;
|
||||
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 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) {
|
||||
if (is_home(t) != t->home)
|
||||
layout(t);
|
||||
theme_paint_root();
|
||||
draw_topbar(t);
|
||||
draw_log(t);
|
||||
draw_todo(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;
|
||||
}
|
||||
|
||||
/* Start screen: centered logo + box. Session: transcript column, input box
|
||||
* and status row at the bottom, optional sidebar on the right. */
|
||||
static void draw_topbar(tui_t *t);
|
||||
|
||||
/* Start screen: centered brand + composer. Session: top chrome, transcript,
|
||||
* input card, status; optional sidebar. */
|
||||
static void layout(tui_t *t) {
|
||||
getmaxyx(stdscr, t->rows, t->cols);
|
||||
if (t->rows < 4)
|
||||
@@ -3002,6 +3108,7 @@ static void layout(tui_t *t) {
|
||||
t->side_w = 0;
|
||||
t->todo_rows = 0;
|
||||
int pad = t->rows >= 20 ? 1 : 0;
|
||||
int topbar = (!t->home && t->rows >= 12) ? 1 : 0;
|
||||
|
||||
if (t->home) {
|
||||
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_x = (t->cols - t->in_w) / 2;
|
||||
} else {
|
||||
if (t->show_side && t->cols >= 120)
|
||||
t->side_w = clampi(t->cols / 4, 32, 42);
|
||||
if (t->show_side && t->cols >= 110)
|
||||
t->side_w = clampi(t->cols / 4, 30, 40);
|
||||
int mw = t->cols - t->side_w;
|
||||
t->in_x = 1;
|
||||
t->in_w = mw - 2;
|
||||
}
|
||||
t->input_rows = input_visual_rows(t);
|
||||
t->in_h = t->input_rows + 1 + 2 * pad;
|
||||
if (t->in_h > t->rows - 2) {
|
||||
t->input_rows = clampi(t->rows - 3, 1, INPUT_MAX_ROWS);
|
||||
if (t->in_h > t->rows - 2 - topbar) {
|
||||
t->input_rows = clampi(t->rows - 3 - topbar, 1, INPUT_MAX_ROWS);
|
||||
t->in_h = t->input_rows + 1;
|
||||
}
|
||||
|
||||
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;
|
||||
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->logw = tui_newwin(t->rows - 1, t->cols, 0, 0);
|
||||
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)
|
||||
t->todo_rows = 1 + (total < TODO_PANEL_MAX ? total : TODO_PANEL_MAX);
|
||||
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;
|
||||
if (log_h < 1) {
|
||||
t->todo_rows = 0;
|
||||
@@ -3055,6 +3167,8 @@ static void layout(tui_t *t) {
|
||||
wbkgdset(t->logw, COLOR_PAIR(CP_PANEL));
|
||||
wbkgdset(t->status, COLOR_PAIR(CP_PANEL));
|
||||
wbkgdset(t->input, COLOR_PAIR(CP_BOX));
|
||||
if (t->header)
|
||||
wbkgdset(t->header, COLOR_PAIR(CP_TOPBAR));
|
||||
if (t->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.hist_browse = -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_load(&st);
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user