From 74247ed5b27ccdc4407d81177bbc34ca0e341eeb Mon Sep 17 00:00:00 2001 From: Barak Michener Date: Thu, 1 Jul 2021 13:15:37 -0700 Subject: [PATCH] add i3tool --- .i3/config | 1 + .i3/i3-barak-tool.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100755 .i3/i3-barak-tool.py diff --git a/.i3/config b/.i3/config index a34a205..badf175 100644 --- a/.i3/config +++ b/.i3/config @@ -74,6 +74,7 @@ bindsym $mod+l focus right #bindsym $mod+j exec --no-startup-id ~/.i3/i3-focus-next #bindsym $mod+k exec --no-startup-id ~/.i3/i3-focus-next --reverse bindsym $mod+y move workspace to output left +bindsym $mod+Return exec --no-startup-id ~/.i3/i3-barak-tool.py promote # alternatively, you can use the cursor keys: #bindsym $mod+Left focus left diff --git a/.i3/i3-barak-tool.py b/.i3/i3-barak-tool.py new file mode 100755 index 0000000..97e9c6e --- /dev/null +++ b/.i3/i3-barak-tool.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +import json +import subprocess +import socket +import struct +import sys + + +_struct_header = '=6sII' + +RUN_COMMAND = 0 +GET_WORKSPACES = 1 +SUBSCRIBE = 4 +GET_OUTPUTS=3 +GET_TREE = 4 + + +def get_i3_socketpath(): + socketpath = subprocess.check_output(["i3", "--get-socketpath"]) + return socketpath.decode().rstrip() + + +def format_i3_command(kind: int, cmd: str): + b = cmd.encode() + return b"i3-ipc" + struct.pack("=II", len(b), kind) + b + + +def _ipc_recv(sock): + data = sock.recv(14) + + if len(data) == 0: + raise EOFError('got EOF from ipc socket') + + _, msg_length, msg_type = _unpack_header(data) + msg_size = 6 + msg_length + while len(data) < msg_size: + data += sock.recv(msg_length) + payload = _unpack(data) + return payload, msg_type + + +def _unpack(data): + _, msg_length, _ = _unpack_header(data) + size = struct.calcsize(_struct_header) + msg_size = size + msg_length + payload = data[size:msg_size] + return payload.decode('utf-8', 'replace') + + +def _unpack_header(data: bytes): + return struct.unpack(_struct_header, data[:struct.calcsize(_struct_header)]) + + +def open_i3_socket(): + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.connect(get_i3_socketpath()) + return client + + +def send_command(client, kind, data): + client.sendall(format_i3_command(kind, data)) + result, _ = _ipc_recv(client) + return json.loads(result) + + +class TreeNode: + def __init__(self, json): + self.id = None + self.rect = None + self.focused = False + self.name = "" + for k, v in json.items(): + if k != "nodes": + self.__setattr__(k, v) + self.parent = None + self.nodes = list(map(TreeNode, json["nodes"])) + for n in self.nodes: + n.parent = self + + def find(self, where=None): + if where is None: + where = lambda _: True + if where(self): + yield self + for n in self.nodes: + yield from n.find(where) + + def __repr__(self) -> str: + return "TreeNode(%d, %s, focused=%s, rect=%s)" % (self.id, self.name, self.focused, str(self.rect)) + + def find_parent(self, where=None): + if self.parent is None: + return None + if where is None or where(self.parent): + return self.parent + return self.parent.find_parent(where) + + def leaves(self): + if len(self.nodes) == 0: + yield self + else: + for n in self.nodes: + yield from n.leaves() + + +def find_biggest_window(tree: TreeNode): + max_leaf = None + max_area = 0 + max_x = -1 + for leaf in tree.leaves(): + rect = leaf.rect + area = rect["width"] * rect["height"] + if area > max_area or (area == max_area and max_x > rect["x"]): + max_area = area + max_leaf = leaf + max_x = rect["x"] + + return max_leaf + + +def current_workspace(tree): + focused = list(tree.find(where=lambda n: n.focused is True)) + if len(focused) == 0: + print("not found") + sys.exit(1) + focused = focused[0] + ws = focused.find_parent(lambda n: n.type == "workspace") + return ws + + +def promote(client): + json = send_command(client, GET_TREE, "") + tree = TreeNode(json) + ws = current_workspace(tree) + master = find_biggest_window(ws) + send_command(client, RUN_COMMAND, "swap container with con_id %s" % master.id) + + +def main(): + if len(sys.argv) < 2: + print("needs an argument") + return + command = sys.argv[1] + client = open_i3_socket() + if command == "promote": + promote(client) + client.close() + +if __name__ == "__main__": + main()