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_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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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