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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
|
#!/usr/bin/env python3
"""Compact mode: shared 1-cell borders instead of gaps — and the border-drag
resize that `gap 0` needs either way.
Panes pack flush against divider lines that meet in real junctions, one frame
rings the tab, and a pane's title rides the line above it. The dividers carry
the same resize drag the gaps did; interior edges give up click-to-split; the
outer frame keeps it. Floats, zoom and the flatten keep the classic frame.
"""
import os
import sys
import tempfile
import time
from harness import Session, check, report
def _cfg(text):
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
return f.name
def _lay(text):
f = tempfile.NamedTemporaryFile("w", suffix=".layout", delete=False)
f.write(text)
f.close()
return f.name
GAP0 = _cfg("gap 0\npadding 0\n")
COMPACT = _cfg("compact true\nhover_delay_ms 10\n")
GRID = _lay(
'layout { tab { pane split="rows" { '
'pane split="cols" { pane; pane }; '
'pane split="cols" { pane; pane } } } }\n'
)
SH = ["/bin/sh", "-c", 'printf "\\033]2;pane\\007"; stty raw -echo; cat']
def drag(s, x0, y0, x1, y1):
s.send(rf"\e[<0;{x0 + 1};{y0 + 1}M")
s.send(rf"\e[<32;{x1 + 1};{y1 + 1}M")
s.send(rf"\e[<0;{x1 + 1};{y1 + 1}m")
s.settle(40)
# ---- gap 0: the border is all there is, so the border drags ---------------
def test_gap0_border_drag_moves_the_boundary():
with Session(SH, cols=80, rows=20, config=GAP0) as s:
s.settle()
s.key("\\\\")
s.settle()
left = min(s.panes(), key=lambda p: p["x"])
bx = left["x"] + left["w"] - 1
by = left["y"] + left["h"] // 2
check(
"flush panes leave no gap, so the cell is the border",
s.snapshot().hit_at(bx, by) == f"border:{left['id']}:r",
s.snapshot().hit_at(bx, by),
)
before = left["w"]
drag(s, bx, by, bx + 6, by)
after = min(s.panes(), key=lambda p: p["x"])["w"]
check(
"dragging the border moved the boundary",
after == before + 6,
f"{before} -> {after}",
)
def test_gap0_rim_drags_but_never_splits():
with Session(SH, cols=80, rows=20, config=GAP0) as s:
s.settle()
s.key("\\\\")
s.settle()
snap = s.snapshot()
left = min(s.panes(), key=lambda p: p["x"])
rim = snap.rim(left["id"], "r")
check("the border still has a rim", rim is not None)
s.click(*rim)
s.settle()
check(
"a rim click still splits nothing", len(s.panes()) == 2, str(len(s.panes()))
)
before = left["w"]
drag(s, rim[0], rim[1], rim[0] - 5, rim[1])
after = min(s.panes(), key=lambda p: p["x"])["w"]
check(
"but a rim drag moves the boundary",
after == before - 5,
f"{before} -> {after}",
)
def test_gap0_handle_click_still_splits():
with Session(SH, cols=120, rows=20, config=GAP0) as s:
s.settle()
s.key("\\\\")
s.settle()
left = min(s.panes(), key=lambda p: p["x"])
h = s.snapshot().handle(left["id"], "r")
s.click(*h)
s.settle()
check(
"a handle click is still the split it always was",
len(s.panes()) == 3,
str(len(s.panes())),
)
# ---- compact: the look ----------------------------------------------------
def test_compact_boundary_costs_one_cell():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
tl, tr = ps[0], ps[1]
check(
"one cell between side-by-side contents",
tr["content_x"] - (tl["content_x"] + tl["content_w"]) == 1,
f"{tl} | {tr}",
)
bl = ps[2]
check(
"one cell between stacked contents",
bl["content_y"] - (tl["content_y"] + tl["content_h"]) == 1,
f"{tl} / {bl}",
)
check(
"content starts one cell in from the screen edge",
tl["content_x"] == 1 and tl["content_y"] == 2,
str(tl),
)
def test_compact_junctions_are_real_glyphs():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
snap = s.snapshot()
scr = snap.screen()
for g in "┬┴├┤┼":
check(f"the lines meet in a {g}", g in scr)
corner = [h for h in snap.hits if h["action"].startswith("corner:")]
check("the crossing is a drag target", len(corner) == 1, str(corner))
c = corner[0]
check(
"and the target sits on the ┼ itself",
snap.line(c["y"])[c["x"]] == "┼",
repr(snap.line(c["y"])),
)
def test_compact_titles_ride_the_shared_line():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
snap = s.snapshot()
ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
bl = ps[2] # bottom-left: its title line is the interior divider
hit = [h for h in snap.hits if h["action"] == f"panetitle:{bl['id']}"]
check(
"an interior pane's name is on the divider above it",
hit and hit[0]["y"] == bl["y"] - 1,
str(hit),
)
check(
"and the name is drawn there",
"pane" in snap.line(bl["y"] - 1),
repr(snap.line(bl["y"] - 1)),
)
# ---- compact: the mouse ---------------------------------------------------
def test_compact_dividers_drag_like_gaps():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
snap = s.snapshot()
ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
tl = ps[0]
dx = tl["x"] + tl["w"] # the divider column
dy = tl["y"] + 2
check(
"the divider is the boundary's own hit",
(snap.hit_at(dx, dy) or "").startswith("edge:"),
snap.hit_at(dx, dy),
)
before = tl["w"]
drag(s, dx, dy, dx + 5, dy)
after = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))[0]["w"]
check(
"dragging it moves the boundary",
after == before + 5,
f"{before} -> {after}",
)
def test_compact_interior_edges_resize_but_do_not_split():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
snap = s.snapshot()
ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
bl = ps[2]
divy = bl["y"] - 1
free = snap.hit_at(bl["x"], divy)
check(
"a divider's free cells stay the resize handle",
(free or "").startswith("edge:"),
free,
)
interior = [
h
for h in snap.hits
if h["action"] in (f"border:{bl['id']}:t", f"brim:{bl['id']}:t")
]
check("an interior edge offers no split", not interior, str(interior))
def test_compact_outer_frame_still_splits():
with Session(SH, cols=100, rows=22, config=COMPACT) as s:
s.settle()
p = s.pane(0)
snap = s.snapshot()
h = snap.handle(p["id"], "l")
check("the outer frame has a handle", h is not None)
check("on the frame's own column", h[0] == p["x"] - 1, str(h))
s.click(*h)
s.settle()
check("and clicking it splits", len(s.panes()) == 2, str(len(s.panes())))
def test_compact_hover_hint_spares_the_title():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
ps = sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
bl = ps[2]
divy = bl["y"] - 1
s.send(rf"\e[<35;{bl['x'] + 2};{divy + 1}M")
time.sleep(0.05)
s.settle(30)
row = s.snapshot().line(divy)
check("the armed divider goes dotted", "┈" in row, repr(row))
check("without ruling through the pane's name", " pane " in row, repr(row))
# ---- compact: the frame's colour and everything classic -------------------
def test_compact_focus_ring_wears_the_frame_colour():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
snap = s.snapshot()
f = s.focused()
ring = snap.style_at(f["x"] - 1, f["y"] + 1)
# another pane's stretch of the frame, for contrast
others = [p for p in s.panes() if p["id"] != f["id"]]
far = max(others, key=lambda p: abs(p["x"] - f["x"]) + abs(p["y"] - f["y"]))
idle = snap.style_at(far["x"] + far["w"], far["y"] + 1)
check("the focused ring is coloured", ring is not None, str(ring))
check(
"differently from an idle stretch of line",
idle is None or (ring or {}).get("fg") != idle.get("fg"),
f"{ring} vs {idle}",
)
def test_compact_dead_pane_epitaph_overlays_the_last_row():
cfg = _cfg('compact true\nkeep_dead "all"\n')
with Session(["/bin/sh", "-c", "exit 3"], cols=60, rows=12, config=cfg) as s:
snap = s.until(lambda sn: "re-run" in sn.screen())
p = s.pane(0)
row = snap.line(p["content_y"] + p["content_h"] - 1)
check(
"the epitaph rides the last content row",
"status 3" in row and "[re-run]" in row,
repr(row),
)
os.unlink(cfg)
def test_compact_float_keeps_the_classic_frame():
with Session(SH, cols=64, rows=22, config=COMPACT, layout=GRID) as s:
s.settle(60)
s.key("f")
s.settle(30)
f = s.focused()
check(
"a float's contents sit inside its own border",
f["content_x"] == f["x"] + 1 and f["content_y"] == f["y"] + 1,
str(f),
)
s.key("f")
s.settle(30)
def test_compact_min_bar_sits_below_a_shared_line():
"""The minimised bar and the bottom panes share no border row. A compact
pane's bottom border is the shared line one cell outside its rect; the
layout reserves that row above the bar and draw_compact_lines strokes it,
ring junctions included. Without it the chips sat directly on the panes'
content -- "the minimised rectangles overlap the bottom pane's border"."""
with Session(SH, cols=90, rows=24, config=COMPACT) as s:
s.settle(30)
s.key("\\\\")
s.settle(30)
s.key("-")
s.settle(30)
s.key("m")
s.settle(30)
snap = s.snapshot()
bar = min(
(h for h in snap.hits if h["action"].startswith("focus:") and h["h"] == 3),
key=lambda h: h["y"],
default=None,
)
check("a chip is in the hit list", bar is not None, str(snap.hits))
if not bar:
return
line = snap.line(bar["y"] - 1)
check(
"the row above the chips is the panes' bottom border",
line.startswith("\u251c") and line.rstrip().endswith("\u2524"),
repr(line),
)
check(
"the divider from above ends into it as a junction",
"\u2534" in line,
repr(line),
)
low = max(p["y"] + p["h"] for p in s.panes() if p["h"])
check(
"no pane reaches the line's row",
low <= bar["y"] - 1,
f"panes end {low}, line at {bar['y'] - 1}",
)
def test_compact_is_a_config_key():
import subprocess
from harness import BIN
env = dict(os.environ, SLOSH_CONFIG="/nonexistent/slosh.kdl")
out = subprocess.run(
[BIN, "--dump-config"], capture_output=True, text=True, env=env
).stdout
check(
"dump-config writes the knob",
any(l.split() and l.split()[0] == "compact" for l in out.splitlines()),
out[:200],
)
r = subprocess.run(
[BIN, "--check", COMPACT], capture_output=True, text=True, env=env
)
check("and --check accepts compact true", r.returncode == 0, r.stdout + r.stderr)
if __name__ == "__main__":
test_gap0_border_drag_moves_the_boundary()
test_gap0_rim_drags_but_never_splits()
test_gap0_handle_click_still_splits()
test_compact_boundary_costs_one_cell()
test_compact_junctions_are_real_glyphs()
test_compact_titles_ride_the_shared_line()
test_compact_dividers_drag_like_gaps()
test_compact_interior_edges_resize_but_do_not_split()
test_compact_outer_frame_still_splits()
test_compact_hover_hint_spares_the_title()
test_compact_focus_ring_wears_the_frame_colour()
test_compact_dead_pane_epitaph_overlays_the_last_row()
test_compact_float_keeps_the_classic_frame()
test_compact_min_bar_sits_below_a_shared_line()
test_compact_is_a_config_key()
sys.exit(report())
|