Skip to content

Instantly share code, notes, and snippets.

@laur89
Last active July 2, 2018 23:25
Show Gist options
  • Save laur89/ea7d92ef6d3f1421b1db8f998db76fde to your computer and use it in GitHub Desktop.
Save laur89/ea7d92ef6d3f1421b1db8f998db76fde to your computer and use it in GitHub Desktop.
i3wm focus changer helper scripts
#!/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()
#!/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