Skip to content

Instantly share code, notes, and snippets.

@tysonholub
Last active January 4, 2022 19:04
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tysonholub/c737d562614aa0d83add66dbec378723 to your computer and use it in GitHub Desktop.
Save tysonholub/c737d562614aa0d83add66dbec378723 to your computer and use it in GitHub Desktop.
Elementary OS: Switch windows of the same application
from subprocess import Popen, PIPE, call
import logging
import logging.handlers
import sys
import os
# NOTE: this script assumes a debian system and requires the wmctrl and xdotool packages
# sudo apt-get install wmctrl xdotool
# NOTE: To get [Alt + ` ]to register on Elementary OS requires removing the keybinding via dconf editor for switch-group/switch-group-backward
# org.gnome.desktop.wm.keybindings.switch-group: []
# org.gnome.desktop.wm.keybindings.switch-group-backward: []
# set custom hotkey for forward/backward window group scrolling (switch-group)
# python /path/to/windows.py group -f
# python /path/to/windows.py group -b
# set custom hotkey for forward/backward window scrolling (switch-windows)
# python /path/to/windows.py window -f
# python /path/to/windows.py window -b
# logging will be placed into
# /path/to/windows.py.log
LOG_FORMAT = '%(asctime)s %(levelname)s (%(pathname)s:%(lineno)d): %(message)s'
fh = logging.handlers.RotatingFileHandler(
os.path.join(os.path.dirname(__file__), 'windows.py.log'),
maxBytes= 5 * 1024 * 1024, # 5 MB
backupCount=1)
fh.setFormatter(logging.Formatter(LOG_FORMAT))
_logger = logging.getLogger('windows.py')
_logger.setLevel(logging.DEBUG)
_logger.addHandler(fh)
_logger.info('Args: {0}'.format(sys.argv))
_logger.info('looking for windows')
if 'group' in sys.argv:
method = 'group'
else:
method = 'window'
if '-b' in sys.argv:
direction = '-b'
else:
direction = '-f'
IGNORE_CLASS = [
'plank.Plank',
'wingpanel.Wingpanel'
]
try:
windows = []
for line in Popen("wmctrl -lx", shell=True, stdout=PIPE).stdout.read().split('\n'):
win = line.split()
if win and int(win[1]) > -1 and win[2] not in IGNORE_CLASS:
# wine apps might have an unusual class like "notepad++.exe.notepad++.exe"
# ultimately we would want this _class to resolve to "notepad++.exe"
_class = win[2].split('.')
windows.append(dict(
id=int(win[0], 16),
desktop=int(win[1]),
_class='.'.join(_class[(len(_class) / 2):])
))
_logger.info('windows found: {0}'.format(windows))
current_window_id = int(Popen("xdotool getactivewindow", shell=True, stdout=PIPE).stdout.read().strip())
_logger.info('current_window_id: {0}'.format(current_window_id))
current_window_class = next((x['_class'] for x in windows if x['id'] == current_window_id), None)
current_window_desktop = next((x['desktop'] for x in windows if x['id'] == current_window_id), None)
if current_window_class and method == 'group':
class_windows = [x for x in windows if x['_class'] == current_window_class and x['desktop'] == current_window_desktop]
_logger.info('Class windows: {0}'.format(class_windows))
if class_windows:
if direction == '-f':
index = 0
for x in xrange(len(class_windows)):
if class_windows[x]['id'] == current_window_id:
index = x
_logger.info('Found window index: {0}'.format(index))
break
if index + 1 >= len(class_windows):
index = 0
else:
index = index + 1
elif direction == '-b':
index = len(class_windows) - 1
for x in xrange(len(class_windows) -1, -1, -1):
if class_windows[x]['id'] == current_window_id:
index = x
_logger.info('Found window index: {0}'.format(index))
break
if index <= 0:
index = len(class_windows) - 1
else:
index = index - 1
_logger.info('Calling class window index {0}'.format(index))
call(['wmctrl', '-i', '-a', str(class_windows[index]['id'])])
else:
_logger.info('class_windows not found')
elif method == 'window':
desktop_windows = [x for x in windows if x['desktop'] == current_window_desktop]
if desktop_windows:
if direction == '-f':
index = 0
for x in xrange(len(desktop_windows)):
if desktop_windows[x]['id'] == current_window_id:
index = x
_logger.info('Found window index: {0}'.format(index))
break
if index + 1 >= len(desktop_windows):
index = 0
else:
index = index + 1
elif direction == '-b':
index = len(desktop_windows) - 1
for x in xrange(len(desktop_windows) -1, -1, -1):
if desktop_windows[x]['id'] == current_window_id:
index = x
_logger.info('Found window index: {0}'.format(index))
break
if index <= 0:
index = len(desktop_windows) - 1
else:
index = index - 1
_logger.info('Calling window {0}'.format(desktop_windows[index]))
call(['wmctrl', '-i', '-a', str(desktop_windows[index]['id'])])
else:
_logger.info('windows not found')
else:
_logger.info('current_window_class not found')
except Exception as e:
_logger.exception("windows.py blew up")
@dmocek
Copy link

dmocek commented Mar 12, 2021

Hi,

I posted this question on the elementaryOS StackExchange (https://elementaryos.stackexchange.com/questions/26650/is-there-a-way-to-create-a-keyboard-shortcut-to-launch-an-application-and-cycle). Since you're part way there, I'll post the question here too.

I've been using a GNOME desktop for quite a while and the GNOME Run or Raise extension (https://github.com/CZ-NIC/run-or-raise). It lets me define a keyboard shortcut (e.g. Alt+t) which will:

Launch the GNOME Terminal if it isn't running.
Give the focus to the running GNOME Terminal if it is running but not the app in focus.
Once the GNOME Terminal is focused, continuing to hit the 't' key will cycle through all of the GNOME Terminal windows if multiple windows are open.
I have shortcuts defined for all of my most often used apps and find it is a very efficient way of switching between the apps I use most often.

My questions are:

  • Does eOS support this functionality natively? If so, how can I get it to work?
  • If not, is there another way to get this functionality? I did find this Python program (https://gist.github.com/tysonholub/c737d562614aa0d83add66dbec378723) which switches between an app's open windows, but this isn't quite what I want. I'm not sure if I can duplicate the 'Run or Raise' functionality using Python.

@tysonholub
Copy link
Author

Sorry, I don't use Elementary OS anymore. Switched to PopOS

@gazambuja
Copy link

Here you have the Python3 compatible version of the file:

https://gist.github.com/gazambuja/addc090a2b1df322f687e90e705510c8

@neogeographica
Copy link

Thanks, this resolved a longstanding peeve!

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