aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 19:14:29 +0200
committeruser <user@clank>2026-08-14 19:14:29 +0200
commite75eecfc64adbf5fae0f4ddc3eca1dae41028fa2 (patch)
tree21b114e4b5a517b9762e25a3057504b69c9e38c8 /src
parentsl0ppty: give the scroll arrow room to breathe (diff)
downloadslosh-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.
Diffstat (limited to 'src')
-rw-r--r--src/app.c91
-rw-r--r--src/config.c92
-rw-r--r--src/config.h47
3 files changed, 191 insertions, 39 deletions
diff --git a/src/app.c b/src/app.c
index 22f3a17..3714775 100644
--- a/src/app.c
+++ b/src/app.c
@@ -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;