diff options
| author | user <user@clank> | 2026-08-15 13:21:35 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 13:21:35 +0200 |
| commit | d24a07a6048e6d6416e97a26483739bdd879e01b (patch) | |
| tree | 764a08f255aeaa71fa8fbdc6bad7fa1b9835171f | |
| parent | sl0ppty: write the session back out as a layout, and a loop that uses it (diff) | |
| download | slosh-d24a07a6048e6d6416e97a26483739bdd879e01b.tar.gz slosh-d24a07a6048e6d6416e97a26483739bdd879e01b.tar.xz slosh-d24a07a6048e6d6416e97a26483739bdd879e01b.zip | |
sl0ppty: say which build you are talking to
The middle of the status line is empty unless the pointer is on something,
which is most of the time. It says which sl0ppty this is now.
That reads as decoration and is not. A session keeps the binary it started
with, so after a rebuild there are two versions in play: the one on disk and
the one you are attached to. This session has been dogfooding sl0ppty for a
while, and "which build is this?" has already been the answer to two bugs
that looked like something else -- once when a config reload appeared to do
nothing, and once when kitty graphics were fixed and still broken on screen.
The banner answers it without asking, and `sl0ppty --version` answers for the
binary, from the same string.
The version is baked in by the Makefile from `git describe --always --dirty`,
into a generated header that is only rewritten when the value changes -- so a
commit rebuilds and a second `make` does nothing.
It takes the hint's slot and yields to it: a hint is about what you are doing
right now, the banner is about always, and it comes back the moment the
pointer moves off. Both give way to the ends of the line, which was already
the rule for hints and is why a narrow terminal loses the middle rather than
its session name. `version_banner false` for anyone who finds it noisy; it is
a separate knob from `hints`, and the tests hold both directions of that.
| -rw-r--r-- | Makefile | 19 | ||||
| -rw-r--r-- | config/config.kdl | 11 | ||||
| -rw-r--r-- | src/app.c | 33 | ||||
| -rw-r--r-- | src/config.c | 3 | ||||
| -rw-r--r-- | src/config.h | 6 | ||||
| -rw-r--r-- | src/main.c | 10 | ||||
| -rw-r--r-- | tests/test_hints.py | 64 |
7 files changed, 133 insertions, 13 deletions
@@ -14,8 +14,14 @@ VT_INC := $(VT_OUT)/include # header rebuilds what actually uses it. Without this a header edit rebuilt # nothing and the only safe move was `make clean`, which is how a 0.1s build # turns into a habit of throwing the whole thing away. +# The version is baked in, because the first question when a session is +# behaving oddly is "which build is this?" -- and a session keeps the binary it +# started with, so the answer is not "whatever is in build/ now". +VERSION := 0.1.0 +GITDESC := $(shell git describe --always --dirty 2>/dev/null || echo unknown) + CFLAGS := -std=c23 -O1 -g -Wall -Wextra -Wno-unused-parameter -MMD -MP \ - -I$(VT_INC) -Isrc + -I$(VT_INC) -Isrc -Ibuild LDFLAGS := SRC := $(wildcard src/*.c) @@ -44,9 +50,18 @@ all: $(BIN) ## build sl0ppty $(BIN): $(OBJ) $(VT_LIB) $(CC) $(OBJ) $(VT_LIB) -o $@ $(LDFLAGS) -build/%.o: src/%.c $(VT_LIB) | build +build/%.o: src/%.c build/version.h $(VT_LIB) | build $(CC) $(CFLAGS) -c $< -o $@ +# Rewritten only when the value actually changes, so `make` after a commit +# rebuilds, and `make` twice in a row does not. +.PHONY: FORCE +build/version.h: FORCE | build + @printf '/* generated by the Makefile */\n#define SL0PPTY_VERSION "%s+%s"\n' \ + '$(VERSION)' '$(GITDESC)' > $@.tmp + @cmp -s $@.tmp $@ || mv $@.tmp $@ + @rm -f $@.tmp + build: @mkdir -p build diff --git a/config/config.kdl b/config/config.kdl index c67519b..cc43953 100644 --- a/config/config.kdl +++ b/config/config.kdl @@ -60,6 +60,17 @@ title_inset 2 // is explaining is worse than no hint. hints true +// When there is no hint to show — which is most of the time, since a hint +// needs the pointer to be on something — that space says which sl0ppty this +// is: `sl0ppty 0.1.0+d9dee7f`. It reads as decoration and is not: a session +// keeps the binary it started with, so after a rebuild the version on screen +// is the one you are actually talking to, and `sl0ppty --version` is the one +// on disk. When those disagree, that is the answer to whatever you were about +// to debug. (contrib/sl0ppty-dev restarts into the new one.) +// +// It gives way to a hint, and both give way to the ends of the line. +version_banner true + pane_buttons true // Plain single-width ASCII, after box-drawing and block characters were each @@ -12,6 +12,7 @@ #include <string.h> #include "json.h" +#include "version.h" #include "graphics.h" #include "kdl.h" @@ -610,22 +611,34 @@ static void draw_status_line(app_t *a, screen_t *s) { if (n > (size_t)(right - x)) line[right - x] = 0; if (n) screen_text(s, x, y, line, STATUS_C, NO_COLOR, 0); - /* The hint sits in what is left between the two ends, centred there rather - * than in the row: centring it in the row would put it under the session - * name on a narrow screen, and a hint that overwrites what it is explaining - * is worse than no hint. It is simply not drawn when it does not fit. */ - if (!CFG.hints || !a->painted) return; - const char *hint = hint_for(a, hit_test(&s->hits, a->ptr_x, a->ptr_y)); - if (!hint || !a->ptr_valid) return; + /* What goes between the two ends: the hint for whatever the pointer is on, + * and when there is none, which sl0ppty this is. The hint wins because it is + * about right now and the banner is about always; the banner takes the slot + * back the moment the pointer moves off, which is most of the time. + * + * Centred in what is *left* rather than in the row: centring it in the row + * would put it under the session name on a narrow screen, and either of + * these overwriting what it sits next to is worse than not being drawn. */ + const char *middle = NULL; + color_t middle_fg = HINT_C; + if (CFG.hints && a->painted && a->ptr_valid) + middle = hint_for(a, hit_test(&s->hits, a->ptr_x, a->ptr_y)); + if (!middle && CFG.version_banner) { + /* Quieter than a hint: this is ambient, and a hint is an answer to + * something you are doing. */ + middle = "sl0ppty " SL0PPTY_VERSION; + middle_fg = STATUS_C; + } + if (!middle) return; uint16_t used = (uint16_t)(x + cells(line)); uint16_t from = (uint16_t)(used + 2); if (right <= from) return; uint16_t span = (uint16_t)(right - from); - uint16_t hw = cells(hint); + uint16_t hw = cells(middle); if (hw + 2 > span) return; - screen_text(s, (uint16_t)(from + (span - hw) / 2), y, hint, HINT_C, NO_COLOR, - 0); + screen_text(s, (uint16_t)(from + (span - hw) / 2), y, middle, middle_fg, + NO_COLOR, 0); } static void draw_toasts(app_t *a, screen_t *s) { diff --git a/src/config.c b/src/config.c index 941d6e9..c31ca76 100644 --- a/src/config.c +++ b/src/config.c @@ -210,6 +210,7 @@ void config_defaults(config_t *c) { c->title_align = ALIGN_CENTER; c->title_inset = 2; c->hints = true; + c->version_banner = true; c->pane_buttons = true; snprintf(c->zoom_mark, sizeof c->zoom_mark, "#"); snprintf(c->zoom_on_mark, sizeof c->zoom_on_mark, "*"); @@ -618,6 +619,8 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { c->title_inset = (uint16_t)kdl_arg_int(kdl_child(root, "title_inset"), 0, c->title_inset); c->hints = kdl_arg_bool(kdl_child(root, "hints"), 0, c->hints); + c->version_banner = + kdl_arg_bool(kdl_child(root, "version_banner"), 0, c->version_banner); c->pane_buttons = kdl_arg_bool(kdl_child(root, "pane_buttons"), 0, c->pane_buttons); const char *zm = kdl_arg(kdl_child(root, "zoom_mark"), 0, NULL); diff --git a/src/config.h b/src/config.h index c093f93..2f844ee 100644 --- a/src/config.h +++ b/src/config.h @@ -98,6 +98,12 @@ typedef struct { * Discoverability for a frame whose affordances are mostly one character * wide; off if you already know them. */ bool hints; + /* What sl0ppty this is, in the middle of the status line, whenever there is + * no hint to put there. The slot is otherwise empty most of the time, and + * "which build am I attached to" is the first question when a session is + * behaving oddly -- a session keeps the binary it started with, so the + * answer is not whatever was built last. */ + bool version_banner; bool pane_buttons; /* Marks of similar visual weight, so the even spacing between the buttons * reads as even: a glyph that floats in a mostly-empty cell donates its own @@ -20,6 +20,7 @@ #include "proto.h" #include "server.h" +#include "version.h" int run_headless(const char *const argv[], uint16_t cols, uint16_t rows, int idle_ms, bool script, const char *layout); @@ -36,7 +37,8 @@ static void term_size(uint16_t *cols, uint16_t *rows) { } static void usage(void) { - fputs("usage: sl0ppty [-s NAME] [--layout FILE] [--no-reload]\n [ls | cmd LINE | -- CMD...]\n", + fputs("usage: sl0ppty [-s NAME] [--layout FILE] [--no-reload]\n" + " [--version] [ls | cmd LINE | -- CMD...]\n", stderr); } @@ -60,6 +62,12 @@ int main(int argc, char **argv) { else if (strcmp(a, "-s") == 0 && i + 1 < argc) name = argv[++i]; else if (strcmp(a, "--layout") == 0 && i + 1 < argc) layout = argv[++i]; else if (strcmp(a, "--no-reload") == 0) watch = false; + else if (strcmp(a, "--version") == 0) { + /* The same string the status line shows, so "which build is this" has + * one answer whether you ask the binary or the session. */ + printf("sl0ppty %s\n", SL0PPTY_VERSION); + return 0; + } else if (strcmp(a, "ls") == 0) list = true; else if (strcmp(a, "cmd") == 0 && i + 1 < argc) cmd_line = argv[++i]; else if (strcmp(a, "--cols") == 0 && i + 1 < argc) cols = (uint16_t)atoi(argv[++i]); diff --git a/tests/test_hints.py b/tests/test_hints.py index f39e874..35f9905 100644 --- a/tests/test_hints.py +++ b/tests/test_hints.py @@ -139,6 +139,67 @@ def test_the_hint_never_writes_over_the_line_it_shares(): and "pane 1/1" in row, repr(row)) +def test_the_version_sits_in_the_slot_when_no_hint_does(): + """The middle of the status line is empty most of the time, and the first + question about a misbehaving session is which build it is running — which + is not "whatever was built last", because a session keeps the binary it + started with.""" + with Session(SH, cols=96, rows=18) as s: + s.settle(20) + check("the banner is there by default", "sl0ppty 0." in bar(s), bar(s)) + + s.api("split", dir="cols") + s.settle(20) + h = hit(s, "close:") + hover(s, h["x"], h["y"]) + check("a hint takes the slot back", "close this pane" in bar(s), bar(s)) + check("and the banner gives way", "sl0ppty 0." not in bar(s), bar(s)) + + hover(s, h["x"], h["y"] + 4) # off the button, into the pane + s.settle(20) + check("the banner returns when the pointer moves off", + "sl0ppty 0." in bar(s), bar(s)) + + +def test_the_banner_and_the_hints_are_separate_knobs(): + off = cfg("version_banner false\n") + with Session(SH, cols=96, rows=18, config=off) as s: + s.settle(20) + check("version_banner false says nothing", "sl0ppty" not in bar(s), + bar(s)) + s.api("split", dir="cols") + s.settle(20) + h = hit(s, "close:") + hover(s, h["x"], h["y"]) + check("but hints still work", "close this pane" in bar(s), bar(s)) + os.unlink(off) + + nohints = cfg("hints false\n") + with Session(SH, cols=96, rows=18, config=nohints) as s: + s.settle(20) + check("hints false leaves the banner alone", "sl0ppty 0." in bar(s), + bar(s)) + s.api("split", dir="cols") + s.settle(20) + h = hit(s, "close:") + hover(s, h["x"], h["y"]) + check("and nothing replaces it", "sl0ppty 0." in bar(s), bar(s)) + os.unlink(nohints) + + +def test_the_banner_gives_way_when_there_is_no_room(): + """Same rule as the hint: a thing in the middle that overwrites the ends is + worse than an empty middle.""" + with Session(SH, cols=44, rows=10) as s: + s.settle(20) + s.api("set-purpose", target="tab", id=s.tabs()[0]["id"], + purpose="a-long-purpose-that-fills-the-line") + s.settle(20) + row = bar(s) + check("the ends win", "pane 1/1" in row and "sl0ppty 0." not in row, + repr(row)) + + def test_hints_can_be_turned_off(): conf = cfg("hints false\n") with Session(SH, cols=96, rows=18, config=conf) as s: @@ -159,4 +220,7 @@ if __name__ == "__main__": test_the_gap_and_the_strip_and_the_bar() test_the_hint_never_writes_over_the_line_it_shares() test_hints_can_be_turned_off() + test_the_version_sits_in_the_slot_when_no_hint_does() + test_the_banner_and_the_hints_are_separate_knobs() + test_the_banner_gives_way_when_there_is_no_room() sys.exit(report()) |
