diff --git a/.config/sway/config.d/autostart_applications b/.config/sway/config.d/autostart_applications index 0b59ce7..a643b4f 100644 --- a/.config/sway/config.d/autostart_applications +++ b/.config/sway/config.d/autostart_applications @@ -7,7 +7,8 @@ exec mako # Network Applet exec nm-applet --indicator - +# Autotiling +exec_always ~/.config/sway/scripts/autotiling # Welcome App exec dex -a -s /etc/xdg/autostart/:~/.config/autostart/ diff --git a/.config/sway/scripts/autotiling b/.config/sway/scripts/autotiling new file mode 100755 index 0000000..4ccfe59 --- /dev/null +++ b/.config/sway/scripts/autotiling @@ -0,0 +1,214 @@ +#!/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(): + return os.getenv("TMPDIR") or os.getenv("TEMP") or os.getenv("TMP") or "/tmp" + + +def save_string(string, file_path): + try: + with open(file_path, "wt") as file: + file.write(string) + 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, splitwidth, splitheight, splitratio): + 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(f"Debug: Autotiling turned off on output {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 / splitratio else "splith" + + if new_layout != con.parent.layout: + result = i3.command(new_layout) + if result[0].success and debug: + print(f"Debug: Switched to {new_layout}", file=sys.stderr) + elif debug: + print(f"Error: Switch failed with err {result[0].error}", file=sys.stderr) + + if e.change in ["new", "move"] and con.percent: + if con.parent.layout == "splitv" and splitheight != 1.0: # top / bottom + # print(f"split top fac {splitheight*100}") + i3.command(f"resize set height {int(con.percent * splitheight * 100)} ppt") + elif con.parent.layout == "splith" and splitwidth != 1.0: # top / bottom: # left / right + # print(f"split right fac {splitwidth*100} ") + i3.command(f"resize set width {int(con.percent * splitwidth * 100)} ppt") + + elif debug: + print("Debug: No focused container found or autotiling on the workspace turned off", file=sys.stderr) + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + + +def get_parser(): + parser = argparse.ArgumentParser(prog="autotiling", description="Script for sway and i3 to automatically switch the horizontal / vertical window split orientation") + + parser.add_argument("-d", "--debug", action="store_true", + help="print debug messages to stderr") + parser.add_argument("-v", "--version", action="version", + version=f"%(prog)s {__version__}, Python {sys.version}", + help="display version information") + parser.add_argument("-o", "--outputs", nargs="*", type=str, default=[], + help="restricts autotiling to certain output; example: autotiling --output DP-1 HDMI-0") + parser.add_argument("-w", "--workspaces", nargs="*", type=str, default=[], + help="restricts autotiling to certain workspaces; example: autotiling --workspaces 8 9") + parser.add_argument("-l", "--limit", type=int, default=0, + help='limit how often autotiling will split a container; ' + 'try "2" if you like master-stack layouts; default: 0 (no limit)') + parser.add_argument("-sw", + "--splitwidth", + help='set the width of the vertical split (as factor); default: 1.0;', + type=float, + default=1.0, ) + parser.add_argument("-sh", + "--splitheight", + help='set the height of the horizontal split (as factor); default: 1.0;', + type=float, + default=1.0, ) + parser.add_argument("-sr", + "--splitratio", + help='Split direction ratio - based on window height/width; default: 1;' + 'try "1.61", for golden ratio - window has to be 61%% wider for left/right split; default: 1.0;', + type=float, + default=1.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", nargs="*", type=str, default=["WINDOW", "MODE"], + help="list of events to trigger switching split orientation; default: WINDOW MODE") + + return parser + +def main(): + args = get_parser().parse_args() + + if args.debug: + if args.outputs: + print(f"autotiling is only active on outputs: {','.join(args.outputs)}") + if args.workspaces: + print(f"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, + splitwidth=args.splitwidth, + splitheight=args.splitheight, + splitratio=args.splitratio + ) + i3 = Connection() + for e in args.events: + try: + i3.on(Event[e], handler) + print(f"{Event[e]} subscribed") + except KeyError: + print(f"'{e}' is not a valid event", file=sys.stderr) + + i3.main() + + +if __name__ == "__main__": + main()