#!/usr/bin/env python3 import i3ipc # # This script requires i3ipc-python package (install it from a system package manager # or pip). # # The scripts allows you to define two new bindings: # bindsym $mod+bracketright nop top_next # bindsym $mod+bracketleft nop top_prev # # The purpose of it is to switch between top-level containers (windows) in a workspace. # One possible usecase is having a workspace with two (or more on large displays) # columns of tabs: one on left and one on right. In such setup, "move left" and # "move right" will only switch tabs inside the column. # # You can add a systemd user service to run this script on startup: # # ~> cat .config/systemd/user/switch-top-level.service # [Install] # WantedBy=graphical-session.target # [Service] # ExecStart=path/to/switch-top-level.py # Restart=on-failure # RestartSec=1 # [Unit] # Requires=graphical-session.target class TopLevelSwitcher: def __init__(self): self.top_to_selected = {} # top i3ipc.Con -> selected container id self.con_to_top = {} # container id -> top i3ipc.Con self.prev = None # previously focused container id self.i3 = i3ipc.Connection() self.i3.on('window::focus', self.on_window_focus) self.i3.on(i3ipc.Event.BINDING, self.on_binding) self.update_top_level() self.i3.main() def top_level(self, node): if len(node.nodes) == 1: return self.top_level(node.nodes[0]) return node.nodes def update_top_level(self): tree = self.i3.get_tree() for ws in tree.workspaces(): for con in self.top_level(ws): self.update_top_level_rec(con, con.id) def update_top_level_rec(self, con: i3ipc.Con, top: i3ipc.Con): self.con_to_top[con.id] = top for child in con.nodes: self.update_top_level_rec(child, top) if len(con.nodes) == 0 and top not in self.top_to_selected: self.top_to_selected[top] = con.id def save_prev(self): if not self.prev: return prev_top = self.con_to_top.get(self.prev) if not prev_top: return self.top_to_selected[prev_top] = self.prev def on_window_focus(self, _i3, event): self.update_top_level() self.save_prev() self.prev = event.container.id def on_top(self, _i3, _event, diff: int): root = self.i3.get_tree() if not self.prev: return top = self.con_to_top[self.prev] ws = [top.id for top in self.top_level(root.find_focused().workspace())] top_idx = ws.index(top) top_idx = (top_idx + diff + len(ws)) % len(ws) next_top = ws[top_idx] next_window = self.top_to_selected.get(next_top) self.i3.command('[con_id=%s] focus' % next_window) def on_binding(self, i3, event): if event.binding.command.startswith('nop top_next'): self.on_top(i3, event, 1) elif event.binding.command.startswith('nop top_prev'): self.on_top(i3, event, -1) if __name__ == '__main__': TopLevelSwitcher()