#!/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):
+def find_workspace(i3, window_id):
"""
- Find the parent of a given window id
+ Find the workspace of a given window id
"""
- def finder(con, parent, gp):
+ def finder(con, workspace):
if con.id == window_id:
- return (parent, gp)
+ return (workspace)
for node in con.nodes:
- res = finder(node, con, parent)
+ res = finder(node, con if con and con.type == 'workspace' else workspace)
if res:
return res
return None
- return finder(i3.get_tree(), None, None)
+ return finder(i3.get_tree(), 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 --add i3ha')
+ i3.command('focus parent')
+ i3.command('focus parent')
+ i3.command('mark --add 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):
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,
+
+ workspace = find_workspace(i3, e.container.id)
# debugging
- #pprint(vars(e))
+ #exit(0)
- parent, gp = find_parent(i3, e.container.id)
+ if not workspace:
+ return
+ #pprint(vars(workspace))
+ #print()
+ for pnode in workspace.nodes:
+ # debugging
+ # if (len(pnode.nodes) >= 1):
+ # print("pnodes: ", pnode.nodes)
- # This gets rid of tabbed container with single windows.
- #if (parent and gp and parent.layout == 'tabbed' and len(parent.nodes) == 1):
- # This gets rid of all single window containers.
- if (parent and gp and len(parent.nodes) == 1):
- 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')
+ 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()