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
|
#!/usr/bin/env python3
"""`rotate-layout`: a quarter turn of the tab, clockwise.
The whole feature is one asymmetry. Turn a *column* of panes clockwise and the
one that was on top is now the one on the right, so a row split becomes a column
split read backwards; turn a *row* of them and the leftmost becomes the top, so
the order stands. Get that backwards and you have a mirror image, which looks
almost right and is not a rotation — the tell is that four turns no longer come
back to where you started, which is what most of this file checks.
"""
import sys
import tempfile
from harness import Session, check, report
SH = ["/bin/sh", "-c", "stty raw -echo; cat"]
def rects(s):
"""Every pane as (id, x, y, w, h), in reading order."""
return [
(p["id"], p["x"], p["y"], p["w"], p["h"])
for p in sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
]
def turn(s, n=1):
for _ in range(n):
s.key(r"\x20") # C-a space: the escape, so the driver's line keeps it
s.settle()
def test_two_rows_become_two_columns_the_right_way_round():
with Session(SH, cols=80, rows=24) as s:
s.settle()
s.key("-") # rows: the first pane on top, the new one below
s.settle()
top, bottom = rects(s)
check(
"it starts as two rows",
top[2] < bottom[2] and top[3] == bottom[3],
str(rects(s)),
)
turn(s)
left, right = sorted(rects(s), key=lambda r: r[1])
check(
"one turn makes two columns",
left[2] == right[2] and left[1] < right[1],
str(rects(s)),
)
check(
"and the pane that was on top is now on the right",
right[0] == top[0],
f"top was {top[0]}, right is {right[0]}",
)
check(
"the one that was underneath is on the left",
left[0] == bottom[0],
f"bottom was {bottom[0]}, left is {left[0]}",
)
turn(s)
a, b = rects(s)
check(
"two turns are two rows again", a[3] == b[3] and a[2] < b[2], str(rects(s))
)
check(
"...with the panes the other way up than they started",
a[0] == bottom[0] and b[0] == top[0],
str(rects(s)),
)
def test_four_turns_are_the_identity():
"""The property that makes this safe to press: it is a permutation of the
tree and nothing else, so pressing it enough times undoes it exactly."""
with Session(SH, cols=100, rows=30) as s:
s.settle()
s.key("\\\\") # a nest: one pane beside a stack of two
s.key("-")
s.settle()
start = rects(s)
check("three panes to turn", len(start) == 3, str(start))
seen = [start]
for i in range(1, 4):
turn(s)
seen.append(rects(s))
check(f"turn {i} changes the arrangement", seen[-1] != start, str(seen[-1]))
turn(s)
check(
"the fourth turn comes back exactly",
rects(s) == start,
f"{start} -> {rects(s)}",
)
def test_a_share_of_one_axis_becomes_a_share_of_the_other():
with Session(SH, cols=100, rows=30) as s:
s.settle()
s.key("\\\\")
s.settle()
for _ in range(4):
s.key("L") # push the boundary right: the left pane grows
s.settle()
left, right = sorted(rects(s), key=lambda r: r[1])
check(
"the columns are uneven", left[3] > right[3] + 8, f"{left[3]} vs {right[3]}"
)
ratio = left[3] / right[3]
turn(s)
top, bottom = sorted(rects(s), key=lambda r: r[2])
check(
"the wider pane is now the taller one",
top[0] == left[0] and top[4] > bottom[4],
f"{rects(s)} (left was {left[0]})",
)
check(
"and it kept roughly the share it had",
abs(top[4] / bottom[4] - ratio) < 0.35,
f"{ratio:.2f} across -> {top[4] / bottom[4]:.2f} down",
)
def test_focus_stays_on_the_pane_you_were_in():
with Session(SH, cols=90, rows=24) as s:
s.settle()
s.key("-")
s.settle()
was = s.focused()["id"]
turn(s, 3)
check(
"the pane you were in is the pane you are in",
s.focused()["id"] == was,
f"{was} -> {s.focused()['id']}",
)
def test_a_turn_that_would_not_fit_is_refused():
"""A share of the height is not always a share the width can afford, and a
node that cannot give its children their floor takes the whole tab down to a
list (D6). An action a person just asked for must not be what does that."""
with Session(SH, cols=100, rows=30) as s:
s.settle()
s.key("-")
s.settle()
for _ in range(5):
s.key("K") # squeeze the top pane: fine as a row, too thin as a column
s.settle()
before = rects(s)
turn(s)
check(
"the layout is exactly as it was",
rects(s) == before,
f"{before} -> {rects(s)}",
)
check(
"and it says why",
"no room to turn it" in s.snapshot().screen(),
repr(s.snapshot().screen()[-200:]),
)
s.key("0") # even shares fit either way round
s.settle()
turn(s)
cols = sorted(rects(s), key=lambda r: r[1])
check(
"evening it out first lets the same turn through",
cols[0][2] == cols[1][2] and cols[0][1] < cols[1][1],
str(rects(s)),
)
def test_one_pane_has_nothing_to_turn():
with Session(SH, cols=80, rows=24) as s:
s.settle()
before = rects(s)
turn(s)
check(
"it says so",
"nothing to turn" in s.snapshot().screen(),
repr(s.snapshot().screen()[-200:]),
)
check(
"and the pane is untouched", rects(s) == before, f"{before} -> {rects(s)}"
)
def test_it_is_offered_by_name():
with Session(SH, cols=110, rows=40) as s:
s.settle()
s.key("?")
s.settle()
check(
"the cheatsheet lists it with its key",
"space turn the layout a quarter" in s.snapshot().screen(),
repr(s.snapshot().screen()[:400]),
)
s.send("q")
s.key("p")
s.send("rotate")
s.settle()
snap = s.snapshot()
check(
"the palette finds it by its config name",
"turn the layout a quarter" in snap.screen(),
repr(snap.screen()[:400]),
)
check(
"and the row runs it",
any(h["action"].startswith("run:") for h in snap.hits),
str([h["action"] for h in snap.hits][:8]),
)
def test_a_rotated_layout_survives_a_dump():
"""The dump is the tree, so a turn has to come back as a turn."""
with Session(SH, cols=90, rows=24) as s:
s.settle()
s.key("-")
s.settle()
turn(s)
kdl = s.api("dump-layout")["kdl"]
check("it dumps as a column split", 'split="cols"' in kdl, kdl)
turned = rects(s)
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
lay.write(kdl)
lay.close()
with Session(SH, cols=90, rows=24, layout=lay.name) as s2:
s2.settle(60)
check(
"and comes back in the same arrangement",
[r[1:] for r in rects(s2)] == [r[1:] for r in turned],
f"{turned} -> {rects(s2)}",
)
if __name__ == "__main__":
test_two_rows_become_two_columns_the_right_way_round()
test_four_turns_are_the_identity()
test_a_share_of_one_axis_becomes_a_share_of_the_other()
test_focus_stays_on_the_pane_you_were_in()
test_a_turn_that_would_not_fit_is_refused()
test_one_pane_has_nothing_to_turn()
test_it_is_offered_by_name()
test_a_rotated_layout_survives_a_dump()
sys.exit(report())
|