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
|
#!/usr/bin/env python3
"""Focus follows the mouse — and the guards that make that liveable.
The feature is three lines. The reason it is not obnoxious is the guards: it
must not steal focus mid-chord, reach past an open finder, interrupt a drag,
or expand a collapsed pane just because the pointer crossed its header.
"""
import os
import sys
import tempfile
from harness import Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']
def cfg(text):
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
return f.name
def hover(s, x, y):
"""Motion with no button held: SGR button 35 = release/none + motion."""
s.send(rf"\e[<35;{x + 1};{y + 1}M")
def focused_id(s):
f = s.focused()
return f["id"] if f else None
def test_hover_focuses():
with Session(SH, cols=70, rows=14) as s:
s.settle()
s.key("\\\\")
s.settle()
left, right = s.panes()
check("the new pane has focus", focused_id(s) == right["id"])
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
check("hovering a pane focuses it", focused_id(s) == left["id"], str(s.panes()))
hover(s, right["content_x"] + 2, right["content_y"] + 1)
s.settle(60)
check("and back", focused_id(s) == right["id"])
hover(s, left["x"] + 4, left["y"]) # the title row
s.settle(60)
check("hovering a title bar focuses it too", focused_id(s) == left["id"])
hover(s, 0, 0) # the gap ring, which belongs to nothing
s.settle(60)
check("hovering nothing changes nothing", focused_id(s) == left["id"])
def test_guards():
with Session(SH, cols=70, rows=14) as s:
s.settle()
s.key("\\\\")
s.settle()
left, right = s.panes()
# mid-chord: the prefix is held, the next key is a command
s.send(r"\x01")
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
check(
"hover does not steal focus mid-chord",
focused_id(s) == right["id"],
str(s.panes()),
)
s.send("g") # release the prefix harmlessly
# with the finder open, the pane underneath must not take focus
s.key("s")
s.settle(60)
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
check(
"hover does not reach past the finder",
focused_id(s) == right["id"],
str(s.panes()),
)
s.send(r"\e")
s.settle(60)
# during a drag, the mouse belongs to the drag
s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 1}M") # press a title
hover(s, left["content_x"] + 2, left["content_y"] + 1) # (button held)
s.settle(60)
check("a drag owns the mouse", focused_id(s) == right["id"], str(s.panes()))
s.send(r"\x01g") # any key ends the drag
def test_hover_does_not_expand_collapsed_panes():
with Session(SH, cols=120, rows=30) as s:
s.settle()
s.key("\\\\")
s.settle()
s.key("\\\\")
s.settle()
s.resize(50, 14)
s.settle()
hidden = [p for p in s.panes() if p["hidden"]]
check("something is collapsed", hidden != [], str(s.panes()))
if not hidden:
return
h = hidden[0]
before = focused_id(s)
hover(s, h["x"] + 3, h["y"])
s.settle(60)
check(
"hovering a collapsed header does not expand it",
[p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
str(s.panes()),
)
check("nor move focus", focused_id(s) == before, str(s.panes()))
# clicking it still does
s.click(h["x"] + 3, h["y"])
s.settle()
check(
"clicking it still expands it",
not [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
)
def test_can_be_turned_off():
path = cfg("focus_follows_mouse false\n")
with Session(SH, cols=70, rows=14, config=path) as s:
s.settle()
s.key("\\\\")
s.settle()
left, right = s.panes()
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
check(
"with the option off, hovering does nothing",
focused_id(s) == right["id"],
str(s.panes()),
)
s.click(left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
check("but clicking still focuses", focused_id(s) == left["id"], str(s.panes()))
os.unlink(path)
def test_panes_still_get_motion():
"""?1003 also fixes something: a pane that tracks hover can now see it."""
tracker = [
"/bin/sh",
"-c",
'stty raw -echo; printf "\\033[?1003h\\033[?1006h"; cat -v',
]
with Session(tracker, cols=70, rows=12) as s:
s.settle()
p = s.pane()
hover(s, p["content_x"] + 4, p["content_y"] + 2)
s.settle()
out = s.snapshot().pane_text(p)
check(
"motion reaches a pane that asked for any-event tracking",
"^[[<35;5;3M" in out,
repr(out[:120]),
)
quiet = [
"/bin/sh",
"-c",
'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v',
]
with Session(quiet, cols=70, rows=12) as s:
s.settle()
p = s.pane()
hover(s, p["content_x"] + 4, p["content_y"] + 2)
s.settle()
out = s.snapshot().pane_text(p)
check(
"but not one that only asked for clicks", "[<" not in out, repr(out[:120])
)
if __name__ == "__main__":
test_hover_focuses()
test_guards()
test_hover_does_not_expand_collapsed_panes()
test_can_be_turned_off()
test_panes_still_get_motion()
sys.exit(report())
|