diff options
| author | user <user@clank> | 2026-08-15 23:14:34 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 23:14:34 +0200 |
| commit | a25a83d9493d552dbfbe62b12eb9f6070d9e6230 (patch) | |
| tree | b8ea4bc48070850dcc093f365f3984ba082cdbb2 /tests/test_include.py | |
| parent | sl0ppty: shaders on the chrome, and two keys for the arrangement (diff) | |
| download | slosh-a25a83d9493d552dbfbe62b12eb9f6070d9e6230.tar.gz slosh-a25a83d9493d552dbfbe62b12eb9f6070d9e6230.tar.xz slosh-a25a83d9493d552dbfbe62b12eb9f6070d9e6230.zip | |
sl0ppty: a config can be made of files
include "themes/amber.kdl"
include "keys/vim.kdl" "shaders/crt.kdl"
which is how you switch a theme by editing one line rather than pasting
one in. Nothing about it is theme-specific: an include is another config
file applied here, so the same line composes keys, shader chains, states
or geometry, and contrib/themes stops being a directory you copy out of.
A relative path is relative to *the file doing the including*, never to
the directory the session was started from -- a config is a thing on disk
that refers to its neighbours, and where you were standing is not part of
what it means. A leading `~` is a home directory, an absolute path is
itself, and an included file may include others.
What you include is the base: the file with the `include` line in it wins
wherever they disagree, and a later include wins over an earlier one.
That holds wherever the line sits, because the loader reads a document by
asking it for the keys it knows rather than walking it top to bottom --
so "here" is not a position it could honour, and "the file you are
editing wins" is the rule worth having anyway. It is what makes a theme
something you can put two lines on top of.
A depth limit rather than a set of files already seen, because the two
failures it has to stop are the same failure: a config that includes
itself and a config eleven files deep are both a mistake, and the message
says which one you have. A file that will not load is skipped and the
rest of the config still applies (D9) -- losing your keybindings over a
mistyped theme name would be the worse answer.
The loader was already shaped for this: `config_load` applied one parsed
root onto a config_t, so it becomes a recursive `load_into` that applies
its includes first and then itself. Errors from an include get their own
buffer, so a broken theme cannot overwrite a message the file that
included it has already produced.
tests/test_include.py covers the two rules that carry the weight -- the
outer file winning either way round, and a nested include resolving
against the file that wrote it -- plus multiple files on one line, `~`, a
binding from an included file, a missing file, a cycle, switching theme
through a reload, and every file in contrib/themes applying as an
include, checked against each file's own frame_focus so it cannot pass by
looking default.
Not yet: an included file is not watched, so editing the theme itself
does not reload the session. Editing the include line does.
Diffstat (limited to 'tests/test_include.py')
| -rwxr-xr-x | tests/test_include.py | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/tests/test_include.py b/tests/test_include.py new file mode 100755 index 0000000..42baa89 --- /dev/null +++ b/tests/test_include.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""`include "themes/nord.kdl"`: a config made of files. + +The thing it is for is switching a theme without pasting one, but nothing about +it is theme-specific — an include is another config file applied here, so the +same line composes keys, shaders, states or geometry. + +Two rules carry the weight, and both are checked below: a relative path is +relative to *the file doing the including*, never to the working directory; and +what you include is the base, so the file that includes it wins. The loader +reads a document by asking it for the keys it knows rather than walking it in +order, so "here" is not a position it could honour anyway. +""" +import os +import re +import sys +import tempfile + +from harness import Session, check, report + +SH = ["/bin/sh", "-c", "stty raw -echo; cat"] + + +def tree(files, home=None): + """Write {relative path: text} into a fresh directory; returns its path.""" + root = tempfile.mkdtemp() + for rel, text in files.items(): + path = os.path.join(root, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write(text) + return root + + +def frame_fg(s): + """The colour of the focused pane's top rule.""" + p = s.pane() + return (s.snapshot().style_at(p["x"] + 5, p["y"]) or {}).get("fg") + + +GREEN = 'theme { frame_focus "#00ff00" }\n' +BLUE = 'theme { frame_focus "#0000ff" }\n' + + +def test_an_included_theme_applies(): + root = tree({"themes/green.kdl": GREEN, "config.kdl": 'include "themes/green.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("the frame wears the included colour", frame_fg(s) == "#00ff00", + str(frame_fg(s))) + + +def test_the_file_doing_the_including_wins(): + """So a theme is a base you can put two lines on top of.""" + root = tree({"themes/green.kdl": GREEN, + "config.kdl": 'include "themes/green.kdl"\n' + BLUE}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("the outer file's colour is the one on screen", + frame_fg(s) == "#0000ff", str(frame_fg(s))) + + # ...and the order of the two lines does not change that, because an include + # is applied before the file it appears in, wherever it appears. + root = tree({"themes/green.kdl": GREEN, + "config.kdl": BLUE + 'include "themes/green.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("even with the include written last", frame_fg(s) == "#0000ff", + str(frame_fg(s))) + + +def test_later_includes_win_over_earlier_ones(): + root = tree({"a.kdl": GREEN, "b.kdl": BLUE, + "config.kdl": 'include "a.kdl"\ninclude "b.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("the last one applied is the one that shows", + frame_fg(s) == "#0000ff", str(frame_fg(s))) + + +def test_one_include_can_take_several_files(): + root = tree({"a.kdl": GREEN, "b.kdl": 'gap 2\n', + "config.kdl": 'include "a.kdl" "b.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("both were applied", + frame_fg(s) == "#00ff00" and s.pane()["x"] == 4, + f"{frame_fg(s)} / x={s.pane()['x']}") + + +def test_a_path_is_relative_to_the_file_that_wrote_it(): + """The nested file says `include "base/core.kdl"`, which only resolves if it + is read relative to the theme file rather than to the top config.""" + root = tree({"themes/base/core.kdl": 'theme { frame_focus "#ff00ff" }\n', + "themes/nested.kdl": 'include "base/core.kdl"\n', + "config.kdl": 'include "themes/nested.kdl"\n'}) + with Session(SH, cols=60, rows=20, config=root + "/config.kdl") as s: + s.settle() + check("the nested include resolved", frame_fg(s) == "#ff00ff", + str(frame_fg(s))) + + +def test_a_leading_tilde_is_a_home_directory(): + home = tree({"mytheme.kdl": 'theme { frame_focus "#abcdef" }\n'}) + root = tree({"config.kdl": 'include "~/mytheme.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl", + env={"HOME": home}) as s: + s.settle() + check("~ expanded to the home directory", frame_fg(s) == "#abcdef", + str(frame_fg(s))) + + +def test_anything_can_be_included_not_only_a_theme(): + root = tree({"keys.kdl": 'keys {\n bind "v" "split-cols"\n}\n', + "config.kdl": 'include "keys.kdl"\n'}) + with Session(SH, cols=90, rows=14, config=root + "/config.kdl") as s: + s.settle() + s.key("v") + s.settle() + check("a binding from an included file works", len(s.panes()) == 2, + str(s.panes())) + + +def test_a_missing_include_costs_a_line_and_no_more(): + """Losing your keybindings over a mistyped theme name would be a worse + answer than a session that carries on without the theme (D9).""" + root = tree({"config.kdl": 'include "themes/nope.kdl"\ngap 2\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("the session is running", s.alive()) + check("and the rest of the file applied anyway", s.pane()["x"] == 4, + str(s.pane())) + + +def test_a_cycle_stops_rather_than_spinning(): + """A config that includes itself is a mistake, and the depth limit is what + turns it into a message instead of a session that never starts.""" + root = tree({"a.kdl": 'include "b.kdl"\ntheme { frame_focus "#00ff00" }\n', + "b.kdl": 'include "a.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/a.kdl") as s: + s.settle() + check("the session started at all", s.alive()) + check("and the file that included itself still applied", + frame_fg(s) == "#00ff00", str(frame_fg(s))) + + +def test_an_include_is_picked_up_by_a_reload(): + root = tree({"themes/t.kdl": GREEN, "config.kdl": 'include "themes/t.kdl"\n'}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + check("green to start", frame_fg(s) == "#00ff00", str(frame_fg(s))) + + # Switching theme is editing one line, which is the point of the feature. + with open(root + "/themes/other.kdl", "w") as f: + f.write(BLUE) + with open(root + "/config.kdl", "w") as f: + f.write('include "themes/other.kdl"\n') + s.api("reload") + s.settle(30) + check("and blue after the include line changed", + frame_fg(s) == "#0000ff", str(frame_fg(s))) + + +def test_every_contrib_theme_can_be_included(): + """They are written to be pasted; including one has to work too, or the + directory is only half a feature. Checked against each file's own + `frame_focus`, so this cannot pass by the theme happening to look default.""" + here = os.path.dirname(os.path.abspath(__file__)) + tdir = os.path.abspath(os.path.join(here, "..", "contrib", "themes")) + themes = sorted(f for f in os.listdir(tdir) if f.endswith(".kdl")) + check("contrib ships themes", len(themes) >= 5, str(themes)) + + root = tree({"config.kdl": ""}) + with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s: + s.settle() + for name in themes: + path = os.path.join(tdir, name) + want = re.search(r'frame_focus\s+"(#[0-9a-fA-F]{6})"', open(path).read()) + check(f"{name} declares a frame colour", want is not None, name) + with open(root + "/config.kdl", "w") as f: + f.write('include "%s"\n' % path) + s.api("reload") + s.settle(30) + check(f"{name} applies as an include", + want and frame_fg(s) == want.group(1).lower(), + f"{name}: wanted {want and want.group(1)}, got {frame_fg(s)}") + + +if __name__ == "__main__": + test_an_included_theme_applies() + test_the_file_doing_the_including_wins() + test_later_includes_win_over_earlier_ones() + test_one_include_can_take_several_files() + test_a_path_is_relative_to_the_file_that_wrote_it() + test_a_leading_tilde_is_a_home_directory() + test_anything_can_be_included_not_only_a_theme() + test_a_missing_include_costs_a_line_and_no_more() + test_a_cycle_stops_rather_than_spinning() + test_an_include_is_picked_up_by_a_reload() + test_every_contrib_theme_can_be_included() + sys.exit(report()) |
