Skip to content

Instantly share code, notes, and snippets.

@SidharthArya
Last active October 25, 2024 18:11
Show Gist options
  • Save SidharthArya/f4d80c246793eb61be0ae928c9184406 to your computer and use it in GitHub Desktop.
Save SidharthArya/f4d80c246793eb61be0ae928c9184406 to your computer and use it in GitHub Desktop.
Sway Windows Manager Alt Tab behavior
#!/usr/bin/env python3
import sys
import json
import subprocess
direction=bool(sys.argv[1] == 't' or sys.argv[1] == 'T')
swaymsg = subprocess.run(['swaymsg', '-t', 'get_tree'], stdout=subprocess.PIPE)
data = json.loads(swaymsg.stdout)
current = data["nodes"][1]["current_workspace"]
workspace = int(data["nodes"][1]["current_workspace"])-1
roi = data["nodes"][1]["nodes"][workspace]
temp = roi
windows = list()
def getNextWindow():
if focus < len(windows) - 1:
return focus+1
else:
return 0
def getPrevWindow():
if focus > 0:
return focus-1
else:
return len(windows)-1
def makelist(temp):
for nodes in "floating_nodes", "nodes":
for i in range(len(temp[nodes])):
if temp[nodes][i]["name"] is None:
makelist(temp[nodes][i])
else:
windows.append(temp[nodes][i])
def focused(temp_win):
for i in range(len(temp_win)):
if temp_win[i]["focused"] == True:
return i
makelist(temp)
# print(len(windows))
focus = focused(windows)
if str(sys.argv[1]) == 't' or str(sys.argv[1]) == 'T':
print(windows[getNextWindow()]["id"])
else:
print(windows[getPrevWindow()]["id"])
bindsym $mod+tab exec swaymsg [con_id=$(swaymsg -t get_tree | ~/.config/sway/alttab t)] focus
bindsym $mod+shift+tab exec swaymsg [con_id=$(swaymsg -t get_tree | ~/.config/sway/alttab f)] focus
@SidharthArya
Copy link
Author

So this is to bring the alttab behavior to sway. Just put the alttab file in your ~/.config/sway/alttab file.
Run chmod +x ~/.config/sway/alttab.
and add the provided the provided bindsym in your sway config file.

PS: I am pretty certain that this should work with i3 as well.

@alejor
Copy link

alejor commented Jan 11, 2020

Hi, thanks for this script. I was looking for this functionality for a long time.
I found some cases it doesn't work, so I consider to share you some fixes I made. Thanks

#!/usr/bin/env python3

import sys
import json
import subprocess

direction=bool(sys.argv[1] == 'next')
swaymsg = subprocess.run(['swaymsg', '-t', 'get_tree'], stdout=subprocess.PIPE)
data = json.loads(swaymsg.stdout)
current = data["nodes"][1]["current_workspace"]

for i in range(len(data["nodes"][1]["nodes"])):
    if data["nodes"][1]["nodes"][i]["name"] == current :
        workspace = i
        break

roi = data["nodes"][1]["nodes"][workspace]
temp = roi
windows = list()

def getNextWindow():

    if focus < len(windows) - 1:
        return focus+1
    else:
        return 0

def getPrevWindow():

    if focus > 0:
        return focus-1
    else:
        return len(windows)-1

def makelist(temp):

    for i in range(len(temp["nodes"])):
        if temp["nodes"][i]["name"] is None:
           makelist(temp["nodes"][i])
        else:
           windows.append(temp["nodes"][i])

def focused(temp_win):

    for i in range(len(temp_win)):
        if temp_win[i]["focused"] == True:
           return i
    
    return 9;

makelist(temp)
focus = focused(windows)

if direction:
    attr = "[con_id="+str(windows[getNextWindow()]["id"])+"]"
else:
    attr = "[con_id="+str(windows[getPrevWindow()]["id"])+"]"

sway = subprocess.run(['swaymsg', attr, 'focus'])
sys.exit(sway.returncode)

And on sway config

bindsym Mod1+tab exec ~/.config/sway/alttab next
bindsym Mod1+shift+tab exec ~/.config/sway/alttab prev

