diff options
| author | user <user@clank> | 2026-08-14 19:14:29 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 19:14:29 +0200 |
| commit | e75eecfc64adbf5fae0f4ddc3eca1dae41028fa2 (patch) | |
| tree | 21b114e4b5a517b9762e25a3057504b69c9e38c8 | |
| parent | sl0ppty: give the scroll arrow room to breathe (diff) | |
| download | slosh-e75eecfc64adbf5fae0f4ddc3eca1dae41028fa2.tar.gz slosh-e75eecfc64adbf5fae0f4ddc3eca1dae41028fa2.tar.xz slosh-e75eecfc64adbf5fae0f4ddc3eca1dae41028fa2.zip | |
sl0ppty: one theme name per decision, and a tab you can see
The count, since it was asked: no hardcoded colours at all in the drawing code.
Every colour already came from the config. The problem was the other way round
— six names were serving about thirty decisions, so the names were the thing
that was hardcoded. frame_focus was the focused frame *and* the split guide
*and* the resize handle *and* the tab hover *and* a collapsed row under the
pointer. Recolouring one recoloured five.
There are thirty-five entries now, one per surface, each documented in
config.kdl by being its own default. Several still share a value: that is a
statement that they look right together, not that they are the same thing, and
the difference only matters the first time someone wants to change one of them.
The tab you are in is filled rather than merely bold, which was the ask. Bold
alone is easy to miss on a strip of short labels, and it was the only thing
distinguishing the tab you were in from the four you were not.
That immediately raised a question the old scheme could not ask: what does
hovering the active tab look like, when it already has the strongest treatment
on the strip? Keeping its fill and brightening its text, because it is still
the tab you are in and clicking it does nothing. My first version gave it no
hover at all, which reads as the hover being broken rather than as the click
being pointless.
Three surfaces were still borrowing a name that did not describe them: the
in-place rename editor was wearing the button colours, a pane's OSC status text
and a suspended pane's hint were wearing the frame's. They have their own now.
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | config/config.kdl | 70 | ||||
| -rw-r--r-- | src/app.c | 91 | ||||
| -rw-r--r-- | src/config.c | 92 | ||||
| -rw-r--r-- | src/config.h | 47 | ||||
| -rw-r--r-- | tests/test_config.py | 58 | ||||
| -rw-r--r-- | tests/test_tabs.py | 47 |
7 files changed, 347 insertions, 64 deletions
@@ -130,6 +130,12 @@ tab, the focused pane's purpose or title, which of the tab's panes it is started yet. Either can be turned off (`status_bar`, `status_line`), and the panes get the row back. +Every colour the compositor draws has its own name in `theme` — the split +guide, the resize handle, a drop target, the scroll indicator, the rows of a +collapsed tab, each state of a tab, the status line, the finder, toasts, the +rename editor. Several share a default, which says they look right together +rather than that they are the same thing. + Configuration is [`config/config.kdl`](config/config.kdl), which documents every setting by being the defaults. Copy it to `~/.config/sl0ppty/config.kdl`. diff --git a/config/config.kdl b/config/config.kdl index 51202b7..70b1a8a 100644 --- a/config/config.kdl +++ b/config/config.kdl @@ -171,23 +171,73 @@ toast_ms 2500 // ── theme ─────────────────────────────────────────────────────────────────── +// Every colour the compositor draws has its own name here. They started as six +// names doing thirty jobs, which meant the split guide could not be recoloured +// without also recolouring the focused frame, because they were the same +// entry. Several still share a default: that says they look right together, +// not that they are the same thing. theme { // What a cell's "terminal default" colour is taken to be when a shader has - // to compute on it (dimming a pane, greying it out). We never learn your - // terminal's real default, and most text is default-coloured, so without - // these a shader would leave nearly everything untouched. Set them to your - // terminal's actual foreground/background or shaded panes will shift - // colour as they turn on. Panes with no shader ignore these entirely and - // still defer to your terminal. + // to compute on it. See the shaders section above. default_fg "#ffffff" default_bg "#000000" - frame_focus "#ff5fd7" - frame_idle "#45454a" - title "#ffffff" + // ── panes ─────────────────────────────────────────────────────────────── + frame_focus "#ff5fd7" // border of the focused pane + frame_idle "#45454a" // border of every other pane + title "#ffffff" // its title, and its OSC 5577 status text + title_idle "#45454a" // the same, on a pane that is not focused + + // A pane's own OSC 5577 buttons. button_fg "#141418" button_bg "#ff5fd7" - button_bg_idle "#55555c" + button_bg_idle "#55555c" // buttons on a pane that is not focused + + // ── things that appear under the pointer ──────────────────────────────── + guide "#ff5fd7" // split guide: armed edge, dashed boundary, arrow + resize "#ff5fd7" // the handle in the gap between two panes + drop_target "#ff5fd7" // frame of a pane a drag could land on + + // ── a pane's scrollback indicator ─────────────────────────────────────── + scroll_fg "#141418" + scroll_bg "#ff5fd7" + + // ── rows of a tab that has collapsed into a list ──────────────────────── + header "#45454a" + header_hover "#ff5fd7" // the rule, under the pointer + header_hover_title "#ffffff" // the name, under the pointer + + // ── the strip along the top ───────────────────────────────────────────── + // The tab you are in is filled rather than merely bold: bold alone is easy + // to miss on a strip of short labels. It keeps that fill while the pointer + // is on it -- it is still the tab you are in -- so its hover lands on the + // text instead. + tab_active_fg "#141418" + tab_active_bg "#ff5fd7" + tab_active_hover_fg "#ffffff" + tab_idle "#45454a" + tab_hover "#ff5fd7" + prefix_fg "#141418" // the "C-a" badge, while the prefix is pending + prefix_bg "#ff5fd7" + tab_count "#45454a" // panes in the whole session + + // ── the line along the bottom ─────────────────────────────────────────── + status "#45454a" // session, tab, pane, and the pane index + status_state "#ffffff" // scrolled / alt screen / not started / exited + + // ── the pane finder ───────────────────────────────────────────────────── + finder_fg "#ffffff" + finder_bg "#45454a" + finder_sel_fg "#141418" + finder_sel_bg "#ff5fd7" + + // ── the in-place editor a double-click opens on a pane title ──────────── + rename_fg "#141418" + rename_bg "#ff5fd7" + + // ── announcements ─────────────────────────────────────────────────────── + toast_fg "#141418" + toast_bg "#ff5fd7" } // ── keys ──────────────────────────────────────────────────────────────────── @@ -65,6 +65,33 @@ bool app_reload_config(char *err, size_t errcap) { #define BTN_FG (CFG.button_fg) #define BTN_BG (CFG.button_bg) #define BTN_BG_IDLE (CFG.button_bg_idle) +#define TITLE_IDLE (CFG.title_idle) +#define GUIDE (CFG.guide) +#define RESIZE_C (CFG.resize) +#define DROP_C (CFG.drop_target) +#define SCROLL_FG (CFG.scroll_fg) +#define SCROLL_BG (CFG.scroll_bg) +#define HEADER (CFG.header) +#define HEADER_HOVER (CFG.header_hover) +#define HEADER_HOVER_TITLE (CFG.header_hover_title) +#define TAB_ACTIVE_FG (CFG.tab_active_fg) +#define TAB_ACTIVE_BG (CFG.tab_active_bg) +#define TAB_ACTIVE_HOVER_FG (CFG.tab_active_hover_fg) +#define TAB_IDLE (CFG.tab_idle) +#define TAB_HOVER (CFG.tab_hover) +#define PREFIX_FG (CFG.prefix_fg) +#define PREFIX_BG (CFG.prefix_bg) +#define TAB_COUNT (CFG.tab_count) +#define STATUS_C (CFG.status) +#define STATUS_STATE (CFG.status_state) +#define FINDER_FG (CFG.finder_fg) +#define FINDER_BG (CFG.finder_bg) +#define FINDER_SEL_FG (CFG.finder_sel_fg) +#define FINDER_SEL_BG (CFG.finder_sel_bg) +#define TOAST_FG (CFG.toast_fg) +#define TOAST_BG (CFG.toast_bg) +#define RENAME_FG (CFG.rename_fg) +#define RENAME_BG (CFG.rename_bg) static const color_t NO_COLOR = {0}; @@ -328,7 +355,7 @@ static void draw_status_line(app_t *a, screen_t *s) { snprintf(cnt, sizeof cnt, "%zu pane%s", nl, nl == 1 ? "" : "s"); uint16_t cw = (uint16_t)strlen(cnt); if (right > x + cw + 2) { - screen_text(s, (uint16_t)(right - cw), y, cnt, FRAME_IDLE, NO_COLOR, 0); + screen_text(s, (uint16_t)(right - cw), y, cnt, STATUS_C, NO_COLOR, 0); right = (uint16_t)(right - cw - 2); } } @@ -356,7 +383,7 @@ static void draw_status_line(app_t *a, screen_t *s) { for (const char *q = ind; *q; q++) if (((unsigned char)*q & 0xC0) != 0x80) iw++; if (right > x + iw + 2) { - screen_text(s, (uint16_t)(right - iw), y, ind, TITLE_FOCUS, NO_COLOR, + screen_text(s, (uint16_t)(right - iw), y, ind, STATUS_STATE, NO_COLOR, ATTR_BOLD); right = (uint16_t)(right - iw - 2); } @@ -383,7 +410,7 @@ static void draw_status_line(app_t *a, screen_t *s) { } if (!n) return; if (n > (size_t)(right - x)) line[right - x] = 0; - screen_text(s, x, y, line, FRAME_IDLE, NO_COLOR, 0); + screen_text(s, x, y, line, STATUS_C, NO_COLOR, 0); } static void draw_toasts(app_t *a, screen_t *s) { @@ -401,7 +428,7 @@ static void draw_toasts(app_t *a, screen_t *s) { uint16_t x = (uint16_t)(s->cols - w - CFG.gap * CFG.gap_aspect); char clipped[160]; snprintf(clipped, sizeof clipped, "%.*s", (int)w, line); - screen_text(s, x, y, clipped, BTN_FG, BTN_BG, ATTR_BOLD); + screen_text(s, x, y, clipped, TOAST_FG, TOAST_BG, ATTR_BOLD); } } @@ -1514,7 +1541,8 @@ static void draw_pane_status(screen_t *s, node_t *leaf, color_t fg, len = room; buf[len] = 0; } - screen_text(s, left, y, buf, focused ? TITLE_FOCUS : fg, NO_COLOR, 0); + screen_text(s, left, y, buf, focused ? TITLE_FOCUS : TITLE_IDLE, + NO_COLOR, 0); } } @@ -1558,7 +1586,7 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { rect_t r = leaf->rect; if (r.w < 4 || r.h < 4) return; uint16_t x1 = (uint16_t)(r.x + r.w - 1), y1 = (uint16_t)(r.y + r.h - 1); - color_t hi = FRAME_FOCUS; + color_t hi = GUIDE; /* An arrow on the new boundary, pointing at the side the new pane will take. * The dashed line says where, and on its own leaves which half is the new @@ -1596,7 +1624,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { bool focused = leaf == cur(a)->focus; bool drop_target = a->drag.kind == DRAG_TITLE && a->drag.target == leaf->id && a->drag.src != leaf->id; - color_t fg = drop_target ? BTN_BG : (focused ? FRAME_FOCUS : FRAME_IDLE); + color_t fg = drop_target ? DROP_C : (focused ? FRAME_FOCUS : FRAME_IDLE); uint16_t attrs = drop_target ? ATTR_BOLD : 0; /* While a pane is being dragged, every other pane is somewhere it could be @@ -1674,7 +1702,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { uint16_t iw = (uint16_t)(nd + 4); /* space, arrow, space, digits, space */ if (iw + 2 < avail) { uint16_t ix = (uint16_t)((has_btn ? btn_x : x1) - iw); - uint16_t drawn = screen_text(s, ix, r.y, ind, BTN_FG, BTN_BG, ATTR_BOLD); + uint16_t drawn = screen_text(s, ix, r.y, ind, SCROLL_FG, SCROLL_BG, ATTR_BOLD); char action[48]; snprintf(action, sizeof action, "scrollbottom:%u", leaf->id); hit_add(&s->hits, ix, r.y, drawn, 1, action); @@ -1718,8 +1746,9 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) { * is being typed, so a half-finished rename can never be mistaken for what * the pane is actually called. */ uint16_t drawn = - screen_text(s, tx, r.y, buf, editing ? BTN_FG : (focused ? TITLE_FOCUS : fg), - editing ? BTN_BG : NO_COLOR, + screen_text(s, tx, r.y, buf, + editing ? RENAME_FG : (focused ? TITLE_FOCUS : TITLE_IDLE), + editing ? RENAME_BG : NO_COLOR, editing || focused ? ATTR_BOLD : 0); /* The title names the pane; it is not an edge. It carves its own cells out @@ -1775,8 +1804,8 @@ static void draw_collapsed(app_t *a, screen_t *s, node_t *n) { /* Foreground only. A filled bar is louder than the thing it is telling you, * and this row has to sit in a list of its own kind without shouting. */ - color_t rule = hot ? FRAME_FOCUS : FRAME_IDLE; - color_t label = hot ? TITLE_FOCUS : FRAME_IDLE; + color_t rule = hot ? HEADER_HOVER : HEADER; + color_t label = hot ? HEADER_HOVER_TITLE : HEADER; /* A collapsed pane draws the top edge of a pane, corners and all. It is not * decoration: this row *is* a pane, closed — so a stack of them reads as a @@ -1939,7 +1968,7 @@ static void draw_cb(node_t *n, void *ud) { } } screen_text(d->s, (uint16_t)(n->content.x + (n->content.w - w) / 2), - (uint16_t)(n->content.y + n->content.h / 2), line, FRAME_IDLE, + (uint16_t)(n->content.y + n->content.h / 2), line, TITLE_IDLE, NO_COLOR, 0); } else { pane_compose(n->pane, d->s, n->content.x, n->content.y, @@ -1965,11 +1994,12 @@ static void draw_tab_strip(app_t *a, screen_t *s) { snprintf(info, sizeof info, "%zu pane%s", np, np == 1 ? "" : "s"); uint16_t iw = (uint16_t)strlen(info); if (right > iw + 4) { - screen_text(s, (uint16_t)(right - iw), y, info, FRAME_IDLE, NO_COLOR, 0); + screen_text(s, (uint16_t)(right - iw), y, info, TAB_COUNT, NO_COLOR, 0); right = (uint16_t)(right - iw - 1); } if (a->prefix && right > 5) { /* the prefix is a mode: say so */ - screen_text(s, (uint16_t)(right - 3), y, "C-a", BTN_FG, BTN_BG, ATTR_BOLD); + screen_text(s, (uint16_t)(right - 3), y, "C-a", PREFIX_FG, PREFIX_BG, + ATTR_BOLD); right = (uint16_t)(right - 4); } @@ -1988,9 +2018,14 @@ static void draw_tab_strip(app_t *a, screen_t *s) { * costs a repaint of a few cells and guarantees the lit cells are the * registered ones. */ uint16_t w = screen_text(s, x, y, label, - active ? TITLE_FOCUS : FRAME_IDLE, NO_COLOR, attrs); + active ? TAB_ACTIVE_FG : TAB_IDLE, + active ? TAB_ACTIVE_BG : NO_COLOR, attrs); + /* Hovering keeps the active tab's fill — it is still the tab you are in — + * so its feedback lands on the text instead. An inactive tab has no fill + * to keep, and brightens. */ if (ptr_on(a, x, y, w, 1)) - screen_text(s, x, y, label, FRAME_FOCUS, NO_COLOR, attrs); + screen_text(s, x, y, label, active ? TAB_ACTIVE_HOVER_FG : TAB_HOVER, + active ? TAB_ACTIVE_BG : NO_COLOR, attrs | ATTR_BOLD); char action[48]; snprintf(action, sizeof action, "tab:%u", t->id); hit_add(&s->hits, x, y, w, 1, action); @@ -2000,9 +2035,9 @@ static void draw_tab_strip(app_t *a, screen_t *s) { /* Not a bare "+": the frame already has one, for splitting a pane. Two * verbs that look identical is a UI bug the fork shipped and noticed. */ if (x + 6 < right) { - uint16_t w = screen_text(s, x, y, " +tab ", FRAME_IDLE, NO_COLOR, 0); + uint16_t w = screen_text(s, x, y, " +tab ", TAB_IDLE, NO_COLOR, 0); if (ptr_on(a, x, y, w, 1)) - screen_text(s, x, y, " +tab ", FRAME_FOCUS, NO_COLOR, 0); + screen_text(s, x, y, " +tab ", TAB_HOVER, NO_COLOR, ATTR_BOLD); hit_add(&s->hits, x, y, w, 1, "newtab"); } } @@ -2049,16 +2084,16 @@ static void draw_resize_hint(app_t *a, screen_t *s, node_t *split, size_t idx, if (split->dir == SPLIT_COLS) { uint16_t x = (uint16_t)(gapr.x + gapr.w / 2); for (uint16_t y = gapr.y; y < gapr.y + gapr.h; y++) - screen_text(s, x, y, active ? "\u2551" : "\u250a", FRAME_FOCUS, NO_COLOR, + screen_text(s, x, y, active ? "\u2551" : "\u250a", RESIZE_C, NO_COLOR, attrs); - screen_text(s, x, (uint16_t)(gapr.y + gapr.h / 2), "\u21d4", FRAME_FOCUS, + screen_text(s, x, (uint16_t)(gapr.y + gapr.h / 2), "\u21d4", RESIZE_C, NO_COLOR, ATTR_BOLD); } else { uint16_t y = (uint16_t)(gapr.y + gapr.h / 2); for (uint16_t x = gapr.x; x < gapr.x + gapr.w; x++) - screen_text(s, x, y, active ? "\u2550" : "\u2508", FRAME_FOCUS, NO_COLOR, + screen_text(s, x, y, active ? "\u2550" : "\u2508", RESIZE_C, NO_COLOR, attrs); - screen_text(s, (uint16_t)(gapr.x + gapr.w / 2), y, "\u21d5", FRAME_FOCUS, + screen_text(s, (uint16_t)(gapr.x + gapr.w / 2), y, "\u21d5", RESIZE_C, NO_COLOR, ATTR_BOLD); } } @@ -2158,12 +2193,12 @@ static void draw_finder(app_t *a, screen_t *s) { for (uint16_t yy = y; yy < y + h; yy++) for (uint16_t xx = x; xx < x + w; xx++) - screen_text(s, xx, yy, " ", NO_COLOR, FRAME_IDLE, 0); + screen_text(s, xx, yy, " ", NO_COLOR, FINDER_BG, 0); char head[128]; snprintf(head, sizeof head, " find: %s\u2588", a->query); - screen_text(s, (uint16_t)(x + 1), (uint16_t)(y + 1), head, TITLE_FOCUS, - FRAME_IDLE, ATTR_BOLD); + screen_text(s, (uint16_t)(x + 1), (uint16_t)(y + 1), head, FINDER_FG, + FINDER_BG, ATTR_BOLD); for (size_t i = 0; i < rows && i < n; i++) { size_t idx = i + (a->sel >= rows ? a->sel - rows + 1 : 0); @@ -2179,8 +2214,8 @@ static void draw_finder(app_t *a, screen_t *s) { bool on = idx == a->sel; uint16_t yy = (uint16_t)(y + 3 + i); uint16_t drawn = screen_text(s, (uint16_t)(x + 1), yy, line, - on ? BTN_FG : TITLE_FOCUS, - on ? BTN_BG : FRAME_IDLE, 0); + on ? FINDER_SEL_FG : FINDER_FG, + on ? FINDER_SEL_BG : FINDER_BG, 0); char action[48]; snprintf(action, sizeof action, "find:%u", e->node->id); hit_add(&s->hits, (uint16_t)(x + 1), yy, drawn, 1, action); diff --git a/src/config.c b/src/config.c index 9fd2cd3..11585d7 100644 --- a/src/config.c +++ b/src/config.c @@ -202,13 +202,56 @@ void config_defaults(config_t *c) { 140); c->state_n[PSTATE_DROP_HOVER] = 2; - c->frame_focus = rgb(0xff, 0x5f, 0xd7); - c->frame_idle = rgb(0x45, 0x45, 0x4a); - c->title_focus = rgb(0xff, 0xff, 0xff); - c->button_fg = rgb(0x14, 0x14, 0x18); - c->button_bg = rgb(0xff, 0x5f, 0xd7); + const color_t accent = rgb(0xff, 0x5f, 0xd7); + const color_t ink = rgb(0x14, 0x14, 0x18); + const color_t dim = rgb(0x45, 0x45, 0x4a); + const color_t bright = rgb(0xff, 0xff, 0xff); + + c->frame_focus = accent; + c->frame_idle = dim; + c->title_focus = bright; + c->title_idle = dim; + + c->button_fg = ink; + c->button_bg = accent; c->button_bg_idle = rgb(0x55, 0x55, 0x5c); + c->guide = accent; + c->resize = accent; + c->drop_target = accent; + + c->scroll_fg = ink; + c->scroll_bg = accent; + + c->header = dim; + c->header_hover = accent; + c->header_hover_title = bright; + + /* The tab you are in is stated in colour, not only in weight: bold alone is + * easy to miss on a strip of short labels. */ + c->tab_active_fg = ink; + c->tab_active_bg = accent; + c->tab_active_hover_fg = bright; + c->tab_idle = dim; + c->tab_hover = accent; + c->prefix_fg = ink; + c->prefix_bg = accent; + c->tab_count = dim; + + c->status = dim; + c->status_state = bright; + + c->finder_fg = bright; + c->finder_bg = dim; + c->finder_sel_fg = ink; + c->finder_sel_bg = accent; + + c->rename_fg = ink; + c->rename_bg = accent; + + c->toast_fg = ink; + c->toast_bg = accent; + c->prefix_key = GHOSTTY_KEY_A; c->prefix_mods = MOD_CTRL; @@ -376,10 +419,41 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { const char *name; color_t *slot; } colors[] = { - {"default_fg", &c->default_fg}, {"default_bg", &c->default_bg}, - {"frame_focus", &c->frame_focus}, {"frame_idle", &c->frame_idle}, - {"title", &c->title_focus}, {"button_fg", &c->button_fg}, - {"button_bg", &c->button_bg}, {"button_bg_idle", &c->button_bg_idle}, + {"default_fg", &c->default_fg}, + {"default_bg", &c->default_bg}, + {"frame_focus", &c->frame_focus}, + {"frame_idle", &c->frame_idle}, + {"title", &c->title_focus}, + {"title_idle", &c->title_idle}, + {"button_fg", &c->button_fg}, + {"button_bg", &c->button_bg}, + {"button_bg_idle", &c->button_bg_idle}, + {"guide", &c->guide}, + {"resize", &c->resize}, + {"drop_target", &c->drop_target}, + {"scroll_fg", &c->scroll_fg}, + {"scroll_bg", &c->scroll_bg}, + {"header", &c->header}, + {"header_hover", &c->header_hover}, + {"header_hover_title", &c->header_hover_title}, + {"tab_active_fg", &c->tab_active_fg}, + {"tab_active_bg", &c->tab_active_bg}, + {"tab_active_hover_fg", &c->tab_active_hover_fg}, + {"tab_idle", &c->tab_idle}, + {"tab_hover", &c->tab_hover}, + {"prefix_fg", &c->prefix_fg}, + {"prefix_bg", &c->prefix_bg}, + {"tab_count", &c->tab_count}, + {"status", &c->status}, + {"status_state", &c->status_state}, + {"finder_fg", &c->finder_fg}, + {"finder_bg", &c->finder_bg}, + {"finder_sel_fg", &c->finder_sel_fg}, + {"finder_sel_bg", &c->finder_sel_bg}, + {"rename_fg", &c->rename_fg}, + {"rename_bg", &c->rename_bg}, + {"toast_fg", &c->toast_fg}, + {"toast_bg", &c->toast_bg}, }; for (size_t i = 0; i < sizeof colors / sizeof *colors; i++) { const char *v = kdl_arg(kdl_child(theme, colors[i].name), 0, NULL); diff --git a/src/config.h b/src/config.h index 9d0b022..869eeaf 100644 --- a/src/config.h +++ b/src/config.h @@ -125,10 +125,53 @@ typedef struct { * terminal exactly as before. */ color_t default_fg, default_bg; - /* theme */ - color_t frame_focus, frame_idle, title_focus; + /* theme + * + * One name per decision. These began as six names doing thirty jobs, which + * meant the split guide could not be recoloured without also recolouring the + * focused frame, because they were the same entry. Several still share a + * default — that is a statement about what looks right together, not about + * them being the same thing. */ + + /* panes */ + color_t frame_focus, frame_idle; + color_t title_focus, title_idle; + + /* things you can press: the pane's own OSC 5577 buttons */ color_t button_fg, button_bg, button_bg_idle; + /* affordances that appear under the pointer */ + color_t guide; /* the split guide: armed edge and dashed boundary */ + color_t resize; /* the handle in the gap between two panes */ + color_t drop_target; /* the frame of a pane a drag could land on */ + + /* the scrollback indicator in a pane's frame */ + color_t scroll_fg, scroll_bg; + + /* rows of a tab that has collapsed into a list */ + color_t header, header_hover, header_hover_title; + + /* the strip along the top */ + color_t tab_active_fg, tab_active_bg; + /* The active tab keeps its fill while the pointer is on it -- it is still + * the tab you are in -- so its hover shows in the text instead. */ + color_t tab_active_hover_fg; + color_t tab_idle, tab_hover; + color_t prefix_fg, prefix_bg; /* the "C-a" badge */ + color_t tab_count; /* how many panes in the session */ + + /* the line along the bottom */ + color_t status, status_state; + + /* the pane finder */ + color_t finder_fg, finder_bg, finder_sel_fg, finder_sel_bg; + + /* the in-place editor a double-click opens on a pane's title */ + color_t rename_fg, rename_bg; + + /* announcements */ + color_t toast_fg, toast_bg; + /* keys */ int prefix_key; uint16_t prefix_mods; diff --git a/tests/test_config.py b/tests/test_config.py index 7566b76..44447e0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,12 +9,17 @@ import os import subprocess import sys import tempfile +import time from harness import Session, check, report, BIN SH = ["/bin/sh", "-c", 'printf "\\033]2;cfg\\007"; stty raw -echo; cat'] +def hover(s, x, y): + s.send(rf"\e[<35;{x + 1};{y + 1}M") + + def cfg(text): f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) f.write(text) @@ -317,6 +322,57 @@ def test_status_pad_holds_both_bars_off_the_edge(): +def test_every_surface_has_its_own_theme_name(): + """The point of splitting six names into thirty-five: recolouring one + surface must not recolour another that happened to share an entry.""" + conf = cfg('theme {\n' + ' guide "#00ff00"\n' + ' tab_active_bg "#0000ff"\n' + ' status "#ff8800"\n' + ' header "#ff00ff"\n' + '}\n') + with Session(SH, cols=80, rows=16, config=conf) as s: + s.settle(20) + snap = s.snapshot() + + tab = [h for h in snap.hits if h["action"].startswith("tab:")][0] + check("tab_active_bg fills the tab you are in", + (snap.style_at(tab["x"] + 1, tab["y"]) or {}).get("bg") == "#0000ff", + str(snap.style_at(tab["x"] + 1, tab["y"]))) + check("status colours the bottom line", + (snap.style_at(4, snap.rows - 2) or {}).get("fg") == "#ff8800", + str(snap.style_at(4, snap.rows - 2))) + + # ...and the frame, which shares a *default* with guide, did not move. + p = s.pane() + check("the focused frame keeps its own colour", + (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7", + str(snap.style_at(p["x"], p["y"] + 1))) + + # The guide is the one that used to be the same entry as the frame. + hover(s, p["x"], p["y"] + 3) + time.sleep(0.35) + s.settle(40) + snap = s.snapshot() + check("and the split guide takes the colour named for it", + (snap.style_at(p["x"], p["y"] + 3) or {}).get("fg") == "#00ff00", + str(snap.style_at(p["x"], p["y"] + 3))) + os.unlink(conf) + + +def test_an_unknown_theme_name_is_refused_not_ignored(): + conf = cfg('theme {\n frame_focus "not-a-colour"\n}\n') + with Session(SH, cols=60, rows=12, config=conf) as s: + s.settle(20) + check("a bad colour costs a warning, not a terminal", s.alive(), "") + p = s.pane() + check("and the compiled-in default is kept", + (s.snapshot().style_at(p["x"], p["y"] + 1) or {}).get("fg") + == "#ff5fd7", + str(s.snapshot().style_at(p["x"], p["y"] + 1))) + os.unlink(conf) + + if __name__ == "__main__": test_geometry() test_status_bar_off() @@ -330,4 +386,6 @@ if __name__ == "__main__": test_status_line_reports_scrollback() test_the_two_pane_counts_answer_different_questions() test_status_pad_holds_both_bars_off_the_edge() + test_every_surface_has_its_own_theme_name() + test_an_unknown_theme_name_is_refused_not_ignored() sys.exit(report()) diff --git a/tests/test_tabs.py b/tests/test_tabs.py index 8f98af3..2b5b6d6 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -174,9 +174,10 @@ def test_json_api(): # ---- hovering the strip ---------------------------------------------------- -STRIP_HOT = "#ff5fd7" # pointer is here -STRIP_ON = "#ffffff" # this is the tab you are in -STRIP_OFF = "#45454a" +ACCENT = "#ff5fd7" # the fill of the tab you are in, and hover on the rest +INK = "#141418" # text on that fill +BRIGHT = "#ffffff" # that text while the pointer is on it +DIM = "#45454a" # a tab you are not in def two_tab_layout(): @@ -200,6 +201,10 @@ def fg_of(snap, h): return (snap.style_at(h["x"] + 1, h["y"]) or {}).get("fg") +def bg_of(snap, h): + return (snap.style_at(h["x"] + 1, h["y"]) or {}).get("bg") + + def attrs_of(snap, h): return (snap.style_at(h["x"] + 1, h["y"]) or {}).get("attrs", []) @@ -211,14 +216,18 @@ def test_hovering_the_strip_lights_what_is_under_the_pointer(): hits = strip_hits(s.snapshot()) check("the strip has two tabs and a +tab", len(hits) == 3, str(hits)) + active_id = "tab:" + str(s.tabs()[0]["id"]) for target, h in hits.items(): hover(s, h["x"] + 1, h["y"]) snap = s.snapshot() + # The tab you are in keeps its fill and brightens its text; the + # others have no fill to keep and take the accent. + want = BRIGHT if target == active_id else ACCENT check(f"hovering {target} lights it", - fg_of(snap, h) == STRIP_HOT, str(fg_of(snap, h))) - others = [fg_of(snap, o) for k, o in hits.items() if k != target] - check(f"and nothing else on the strip", - all(c != STRIP_HOT for c in others), str(others)) + fg_of(snap, h) == want, f"{fg_of(snap, h)} wanted {want}") + others = [(k, fg_of(snap, o)) for k, o in hits.items() if k != target] + check("and nothing else on the strip", + all(c in (INK, DIM) for _, c in others), str(others)) os.unlink(lay) @@ -232,16 +241,24 @@ def test_weight_says_active_and_colour_says_pointer(): active = hits["tab:" + str(s.tabs()[0]["id"])] snap = s.snapshot() - check("the active tab is bold and white while untouched", - fg_of(snap, active) == STRIP_ON and "bold" in attrs_of(snap, active), - f"{fg_of(snap, active)} {attrs_of(snap, active)}") + check("the tab you are in is filled, not merely bold", + fg_of(snap, active) == INK and bg_of(snap, active) == ACCENT, + f"{fg_of(snap, active)} on {bg_of(snap, active)}") + check("and still bold", "bold" in attrs_of(snap, active), + str(attrs_of(snap, active))) + + inactive = [h for k, h in hits.items() + if k.startswith("tab:") and h is not active][0] + check("a tab you are not in has no fill at all", + bg_of(snap, inactive) is None and fg_of(snap, inactive) == DIM, + f"{fg_of(snap, inactive)} on {bg_of(snap, inactive)}") hover(s, active["x"] + 1, active["y"]) snap = s.snapshot() - check("hovering it changes its colour", - fg_of(snap, active) == STRIP_HOT, str(fg_of(snap, active))) - check("but it keeps the weight that says you are in it", - "bold" in attrs_of(snap, active), str(attrs_of(snap, active))) + check("hovering it brightens the text", + fg_of(snap, active) == BRIGHT, str(fg_of(snap, active))) + check("but it keeps the fill that says you are in it", + bg_of(snap, active) == ACCENT, str(bg_of(snap, active))) os.unlink(lay) @@ -263,7 +280,7 @@ def test_leaving_the_strip_puts_it_out_and_hover_switches_nothing(): hover(s, p["content_x"] + 2, p["content_y"] + 1) snap = s.snapshot() check("nothing on the strip is lit once the pointer leaves it", - all(fg_of(snap, h) != STRIP_HOT for h in hits.values()), + all(fg_of(snap, h) in (INK, DIM) for h in hits.values()), str([fg_of(snap, h) for h in hits.values()])) # ...but clicking still switches. |
