aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-16 00:38:17 +0200
committeruser <user@clank>2026-08-16 00:38:17 +0200
commit8079f982bb240d6594b060caee7a5ac6f89e39b3 (patch)
treee316fd3504360d1123774eb3a1a35459ef5ce725
parentsl0ppty: sl0ppty --check (diff)
downloadslosh-8079f982bb240d6594b060caee7a5ac6f89e39b3.tar.gz
slosh-8079f982bb240d6594b060caee7a5ac6f89e39b3.tar.xz
slosh-8079f982bb240d6594b060caee7a5ac6f89e39b3.zip
sl0ppty: padding, per side
padding 1 // every side padding 0 2 // vertical, horizontal padding 2 0 0 1 // top, right, bottom, left One value, two, or four in CSS order, because that is the order anybody who has written a stylesheet already knows. `pad` becomes four fields and the content rect is deflated per side rather than symmetrically, which is the only real change: everything downstream already worked off `n->content`. The units do not change with how many numbers you write. They are ROWS, like `gap`, and a horizontal one is multiplied by `gap_aspect` on the way out -- so `padding 1`, `padding 1 1` and `padding 1 1 1 1` are one padding rather than three, and a ring that looks square is still one number rather than an arithmetic problem. Anyone who wants plain cells sets `gap_aspect 1` and gets them everywhere, which is what that knob is for. Three values is refused with a line saying what it wanted. CSS reads it as top/horizontal/bottom, and a line whose meaning you have to look up is a line nobody can read -- and `sl0ppty --check` now has something to say about it, which is the difference between refusing and ignoring. The dump writes the shortest form that means what is in force, so `padding 0` stays `padding 0` and a dump of a dump is still the same dump. Checked by measuring both rects: a pane's insets from its own frame, four numbers, for every form -- including that `gap_aspect 1` turns the horizontal ones into cells, and that a refused padding leaves the rest of the file applied.
-rw-r--r--config/config.kdl15
-rw-r--r--docs/config.md19
-rw-r--r--src/app.c22
-rw-r--r--src/config.c43
-rw-r--r--src/config.h8
-rw-r--r--tests/test_config.py64
6 files changed, 158 insertions, 13 deletions
diff --git a/config/config.kdl b/config/config.kdl
index 77201a9..b0ff1f9 100644
--- a/config/config.kdl
+++ b/config/config.kdl
@@ -80,7 +80,20 @@ gap 1
// a gap of 1 row and 2 columns looks square.
gap_aspect 2
-// Cells between a pane's frame and its content.
+// Space between a pane's frame and its contents, in ROWS like `gap` -- a
+// horizontal one is multiplied by `gap_aspect`, so `padding 1` is a ring that
+// looks square rather than a squashed one. One number does every side; two are
+// vertical then horizontal; four are CSS order, top right bottom left:
+//
+// padding 1 // all round
+// padding 0 2 // none above or below, two either side
+// padding 1 2 1 2 // the same thing, spelled out
+// padding 2 0 0 1 // room for a title above, a gutter on the left
+//
+// A number means the same thing however many of them you write, which is why
+// the first three of those are two paddings rather than three. Three values is
+// refused: CSS reads it as top/horizontal/bottom, and a line whose meaning you
+// have to look up is a line nobody can read.
padding 0
// ╭╮╰╯ versus ┌┐└┘
diff --git a/docs/config.md b/docs/config.md
index e18f91e..b504111 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -109,6 +109,25 @@ theme { frame_focus "#00ff88" } // ...but that one colour is mine
| effects | `shaders { }`, `states { }` — see [shaders](shaders.md) and [chrome](chrome.md) |
| keys | `keys { }` — see [keys](keys.md) |
+Two of those take more than one value. `gap` is in **rows**, and `gap_aspect`
+says how many columns a row is worth (2 by default, because a cell is about
+twice as tall as it is wide) — so both `gap` and `padding` are written in rows
+and come out looking square.
+
+`padding` is the space between a pane's frame and its contents, written the way
+CSS does it:
+
+```kdl
+padding 1 // every side
+padding 0 2 // vertical, horizontal
+padding 2 0 0 1 // top, right, bottom, left
+```
+
+A number means the same thing however many of them you write, so `padding 1`,
+`padding 1 1` and `padding 1 1 1 1` are one padding rather than three. Three
+values is refused — CSS reads it as top/horizontal/bottom, and a line whose
+meaning you have to look up is a line nobody can read.
+
## Pane states
A pane is tinted by what is *true* of it, which is the part you cannot discover
diff --git a/src/app.c b/src/app.c
index fefb2ba..e339a33 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1144,15 +1144,21 @@ static void layout_node(node_t *n, rect_t r, layout_ctx_t *ctx) {
n->collapsed = false;
if (n->kind == NODE_LEAF) n->hidden = false;
if (n->kind == NODE_LEAF) {
- /* content is the frame deflated by its border and padding; a rect too
- * small for a frame gets none, and the pane takes the whole thing. */
- uint16_t bx = r.w >= 3 && r.h >= 3 ? 1 + CFG.pad * CFG.gap_aspect : 0;
- uint16_t by = r.w >= 3 && r.h >= 3 ? 1 + CFG.pad : 0;
+ /* content is the frame deflated by its border and its padding, per side; a
+ * rect too small for a frame gets neither, and the pane takes the whole
+ * thing. Horizontal padding is aspect-corrected, like the gap: the config's
+ * numbers are rows, so a square-looking ring is one number rather than an
+ * arithmetic problem. */
+ bool framed = r.w >= 3 && r.h >= 3;
+ uint16_t left = framed ? 1 + CFG.pad_left * CFG.gap_aspect : 0;
+ uint16_t right = framed ? 1 + CFG.pad_right * CFG.gap_aspect : 0;
+ uint16_t top = framed ? 1 + CFG.pad_top : 0;
+ uint16_t bottom = framed ? 1 + CFG.pad_bottom : 0;
n->content = (rect_t){
- .x = (uint16_t)(r.x + bx),
- .y = (uint16_t)(r.y + by),
- .w = (uint16_t)(r.w > 2 * bx ? r.w - 2 * bx : 1),
- .h = (uint16_t)(r.h > 2 * by ? r.h - 2 * by : 1),
+ .x = (uint16_t)(r.x + left),
+ .y = (uint16_t)(r.y + top),
+ .w = (uint16_t)(r.w > left + right ? r.w - left - right : 1),
+ .h = (uint16_t)(r.h > top + bottom ? r.h - top - bottom : 1),
};
if (ctx->apply && !n->hidden)
pane_resize(n->pane, n->content.w, n->content.h);
diff --git a/src/config.c b/src/config.c
index f01619b..4618eea 100644
--- a/src/config.c
+++ b/src/config.c
@@ -450,7 +450,7 @@ void config_defaults(config_t *c) {
memset(c, 0, sizeof *c);
c->gap = 1;
c->gap_aspect = 2;
- c->pad = 0;
+ c->pad_top = c->pad_right = c->pad_bottom = c->pad_left = 0;
c->rounded = true;
c->title_align = ALIGN_CENTER;
c->title_inset = 2;
@@ -981,7 +981,16 @@ char *config_render(const config_t *c) {
cb_add(&b, "gap %u\n", c->gap);
cb_add(&b, "gap_aspect %u // columns per row, so a gap looks square\n",
c->gap_aspect);
- cb_add(&b, "padding %u\n", c->pad);
+ /* The shortest form that means what is in force, because `padding 0` reads
+ * better than `padding 0 0 0 0` and round-trips to the same four numbers. */
+ if (c->pad_top == c->pad_right && c->pad_top == c->pad_bottom &&
+ c->pad_top == c->pad_left)
+ cb_add(&b, "padding %u\n", c->pad_top);
+ else if (c->pad_top == c->pad_bottom && c->pad_right == c->pad_left)
+ cb_add(&b, "padding %u %u\n", c->pad_top, c->pad_right);
+ else
+ cb_add(&b, "padding %u %u %u %u // top right bottom left\n", c->pad_top,
+ c->pad_right, c->pad_bottom, c->pad_left);
cb_add(&b, "rounded %s\n", yesno(c->rounded));
cb_add(&b, "title_align \"%s\"\n",
c->title_align == ALIGN_LEFT ? "left"
@@ -1287,7 +1296,35 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
c->gap = (uint16_t)kdl_arg_int(kdl_child(root, "gap"), 0, c->gap);
c->gap_aspect =
(uint16_t)kdl_arg_int(kdl_child(root, "gap_aspect"), 0, c->gap_aspect);
- c->pad = (uint16_t)kdl_arg_int(kdl_child(root, "padding"), 0, c->pad);
+ /* `padding 1`, `padding 0 2`, or `padding 1 2 1 2` -- one value for every
+ * side, two for vertical and horizontal, four in CSS order (top, right,
+ * bottom, left), because that is the order everybody who has written a
+ * stylesheet already knows. Three is refused rather than guessed at: CSS says
+ * top/horizontal/bottom and a reader who has to look that up is a reader who
+ * cannot see what the line does. */
+ {
+ const kdl_node_t *pn = kdl_child(root, "padding");
+ if (pn) {
+ long v[4];
+ size_t n = pn->nargs < 4 ? pn->nargs : 4;
+ for (size_t i = 0; i < n; i++) v[i] = kdl_arg_int(pn, i, 0);
+ if (pn->nargs == 1) {
+ c->pad_top = c->pad_right = c->pad_bottom = c->pad_left = (uint16_t)v[0];
+ } else if (pn->nargs == 2) {
+ c->pad_top = c->pad_bottom = (uint16_t)v[0];
+ c->pad_right = c->pad_left = (uint16_t)v[1];
+ } else if (pn->nargs == 4) {
+ c->pad_top = (uint16_t)v[0];
+ c->pad_right = (uint16_t)v[1];
+ c->pad_bottom = (uint16_t)v[2];
+ c->pad_left = (uint16_t)v[3];
+ } else {
+ complain(c, err, errcap, pn->line,
+ "padding takes 1, 2 or 4 values (all, vertical horizontal, or "
+ "top right bottom left), not %zu", pn->nargs);
+ }
+ }
+ }
c->rounded = kdl_arg_bool(kdl_child(root, "rounded"), 0, c->rounded);
c->status_bar = kdl_arg_bool(kdl_child(root, "status_bar"), 0, c->status_bar);
c->status_line =
diff --git a/src/config.h b/src/config.h
index e8c7f98..c0f8e2b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -102,7 +102,13 @@ typedef enum { ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT } align_t;
typedef struct {
/* geometry */
- uint16_t gap, gap_aspect, pad;
+ uint16_t gap, gap_aspect;
+ /* Space between a pane's frame and its contents, per side, in the same unit
+ * `gap` uses: ROWS. A horizontal one is multiplied by `gap_aspect` on the way
+ * out, so `padding 1` is a square-looking ring rather than a squashed one —
+ * and so a number means the same thing wherever it appears, however many of
+ * them were written. Written as 1, 2 or 4 values, CSS order. */
+ uint16_t pad_top, pad_right, pad_bottom, pad_left;
bool rounded;
align_t title_align;
/* Cells between a frame's corner and the start of its title. The title
diff --git a/tests/test_config.py b/tests/test_config.py
index 3c8cbcb..0ba9f5c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -46,6 +46,67 @@ def test_geometry():
os.unlink(path)
+def insets(pane):
+ """A pane's content rect as four insets from its frame: top, right, bottom,
+ left. What `padding` is actually about, and the only way to see it is to
+ measure both rects."""
+ return (pane["content_y"] - pane["y"],
+ (pane["x"] + pane["w"]) - (pane["content_x"] + pane["content_w"]),
+ (pane["y"] + pane["h"]) - (pane["content_y"] + pane["content_h"]),
+ pane["content_x"] - pane["x"])
+
+
+def pad_insets(text, cols=60, rows=16):
+ path = cfg(text)
+ with Session(SH, cols=cols, rows=rows, config=path) as s:
+ s.settle()
+ got = insets(s.pane())
+ os.unlink(path)
+ return got
+
+
+def test_padding_takes_one_two_or_four_values():
+ """One for every side, two for vertical and horizontal, four in CSS order.
+ A number means the same thing however many of them were written, which is
+ why all three of these are the same padding."""
+ check("no padding is just the border", pad_insets("padding 0\n") == (1, 1, 1, 1),
+ str(pad_insets("padding 0\n")))
+
+ one = pad_insets("padding 1\n")
+ check("one value pads every side", one == (2, 3, 2, 3), str(one))
+ check("...and two equal values say the same thing",
+ pad_insets("padding 1 1\n") == one, str(pad_insets("padding 1 1\n")))
+ check("...as do four", pad_insets("padding 1 1 1 1\n") == one,
+ str(pad_insets("padding 1 1 1 1\n")))
+
+ # Horizontal padding is aspect-corrected, like the gap: the numbers are rows,
+ # so a ring that looks square is one number rather than arithmetic.
+ two = pad_insets("padding 0 2\n")
+ check("two values are vertical then horizontal", two == (1, 5, 1, 5), str(two))
+ check("and gap_aspect 1 makes the units plain cells",
+ pad_insets("gap_aspect 1\npadding 0 2\n") == (1, 3, 1, 3),
+ str(pad_insets("gap_aspect 1\npadding 0 2\n")))
+
+ four = pad_insets("padding 2 0 0 1\n")
+ check("four values are top right bottom left", four == (3, 1, 1, 3), str(four))
+
+
+def test_a_padding_nobody_can_read_is_refused():
+ """Three values is CSS for top/horizontal/bottom, which a reader has to look
+ up -- so it is refused rather than guessed at, and the rest of the file still
+ applies."""
+ path = cfg("padding 1 2 3\ngap 2\n")
+ with Session(SH, cols=60, rows=16, config=path) as s:
+ s.settle()
+ check("the padding was not applied", insets(s.pane()) == (1, 1, 1, 1),
+ str(insets(s.pane())))
+ check("but the rest of the file was", s.pane()["y"] == 3, str(s.pane()))
+ reply = s.api("reload")
+ check("and it says what it wanted", "padding takes 1, 2 or 4" in
+ reply.get("warning", ""), str(reply))
+ os.unlink(path)
+
+
def test_status_bar_off():
path = cfg("status_bar false\n")
with Session(SH, cols=50, rows=10, config=path) as s:
@@ -426,6 +487,9 @@ def test_an_unknown_theme_name_is_refused_not_ignored():
if __name__ == "__main__":
test_geometry()
test_status_bar_off()
+ test_padding_takes_one_two_or_four_values()
+ test_a_padding_nobody_can_read_is_refused()
+ test_a_chord_can_be_written_the_way_the_cheatsheet_prints_it()
test_theme()
test_keys()
test_min_pane_drives_collapse()