Thanks again. Have a nice day,
Alejandro

@alejor
Copy link

alejor commented Jan 11, 2020

I struggled by sometimes when it doesn't work but that was because I use zsh and it parses [ ] on a 'special' way... so I reworked the way it launches swaymsg in that debug process. I also found that script doesn't work when workspaces aren't continuous, I mean, not 1,2,3,4,5,6,7 but like only 2,5,7 workspaces active. In this case your script don't switch the focus. Thanks,

@SidharthArya
Copy link
Author

I struggled by sometimes when it doesn't work but that was because I use zsh and it parses [ ] on a 'special' way... so I reworked the way it launches swaymsg in that debug process. I also found that script doesn't work when workspaces aren't continuous, I mean, not 1,2,3,4,5,6,7 but like only 2,5,7 workspaces active. In this case your script don't switch the focus. Thanks,

I will look into these issues soon enough. if you are facing zsh issues i would suggest using bash -c in your exec command. Thank you for your suggestions, i will merge them once i figure out the issues you are facing and why. .

@gorsheninmv
Copy link

Hi guys! Your implementations don`t work with floating windows. I`m quite newbie in python so that my desicion isn`t perfect. But I would like to share it with you. I just modified makelist function. Hope it helps you:

def makelist(temp):

    for nodes in "floating_nodes", "nodes":
        for i in range(len(temp[nodes])):
            if temp[nodes][i]["name"] is None:
               makelist(temp[nodes][i])
            else:
               windows.append(temp[nodes][i])

Thanks for the script!

@alejor
Copy link

alejor commented Feb 13, 2020

Great improvement, gorsheninmv, thanks :)

@SidharthArya
Copy link
Author

@gorsheninmv , thank you for the improvement.

@curiositry
Copy link

curiositry commented May 15, 2020

I have been successfully using @alejor's version for a while. Now, after an update to sway I believe, it is giving an error for line 11:

Traceback (most recent call last):
  File "/home/[user]/.config/sway/alttab", line 11, in <module>
    current = data["nodes"][1]["current_workspace"]
KeyError: 'nodes'

@SidharthArya
Copy link
Author

@curiositry, @alejor 's version work fine. I have just tested both. Feel free to post again if it doesn't work.

@alejor
Copy link

alejor commented May 15, 2020

@curiositry, I just updated my sway to latest commit and still it works fine. So we would know more details to give you any advice, :)

@curiositry
Copy link

Thanks for your replies @SidharthArya and @alejor. I have updated to @SidharthArya's latest version, and it is now working. (Curiously, it didn't work until I rebooted; reloading Sway didn't suffice.)

@SidharthArya
Copy link
Author

@curiositry Glad to hear it worked.

Copy link

ghost commented Apr 21, 2021

Adding multi-monitor support to the above contributions:

#!/usr/bin/env python3

import sys
import json
import subprocess

direction=bool(sys.argv[1] == 'next')
swaymsg = subprocess.run(['swaymsg', '-t', 'get_tree'], stdout=subprocess.PIPE)
data = json.loads(swaymsg.stdout)

def setup():
    def dig(nodes):
        if nodes["focused"]:
            return True

        for node_type in "nodes", "floating_nodes":
                if node_type in nodes:
                    for node in nodes[node_type]:
                        if node["focused"] or dig(node):
                            return True

        return False

    for monitor in data["nodes"]:
        for workspace in monitor["nodes"]:

            if workspace["focused"] or dig(workspace):
                return workspace

workspace = setup()
temp = workspace
windows = list()

def getNextWindow():

    if focus < len(windows) - 1:
        return focus+1
    else:
        return 0

def getPrevWindow():

    if focus > 0:
        return focus-1
    else:
        return len(windows)-1

def makelist(temp):
    for nodes in "floating_nodes", "nodes":
        for i in range(len(temp[nodes])):
            if temp[nodes][i]["name"] is None:
               makelist(temp[nodes][i])
            else:
               windows.append(temp[nodes][i])


def focused(temp_win):

    for i in range(len(temp_win)):
        if temp_win[i]["focused"] == True:
           return i
    
    return 9

makelist(temp)
focus = focused(windows)

if direction:
    attr = "[con_id="+str(windows[getNextWindow()]["id"])+"]"
else:
    attr = "[con_id="+str(windows[getPrevWindow()]["id"])+"]"

sway = subprocess.run(['swaymsg', attr, 'focus'])
sys.exit(sway.returncode)

Copy link

ghost commented Apr 21, 2021

And here is an extension for a multi-monitor aware win+tab to flip workspaces open on a monitor:

#!/usr/bin/env python3
# Original: https://gist.github.com/SidharthArya/f4d80c246793eb61be0ae928c9184406

import sys
import json
import subprocess

target_windows = bool(sys.argv[1] == 'window')
direction=bool(sys.argv[2] == 'next')
swaymsg = subprocess.run(['swaymsg', '-t', 'get_tree'], stdout=subprocess.PIPE)
data = json.loads(swaymsg.stdout)

def setup():
    def dig(nodes):
        if nodes["focused"]:
            return True

        for node_type in "nodes", "floating_nodes":
                if node_type in nodes:
                    for node in nodes[node_type]:
                        if node["focused"] or dig(node):
                            return True

        return False

    for monitor in data["nodes"]:
        for workspace in monitor["nodes"]:
            if workspace["focused"] or dig(workspace):
                return monitor, workspace

monitor, workspace = setup()

def getNext(target_list, focus):

    if focus < len(target_list) - 1:
        return focus+1
    else:
        return 0

def getPrev(target_list, focus):

    if focus > 0:
        return focus-1
    else:
        return len(target_list)-1

def makelist_windows(temp, target_list = []):
    for nodes in "floating_nodes", "nodes":
        for i in range(len(temp[nodes])):
            if temp[nodes][i]["name"] is None:
               makelist_windows(temp[nodes][i], target_list)
            else:
               target_list.append(temp[nodes][i])

    return target_list

def makelist_workspaces(workspaces, target_list = []):
    for workspace in monitor["nodes"]:
        target_list.append(workspace)
    
    return target_list

def focused_window(temp_win):
    for i in range(len(temp_win)):
        if temp_win[i]["focused"] == True:
           return i

def focused_workspace(workspaces, current_workspace):
    for i in range(len(workspaces)):
        if workspaces[i]["name"] == current_workspace["name"]:
           return i

if target_windows:
    target_list = makelist_windows(workspace)
    focus = focused_window(target_list)

    if direction:
        attr = "[con_id="+str(target_list[getNext(target_list, focus)]["id"])+"]"
    else:
        attr = "[con_id="+str(target_list[getPrev(target_list, focus)]["id"])+"]"

    sway = subprocess.run(['swaymsg', attr, 'focus'])
    sys.exit(sway.returncode)

else:
    target_list = makelist_workspaces(monitor)
    if len(target_list) > 1:
        focus = focused_workspace(target_list, workspace)

        if direction:
            attr = target_list[getNext(target_list, focus)]["name"]
        else:
            attr = target_list[getPrev(target_list, focus)]["name"]

        sway = subprocess.run(['swaymsg', 'workspace', attr])
        sys.exit(sway.returncode)

@RayZ0rr
Copy link

RayZ0rr commented Apr 27, 2022

Will this work in i3?

@josch
Copy link

josch commented Dec 17, 2022

There is quite some room for simplification by using the i3ipc python module:

#!/usr/bin/env python3
import i3ipc
import sys
def main():
    sway = i3ipc.Connection()
    for workspace in [ws for output in sway.get_tree().nodes for ws in output.nodes]:
        focus = workspace.find_focused()
        if focus is None:
            continue
        descendants = [d for d in workspace.descendants() if d.name is not None]
        focus = descendants.index(focus)
        focus = (focus + 1 if sys.argv[1] == "next" else focus - 1) % len(descendants)
        sway.command(f"[con_id={descendants[focus].id}] focus")
        sys.exit()
if __name__ == '__main__':
    main()

@aloispichler
Copy link

Does it work on hyprland as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment