mirror of
https://github.com/swaywm/sway.git
synced 2025-03-14 10:43:52 +00:00
Add a contrib script to partially replicate i3's append_layout
layout restoration scheme. Implemented entirely with existing IPC commands. Based on @9ary's ws-1.py Signed-off-by: Nolan Leake <nolan@sigbus.net>
This commit is contained in:
parent
4cdc4ac63a
commit
6fd5ff109d
1 changed files with 221 additions and 0 deletions
221
contrib/sway-layout
Executable file
221
contrib/sway-layout
Executable file
|
@ -0,0 +1,221 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
#Based on https://github.com/9ary/dotfiles/blob/master/i3/ws-1.py
|
||||
#Generalized and extended by Nolan Leake <nolan@sigbus.net>
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#Example layout:
|
||||
# {
|
||||
# "layout": "splith",
|
||||
# "nodes": [
|
||||
# {
|
||||
# "layout": "splitv",
|
||||
# "width": 60,
|
||||
# "nodes": [
|
||||
# {
|
||||
# "swallows": {"class": "Audacious"}
|
||||
# }
|
||||
# ]
|
||||
# },
|
||||
# {
|
||||
# "layout": "splitv",
|
||||
# "width": 40,
|
||||
# "nodes": [
|
||||
# {
|
||||
# "swallows": {"app_id": "^Alacritty$"}
|
||||
# },
|
||||
# {
|
||||
# "swallows": {"cmd": "exec alacritty"}
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
#This layout will match an externally started audacious (an Xwayland app), by
|
||||
# its X11 class, then match an externally started alacritty by its Wayland
|
||||
# app_id, and then internally start another alacritty, matching it because its
|
||||
# window PID is the cmd's PID, or a child of it.
|
||||
#
|
||||
#NOTE: A "cmd" will match on PID unless there are other matches in the swallow,
|
||||
# in which case the PID match will be ignored. This is useful for things like
|
||||
# windows spawned via emacsclient, where the PID of the window will end up
|
||||
# being from the emacs daemon, not the emacsclient instance.
|
||||
|
||||
import asyncio, re, argparse, json, subprocess, sys, os
|
||||
from i3ipc.aio import Connection
|
||||
from i3ipc import Event
|
||||
|
||||
def pid_is_descendent(pid, parent_pid):
|
||||
if str(pid) == str(parent_pid):
|
||||
return True
|
||||
try:
|
||||
with open(f'/proc/{pid}/stat', 'rb') as f:
|
||||
l = f.read()
|
||||
ppid = int(l[l.rfind(b')') + 2:].split()[1])
|
||||
except FileNotFoundError: #No Linux compatible procfs, try psutil
|
||||
import psutil
|
||||
ppid = psutil.Process(pid).ppid()
|
||||
if ppid == 0:
|
||||
return False
|
||||
return pid_is_descendent(ppid, parent_pid)
|
||||
|
||||
def iter_leaves(subtree):
|
||||
for node in subtree["nodes"]:
|
||||
node["parent"] = subtree
|
||||
if node.get("swallows"):
|
||||
yield node
|
||||
if node.get("nodes") is not None:
|
||||
yield from iter_leaves(node)
|
||||
|
||||
def try_match(con, leaves):
|
||||
if not getattr(try_match, 'already_ids', None):
|
||||
try_match.already_ids = set()
|
||||
|
||||
if con.id in try_match.already_ids:
|
||||
return #Something already matched this container.
|
||||
|
||||
for leaf in leaves:
|
||||
if leaf.get("con"):
|
||||
continue #Something already matched this leaf.
|
||||
|
||||
if 'pid' in leaf['swallows']:
|
||||
pid = getattr(con, 'pid', None)
|
||||
if pid and pid_is_descendent(con.pid, leaf['swallows']['pid']):
|
||||
leaf['con'] = con
|
||||
try_match.already_ids.add(con.id)
|
||||
return
|
||||
else:
|
||||
for key, pattern in leaf["swallows"].items():
|
||||
if key in ('class', 'instance', 'title'):
|
||||
key = 'window_' + key
|
||||
|
||||
if (value := getattr(con, key, None)) is None:
|
||||
break
|
||||
if re.search(pattern, value) is None:
|
||||
break
|
||||
else:
|
||||
leaf["con"] = con
|
||||
try_match.already_ids.add(con.id)
|
||||
return
|
||||
|
||||
def check_all_leaves_matched(leaves):
|
||||
for leaf in leaves:
|
||||
if leaf.get("con") is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def apply_layout(ws, subtree, to_split=None):
|
||||
for node in subtree["nodes"]:
|
||||
if con := node.get("con"):
|
||||
if ws:
|
||||
await con.command(f"move workspace {ws}")
|
||||
else:
|
||||
await con.command('scratchpad show')
|
||||
await con.command("floating disable")
|
||||
if to_split:
|
||||
if to_split == 'splitv':
|
||||
await con.command("split v")
|
||||
else:
|
||||
await con.command("split h")
|
||||
await con.command(f"layout {subtree['layout']}")
|
||||
to_split = None
|
||||
await con.command("focus")
|
||||
elif node.get("nodes") is not None:
|
||||
await apply_layout(ws, node, subtree['layout'])
|
||||
if con := subtree["nodes"][0].get("con"):
|
||||
await con.command("focus parent")
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description='Setup a workspace layout based on a layout config file.')
|
||||
parser.add_argument('--bg', default=False, action='store_true',
|
||||
help='daemonize after listening to new windows starts')
|
||||
parser.add_argument('--ws', default=None, help='workspace number to setup')
|
||||
parser.add_argument('--match-existing', action='store_true',
|
||||
help='match windows that already exist')
|
||||
parser.add_argument('layout_file',
|
||||
help='json file containing the workspace layout')
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.layout_file, 'r') as f:
|
||||
layout = json.load(f)
|
||||
|
||||
sway = await Connection().connect()
|
||||
|
||||
leaves = list(iter_leaves(layout))
|
||||
|
||||
#Subscribe to events first to make sure we don't miss anything.
|
||||
watched_ids = set()
|
||||
def on_window(self, event):
|
||||
if event.change == Event.WINDOW_NEW:
|
||||
watched_ids.add(event.container.id)
|
||||
if event.change == Event.WINDOW_TITLE:
|
||||
if not (event.container.id in watched_ids or args.match_existing):
|
||||
return
|
||||
try_match(event.container, leaves)
|
||||
if check_all_leaves_matched(leaves):
|
||||
sway.main_quit()
|
||||
sway.on(Event.WINDOW_NEW, on_window)
|
||||
sway.on(Event.WINDOW_TITLE, on_window)
|
||||
|
||||
#Fork into bg if requested, now that we're listening to window events.
|
||||
if args.bg:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
#Run any cmd directives.
|
||||
for leaf in leaves:
|
||||
swallows = leaf['swallows']
|
||||
cmd = swallows.get('cmd', None)
|
||||
if cmd:
|
||||
proc = subprocess.Popen(cmd, close_fds=True, shell=True)
|
||||
if len(swallows) == 1:
|
||||
#Since we're not already matching on any other criteria,
|
||||
# match on the PID.
|
||||
swallows['pid'] = proc.pid
|
||||
|
||||
#If requested, try to match any existing windows.
|
||||
if args.match_existing:
|
||||
for con in await sway.get_tree():
|
||||
try_match(con, leaves)
|
||||
|
||||
if not check_all_leaves_matched(leaves):
|
||||
# Wait until all windows have appeared
|
||||
await sway.main()
|
||||
|
||||
#This really shouldn't be necessary, but we get a warning otherwise
|
||||
# (this could be a bug in i3ipc).
|
||||
sway.off(on_window)
|
||||
|
||||
scratchpad_windows = []
|
||||
try:
|
||||
#Move all our windows to the scratchpad temporarily.
|
||||
for leaf in leaves:
|
||||
scratchpad_windows.append(leaf["con"])
|
||||
await leaf["con"].command("move scratchpad")
|
||||
|
||||
#Recusrively apply layout to our discovered leaves.
|
||||
await apply_layout(args.ws, layout)
|
||||
|
||||
#Resize containers for windows that have sizes specified.
|
||||
for leaf in leaves:
|
||||
con = leaf["con"]
|
||||
await con.command("focus")
|
||||
await con.command(f"resize set {leaf.get('width', 0)} "
|
||||
f"{leaf.get('height', 0)}")
|
||||
while leaf := leaf.get("parent"):
|
||||
await sway.command("focus parent")
|
||||
await sway.command(f"resize set {leaf.get('width', 0)} "
|
||||
f"{leaf.get('height', 0)}")
|
||||
|
||||
await leaves[0]["con"].command("focus")
|
||||
except:
|
||||
#Let's at least not leave hidden windows in the scratchpad...
|
||||
while len(scratchpad_windows):
|
||||
await scratchpad_windows.pop().command('scratchpad show')
|
||||
raise
|
||||
|
||||
asyncio.run(main())
|
Loading…
Add table
Reference in a new issue