#!/bin/python3 """A simple windowswitcher for sway""" # switch_window, get_windows and extract_nodes_iterative is derived from # https://github.com/tobiaspc/wofi-scripts Copyright (c) 2020 Tobi which # is covered by the MIT License. However this program is licenced under # the GPLv3+ Copyright (c) 2020 Micke Nordin. See LICENSE file for details. import json import math import subprocess import wx class SwaySwitch(wx.Frame): # pylint: disable=no-member """Frame for the swayswitcher""" def __init__(self, *args, **kw): # pylint: disable=unused-argument """Constructor""" wx.Frame.__init__(self, None, title="", style=wx.STAY_ON_TOP) # pylint: disable=no-member # create a panel in the frame self.pnl = wx.Panel(self) # pylint: disable=no-member # get windows from sway windows = get_windows() label_len = 20 # and create a sizer to manage the layout of child widgets x_and_y = int(math.sqrt(len(windows)) + 0.5) sizer = wx.GridSizer(x_and_y) # pylint: disable=no-member self.pnl.SetSizer(sizer) for window in windows: try: label = window['window_properties']['class'] except KeyError: try: label = window['app_id'] if len(label) < label_len: label = label + ':' + window['name'][:label_len - len(label)] except KeyError: label = window['name'] if len(label) > label_len: label = label[:label_len] winid = window['id'] size = wx.Window.GetFont(self).GetPointSize() * label_len # pylint: disable=no-member btn = wx.Button( # pylint: disable=no-member parent=self.pnl, id=winid, label=label, size=wx.Size(size, size)) # pylint: disable=no-member btn.Bind( wx.EVT_BUTTON, lambda event, mwinid=winid: self.switch_window(event, mwinid)) sizer.Add(btn, 0, wx.ALIGN_CENTER) # pylint: disable=no-member # Set up esc keybinding self.Bind(wx.EVT_CHAR_HOOK, lambda event: self.on_key_press(event)) # pylint: disable=unnecessary-lambda sizer.Fit(self) sizer.Layout() def on_key_press(self, event): """Intercept esc key press""" keycode = event.GetUnicodeKey() if keycode == wx.WXK_ESCAPE: # pylint: disable=no-member self.Close(True) else: event.Skip(True) def switch_window(self, event, winid): # pylint: disable=unused-argument """Switches the focus to the given id""" command = "swaymsg [con_id={}] focus".format(winid) subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) self.Close(True) def get_windows(): """Returns a list of all json window objects""" command = "swaymsg -t get_tree" process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = json.loads(process.communicate()[0]) # Select outputs that are active windows = [] for output in data['nodes']: # The scratchpad (under __i3) is not supported if output.get('name') != '__i3' and output.get('type') == 'output': workspaces = output.get('nodes') for workspace in workspaces: if workspace.get('type') == 'workspace': windows += extract_nodes_iterative(workspace) return windows def extract_nodes_iterative(workspace): """Extracts all windows from a sway workspace json object""" all_nodes = [] floating_nodes = workspace.get('floating_nodes') for floating_node in floating_nodes: all_nodes.append(floating_node) nodes = workspace.get('nodes') for node in nodes: # Leaf node if len(node.get('nodes')) == 0: all_nodes.append(node) # Nested node, handled iterative else: for inner_node in node.get('nodes'): nodes.append(inner_node) return all_nodes # Entry point if __name__ == "__main__": # When this module is run (not imported) then create the app, the # frame, show it, and start the event loop. app = wx.App() frm = SwaySwitch(None, title="") frm.Show() app.MainLoop()