1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
|
#!/usr/bin/env python3
"""What slosh says, out of the box, about a pane you are not in.
Three states mean "these cells are not a running program's present": its
program exited, it never started, or you are looking at the past. None of them
is discoverable by looking unless something says so, which is the whole
argument for shading them, so all three are shaded by default.
Two states mean "this is not the pane you are in" — unfocused, and one being
dragged. Those are ambient contrast, which is a taste, and a taste shipped as
a default is how a tool gets a reputation for fighting you. This file asserts
that line in both directions: the first three are coloured, the fourth is not.
"""
import os
import sys
import tempfile
from harness import Session, check, report
# A pane with a screenful of known-coloured text.
SH = [
"/bin/sh",
"-c",
'printf "\\033]2;p\\007"; i=0; '
'while [ $i -lt 40 ]; do printf "line %s ABCDEFGHIJKLMNOP\\n" $i; i=$((i+1)); done; '
"read x",
]
# Only the theme, so "what colour is this cell" has one answer. No states: the
# point is what slosh does when you have said nothing.
CFG = None
def cfg():
global CFG
if CFG is None:
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write('theme { default_fg "#ffffff" default_bg "#000000" }\n')
f.close()
CFG = f.name
return CFG
def fg(snap, pane, row=0, col=0):
run = snap.style_at(pane["content_x"] + col, pane["content_y"] + row)
return (run or {}).get("fg")
def test_a_dead_pane_is_coloured_by_default():
# A *commanded* pane: only those keep their corpse by default, which is
# the only kind there is to colour.
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
lay.write(
"layout {\n tab {\n"
" pane command=\"printf '\\\\033]2;p\\\\007'; echo DONE; exit 3\"\n"
" }\n}\n"
)
lay.close()
with Session(SH, cols=50, rows=10, config=cfg(), layout=lay.name) as s:
snap = s.until_text("[process exited")
check(
"its contents are not left looking live",
fg(snap, s.pane()) not in (None, "#ffffff"),
str(fg(snap, s.pane())),
)
os.unlink(lay.name)
def test_a_suspended_pane_is_coloured_by_default():
layout = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
layout.write(
'layout {\n tab name="t" {\n'
' pane command="echo hello; read x" suspended=true\n'
" }\n}\n"
)
layout.close()
with Session(SH, cols=60, rows=12, config=cfg(), layout=layout.name) as s:
s.settle(60)
pane = s.pane()
snap = s.snapshot()
check(
"a pane that never started does not look like one that did",
fg(snap, pane) not in (None, "#ffffff"),
f"{fg(snap, pane)} — {snap.pane_text(pane)[:40]!r}",
)
os.unlink(layout.name)
def test_scrollback_is_coloured_by_default():
with Session(SH, cols=60, rows=12, config=cfg()) as s:
s.until_text("line 39")
pane = s.pane()
live = fg(s.snapshot(), pane)
s.send(r"\x01\x1b[5~") # C-a PgUp: into the scrollback
s.settle(20)
past = fg(s.snapshot(), pane)
check(
"the pane is scrolled",
s.panes() and "▲" in s.snapshot().screen(),
s.snapshot().screen()[:200],
)
check(
"looking at the past looks different from the present",
past != live,
f"live {live}, scrolled {past}",
)
s.send(r"\x01\x1b[F") # C-a End: back to the bottom
s.settle(20)
check(
"and it goes back when you do",
fg(s.snapshot(), pane) == live,
f"{fg(s.snapshot(), pane)} vs {live}",
)
def test_scrollback_fades_at_the_edges_that_continue():
"""While scrolled, the viewport's top and bottom rows melt towards the
edge -- the way a scrollable list fades where it continues -- and each
fade exists only while there is content past that edge: `(above > 0)` is
a binary gate, so hitting the top of the buffer turns the top edge solid.
Reaching the bottom ends the scrolled state entirely, which is the same
statement about the other edge."""
with Session(SH, cols=60, rows=20, config=cfg()) as s:
s.until_text("line 39")
pane = s.pane()
mid = pane["content_h"] // 2
last = pane["content_h"] - 1
s.send(r"\x01\x1b[5~") # C-a PgUp: mid-buffer, content both ways
s.settle(30)
snap = s.snapshot()
top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last))
check(
"mid-buffer, the top rows melt into the edge",
top != m,
f"top {top}, mid {m}",
)
check("...and the bottom rows too", bot != m, f"bottom {bot}, mid {m}")
s.send(r"\x01\x1b[H") # C-a Home: the very top of the buffer
s.settle(30)
snap = s.snapshot()
top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last))
check(
"at the top of the buffer the top edge turns solid",
top == m,
f"top {top}, mid {m}",
)
check(
"while the bottom still says the present is below",
bot != m,
f"bottom {bot}, mid {m}",
)
s.send(r"\x01\x1b[F") # C-a End: back to the present
s.settle(30)
snap = s.snapshot()
top, m, bot = (fg(snap, pane, row=r) for r in (0, mid, last))
check(
"and the present is opaque everywhere",
top == m == bot,
f"top {top}, mid {m}, bottom {bot}",
)
def test_an_unfocused_pane_is_dimmed_by_default():
"""This used to assert the opposite, deliberately: ambient contrast is a
taste, and the argument against shipping a taste was that turning it off
should be obvious. It is now — `dim_unfocused 0` — which is what changed.
The knob is the escape hatch that makes the default defensible."""
with Session(SH, cols=90, rows=14, config=cfg()) as s:
s.until_text("line 39")
before = fg(s.snapshot(), s.pane())
s.key("\\\\") # split; the left pane loses focus
s.settle(40)
unfocused = [p for p in s.panes() if not p["focused"]][0]
after = fg(s.snapshot(), unfocused)
check(
"the pane you left is pushed back", after != before, f"{before} -> {after}"
)
check("but still plainly readable", after not in (None, "#000000"), str(after))
def test_dim_unfocused_zero_turns_it_off():
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
conf.write('theme { default_fg "#ffffff" default_bg "#000000" }\ndim_unfocused 0\n')
conf.close()
with Session(SH, cols=90, rows=14, config=conf.name) as s:
s.until_text("line 39")
before = fg(s.snapshot(), s.pane())
s.key("\\\\")
s.settle(40)
unfocused = [p for p in s.panes() if not p["focused"]][0]
check(
"nothing is done to it",
fg(s.snapshot(), unfocused) == before,
f"{before} -> {fg(s.snapshot(), unfocused)}",
)
os.unlink(conf.name)
def test_a_states_block_beats_the_knob():
"""Both directions, asserted on the colour rather than on "did it change":
a chain replaces the knob's dim, and an empty block means "nothing" rather
than "you did not say". (A grayscale chain makes a poor probe here — it
leaves white text white, so it looks like nothing happened.)"""
cases = [
(
'dim_unfocused 60\nstates { unfocused { tint amount=255 color="#00ff00" } }\n',
"#00ff00",
"a chain replaces the knob",
),
(
"dim_unfocused 60\nstates { unfocused { } }\n",
None,
"an empty block means nothing at all",
),
("dim_unfocused 60\n", "#c3c3c3", "and the knob applies when nobody said"),
]
for text, want, label in cases:
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
conf.write('theme { default_fg "#ffffff" default_bg "#000000" }\n' + text)
conf.close()
with Session(SH, cols=90, rows=14, config=conf.name) as s:
s.until_text("line 39")
s.key("\\\\")
s.settle(40)
unfocused = [p for p in s.panes() if not p["focused"]][0]
got = fg(s.snapshot(), unfocused)
check(label, got == want, f"{got}, wanted {want}")
os.unlink(conf.name)
def test_the_scrolled_wash_follows_the_theme():
"""The wash over scrollback is theme's scroll_bg, and that promise is only
kept if the wash is derived *after* the theme block is read: it used to be
baked from the compiled palette, so every theme shipped left the default
accent smeared over its scrollback. A chain somebody wrote still stands,
whichever file of an include chain it was written in."""
def scrolled_fg(text, include=None):
d = tempfile.mkdtemp(prefix="slosh-scrolled-")
if include is not None:
open(os.path.join(d, "inc.kdl"), "w").write(include)
conf = os.path.join(d, "config.kdl")
# One theme block per file: the loader reads the first node by name,
# so a second `theme { }` would be a block that quietly says nothing.
open(conf, "w").write(text)
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("line 39")
pane = s.pane()
s.send(r"\x01\x1b[5~") # C-a PgUp: into the scrollback
s.settle(20)
# Mid-viewport, clear of the derived edge fades (four rows a
# side): this test is about the wash, and the wash alone.
got = fg(s.snapshot(), pane, row=pane["content_h"] // 2)
return got
# White text tinted 22/255 towards pure green keeps green at full and
# pulls the other channels down: green stays the largest channel, which is
# the assertion, so the exact rounding stays the implementation's.
def greenish(c):
return c is not None and c[3:5] == "ff" and c[1:3] < "ff"
base = (
"theme {\n"
' default_fg "#ffffff"\n'
' default_bg "#000000"\n'
' scroll_bg "#00ff00"\n'
"}\n"
)
got = scrolled_fg(base)
check(
"a theme's scroll_bg moves the wash with the indicator", greenish(got), str(got)
)
got = scrolled_fg(
base + 'states { scrolled { tint amount=255 color="#ff0000" } }\n'
)
check("a chain you wrote beats the derivation", got == "#ff0000", str(got))
got = scrolled_fg('include "inc.kdl"\n', include=base)
check("it follows a theme brought in by an include", greenish(got), str(got))
got = scrolled_fg(
'include "inc.kdl"\n' + base,
include='states { scrolled { tint amount=255 color="#ff0000" } }\n',
)
check(
"and a chain written in an include is not re-derived over",
got == "#ff0000",
str(got),
)
def test_a_flattened_tab_still_says_which_panes_are_not_live():
"""A collapsed header is chrome, and the shader pass never touches chrome
(D13) — so the words have to carry what the colour carries everywhere
else. Collapsing a tab must not hide the fact."""
layout = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
layout.write(
'layout {\n tab name="t" {\n'
' pane command="stty raw -echo; cat"\n'
' pane command="echo hi; read x" suspended=true\n'
" }\n}\n"
)
layout.close()
with Session(SH, cols=100, rows=14, config=cfg(), layout=layout.name) as s:
s.settle(60)
s.resize(40, 12) # too small for two: the tab flattens
s.settle(40)
screen = s.snapshot().screen()
check("the header says the pane never started", "not started" in screen, screen)
os.unlink(layout.name)
for name, fn in sorted(list(globals().items())):
if name.startswith("test_"):
fn()
sys.exit(report())
|