Last active
July 2, 2018 23:25
-
-
Save laur89/ea7d92ef6d3f1421b1db8f998db76fde to your computer and use it in GitHub Desktop.
i3wm focus changer helper scripts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# Enables changing focus _from_ tabbed or stacked | |
# container without first cycling through tabs/stacks; | |
# | |
# Depends on i3ipc: pip3 install --upgrade i3ipc | |
# | |
# i3 config should be something like: | |
# bindsym $mod+h nop | |
# bindsym $mod+j nop | |
# bindsym $mod+k nop | |
# bindsym $mod+l nop | |
# exec --no-startup-id i3-cycle-exclude-tabs.py | |
# | |
# note the actual direction binding is configured in this script in 'movements' dict; | |
# | |
################################### | |
import i3ipc | |
class config: | |
mod = 'Mod4' | |
command = 'nop' | |
movements = { | |
'j': 'down', | |
'k': 'up', | |
'h': 'left', | |
'l': 'right' | |
} | |
def main(movement): | |
"""Run focus change logic cycling through split{h,v} only. | |
Keyword arguments: | |
movement -- direction of required focus change (right, left, up, down) | |
""" | |
con = i3.get_tree().find_focused() | |
dir = 1 if movement == 'right' or movement == 'down' else -1 | |
up_or_down = movement == 'up' or movement == 'down' # ie !up_or_down means 'left or right' | |
parents = [] | |
while con.parent and con.parent.type == 'con': # don't go as high as workspace (& output) | |
con = con.parent | |
parents.append(con) | |
# do not continue with the logic if we're simply trying | |
# to change focus between two sibling splitv/splith containers; | |
# without this check, those siblings might become inaccessible; | |
for con in parents: | |
if focusing_sibling_split(con, up_or_down, dir): | |
focus_for_sibling_movement(con) | |
default(movement) | |
return | |
# check if we're crossing a boundary of a split container; if so, we want to | |
# select the parent before continuing, otherwise we might end up | |
# staying in same container (eg when focus on splith container on the | |
# right side of the screen, rightmost split selected - i3 wouldn't switch | |
# to container on the other side of the screen, but to the other side of | |
# current split{v,h} container); | |
# This feature is probably the most nice-to-haveish one, ie no | |
# issue to remove if causing problems. | |
################## | |
if handle_split_exit(parents, up_or_down, movement, dir): return # needed already here, as split_exit logic runs | |
# from change_foc ONLY when inside wrapped container; | |
change_focus_avoiding_tabs(parents, up_or_down, movement, dir) | |
def handle_split_exit(parents, up_or_down, movement, dir): | |
"""Handle changing focus across split container border. | |
Keyword arguments: | |
parents -- list of parent containers of currently focused container | |
up_or_down -- True, if required movement is 'up' or 'down' | |
movement -- direction of required focus change (right, left, up, down) | |
dir -- whether we're moving forward in container list (1), or back (-1) | |
Return True, if crossing boundary was detected, and focus change was handled. | |
""" | |
orig_focused = i3.get_tree().find_focused() | |
for i, con in enumerate(parents): | |
if is_focusing_out_of_split(con, up_or_down, dir): | |
con.command('focus') | |
change_focus_avoiding_tabs(parents[i+1:], up_or_down, movement, dir) | |
if focused_client_hasnt_changed(con): | |
orig_focused.command('focus') | |
# we tried changing focus from detected split container parent, | |
# but nothing changed; likely some top-level container was | |
# focused (eg all of right hsplit, while trying to go UP | |
# from top half of the vsplit living in that right hsplit; if | |
# that top one would be a stacked container, it'd change the | |
# tabs instead, if we just used default here). | |
change_focus_avoiding_tabs(parents, up_or_down, movement, dir) | |
return True | |
return False | |
def change_focus_avoiding_tabs(parents, up_or_down, movement, dir): | |
"""Change focus while ignoring tab-only focus change. | |
Keyword arguments: | |
parents -- list of parent containers of currently focused container | |
up_or_down -- True, if required movement is 'up' or 'down' | |
movement -- direction of required focus change (right, left, up, down) | |
dir -- whether we're moving forward in container list (1), or back (-1) | |
Return void. | |
""" | |
orig_focused = i3.get_tree().find_focused() | |
for i, con in enumerate(reversed(parents)): | |
if is_wrapped_container_and_should_select(con, up_or_down): | |
con.command('focus') | |
if handle_split_exit(parents[len(parents)-i:], up_or_down, movement, dir): | |
return | |
else: | |
con.command('focus ' + movement) | |
# sanity: if focus didn't change, de-select parent: | |
if focused_client_hasnt_changed(con): | |
orig_focused.command('focus') | |
else: | |
return | |
default(movement) | |
def focused_client_hasnt_changed(previously_focused_con): | |
"""Checks if currently focused container is same as provided one. | |
Keyword arguments: | |
previously_focused_con -- container to check focus change against. | |
Return True if currently focused container is the same as provided one. | |
""" | |
return previously_focused_con.id == i3.get_tree().find_focused().id | |
def default(movement): | |
"""Executes default i3 focus command for given direction | |
Keyword arguments: | |
movement -- direction of required focus change (right, left, up, down) | |
""" | |
i3.command('focus ' + movement) | |
def is_wrapped_container_and_should_select(con, up_or_down): | |
"""Checks if given container is a wrapper one, and we're focusing in its direction. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
Return True if given container is tabbed or stacked containing more than | |
one children, and our required focus change direction matches the layout | |
(e.g. right or left for tabbed). | |
""" | |
return len(con.nodes) > 1 and is_focusing_in_wrapped_container(con, up_or_down) | |
def is_focusing_in_wrapped_container(con, up_or_down): | |
"""Checks if given container is a wrapper one, and we're focusing in its direction. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
Return True if given container is tabbed or stacked one, and our | |
required focus change direction matches the layout | |
(e.g. right or left for tabbed). | |
""" | |
return ((not up_or_down and con.layout == 'tabbed') or | |
(up_or_down and con.layout == 'stacked')) | |
def moving_in_splits(con, up_or_down): | |
"""Checks if given container is a split one, and we're focusing in its direction. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
Return True if given container is splitv or splith, and our | |
required focus change direction matches the layout | |
(e.g. up or down for splitv). | |
""" | |
return ((not up_or_down and con.layout == 'splith') or | |
(up_or_down and con.layout == 'splitv')) | |
def is_focusing_out_of_split(con, up_or_down, dir): | |
"""Checks if our focus change is crossing split container boundary. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
dir -- whether we're moving forward in container list (1), or back (-1) | |
Return True if given container is splitv or splith, our | |
required focus change direction matches the layout | |
(e.g. up or down for splitv), and the focus change would | |
exit given container. | |
""" | |
return (len(con.nodes) > 1 and len(con.parent.nodes) > 1 | |
and moving_in_splits(con, up_or_down) and exiting_container(con, dir)) | |
def focusing_sibling_split(con, up_or_down, dir): | |
"""Checks if our focus change is towards a sibling in a split container. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
dir -- whether we're moving forward in container list (1), or back (-1) | |
Return True if given container is splitv or splith, our | |
required focus change direction matches the layout | |
(e.g. up or down for splitv), and the focus change would | |
not exit given container (ie we're focusing another sibling | |
in given split container). | |
""" | |
return len(con.nodes) > 1 and moving_in_splits(con, up_or_down) and not exiting_container(con, dir) | |
def exiting_container(con, dir): | |
"""Checks if our focus change would exit given container. | |
Keyword arguments: | |
con -- container to check | |
dir -- whether we're moving forward in container list (1), or back (-1) | |
Return True if required focus change would exit the given | |
container (ie we'd exit the children nodes list array from | |
either ends). | |
""" | |
focused_idx = -1 | |
for i, node in enumerate(con.nodes): | |
if is_focused(node): | |
focused_idx = i | |
break | |
return (focused_idx == 0 and dir == -1) or (focused_idx == len(con.nodes)-1 and dir == 1) | |
def is_focused(con): | |
"""Checks if given container, or any of its children, is focused. | |
Keyword arguments: | |
con -- container to check | |
Return True if given container or any of its children, | |
recursively, is focused. | |
""" | |
if con.focused: return True | |
for n in con.nodes: | |
if is_focused(n): return True | |
return False | |
def find_node(node, id): | |
"""Search for a container by id from given node. | |
Keyword arguments: | |
node -- root node to seek required container from | |
id -- container id to search for | |
Return container whose id matches given id, or None | |
if container cannot be found under given root node. | |
""" | |
if node.id == id: return node | |
for n in node.nodes: | |
i = find_node(n, id) | |
if i != None: return i | |
return None | |
# without this, we might get focus change between tabs, instead | |
# of shifting focus _from_ tabbed container to its sibling: | |
def focus_for_sibling_movement(node): | |
"""Focus given container, if its _focus_ array is empty | |
or its layout is stacked or tabbed. | |
Without this check it would be difficult to focus sibling | |
container if currently focused one is a tabbed container, | |
in which case we might end up focusing a tab in it, instead | |
of tabbed container's sibling. | |
Keyword arguments: | |
node -- node to focus | |
Return void. | |
""" | |
if node.focus and not (node.layout == 'stacked' or node.layout == 'tabbed'): | |
focus_for_sibling_movement(find_node(node, node.focus[0])) | |
else: | |
node.command('focus') | |
def on_binding(i3_conn, e): | |
if (e.binding.command == config.command and len(e.binding.mods) == 1 and | |
e.binding.mods[0] == config.mod and e.binding.symbol in config.movements): | |
main(config.movements[e.binding.symbol]) | |
################# | |
# Entry: | |
################# | |
i3 = i3ipc.Connection() | |
i3.on('binding::run', on_binding) | |
i3.main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# Enables changing focus _in_ tabbed or stacked | |
# container without leaving the parent. | |
# | |
# Depends on i3ipc: pip3 install --upgrade i3ipc | |
# | |
# i3 config should be something like: | |
# bindsym $mod1+h nop | |
# bindsym $mod1+j nop | |
# bindsym $mod1+k nop | |
# bindsym $mod1+l nop | |
# exec --no-startup-id i3-cycle-tabs.py | |
# | |
# note the actual direction binding is configured in this script in 'movements' dict; | |
# | |
################################### | |
import i3ipc | |
class config: | |
mod = 'Mod1' | |
command = 'nop' | |
movements = { | |
'j': 'down', | |
'k': 'up', | |
'h': 'left', | |
'l': 'right' | |
} | |
def main(direction): | |
"""Run focus change logic, cycling only through tabs or stacks. | |
Keyword arguments: | |
direction -- direction of required focus change (right, left, up, down) | |
""" | |
con = i3.get_tree().find_focused() | |
up_or_down = direction == 'up' or direction == 'down' # ie !up_or_down means 'left or right' | |
while con.parent and con.parent.type == 'con': | |
con = con.parent | |
if is_focusing_in_wrapped_container(con, up_or_down): | |
children = con.nodes | |
focused_idx = -1 | |
for i, node in enumerate(children): | |
if is_focused(node): | |
focused_idx = i | |
break | |
if focused_idx == -1: break | |
dir = 1 if direction == 'right' or direction == 'down' else -1 | |
if focused_idx == 0 and dir == -1: | |
focus(children[-1]) | |
elif focused_idx == len(children)-1 and dir == 1: | |
focus(children[0]) | |
else: | |
focus(children[focused_idx + dir]) | |
return | |
i3.command('focus ' + direction) | |
def is_focused(node): | |
"""Checks if given container, or any of its children, is focused. | |
Keyword arguments: | |
node -- container to check | |
Return True if given container or any of its children, | |
recursively, is focused. | |
""" | |
if node.focused: return True | |
for n in node.nodes: | |
if is_focused(n): return True | |
return False | |
def find_node(node, id): | |
"""Search for a container by id from given node. | |
Keyword arguments: | |
node -- root node to seek required container from | |
id -- container id to search for | |
Return container whose id matches given id, or None | |
if container cannot be found under given root node. | |
""" | |
if node.id == id: return node | |
for n in node.nodes: | |
i = find_node(n, id) | |
if i != None: return i | |
return None | |
def focus(node): | |
"""Focus given node, going down through its _focus_ history. | |
Keyword arguments: | |
node -- root node to focus | |
Return void. | |
""" | |
if node.focus: | |
focus(find_node(node, node.focus[0])) | |
else: | |
node.command('focus') | |
def is_focusing_in_wrapped_container(con, up_or_down): | |
"""Check if given container is stacked of tabbed, and matches our movement. | |
Keyword arguments: | |
con -- container to check | |
up_or_down -- True, if required movement is 'up' or 'down' | |
Return True if given container is tabbed or stacked, and | |
required focus change direction matches the layout | |
(e.g. right or left for tabbed). | |
""" | |
return (len(con.nodes) > 1 and | |
( | |
(not up_or_down and con.layout == 'tabbed') or | |
(up_or_down and con.layout == 'stacked') | |
) | |
) | |
def on_binding(i3_conn, e): | |
if (e.binding.command == config.command and len(e.binding.mods) == 1 and | |
e.binding.mods[0] == config.mod and e.binding.symbol in config.movements): | |
main(config.movements[e.binding.symbol]) | |
################# | |
# Entry: | |
################# | |
i3 = i3ipc.Connection() | |
i3.on('binding::run', on_binding) | |
i3.main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment