It is still desirable to have not only Alt Tab 'Select and Toggle' capability as provided by Jaciss's excellent script, but also a popup splash showing a list of active windows for selection when using Mutter windows manager.
I wrote a popup daemon in python 3 to work together with a modified version of Jaciss's Alt Tab script - modified to allow time for selecting the required window from the popup. The script is a drop-in replacement for Jaciss's script and communicates with the popup daemon through temporary files. The script counts the number of times the Tab key is pressed with the Alt key held down and waits for the Alt key to be released. If the popup is not running it then also raises the selected window. However if the popup is running, it signals each Tab press and Alt Up to the popup daemon. On first Tab press the popup daemon, creates, displays and with subsequent Tab presses updates a splash list (application name and windows name with selected window in bold) of focusable windows, then raises the selected window as chosen from the splash after the Alt key is released.
The script is totally reliant on the continuing availability of X11 functionality so should continue to work for the moment but may not survive the eventual move to Wayland.
It is to be hoped that we do not move to Wayland before xWayland supports all the commonly used features of X11 including those required by this script.
I attach the code for the revised script and the popup daemon below as well as the code for some support utilities. The following files are required:
Suggested file name |
Suggested directory |
Function |
alttab.sh |
/home/pi/bin/ |
Replacement for Jaciss's script activated by xbindkeys in response to Alt Tab key press |
popup.py |
/home/pi/bin/ |
Daemon providing popup splash , started via autostart desktop shortcut and script |
startpopup.sh |
/home/pi/bin/ |
Script to start popup daemon called from desktop shortcut in autostart |
startpopup.desktop |
/home/pi/.config/autostart/ |
Desktop shortcut to autostart daemon |
.xbindkeysrc |
/home/pi/ |
xbindkeys configuration file |
The files in /home/pi/bin
need execute permission.
The following packages, which may not already be installed, are required and can be installed via apt install
:
x11-utils xbindkeys xdotool python3-tk
Alt Tab needs to be disabled by issuing the following command (not as root)
gsettings set org.gnome.desktop.wm.keybindings cycle-windows []
It can be re-enabled, if needs be, by issuing the following command
gsettings reset org.gnome.desktop.wm.keybindings cycle-windows
The popup daemon uses the tkinter graphics library as requirements are undemanding and tkinter is part of the python core language. The daemon displays the popup, with a short delay, the first time the Tab is pressed with Alt key held down with the next item in stacking order preselected. The popup disappears after the user has tabbed through to the required window and released the Alt key. The Alt key needs to be released after the Tab key. If the popup remains (because Alt key was released first), just press and release the Alt key. For toggling between the last two focussed windows a quick Alt Tab and release can be done without waiting for the popup daemon to appear.
Notes :
- To view the log file for
alttab.sh
, issue the following command in a terminal window:
tail -f /tmp/tabs.log
and enable logging in the script by deleting the value 1
in the last but one line of the script
- To view the log file for
popup.py
, disable the popup (if running) by issuing the following
command in a terminal window
bash /tmp/killpopupup.sh
and run popup.py
from an IDE such as geany
- In running thd in the script, thdevent is set to the value
/dev/input/event0
because keyboard events came as event0 in my case. If Alt Up detection is not working try with the
value of thdevent changed to /dev/input/event*
- If running the script without the popup, note that the existence of any running program with
popup.py
in its name
will prevent the the script from raising the newly required window. In such case kill any running instances of popup.py
If any problems please let me know and I'll try to fix them !
alttab.sh
#!/bin/bash
# 07/03/22 by drdrm46 with many many thanks to Jaciss !
# To be run from xbindkeys with /home/pi/.xbindkeysrc containing :
# #Alt Tab select and toggle with popup support
# bash alttab.sh"
# Alt+Tab
# This script runs every time Tab key is pressed with Alt key held down,
# checks for Alt key up and terminates after Alt key is released or
# timeout. Multiple instances may run simultaneously for a short while.
function execute
{
initialise
count_tabs
detect_alt_up
if [ $altup ] ; then
reset_tab_count
is_popup_running
if [ ! $running ] ; then
activate_window # if popup.py daemon not enabled
fi
LOG="EXITING Alt Tab found in Run No : $runno" ; log
elif [ $timeout ] ; then
reset_tab_count
LOG="EXITING Timeout in Run No : $runno" ; log
elif [ $abort ] ; then
LOG="ABORTING earlier Run No $runno" ; log
fi
}
function initialise
{
log_file="/tmp/tabs.log" # General logging
run_file="/tmp/runno.txt" # Distinguish current from previous runs
ttfile='/tmp/tabtimes.txt' # Count tabs + signal to popup.py daemon
key_file="/tmp/keys.log" # For output of 'thd' daemon
mod_file="/tmp/modulus.txt" # Save count of focusable windows
# May need to change event id depending on keyboard config eg 0 to * :
thdevent="/dev/input/event0" # thd event filter for keyboard
read runno < $run_file
((runno+=1))
echo $((runno%64)) > $run_file
LOG=" " ; log
LOG="NEW RUN $runno " ; log
}
function count_tabs
{
tabcount=0
lasttime=0
if [ -f "$ttfile" ] ; then
read tabcount lasttime < $ttfile
fi
((tabcount+=1))
tnow=`date +%s`
tdiff=$(($tnow-$lasttime))
LOG="tabs : $tabcount tdiff : $tdiff "; log
lasttime=$tnow
echo "$tabcount $lasttime" > $ttfile
}
function detect_alt_up
{
killthd="/tmp/thd$runno"
LOG="Starting thd in Run No : $runno" ; log
thd --dump $thdevent >> $key_file & # Save key events
echo "kill -9 $!" > $killthd
i=0
abort=
timeout=
altup=
while [ ! $abort ] && [ ! $timeout ] && [ ! $altup ] ; do
read latestno < $run_file
if [ $runno == $latestno ] ; then
if [ $i -gt 100 ] ; then
timeout=1
else
lastline=$( tail -n 1 $key_file )
if [[ $lastline == *"LEFTALT"* ]] && [[ $lastline == *"0"* ]] ; then
altup=1
LOG="Found Alt Up in Loop $i" ; log
echo "0" >> $key_file
fi
sleep 0.1
fi
else
abort=1
fi
((i+=1))
done
LOG="Killing thd in Run No : $runno" ; log
bash $killthd
rm $killthd
}
function reset_tab_count
{
echo "0 $lasttime" > $ttfile
num_presses=$(($tabcount+2)) #skip "panel" window and active window (+2)
LOG="Effective tab presses (tab count + 2) : $num_presses" ; log
}
function is_popup_running
{
running=
# Use distinctive part of popup filename with [] around first letter
ps -aux | grep "[p]opup.py"
ok=$?
if [ $ok == 0 ] ; then
running=1
fi
}
function activate_window
{
# get window stack list
stacklist=`xprop -root | grep '_NET_CLIENT_LIST_STACKING(WINDOW)' | sed 's/.*#//'`
# reverse the order
stacklist=`echo $stacklist | sed 's/, /\n/g' | tac | sed ':a;$!{N;ba};s/\n/, /g'`
# loop through stack until we get to our $indexplus2 index; raise window
j=0
echo $stacklist | sed -n 1'p' | tr ',' '\n' | while read win_id; do
((j+=1))
echo $j > $mod_file
done
read total < $mod_file
focus=$((total-2))
index=$((tabcount%focus))
indexplus2=$((index+2))
LOG="total windows $total focusable $focus tabcount $tabcount index $index index+2 $indexplus2"; log
j=0
echo $stacklist | sed -n 1'p' | tr ',' '\n' | while read win_id; do
((j+=1))
wname=`xprop -id $win_id WM_NAME` #you can get _NET_WM_ICON as well
LOG="$j: $win_id $wname" ; log
if (( $j == $indexplus2 )); then
LOG="raise $win_id" ; log
xdotool windowactivate $win_id
fi
done
}
function log
{
if [ ! $nolog ] ; then
timenow=`date +%T`
echo "$runno $timenow $LOG" >> $log_file
fi
}
nolog=1 # Set to disable all logging, blank for logging
execute
popup.py
#!/usr/bin/env python3
# drdrm46 07/03/22
# Start up as daemon with "nohup python3 /home/pi/bin/popup.py &"
# To be used with associated "alttab.sh" bash script
# Filename must match name in "is_popup_running" in bash script
import sys,shlex,os
import tkinter as tk # Requires python3-tk package installed
import subprocess as sp
Goldtabcount=0
Gwinnamelist=[]
Gwinclasslist=[]
Gwinidlist=[]
Glablist=[]
Goldwinid=None
Gnewwinid=None
Gstarted=False
def main():
ttfile='/tmp/tabtimes.txt' # To communicate with script
timeout=100
leftoffset=0.8
fonts=(( "Piboto",11,"normal"),("Piboto",11,"bold" ))
aspects=(500,1000)
root=tk.Tk()
root.wm_attributes('-type', 'splash')
width=root.winfo_screenwidth()
height=root.winfo_screenheight()
geom='+'+str(int(leftoffset*width/2))+'+'+ str(int(height/2))
root.geometry(geom)
popupid=tk.Frame(root)
popupid.grid()
# Hack to ensure that popup is properly withdrawn on startup
hack(root,popupid)
root.after(timeout,checkforchange,root,popupid,ttfile,timeout,fonts,aspects)
root.mainloop()
def checkforchange(root,popupid,ttfile,timeout,fonts,aspects):
root.after(timeout,checkforchange,root,popupid,ttfile,timeout,fonts,aspects)
global Goldtabcount
global Gwinnamelist
global Gwinclasslist
global Gwinidlist
global Glablist
global Goldwinid
global Gnewwinid
tabcount=get_tab_count(ttfile)
if tabcount is not None:
if tabcount==0 and Goldtabcount != 0 : # Found Alt Up
rows=len(Gwinnamelist)
Goldwinid=Gwinidlist[0]
index=Goldtabcount%rows
Gnewwinid=Gwinidlist[index]
focus_window(Goldwinid) # in case popup breaks stacking order
focus_window(Gnewwinid)
print( "Times Tab pressed :",Goldtabcount,\
"Focusable windows :",rows,\
"Index into ordered window list :",index)
print( "Switching from ", Gwinnamelist[0])
print( " to ", Gwinnamelist[index])
clear_splash(Gwinnamelist,Glablist)
Gwinidlist=[]
Gwinnamelist=[]
Goldwinid=None
Gnewwinid=None
Glablist=[]
root.withdraw()
elif tabcount==1 and Goldtabcount==0: # Found first Alt Tab
Gwinidlist,Gwinnamelist,Gwinclasslist=get_window_info()
Glablist=fill_splash(popupid,fonts,aspects,tabcount,Gwinnamelist,\
Gwinclasslist)
root.deiconify()
elif tabcount>1 and Goldtabcount!=tabcount: # Found more Alt Tabs
update_splash(tabcount,fonts,Gwinnamelist,Glablist)
Goldtabcount=tabcount
def get_tab_count(ttfile):
tabcount=None
try:
f=open(ttfile)
tabinfo=f.read()
tabcount=int(tabinfo.split()[0])
f.close()
except:
print(ttfile+' not found')
return tabcount
def hack(root,popupid):
# Hack to ensure that popup is properly withdrawn on startup
global Gstarted
if not Gstarted:
messid=tk.Label(popupid)
messid.config(text="Popup running")
messid.grid()
messid.destroy()
root.withdraw()
Gstarted=True
def clear_splash(winnamelist,lablist):
if winnamelist is not None:
rows=len(winnamelist)
for row in range(rows):
lablist[row].destroy()
for row in range(rows):
lablist[row+rows].destroy()
return
def fill_splash(popupid,fonts,aspects,tabcount,winnamelist,winclasslist):
lablist=[]
rows=len(winnamelist)
for row in range(rows):
lab=tk.Message(popupid,aspect=aspects[1])
lablist.append(lab)
lab.config(text=winnamelist[row])
if row==tabcount:
lab.config(font=fonts[1])
else:
lab.config(font=fonts[0])
lab.grid(row=row,column=2,sticky=tk.W)
for row in range(rows):
lab=tk.Message(popupid,aspect=aspects[0])
lablist.append(lab)
lab.config(text=winclasslist[row])
if row==tabcount:
lab.config(font=fonts[1])
else:
lab.config(font=fonts[0])
lab.grid(row=row,column=1,sticky=tk.W)
return lablist
def update_splash(tabcount,fonts,winnamelist,lablist):
rows=len(winnamelist)
for row in range(rows):
lab=lablist[row]
if row==tabcount%rows:
lab.config(font=fonts[1])
else:
lab.config(font=fonts[0])
for row in range(rows):
lab=lablist[row+rows]
if row==tabcount%rows:
lab.config(font=fonts[1])
else:
lab.config(font=fonts[0])
def get_window_info():
command="xprop -root"
cmd=shlex.split(command)
handle = sp.Popen ( cmd , stdout = sp.PIPE)
stdout, stderr = handle.communicate()
winfo=str(stdout).split('\\n')
i=0
for info in winfo:
if '_NET_CLIENT_LIST_STACKING(WINDOW):' in info:
winids=info.split(' ')
i=i+1
winidlist=[]
i=0
for item in winids:
if i>3 :
winidlist.append(item)
i=i+1
winnamelist=[]
for id in winidlist:
handle = sp.Popen( ['xprop', '-id',id,'WM_NAME'],\
stdout = sp.PIPE )
stdout, stderr = handle.communicate()
print(str(stdout))
wnameinfo=str(stdout).split(',')
winname1=str(wnameinfo).split()[2:]
winname2=' '.join(winname1)
winname=winname2.replace("\\\\n\\'']"," ")
winname=winname.replace('"',' ')
winnamelist.append(winname)
winclasslist=[]
for id in winidlist:
handle = sp.Popen( ['xprop', '-id',id,'WM_CLASS'], \
stdout = sp.PIPE )
stdout, stderr = handle.communicate()
wclassinfo=str(stdout).split(',')
winclass1=str(wclassinfo).split()
winclass2=winclass1[4]
winclass=winclass2.replace("\\\\n\\'']"," ")
winclass=winclass.replace('"',' ')
winclass=winclass.replace('"',' ')
winclasslist.append(winclass)
idlist=[]
namelist=[]
classlist=[]
ii=len(winidlist)
print(" ")
print("ALL WINDOWS IN STACKING ORDER")
for i in range(ii):
print (i, winidlist[i],winclasslist[i],winnamelist[i])
for i in range(ii):
j=ii-i-1
if not (("panel" in winnamelist[j]) or \
("pcmanfm" in winnamelist[j])):
idlist.append(winidlist[j])
namelist.append(winnamelist[j])
classlist.append(winclasslist[j])
ii=len(idlist)
print("FOCUSABLE WINDOWS IN REVERSE STACKING ORDER ")
for i in range(ii):
print (i, idlist[i],classlist[i],namelist[i])
return idlist,namelist,classlist
def focus_window(id):
command="xdotool windowactivate "+id
cmd=shlex.split(command)
handle = sp.Popen ( cmd , stdout = sp.PIPE , stderr = sp.PIPE)
sout,serr = handle.communicate()
if not (sout==b'' and serr==b''):
print ("ERROR in focus_window","stdout=", str(sout),\
"stderr=",str(serr))
return
if __name__ == "__main__":
main()
startpopup.sh
#!/bin/bash
sleep 10
nohup python3 /home/pi/bin/popup.py & > "/tmp/nohup.out"
pid=$!
echo "kill $pid" >/tmp/killpopup.sh
startpopup.desktop
[Desktop Entry]
Name=Start Popup
Type=Application
Exec=bash /home/pi/bin/startpopup.sh``
.xbindkeysrc
# Alt Tab select and toggle with popup
"bash alttab.sh"
Alt+Tab