#!/usr/bin/env python3 # I, Ian Kelling, follow the GNU license recommendations at # https://www.gnu.org/licenses/license-recommendations.en.html. They # recommend that small programs, < 300 lines, be licensed under the # Apache License 2.0. This file contains or is part of one or more small # programs. If a small program grows beyond 300 lines, I plan to change # to a recommended GPL license. # Copyright 2024 Ian Kelling # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This gets rid of all single window containers. # If a single window container was nested in another, # it ignores that, but I don't generally expect to create them and # we change focus enough that we would kill them off. # Note: I spent a lot of time figuring out how to do this properly, # https://github.com/i3/i3/issues/3808 there are a bunch of links # which suggest either float toggle; float toggle, which doesn't put windows back in the same place, or doing a move, which only actually works if you are moving in a direction which does not have a container there, else your window joins the container, and . import sys import os from i3ipc import Connection, Event from pprint import pprint def find_parent(i3, window_id): """ Find the parent of a given window id """ def finder(con, parent, workspace): if con.id == window_id: return (parent, workspace) for node in con.nodes: res = finder(node, con, con if con and con.type == 'workspace' else workspace) if res: return res return None return finder(i3.get_tree(), None, None) def kill_single_win_containers(i3, e, node, parent): if len(parent.nodes) == 1 and len(node.nodes) == 0: print("d1: killing parent") # parent is a single window container, kill it. # Note: based on testing, # i3 takes care of not calling this program for # events which we create within it. Otherwise, # we could create our disabling file here # and delete it later if it wasn't already there. i3.command('[con_id=%s] focus' % node.id) i3.command('mark i3ha') i3.command('focus parent') i3.command('focus parent') i3.command('mark i3hb') i3.command('[con_mark="i3ha"] focus') i3.command('move window to mark i3hb') i3.command('unmark i3ha') i3.command('unmark i3hb') # back to our original focus i3.command('[con_id=%s] focus' % e.container.id) elif len(node.nodes) >= 1: for child in node.nodes: kill_single_win_containers(i3, e, child, node) def focus_hook(i3, e): """ Set the layout/split for the currently focused window to either vertical or horizontal, depending on its width/height """ if os.path.isfile("/tmp/iank-i3-no-auto"): return # I identify container vs a real windows by the fact that it has nodes. # looking through the data, another notable difference is that it has # 'window': None, # 'window_type': None, parent, workspace = find_parent(i3, e.container.id) # debugging #exit(0) if not workspace: return #pprint(vars(workspace)) #print() for pnode in workspace.nodes: # debugging # if (len(pnode.nodes) >= 1): # print("pnodes: ", pnode.nodes) for node in pnode.nodes: kill_single_win_containers(i3, e, node, pnode) def main(): i3 = Connection() i3.on(Event.WINDOW_FOCUS, focus_hook) # if we don't have move, and we move a window out of a container, # leaving behind a single window container, then we move it back, it # will go into the container. We could expect that if we do it # quickly, but it would be unexpected after a few seconds and we # forget that it was a container. i3.on(Event.WINDOW_MOVE, focus_hook) i3.main() if __name__ == "__main__": main()