215 lines
7.1 KiB
Python
215 lines
7.1 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
"""
|
||
|
This script uses the i3ipc python module to switch the layout splith / splitv
|
||
|
for the currently focused window, depending on its dimensions.
|
||
|
It works on both sway and i3 window managers.
|
||
|
|
||
|
Inspired by https://github.com/olemartinorg/i3-alternating-layout
|
||
|
|
||
|
Copyright: 2019-2021 Piotr Miller & Contributors
|
||
|
e-mail: nwg.piotr@gmail.com
|
||
|
Project: https://github.com/nwg-piotr/autotiling
|
||
|
License: GPL3
|
||
|
|
||
|
Dependencies: python-i3ipc>=2.0.1 (i3ipc-python)
|
||
|
"""
|
||
|
import argparse
|
||
|
import os
|
||
|
import sys
|
||
|
from functools import partial
|
||
|
|
||
|
from i3ipc import Connection, Event
|
||
|
|
||
|
try:
|
||
|
from .__about__ import __version__
|
||
|
except ImportError:
|
||
|
__version__ = "unknown"
|
||
|
|
||
|
|
||
|
def temp_dir():
|
||
|
if os.getenv("TMPDIR"):
|
||
|
return os.getenv("TMPDIR")
|
||
|
elif os.getenv("TEMP"):
|
||
|
return os.getenv("TEMP")
|
||
|
elif os.getenv("TMP"):
|
||
|
return os.getenv("TMP")
|
||
|
|
||
|
return "/tmp"
|
||
|
|
||
|
|
||
|
def save_string(string, file):
|
||
|
try:
|
||
|
file = open(file, "wt")
|
||
|
file.write(string)
|
||
|
file.close()
|
||
|
except Exception as e:
|
||
|
print(e)
|
||
|
|
||
|
|
||
|
def output_name(con):
|
||
|
if con.type == "root":
|
||
|
return None
|
||
|
|
||
|
if p := con.parent:
|
||
|
if p.type == "output":
|
||
|
return p.name
|
||
|
else:
|
||
|
return output_name(p)
|
||
|
|
||
|
|
||
|
def switch_splitting(i3, e, debug, outputs, workspaces, depth_limit):
|
||
|
try:
|
||
|
con = i3.get_tree().find_focused()
|
||
|
output = output_name(con)
|
||
|
# Stop, if outputs is set and current output is not in the selection
|
||
|
if outputs and output not in outputs:
|
||
|
if debug:
|
||
|
print(
|
||
|
"Debug: Autotiling turned off on output {}".format(output),
|
||
|
file=sys.stderr,
|
||
|
)
|
||
|
return
|
||
|
|
||
|
if con and not workspaces or (str(con.workspace().num) in workspaces):
|
||
|
if con.floating:
|
||
|
# We're on i3: on sway it would be None
|
||
|
# May be 'auto_on' or 'user_on'
|
||
|
is_floating = "_on" in con.floating
|
||
|
else:
|
||
|
# We are on sway
|
||
|
is_floating = con.type == "floating_con"
|
||
|
|
||
|
if depth_limit:
|
||
|
# Assume we reached the depth limit, unless we can find a workspace
|
||
|
depth_limit_reached = True
|
||
|
current_con = con
|
||
|
current_depth = 0
|
||
|
while current_depth < depth_limit:
|
||
|
# Check if we found the workspace of the current container
|
||
|
if current_con.type == "workspace":
|
||
|
# Found the workspace within the depth limitation
|
||
|
depth_limit_reached = False
|
||
|
break
|
||
|
|
||
|
# Look at the parent for next iteration
|
||
|
current_con = current_con.parent
|
||
|
|
||
|
# Only count up the depth, if the container has more than
|
||
|
# one container as child
|
||
|
if len(current_con.nodes) > 1:
|
||
|
current_depth += 1
|
||
|
|
||
|
if depth_limit_reached:
|
||
|
if debug:
|
||
|
print("Debug: Depth limit reached")
|
||
|
return
|
||
|
|
||
|
is_full_screen = con.fullscreen_mode == 1
|
||
|
is_stacked = con.parent.layout == "stacked"
|
||
|
is_tabbed = con.parent.layout == "tabbed"
|
||
|
|
||
|
# Exclude floating containers, stacked layouts, tabbed layouts and full screen mode
|
||
|
if (not is_floating
|
||
|
and not is_stacked
|
||
|
and not is_tabbed
|
||
|
and not is_full_screen):
|
||
|
new_layout = "splitv" if con.rect.height > con.rect.width else "splith"
|
||
|
|
||
|
if new_layout != con.parent.layout:
|
||
|
result = i3.command(new_layout)
|
||
|
if result[0].success and debug:
|
||
|
print("Debug: Switched to {}".format(new_layout), file=sys.stderr)
|
||
|
elif debug:
|
||
|
print("Error: Switch failed with err {}".format(result[0].error), file=sys.stderr, )
|
||
|
|
||
|
elif debug:
|
||
|
print("Debug: No focused container found or autotiling on the workspace turned off", file=sys.stderr)
|
||
|
|
||
|
except Exception as e:
|
||
|
print("Error: {}".format(e), file=sys.stderr)
|
||
|
|
||
|
|
||
|
def main():
|
||
|
parser = argparse.ArgumentParser()
|
||
|
parser.add_argument("-d",
|
||
|
"--debug",
|
||
|
action="store_true",
|
||
|
help="print debug messages to stderr")
|
||
|
parser.add_argument("-v",
|
||
|
"--version",
|
||
|
action="version",
|
||
|
version="%(prog)s {}, Python {}".format(__version__, sys.version),
|
||
|
help="display version information", )
|
||
|
parser.add_argument("-o",
|
||
|
"--outputs",
|
||
|
help="restricts autotiling to certain output; "
|
||
|
"example: autotiling --output DP-1 HDMI-0",
|
||
|
nargs="*",
|
||
|
type=str,
|
||
|
default=[], )
|
||
|
parser.add_argument("-w",
|
||
|
"--workspaces",
|
||
|
help="restricts autotiling to certain workspaces; example: autotiling --workspaces 8 9",
|
||
|
nargs="*",
|
||
|
type=str,
|
||
|
default=[], )
|
||
|
parser.add_argument("-l",
|
||
|
"--limit",
|
||
|
help='limit how often autotiling will split a container; '
|
||
|
'try "2", if you like master-stack layouts; default: 0 (no limit)',
|
||
|
type=int,
|
||
|
default=0, )
|
||
|
"""
|
||
|
Changing event subscription has already been the objective of several pull request. To avoid doing this again
|
||
|
and again, let's allow to specify them in the `--events` argument.
|
||
|
"""
|
||
|
parser.add_argument("-e",
|
||
|
"--events",
|
||
|
help="list of events to trigger switching split orientation; default: WINDOW MODE",
|
||
|
nargs="*",
|
||
|
type=str,
|
||
|
default=["WINDOW", "MODE"])
|
||
|
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
if args.debug and args.outputs:
|
||
|
print("autotiling is only active on outputs:", ",".join(args.outputs))
|
||
|
|
||
|
if args.debug and args.workspaces:
|
||
|
print("autotiling is only active on workspaces:", ','.join(args.workspaces))
|
||
|
|
||
|
# For use w/ nwg-panel
|
||
|
ws_file = os.path.join(temp_dir(), "autotiling")
|
||
|
if args.workspaces:
|
||
|
save_string(','.join(args.workspaces), ws_file)
|
||
|
else:
|
||
|
if os.path.isfile(ws_file):
|
||
|
os.remove(ws_file)
|
||
|
|
||
|
if not args.events:
|
||
|
print("No events specified", file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
|
||
|
handler = partial(
|
||
|
switch_splitting,
|
||
|
debug=args.debug,
|
||
|
outputs=args.outputs,
|
||
|
workspaces=args.workspaces,
|
||
|
depth_limit=args.limit,
|
||
|
)
|
||
|
i3 = Connection()
|
||
|
for e in args.events:
|
||
|
try:
|
||
|
i3.on(Event[e], handler)
|
||
|
print("{} subscribed".format(Event[e]))
|
||
|
except KeyError:
|
||
|
print("'{}' is not a valid event".format(e), file=sys.stderr)
|
||
|
|
||
|
i3.main()
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
# execute only if run as a script
|
||
|
main()
|