sway new autotiling
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
exec dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=sway
|
||||
exec_always autotiling -l 2
|
||||
exec_always ~/.config/sway/scripts/autotiling-new.py
|
||||
exec_always ~/.config/sway/scripts/workspace-colors.py
|
||||
exec_always ~/.config/sway/scripts/eww-bars.sh
|
||||
exec_always mako
|
||||
|
||||
166
sway/.config/sway/scripts/autotiling-new.py
Executable file
166
sway/.config/sway/scripts/autotiling-new.py
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Give newly opened sway/i3 windows the split direction matching the focused
|
||||
window's aspect ratio:
|
||||
|
||||
focused window wider than tall -> new window lands side by side (splith)
|
||||
focused window taller than wide -> new window lands stacked (splitv)
|
||||
|
||||
Mechanism (same as i3-alternating-layout): when a window gains focus, if its
|
||||
aspect disagrees with its parent's orientation, the window is wrapped in a
|
||||
fresh container with the matching orientation. The next window opened while
|
||||
this one is focused is inserted INTO that wrapper, so it lands in the right
|
||||
direction. Wrapping a single window is invisible until the next window opens;
|
||||
existing multi-window layouts are never re-flowed.
|
||||
|
||||
wezterm-nudge collision: the nudge script floats a window when its resize
|
||||
fails (the window is alone in its wrapper). Sway destroys the empty wrapper
|
||||
on float and re-tiles the window into the workspace on un-float, undoing the
|
||||
wrap. This script therefore also listens for the "floating" change event and
|
||||
re-creates the wrapper after the window is re-tiled. The whole dance is
|
||||
invisible because the nudge keeps the window at opacity 0 until it finishes.
|
||||
|
||||
Why not upstream `autotiling`: it reacts to every WINDOW/MODE event and flips
|
||||
the focused window's parent layout on each one, so merely focusing one of two
|
||||
side-by-side windows flips the pair to stacked.
|
||||
|
||||
Single-instance guard: a second copy exits immediately. This makes
|
||||
`exec_always` safe to re-run on config reloads (watch-outputs.sh reloads).
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
from i3ipc import Connection, Event
|
||||
|
||||
PIDFILE = "/tmp/autotiling-new.pid"
|
||||
|
||||
|
||||
def already_running():
|
||||
try:
|
||||
with open(PIDFILE) as f:
|
||||
pid = int(f.read().strip())
|
||||
except (FileNotFoundError, ValueError):
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def wrap_if_needed(i3, con, debug):
|
||||
"""Wrap `con` in a container whose orientation matches its aspect ratio,
|
||||
but only if the current parent orientation disagrees."""
|
||||
if con.type != "con":
|
||||
return
|
||||
if con.fullscreen_mode == 1:
|
||||
return
|
||||
parent = con.parent
|
||||
if parent is None or parent.type == "floating_con":
|
||||
return
|
||||
if parent.layout not in ("splith", "splitv"):
|
||||
return
|
||||
if con.rect.width <= 0 or con.rect.height <= 0:
|
||||
return
|
||||
|
||||
parent_horizontal = parent.layout == "splith"
|
||||
tall = con.rect.height > con.rect.width
|
||||
if tall and parent_horizontal:
|
||||
split_dir = "vertical" # tall window -> stacked next time
|
||||
elif not tall and not parent_horizontal:
|
||||
split_dir = "horizontal" # wide window -> side by side next time
|
||||
else:
|
||||
return # aspect and parent orientation already agree
|
||||
|
||||
result = i3.command(f"[con_id={con.id}] split {split_dir}")
|
||||
if debug:
|
||||
if result and result[0].success:
|
||||
print(f"autotiling-new: split {split_dir} on con {con.id} "
|
||||
f"({con.rect.width}x{con.rect.height}, parent {parent.layout})",
|
||||
file=sys.stderr)
|
||||
else:
|
||||
print(f"autotiling-new: failed: {result}", file=sys.stderr)
|
||||
|
||||
|
||||
def wait_committed(i3, con_id, timeout=0.8):
|
||||
"""Wait until the window is tiled with stable, non-zero geometry. Sway
|
||||
emits window events before the arrange transaction commits, so rects can
|
||||
be stale or zero at event time; this polls until they settle."""
|
||||
deadline = time.monotonic() + timeout
|
||||
prev = None
|
||||
con = None
|
||||
while time.monotonic() < deadline:
|
||||
con = i3.get_tree().find_by_id(con_id)
|
||||
if con is not None and con.parent is not None \
|
||||
and con.parent.type != "floating_con":
|
||||
r = (con.rect.width, con.rect.height)
|
||||
if prev == r and r[0] > 0 and r[1] > 0:
|
||||
return con
|
||||
prev = r
|
||||
time.sleep(0.03)
|
||||
return con
|
||||
|
||||
|
||||
def handle(i3, e, debug):
|
||||
try:
|
||||
if e.container is None:
|
||||
return
|
||||
con_id = e.container.id
|
||||
|
||||
if e.change == "focus":
|
||||
# Usually the arrange has already committed; use the rect as-is.
|
||||
con = i3.get_tree().find_by_id(con_id)
|
||||
if con is not None and con.parent is not None \
|
||||
and con.parent.type != "floating_con" \
|
||||
and con.rect.width > 0 and con.rect.height > 0:
|
||||
wrap_if_needed(i3, con, debug)
|
||||
else:
|
||||
con = wait_committed(i3, con_id)
|
||||
if con is not None:
|
||||
wrap_if_needed(i3, con, debug)
|
||||
|
||||
elif e.change == "floating":
|
||||
# The wezterm-nudge float-toggle destroyed the wrapper; re-tile is
|
||||
# in progress. Skip if the window stayed floating, otherwise wait
|
||||
# for the tiled geometry to commit and re-wrap.
|
||||
con = i3.get_tree().find_by_id(con_id)
|
||||
if con is None:
|
||||
return
|
||||
if con.parent is not None and con.parent.type == "floating_con":
|
||||
return
|
||||
con = wait_committed(i3, con_id)
|
||||
if con is not None:
|
||||
wrap_if_needed(i3, con, debug)
|
||||
except Exception as ex:
|
||||
print(f"autotiling-new: {ex}", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
if already_running():
|
||||
print("autotiling-new: already running, exiting", file=sys.stderr)
|
||||
return 0
|
||||
with open(PIDFILE, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="autotiling-new",
|
||||
description="Split direction for new windows follows the focused window's aspect ratio",
|
||||
)
|
||||
parser.add_argument("-d", "--debug", action="store_true",
|
||||
help="print debug messages to stderr")
|
||||
args = parser.parse_args()
|
||||
|
||||
i3 = Connection()
|
||||
handler = partial(handle, debug=args.debug)
|
||||
i3.on(Event.WINDOW, handler)
|
||||
i3.main()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user