|
|
|
@ -18,7 +18,7 @@ import wx
|
|
|
|
|
|
|
|
|
|
class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
"""Frame for the swayswitcher"""
|
|
|
|
|
def __init__(self, *args, **kw): # pylint: disable=unused-argument,too-many-locals,too-many-branches,too-many-statements
|
|
|
|
|
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
|
|
|
|
@ -26,7 +26,27 @@ class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
|
|
|
|
|
# Some xdg data
|
|
|
|
|
self.home = os.environ.get('HOME')
|
|
|
|
|
self.base_dirs = []
|
|
|
|
|
self.base_dirs = self.get_base_dirs()
|
|
|
|
|
# get windows from sway
|
|
|
|
|
windows = get_windows()
|
|
|
|
|
|
|
|
|
|
# Get config
|
|
|
|
|
self.label_len = 20
|
|
|
|
|
self.icon_size = 128
|
|
|
|
|
|
|
|
|
|
self.icon_dirs = self.get_icon_dirs()
|
|
|
|
|
|
|
|
|
|
# and create a sizer to manage the layout of child widgets
|
|
|
|
|
x_and_y = int(math.sqrt(len(windows)) + 0.5)
|
|
|
|
|
self.sizer = wx.GridSizer(x_and_y) # pylint: disable=no-member
|
|
|
|
|
self.pnl.SetSizer(self.sizer)
|
|
|
|
|
self.set_buttons(windows)
|
|
|
|
|
self.sizer.Fit(self)
|
|
|
|
|
self.sizer.Layout()
|
|
|
|
|
|
|
|
|
|
def get_base_dirs(self):
|
|
|
|
|
"""We want to follow the XDG standard if possible"""
|
|
|
|
|
base_dirs = []
|
|
|
|
|
xdg_dirs = os.environ.get('XDG_DATA_DIRS')
|
|
|
|
|
pos_dirs = []
|
|
|
|
|
if xdg_dirs:
|
|
|
|
@ -39,72 +59,8 @@ class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
]
|
|
|
|
|
for pos_dir in pos_dirs:
|
|
|
|
|
if os.path.exists(pos_dir):
|
|
|
|
|
self.base_dirs.append(pos_dir)
|
|
|
|
|
|
|
|
|
|
# get windows from sway
|
|
|
|
|
windows = get_windows()
|
|
|
|
|
label_len = 20
|
|
|
|
|
|
|
|
|
|
# Icon size
|
|
|
|
|
self.icon_size = 128
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
inner_sizer = wx.BoxSizer(orient=wx.VERTICAL) # pylint: disable=no-member
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
# This is setting up an inner sizer with a static text label and an image icon
|
|
|
|
|
label = "ws" + str(window['workspace']) + ": " + label
|
|
|
|
|
winid = window['id']
|
|
|
|
|
size = wx.Window.GetFont(self).GetPointSize() * label_len # pylint: disable=no-member
|
|
|
|
|
command = get_command(window['pid'])
|
|
|
|
|
desktop_file = self.get_desktop_file(command)
|
|
|
|
|
icon = self.get_icon(desktop_file) # pylint: disable=no-member
|
|
|
|
|
if icon:
|
|
|
|
|
if icon.endswith(".svg"):
|
|
|
|
|
svgpng = cairosvg.svg2png(
|
|
|
|
|
bytestring=open(icon).read().encode('utf-8'))
|
|
|
|
|
image = wx.Image(BytesIO(svgpng), wx.BITMAP_TYPE_PNG) # pylint: disable=no-member
|
|
|
|
|
else:
|
|
|
|
|
unscaled_bitmap = wx.Bitmap(self.get_icon(desktop_file)) # pylint: disable=no-member
|
|
|
|
|
image = unscaled_bitmap.ConvertToImage()
|
|
|
|
|
image = image.Scale(self.icon_size, self.icon_size,
|
|
|
|
|
wx.IMAGE_QUALITY_HIGH) # pylint: disable=no-member
|
|
|
|
|
bitmap = wx.Bitmap(image) # pylint: disable=no-member
|
|
|
|
|
else:
|
|
|
|
|
bitmap = wx.Bitmap() # pylint: disable=no-member
|
|
|
|
|
btn = wx.BitmapButton( # pylint: disable=no-member
|
|
|
|
|
self.pnl,
|
|
|
|
|
id=winid,
|
|
|
|
|
bitmap=bitmap,
|
|
|
|
|
size=wx.Size(size, size)) # pylint: disable=no-member
|
|
|
|
|
btn.Bind(
|
|
|
|
|
wx.EVT_BUTTON,
|
|
|
|
|
lambda event, mwinid=winid: self.switch_window(event, mwinid))
|
|
|
|
|
# Set up esc keybinding
|
|
|
|
|
self.Bind(wx.EVT_CHAR_HOOK, lambda event: self.on_key_press(event)) # pylint: disable=unnecessary-lambda
|
|
|
|
|
statictext = wx.StaticText(self.pnl, -1, label) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Add(statictext, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Add(btn, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Fit(self)
|
|
|
|
|
inner_sizer.Layout()
|
|
|
|
|
sizer.Add(inner_sizer, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
sizer.Fit(self)
|
|
|
|
|
sizer.Layout()
|
|
|
|
|
base_dirs.append(pos_dir)
|
|
|
|
|
return base_dirs
|
|
|
|
|
|
|
|
|
|
def get_desktop_file(self, command):
|
|
|
|
|
"""From here we return first dektopfile"""
|
|
|
|
@ -114,29 +70,38 @@ class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
if os.path.exists(directory):
|
|
|
|
|
command = "grep -s -l {} {}/*.desktop".format(
|
|
|
|
|
command, directory)
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
for data in [i for i in process.communicate() if i]:
|
|
|
|
|
for data in [i for i in run_command(command) if i]:
|
|
|
|
|
for result in data.decode().split('\n'):
|
|
|
|
|
desktop_file = result
|
|
|
|
|
break
|
|
|
|
|
return desktop_file
|
|
|
|
|
|
|
|
|
|
def get_icon(self, desktop_file): # pylint: disable=too-many-branches
|
|
|
|
|
""" Find icon from a desktop file"""
|
|
|
|
|
command = "grep Icon= {}".format(desktop_file)
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
for data in [i.rstrip() for i in process.communicate() if i]:
|
|
|
|
|
try:
|
|
|
|
|
icon_name = data.decode().split('=')[1]
|
|
|
|
|
except IndexError:
|
|
|
|
|
return ""
|
|
|
|
|
# Find icon dirs
|
|
|
|
|
def get_icon_bitmap(self, icon):
|
|
|
|
|
"""Create a bitmap with right size from svg or png"""
|
|
|
|
|
if icon:
|
|
|
|
|
if icon.endswith(".svg"):
|
|
|
|
|
svgpng = cairosvg.svg2png(
|
|
|
|
|
bytestring=open(icon).read().encode('utf-8'))
|
|
|
|
|
image = wx.Image(BytesIO(svgpng), wx.BITMAP_TYPE_PNG) # pylint: disable=no-member
|
|
|
|
|
else:
|
|
|
|
|
unscaled_bitmap = wx.Bitmap(icon) # pylint: disable=no-member
|
|
|
|
|
image = unscaled_bitmap.ConvertToImage()
|
|
|
|
|
image = image.Scale(self.icon_size, self.icon_size,
|
|
|
|
|
wx.IMAGE_QUALITY_HIGH) # pylint: disable=no-member
|
|
|
|
|
bitmap = wx.Bitmap(image) # pylint: disable=no-member
|
|
|
|
|
else:
|
|
|
|
|
bitmap = wx.Bitmap() # pylint: disable=no-member
|
|
|
|
|
return bitmap
|
|
|
|
|
|
|
|
|
|
def get_icon_path(self, desktop_file):
|
|
|
|
|
"""Glue function to get icon path"""
|
|
|
|
|
icon_name = get_icon_name(desktop_file)
|
|
|
|
|
pos_icons = self.get_pos_icons(icon_name)
|
|
|
|
|
icon_path = self.get_right_size_icon(pos_icons)
|
|
|
|
|
return icon_path
|
|
|
|
|
|
|
|
|
|
def get_icon_dirs(self):
|
|
|
|
|
"""Find icon dirs"""
|
|
|
|
|
icon_dirs = []
|
|
|
|
|
for base_dir in [
|
|
|
|
|
directory for directory in self.base_dirs if directory
|
|
|
|
@ -145,28 +110,48 @@ class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
icon_dirs.append(directory)
|
|
|
|
|
icon_dirs.append(self.home + "/.icons")
|
|
|
|
|
icon_dirs.append("/usr/share/pixmaps")
|
|
|
|
|
|
|
|
|
|
return icon_dirs
|
|
|
|
|
|
|
|
|
|
def get_label(self, window):
|
|
|
|
|
"""Construct the label for the window"""
|
|
|
|
|
try:
|
|
|
|
|
label = window['window_properties']['class']
|
|
|
|
|
except KeyError:
|
|
|
|
|
try:
|
|
|
|
|
label = window['app_id']
|
|
|
|
|
if len(label) < self.label_len:
|
|
|
|
|
label = label + ':' + window['name'][:self.label_len -
|
|
|
|
|
len(label)]
|
|
|
|
|
except KeyError:
|
|
|
|
|
label = window['name']
|
|
|
|
|
if len(label) > self.label_len:
|
|
|
|
|
label = label[:self.label_len]
|
|
|
|
|
|
|
|
|
|
label = "ws" + str(window['workspace']) + ": " + label
|
|
|
|
|
return label
|
|
|
|
|
|
|
|
|
|
def get_pos_icons(self, icon_name):
|
|
|
|
|
"""Collect possible icons, prefer svg"""
|
|
|
|
|
possible_icons = []
|
|
|
|
|
for icon_dir in [directory for directory in icon_dirs if directory]:
|
|
|
|
|
for icon_dir in [
|
|
|
|
|
directory for directory in self.icon_dirs if directory
|
|
|
|
|
]:
|
|
|
|
|
if os.path.exists(icon_dir):
|
|
|
|
|
command = "find {} -name *{}.svg".format(icon_dir, icon_name)
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
result = process.communicate()[0].split()
|
|
|
|
|
result = run_command(command)[0].split()
|
|
|
|
|
if not result:
|
|
|
|
|
command = "find {} -name *{}.png".format(
|
|
|
|
|
icon_dir, icon_name)
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
result = process.communicate()[0].split()
|
|
|
|
|
result = run_command(command)[0].split()
|
|
|
|
|
for data in result:
|
|
|
|
|
icon_cand = data.decode()
|
|
|
|
|
if icon_cand not in possible_icons:
|
|
|
|
|
possible_icons.append(icon_cand)
|
|
|
|
|
# Try to find prefered size
|
|
|
|
|
return possible_icons
|
|
|
|
|
|
|
|
|
|
def get_right_size_icon(self, possible_icons):
|
|
|
|
|
"""Try to find prefered size"""
|
|
|
|
|
icon = None
|
|
|
|
|
for pos_icon in possible_icons:
|
|
|
|
|
if pos_icon.endswith(".svg"):
|
|
|
|
@ -188,37 +173,69 @@ class SwaySwitch(wx.Frame): # pylint: disable=no-member
|
|
|
|
|
if keycode == wx.WXK_ESCAPE: # pylint: disable=no-member
|
|
|
|
|
# enter normal mode and exit
|
|
|
|
|
command = 'swaymsg mode "default"'
|
|
|
|
|
subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
|
|
|
|
|
run_command(command)
|
|
|
|
|
self.Close(True)
|
|
|
|
|
else:
|
|
|
|
|
event.Skip(True)
|
|
|
|
|
|
|
|
|
|
def set_buttons(self, windows):
|
|
|
|
|
"""Loop over windows and create buttons for them"""
|
|
|
|
|
for window in windows:
|
|
|
|
|
# This is setting up an inner sizer with a static text label and an image icon
|
|
|
|
|
inner_sizer = wx.BoxSizer(orient=wx.VERTICAL) # pylint: disable=no-member
|
|
|
|
|
label = self.get_label(window)
|
|
|
|
|
winid = window['id']
|
|
|
|
|
size = wx.Window.GetFont(self).GetPointSize() * self.label_len # pylint: disable=no-member
|
|
|
|
|
command = get_command(window['pid'])
|
|
|
|
|
desktop_file = self.get_desktop_file(command)
|
|
|
|
|
icon_path = self.get_icon_path(desktop_file)
|
|
|
|
|
bitmap = self.get_icon_bitmap(icon_path)
|
|
|
|
|
btn = wx.BitmapButton( # pylint: disable=no-member
|
|
|
|
|
self.pnl,
|
|
|
|
|
id=winid,
|
|
|
|
|
bitmap=bitmap,
|
|
|
|
|
size=wx.Size(size, size)) # pylint: disable=no-member
|
|
|
|
|
btn.Bind(
|
|
|
|
|
wx.EVT_BUTTON,
|
|
|
|
|
lambda event, mwinid=winid: self.switch_window(event, mwinid))
|
|
|
|
|
# Set up esc keybinding
|
|
|
|
|
self.Bind(wx.EVT_CHAR_HOOK, lambda event: self.on_key_press(event)) # pylint: disable=unnecessary-lambda
|
|
|
|
|
statictext = wx.StaticText(self.pnl, -1, label) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Add(statictext, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Add(btn, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
inner_sizer.Fit(self)
|
|
|
|
|
inner_sizer.Layout()
|
|
|
|
|
self.sizer.Add(inner_sizer, 0, wx.ALIGN_CENTER) # pylint: disable=no-member
|
|
|
|
|
|
|
|
|
|
def switch_window(self, event, winid): # pylint: disable=unused-argument
|
|
|
|
|
"""Switches the focus to the given id and enter normalmode"""
|
|
|
|
|
command = 'swaymsg [con_id={}] focus, mode "default"'.format(winid)
|
|
|
|
|
subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
|
|
|
|
|
run_command(command)
|
|
|
|
|
self.Close(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_command(pid):
|
|
|
|
|
"""Returns a list of all json window objects"""
|
|
|
|
|
command = "ps h c -p {} -o command".format(pid)
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
result = process.communicate()[0].rstrip().decode()
|
|
|
|
|
result = run_command(command)[0].rstrip().decode()
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_icon_name(desktop_file):
|
|
|
|
|
""" Find icon from a desktop file"""
|
|
|
|
|
command = "grep Icon= {}".format(desktop_file)
|
|
|
|
|
for data in [i.rstrip() for i in run_command(command) if i]:
|
|
|
|
|
try:
|
|
|
|
|
icon_name = data.decode().split('=')[1]
|
|
|
|
|
return icon_name
|
|
|
|
|
except IndexError:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
data = json.loads(run_command(command)[0])
|
|
|
|
|
|
|
|
|
|
# Select outputs that are active
|
|
|
|
|
windows = []
|
|
|
|
@ -260,6 +277,16 @@ def extract_nodes_iterative(workspace):
|
|
|
|
|
return all_nodes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_command(command):
|
|
|
|
|
"""Run a command on system and capture result"""
|
|
|
|
|
process = subprocess.Popen(command,
|
|
|
|
|
shell=True,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
result = process.communicate()
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Entry point
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
# When this module is run (not imported) then create the app, the
|
|
|
|
|