Skip to content

Instantly share code, notes, and snippets.

@elundmark
Last active September 21, 2020 11:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elundmark/f1f5cdee35a6305af23b62847ae79386 to your computer and use it in GitHub Desktop.
Save elundmark/f1f5cdee35a6305af23b62847ae79386 to your computer and use it in GitHub Desktop.
i3wm: Switch between same program windows
#!/usr/bin/env python3
"""
i3wm Python Script: Jump fwd/backward N positions in app windows sharing the
same 'class'
Download: https://gist.github.com/elundmark/f1f5cdee35a6305af23b62847ae79386
Updated: 2020-09-21 13:28:54
"""
import sys
import json
import subprocess
# often used key names
wp = "window_properties"
wr = "window_role"
wi = "window"
si = "siblings"
foc = "focused"
cl = "class"
pos = "position"
def usage():
"""Help text"""
print("Usage: %s [ -N | +N ] [ --debug ]" % sys.argv[0], file=sys.stderr)
sys.exit(1)
def main():
"""Main program function"""
def extract_same_windows(nested_json):
"""
Flatten json object with nested "nodes" lists down to what we want
Thanks: https://bit.ly/3jZRSgh
"""
out = { "siblings": [], "focused": {} }
def flatten(x):
# see output of 'i3-msg -t get_tree' to understand this better
if isinstance(x, dict):
if "nodes" in x and isinstance(x["nodes"], list):
# traverse all "nodes" lists
for v in x["nodes"]:
# user windows has a wi integer - but we still don't
# know what the class is we're looking for, so for now
# save them all and filter them later
if wi in v and isinstance(v[wi], int):
out[si].append(v)
# Find the focused window
if foc in v and v[foc]:
out[foc][wi] = int(v[wi])
# save the focused class name
if wp in v and cl in v[wp]:
out[foc][cl] = v[wp][cl]
for a in x:
flatten(x[a])
elif isinstance(x, list):
i = 0
for a in x:
flatten(a)
i += 1
# start the flattening process
flatten(nested_json)
# filter out dict's that share the focus'd class
out[si] = [
v for v in out[si] if wp in v and cl in v[wp] and v[wp][cl] \
== out[foc][cl]
]
# my own preference: filter out any scratchpads
out[si] = [ v for v in out[si] if not ( wp in v and wr in v[wp] \
and isinstance(v[wp][wr], str) and "scratchpad" in v[wp][wr] ) ]
# save the position so we know "where" we are later
for i in range(len(out[si])):
if out[si][i][wi] == out[foc][wi]:
out[foc][pos] = i
# finished!
return out
# main vars
debugging = False
args = sys.argv[1:]
if "--debug" in args:
debugging = True
args.pop(args.index("--debug"))
if len(args) == 1:
# you can set this number to any integer, to jump 2 windows back: `-2'
# throws an error if not an integer looking string
direction = int(args[0])
else:
usage()
# execute i3-msg and get its stdout
i3_tree_stdout = subprocess.check_output(["i3-msg", "-t", "get_tree"])
# convert to object
i3_tree = json.loads(i3_tree_stdout.decode("utf8"))
if debugging:
print("i3_tree:\n\n%s" % json.dumps(i3_tree))
# extract all same windows plus get the focused one
found = extract_same_windows(i3_tree)
app_win_len = len(found[si])
# if we didn't find any position we should exit
if pos not in found[foc]:
if debugging:
print("No %s found" % pos)
sys.exit(1)
# calculate target window considering direction could be Any integer
# and we need to round robin
target_window = found[si][ ( found[foc][pos] + direction ) % app_win_len ]
if debugging:
print("\nfound:\n\n%s" % json.dumps(found))
print("\ntarget_window:\n\n%s" % json.dumps(target_window))
print("""
app_win_len: %s, found['%s']['%s']: %s, target sibling index: %s
""" % (
app_win_len,
foc,
pos,
found[foc][pos],
(found[foc][pos] + direction) % app_win_len
)
)
# focus it using i3-msg - even if it's the same window targeted
subprocess.call(["i3-msg", '[id="%s"] focus' % target_window[wi]])
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment