#!/usr/bin/bash # 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) from i3ipc import Connection, Event 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()