-
Notifications
You must be signed in to change notification settings - Fork 9
/
n_col.py
233 lines (193 loc) · 9.58 KB
/
n_col.py
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
import math
import logging
import sys
import time
import traceback
from typing import Callable, Optional
import i3ipc
import common
import cycle_windows
import layout
import move_counter
import transformations
def balance_cols(i3: i3ipc.Connection,
col1: i3ipc.Con, col1_expected: int,
col2: i3ipc.Con) -> bool:
logging.debug(f"Balancing columns of container {col1.id} and {col2.id}. "
f"Column 1 has {len(col1.nodes)} nodes (expected {col1_expected}) "
f"and column 2 has {len(col2.nodes)}")
caused_mutation = False
if len(col1.nodes) < col1_expected and col2.nodes:
logging.debug(f"Moving container {col2.nodes[0].id} left.")
common.move_container(col2.nodes[0], col1)
col1.nodes.append(col2.nodes.pop(0))
caused_mutation = True
elif len(col1.nodes) > col1_expected and len(col1.nodes) > 1:
logging.debug(f"Moving container {col1.nodes[-1].id} right.")
common.add_node_to_front(i3, col2, col1.nodes[-1])
col2.nodes.insert(0, col1.nodes.pop(-1))
caused_mutation = True
return caused_mutation
class NCol(layout.Layout):
def __init__(self, n_columns: int, *args, **kwargs):
super().__init__(*args, **kwargs)
self.n_columns = n_columns
def __repr__(self) -> str:
return f"{type(self).__name__}({self.workspace_id}, {self.n_columns}, {self.n_masters})"
def reflow(self, i3: i3ipc.Connection, workspace: i3ipc.Con) -> bool:
if len(workspace.leaves()) == 1:
return False
for node in workspace.nodes:
common.ensure_split(node, self.transform_command("splitv"))
workspace = common.refetch_container(i3, workspace)
leaves = workspace.leaves()
masters = leaves[:self.n_masters]
slaves = leaves[self.n_masters:]
n_slaves = len(slaves)
slaves_per_col = math.ceil(n_slaves / (self.n_columns - 1))
logging.debug(f"Reflowing {len(leaves)} leaves into {self.n_masters} masters "
f"and {n_slaves} slaves with {slaves_per_col} slaves per column.")
nodes = workspace.nodes[
::(-1 if
((transformations.Transformation.REFLECTX in self.active_transformations and
workspace.layout == "splith") or
(transformations.Transformation.REFLECTY in self.active_transformations and
workspace.layout == "splitv"))
else 1)
]
caused_mutation = False
for i, cur_col in enumerate(nodes):
logging.debug(f"Examining column {i} (container {cur_col.id}), which has {len(cur_col.nodes)} nodes.")
if i == len(nodes) - 1 and i > 0: # last pane
# If the cur or prev column is the master, don't move anything into it here.
if i > 1:
prev_col = nodes[i-1]
caused_mutation |= balance_cols(i3, prev_col, slaves_per_col, cur_col)
if len(cur_col.nodes) > 1:
if len(nodes) < self.n_columns:
logging.debug(f"Found {len(nodes)} columns, but expected {self.n_columns}; "
f"moving container {cur_col.nodes[-1]} right.")
move_counter.increment()
# Move changes focus to the container being moved, so refocused what
# we focued before the move.
focused = workspace.find_focused()
cur_col.nodes[-1].command(self.transform_command("move right"))
if focused:
focused.command("focus")
caused_mutation = True
workspace = common.refetch_container(i3, workspace)
elif len(nodes) > self.n_columns:
logging.debug(f"Found {len(nodes)} columns, but expected {self.n_columns}; "
f"moving container {cur_col.nodes[0].id} left.")
move_counter.increment()
focused = workspace.find_focused()
cur_col.nodes[0].command(self.transform_command("move left"))
if focused:
focused.command("focus")
caused_mutation = True
workspace = common.refetch_container(i3, workspace)
elif i == 0: # master pane
if len(cur_col.nodes) > self.n_masters and len(nodes) == 1:
logging.debug(f"Found a single column with {len(cur_col.nodes)} containers (the master pane), "
f"but expected {self.n_masters} containers; "
f"moving container {cur_col.nodes[-1].id} right.")
move_counter.increment()
focused = workspace.find_focused()
cur_col.nodes[0].command(self.transform_command("move left"))
if focused:
focused.command("focus")
caused_mutation = True
workspace = common.refetch_container(i3, workspace)
if len(nodes) > 1:
next_col = nodes[i+1]
caused_mutation |= balance_cols(i3, cur_col, self.n_masters, next_col)
else:
next_col = nodes[i+1]
caused_mutation |= balance_cols(i3, cur_col, slaves_per_col, next_col)
return caused_mutation
def layout(self, i3: i3ipc.Connection, event: Optional[i3ipc.Event]) -> None:
workspace = self.workspace(i3)
if not self.old_workspace:
self.old_workspace = workspace
if not workspace: # the workspace no longer exists
logging.debug(f"Workspace no longer exists, not running layout.")
return
logging.debug(f"Running layout for workspace {workspace.id}.")
should_reflow = event is None
post_hooks: list[Callable[[], None]] = []
# Have new windows displace the current window instead of being opened below them.
if event and event.change == "new":
# Dialog windows are created as normal windows and then made to float
# (https://github.com/swaywm/sway/commit/c9be0145576433e71f8b7732f7ff5ddee0d36076),
# so by the time we get there, recheck if we actually have a new leaf.
# Yes, this is a race, and it may be necessary to add a sleep here, but
# this seems to work fine now and sway really should win the race (as we
# want it to) as that's all happening internally in C, not after IPC
# back-and-forth in Python.
workspace = common.refetch_container(i3, workspace)
old_leaf_ids = {leaf.id for leaf in self.old_workspace.leaves()}
leaf_ids = {leaf.id for leaf in workspace.leaves()}
if old_leaf_ids != leaf_ids:
cycle_windows.swap_with_prev_window(
i3, event, window=workspace.find_by_id(event.container.id))
should_reflow = True
# Similarly, fullscreen windows are created as normal windows and them
# changed to be fullscreen.
if (con := workspace.find_by_id(event.container.id)) and con.fullscreen_mode == 1:
logging.debug(f"New container {con.id} was fullscreen. Setting to fullscreen again.")
post_hooks.append(lambda: con.command("focus"))
post_hooks.append(lambda: con.command("fullscreen"))
elif event and event.change == "close":
# Focus the "next" window instead of the last-focused window in the other
# column. Unless the window is floating, in which case let sway focus the
# last focused window in the workspace.
old_leaf_ids = {leaf.id for leaf in self.old_workspace.leaves()}
leaf_ids = {leaf.id for leaf in workspace.leaves()}
if (old_leaf_ids != leaf_ids and
workspace.id == common.get_focused_workspace(i3).id and
(closed_container := self.old_workspace.find_by_id(event.container.id)) and
not common.is_floating(closed_container)):
should_reflow = True
logging.debug(f"Looking at container {closed_container.id}: {closed_container.__dict__}")
window_was_fullscreen = closed_container.fullscreen_mode == 1
next_window = closed_container
for _ in range(len(old_leaf_ids)):
next_window = cycle_windows.find_next_window(next_window)
if next_window and next_window.id in leaf_ids:
next_window.command("focus")
if window_was_fullscreen:
logging.debug(f"Closed container {closed_container.id} was fullscreen. "
"Setting next container to fullscreen.")
post_hooks.append(lambda: next_window.command("fullscreen"))
break
elif event and event.change == "move":
if move_counter.value:
logging.debug(f"Move counter was non-zero ({move_counter.value}), ignoring move event.")
move_counter.decrement()
return
else:
should_reflow = True
# split commands bring focus to the workspace of the window they are run
# on, and they may be run as part of the reflow layer, so refocus the
# current workspace at the end.
focused_workspace = common.get_focused_workspace(i3)
post_hooks.append(lambda: i3.command(f"workspace {focused_workspace.name}"))
window_of_event = workspace.find_by_id(event.container.id)
cycle_windows.swap_with_prev_window(
i3, event, window=window_of_event, focus_after_swap=False)
layout.relayout_old_workspace(i3, workspace)
ever_reflowed = should_reflow
while should_reflow:
workspace = common.refetch_container(i3, workspace)
should_reflow = self.reflow(i3, workspace)
# Move the mouse nicely to the middle of the focused window instead of it
# continuing to sit in its old position or on a window boundary.
if (ever_reflowed and
workspace.id == common.get_focused_workspace(i3).id and
(focused := workspace.find_focused())):
logging.debug(f"Refocusing container {focused.id}.")
cycle_windows.refocus_window(i3, focused)
for hook in post_hooks:
hook()
self.old_workspace = common.refetch_container(i3, workspace)
#logging.debug(f"Storing workspace:\n{common.tree_str(self.old_workspace)}")