diff --git a/etc/sway/modes/default.conf b/etc/sway/modes/default.conf index f7f1dd2..8cb4147 100644 --- a/etc/sway/modes/default.conf +++ b/etc/sway/modes/default.conf @@ -79,6 +79,10 @@ $bindsym Mod1+Tab exec env RUST_BACKTRACE=1 swayr switch-to-urgent-or-lru-window ## Navigation // Switch to the last recently used workspace ## $bindsym $mod+Tab workspace back_and_forth +## Navigation // Switching between containers in the workspace // $mod + [] ## +$bindsym $mod+bracketright nop top_next +$bindsym $mod+bracketleft nop top_prev + # # Workspaces: # diff --git a/usr/lib/systemd/user/switch-top-level.service b/usr/lib/systemd/user/switch-top-level.service new file mode 100644 index 0000000..d40a93b --- /dev/null +++ b/usr/lib/systemd/user/switch-top-level.service @@ -0,0 +1,10 @@ +[Unit] +Requires=graphical-session.target + +[Service] +ExecStart=/usr/share/sway/scripts/switch-top-level.py +Restart=on-failure +RestartSec=1 + +[Install] +WantedBy=graphical-session.target diff --git a/usr/share/sway/scripts/switch-top-level.py b/usr/share/sway/scripts/switch-top-level.py new file mode 100755 index 0000000..a00700d --- /dev/null +++ b/usr/share/sway/scripts/switch-top-level.py @@ -0,0 +1,98 @@ +#!/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()