151 lines
3.6 KiB
Python
Executable file
151 lines
3.6 KiB
Python
Executable file
#!/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()
|