Textual Alt Tab
A late home-cooked one:
In action

How to set up
The setup exists of two tiny scripts, to be saved into one and the same directory:
script 1
#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version('Wnck', '3.0')
from gi.repository import Gtk, Wnck, Gdk
import subprocess
css_data = """
.activestyle {
background-color: grey;
color: white;
border-width: 1px;
border-radius: 0px;
border-color: white;
}
.defaultstyle {
border-width: 0px;
color: black;
background-color: white;
}
"""
class AltTabStuff(Gtk.Window):
def __init__(self):
# css
self.provider = Gtk.CssProvider.new()
self.provider.load_from_data(css_data.encode())
Gtk.Window.__init__(
self, title="AltTab replacement"
)
self.curr_index = 0
self.connect('key-press-event', self.get_key)
self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
self.set_decorated(False)
buttongrid = Gtk.Grid()
self.add(buttongrid)
self.connect("delete_event", Gtk.main_quit)
wins = get_winlist()
self.buttonindex = 0
self.buttonsets = []
index = 0
for w in wins:
button = Gtk.Button("\t" + w.get_name())
button.set_relief(Gtk.ReliefStyle.NONE)
buttongrid.attach(button, 0, index, 1, 1)
index = index + 1
button.connect("clicked", raise_window, w)
self.buttonsets.append([button, w])
self.set_focus()
self.show_all()
Gtk.main()
def set_focus(self):
for b in self.buttonsets:
button = b[0]
self.set_style(button, active=False)
newactive = self.buttonsets[self.buttonindex][0]
self.set_style(newactive, active=True)
n_buttons = len(self.buttonsets)
self.buttonindex = self.buttonindex + 1
if self.buttonindex >= n_buttons:
self.buttonindex = 0
def set_style(self, button, active):
st_cont = button.get_style_context()
if active:
st_cont.add_class("activestyle")
st_cont.remove_class("defaultstyle")
else:
st_cont.remove_class("activestyle")
st_cont.add_class("defaultstyle")
Gtk.StyleContext.add_provider(
st_cont,
self.provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
def get_key(self, val1, val2):
keyname = Gdk.keyval_name(val2.keyval)
if keyname == "Tab":
self.set_focus()
elif keyname == "Alt_L":
window = self.buttonsets[self.buttonindex-1][1]
button = self.buttonsets[self.buttonindex-1][0]
raise_window(button, window)
elif keyname == "Escape":
Gtk.main_quit()
def raise_window(button, window):
subprocess.Popen(["wmctrl", "-ia", str(window.get_xid())])
Gtk.main_quit()
def check_windowtype(window):
try:
return "WNCK_WINDOW_NORMAL" in str(
window.get_window_type()
)
except AttributeError:
pass
def get_winlist(scr=None):
"""
"""
if not scr:
scr = Wnck.Screen.get_default()
scr.force_update()
windows = [w for w in scr.get_windows() if check_windowtype(w)]
return windows
AltTabStuff()
script 2
#!/bin/bash
dr=`dirname $0`
f=$dr'/alttab_runner'
if ! pgrep -f $f
then
$f
else
echo "runs"
fi
Do the following steps:
Make sure both Wnck
and wmctrl
are installed:
sudo apt install python3-gi gir1.2-wnck-3.0 wmctrl
Save script 1 into an empty file as (exactly) alttab_runner
, script 2 as (exactly) alttab_alternative
. make both scripts executable
Disable the existing Alt-Tab:
gsettings set org.gnome.desktop.wm.keybindings switch-applications '[]'
Set the shortcut (exactly) Alt-Tab to run script 2:
/path/to/alttab_alternative
Usage
Press Alt + Tab to call the switcher (as in the picture), release Alt and press Tab to cycle through the windows, press Alt again to pick the selected window from the list.
Escape will dismiss (close) the switcher.
Options
If you'd like different colors, you can play with the css in script 1 to set your own styling.

To do so, edit this section, where activestyle
is obviously the currently selected item:
css_data = """
.activestyle {
background-color: blue;
color: white;
border-width: 1px;
border-radius: 0px;
border-color: white;
}
.defaultstyle {
border-width: 0px;
color: black;
background-color: white;
}
"""
See for Gtk css options here on font and buttons.
EDIT
If you'd like to stick to exactly Alt + Tab, in the exact key behaviour as the usual one, use instead of script one:
#!/bin/bash
dr=`dirname $0`
user=$USER
f=$dr'/alttab_runner'
trg='/tmp/'$user'_alttab_trigger'
if ! pgrep -f $f
then
$f
else
echo "runs"
touch $trg
fi
And instead of script 2:
#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version('Wnck', '3.0')
from gi.repository import Gtk, Wnck, Gdk
import subprocess
from threading import Thread
import time
import os
trigger = os.path.join("/tmp", os.environ["USER"] + "_alttab_trigger")
css_data = """
.activestyle {
background-color: grey;
color: white;
border-width: 1px;
border-radius: 0px;
border-color: white;
}
.defaultstyle {
border-width: 1px;
color: black;
background-color: white;
}
"""
class AltTabStuff(Gtk.Window):
def __init__(self):
# apply css
self.provider = Gtk.CssProvider.new()
self.provider.load_from_data(css_data.encode())
Gtk.Window.__init__(
self, title="AltTab replacement"
)
self.curr_index = 0
self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
self.set_decorated(False)
buttongrid = Gtk.Grid()
self.add(buttongrid)
self.connect("delete_event", Gtk.main_quit)
wins = get_winlist()
self.buttonindex = 0
self.buttonsets = []
index = 0
for w in wins:
button = Gtk.Button("\t" + w.get_name())
button.set_relief(Gtk.ReliefStyle.NONE)
buttongrid.attach(button, 0, index, 1, 1)
index = index + 1
button.connect("clicked", raise_window, w)
self.buttonsets.append([button, w])
self.set_focus()
# thread to watch the trigger file
self.timer = Thread(target=self.wait)
self.timer.setDaemon(True)
self.timer.start()
self.show_all()
Gtk.main()
def set_focus(self):
# rotate the focus + styling
for b in self.buttonsets:
button = b[0]
self.set_style(button, active=False)
newactive = self.buttonsets[self.buttonindex][0]
newselected = self.buttonsets[self.buttonindex][1]
time.sleep(0.03)
self.set_style(newactive, active=True)
n_buttons = len(self.buttonsets)
self.buttonindex = self.buttonindex + 1
if self.buttonindex >= n_buttons:
self.buttonindex = 0
return newselected
def wait(self):
"""
wait loop; see if trigger file pops up, or we need to quit on immediate
key release
"""
newfocus = self.buttonsets[0][1]
while True:
time.sleep(0.05)
if not self.key_checker():
# try/except, in case no windows on workspace
try:
self.activate(str(newfocus.get_xid()))
except TypeError:
pass
Gtk.main_quit()
if os.path.exists(trigger):
os.remove(trigger)
newfocus = self.set_focus()
def activate(self, arg1, arg2=None):
# activate the selected window, close preview window
w = arg2 or arg1
subprocess.Popen(["wmctrl", "-ia", w])
Gtk.main_quit()
def set_style(self, button, active):
st_cont = button.get_style_context()
if active:
# st_cont.add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION)
st_cont.add_class("activestyle")
st_cont.remove_class("defaultstyle")
else:
st_cont.remove_class("activestyle")
# st_cont.remove_class("suggested-action")
st_cont.add_class("defaultstyle")
Gtk.StyleContext.add_provider(
st_cont,
self.provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
def key_checker(self):
# check if keys are in a pressed state
exclude = ["Button", "Virtual", "pointer"]
keyboards = [
k for k in get(["xinput", "--list"]).splitlines()
if not any([s in k for s in exclude])
]
dev_ids = [[
s.split("=")[1] for s in k.split() if "id=" in s
][0] for k in keyboards]
pressed = False
for d in dev_ids:
if "down" in get(["xinput", "--query-state", d]):
pressed = True
break
return pressed
def get(cmd):
# just a helper
try:
return subprocess.check_output(cmd).decode("utf-8").strip()
except (subprocess.CalledProcessError, TypeError, UnicodeDecodeError):
pass
def raise_window(button, window):
subprocess.Popen(["wmctrl", "-ia", str(window.get_xid())])
Gtk.main_quit()
def check_windowtype(window):
try:
return "WNCK_WINDOW_NORMAL" in str(
window.get_window_type()
)
except AttributeError:
pass
def get_winlist(scr=None):
if not scr:
scr = Wnck.Screen.get_default()
scr.force_update()
windows = [w for w in scr.get_windows() if check_windowtype(w)]
return windows
AltTabStuff()
Setup is exactly as the first version:
Make sure both Wnck
and wmctrl
are installed:
sudo apt install python3-gi gir1.2-wnck-3.0 wmctrl
Save script 1 into an empty file as (exactly) alttab_runner
, script 2 as (exactly) alttab_alternative
. make both scripts executable
Disable the existing Alt-Tab:
gsettings set org.gnome.desktop.wm.keybindings switch-applications '[]'
Set the shortcut (exactly) Alt-Tab to run script 2:
/path/to/alttab_alternative