Block Unity keyboard shortcuts when a certain application is active
The great JetBrains IDEs (IDEA et al.) assign pretty much every conceivable keyboard shortcut to some function. While mildly overwhelming at times, it also makes for efficient use.
My problem is that Unity assigns some of these shortcuts as well, and they take precedence. One particularly annoying example is CTRL + ALT + L. The issue has been explored before here.
However, neither of the approaches is satisfactory.
- Turning off system shortcuts globally impedes my overall productivity with the system.
- Switching to a different keymap in IDEA will confuse the hell out of me when I develop on different platforms (and have to choose different mappings).
Is there a way to turn off system shortcuts only when a certain application is active, i.e. running and in focus?
I'd be willing to run a script every time I launch the application.
Solution 1:
How to automatically disable multiple (specific) shortcuts if (and as long as) a specific application's window is active
The script below will disable specific key shortcuts when an arbitrary application's window is active.
Although you mentioned""I'd be willing to run a script every time I launch the application.", There is no reason to kill the script afterwards, it is extremely low on juice.
The script
#!/usr/bin/env python3
import subprocess
import time
import os
app = "gedit"
f = os.path.join(os.environ["HOME"], "keylist")
def run(cmd):
subprocess.Popen(cmd)
def get(cmd):
try:
return subprocess.check_output(cmd).decode("utf-8").strip()
except:
pass
def getactive():
return get(["xdotool", "getactivewindow"])
def setkeys(val):
# --- add the keys to be disabled below
keys = [
["org.gnome.settings-daemon.plugins.media-keys", "logout"],
["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
]
# ---
writelist = []
if not val:
try:
values = open(f).read().splitlines()
except FileNotFoundError:
values = []
for i, key in enumerate(keys):
try:
cmd = ["gsettings", "set"]+key+[values[i]]
except IndexError:
cmd = ["gsettings", "reset"]+key
run(cmd)
else:
for key in keys:
cmd = ["gsettings", "set"]+key+["['']"]
read = get(["gsettings", "get"]+key)
writelist.append(read)
run(cmd)
if writelist:
open(f, "wt").write("\n".join(writelist))
front1 = None
while True:
time.sleep(1)
pid = get(["pgrep", app])
if pid:
try:
active = get(["xdotool", "getactivewindow"])
relevant = get(["xdotool", "search", "--all", "--pid", pid]).splitlines()
front2 = active in relevant
except AttributeError:
front2 = front1
else:
front2 = False
if front2 != front1:
if front2:
setkeys(True)
else:
setkeys(False)
front1 = front2
How to use
-
The script needs
xdotool
:sudo apt-get install xdotool
Copy the script into an empty file, save it as
disable_shortcuts.py
-
In the head of the script, replace in the line:
app = "gedit"
"gedit" by your application, meaning: the process name that owns the window.
-
Test-run the script by the command:
python3 /path/to/disable_shortcuts.py
-
If all works fine, add it to Startup Applications: Dash > Startup Applications > Add. Add the command:
/bin/bash -c "sleep 15 && python3 /path/to/disable_shortcuts.py"
Adding more shortcuts to be disabled
As an example, I added the shortcut you mentioned: CTRL + ALT + L. Shortcuts are set in the dconf
database, and can be set or disabled using gsettings
.
In the script, these gsettings
entries are set in the function: setkeys()
def setkeys(val):
# --- add the keys to be disabled below
keys = [
["org.gnome.settings-daemon.plugins.media-keys", "screensaver"]
]
# ---
An example to add (disabling) the log out shortcut:
- Open a terminal window, run the command
dconf watch /
- Open System Settings > "Keyboard" > "Shortcuts" > "System"
-
Re-set the shortcut to itself. In the terminal, you can see the
gsettings
key that belongs to the shortcut: -
Now we have to add the found key (in a slightly different appearance):
["org.gnome.settings-daemon.plugins.media-keys", "logout"]
...to the "keys" list in our function:
def setkeys(val): # --- add the keys to be disabled below keys = [ ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"], ["org.gnome.settings-daemon.plugins.media-keys", "logout"], ]
Now both CTRL + ALT + L and CTRL + ALT + Delete are disabled if your application is in front.
Explanation
As mentioned, shortcuts, like the ones you mention, are set in the dconf
database. In the example CTRL + ALT + L, the key to set or edit the schortcut is:
org.gnome.settings-daemon.plugins.media-keys screensaver
To disable the key, the command is:
gsettings set org.gnome.settings-daemon.plugins.media-keys screensaver ""
To reset the key to its default value:
gsettings reset org.gnome.settings-daemon.plugins.media-keys screensaver
The script looks once per second if:
- your application runs at all
- if so, it looks if any of its windows is active
-
again (only) if so, it disables the shortcuts, listed in
# --- add the keys to be disabled below keys = [ ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"], ["org.gnome.settings-daemon.plugins.media-keys", "logout"], ]
...waiting for the next change in state.
If the active window is not one of your application any more, the keys, mentioned in the list, are reset to default.
Note
As mentioned earlier, the additional burden to the processor of the script is nihil. You could very well run it on startup, as explained in "How to use".
Affecting multiple applications
As discussed in comments, in OP's specific case, it is useful to apply disabling shortcuts on a group of applications, all residing in one directory.
Below a version to apply this on all applications of which the output of
pgrep -f
will include a specific directory. In my example, I set the /opt
directory, so if the active window is one of any of the applications in /opt
, the set shortcuts will be disabled.
bringing a window of one of the applications in /opt to front will disable the logout shortcut
re- enabling the shortcut if another window gets focus
The script
#!/usr/bin/env python3
import subprocess
import time
import os
appdir = "/opt"
f = os.path.join(os.environ["HOME"], "keylist")
def run(cmd):
subprocess.call(cmd)
def get(cmd):
try:
return subprocess.check_output(cmd).decode("utf-8").strip()
except:
pass
def getactive():
return get(["xdotool", "getactivewindow"])
def setkeys(val):
# --- add the keys to be disabled below
keys = [
["org.gnome.settings-daemon.plugins.media-keys", "logout"],
["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
["org.gnome.desktop.wm.keybindings", "begin-move"],
]
# ---
writelist = []
if not val:
try:
values = open(f).read().splitlines()
except FileNotFoundError:
values = []
# for key in keys:
for i, key in enumerate(keys):
try:
cmd = ["gsettings", "set"]+key+[values[i]]
except IndexError:
cmd = ["gsettings", "reset"]+key
run(cmd)
else:
for key in keys:
cmd = ["gsettings", "set"]+key+["['']"]
read = get(["gsettings", "get"]+key)
writelist.append(read)
run(cmd)
if writelist:
open(f, "wt").write("\n".join(writelist))
front1 = None
while True:
time.sleep(1)
# check if any of the apps runs at all
checkpids = get(["pgrep", "-f", appdir])
# if so:
if checkpids:
checkpids = checkpids.splitlines()
active = getactive()
# get pid frontmost (doesn't work on pid 0)
match = [l for l in get(["xprop", "-id", active]).splitlines()\
if "_NET_WM_PID(CARDINAL)" in l]
if match:
# check if pid is of any of the relevant apps
pid = match[0].split("=")[1].strip()
front2 = True if pid in checkpids else False
else:
front2 = False
else:
front2 = False
if front2 != front1:
if front2:
setkeys(True)
else:
setkeys(False)
front1 = front2
How to use
-
Like the first script,
xdotool
needs to be installed:sudo apt-get install xdotool
Copy the script into an empty file, save it as
disable_shortcuts.py
-
In the head of the script, replace in the line:
appdir = "/opt"
"/opt" by the directory your applications are.
-
Test-run the script by the command:
python3 /path/to/disable_shortcuts.py
-
If all works fine, add it to Startup Applications: Dash > Startup Applications > Add. Add the command:
/bin/bash -c "sleep 15 && python3 /path/to/disable_shortcuts.py"
Adding other shortcuts to the list works exactly similar to version 1 of the script.
Does it work on all applications?
In your answer, you mention:
xprop does not reveal PIDs for all windows. Failing example: stopwatch.
Windows with pid 0 (like tkinter windows, including Idle), have no window- id in the output of xprop -id
. Idle
does not have any clashing shortcuts though in my experience. If you run into any application with pid 0 that would require disabling specific shortcuts, please mention.
In that case, a possible escape would be to convert the output of
xdotool getactivewindow
to hex, the format wmctrl
uses, and subsequently look up the corresponding pid
in the output of
wmctrl -lp
Although that seemed the most obvious thing to do to start with, I didn't use it in the script to keep the script as light-weight as possible.
Solution 2:
Based on (an older version of) Jacob Vlijm's answer I wrote this version that solves these additional problems:
- Honors changes the user makes while the script is running.
- Does not reset values the user had set to the defaults.
- Stores a backup of the settings in case the script quits while shortcuts are disabled.
-
Handles(This may have been a non-issue.)gsettings
anddconf
shortcuts.
Open problems:
- I can not find where some shortcuts such as Alt + ` and Alt + F1 are set. These come from Unity resp. Compiz; how can we programmatically change the shortcuts shown when you hold Super?
-
xprop
does not reveal PIDs for all windows. Failing example:stopwatch
. (Jaco Vlijm has some ideas.)
#!/usr/bin/env python3
import subprocess
import time
import os
# Path pattern to block
apppattern = "myprocess"
# Write a backup that can restore the settings at the
# start of the script.
# Leave empty to not write a backup.
backupfile = "~/.keymap_backup"
# Add the keys to be disabled below.
shortcuts = {
"org.gnome.settings-daemon.plugins.media-keys/key" : "gsettings",
"/org/gnome/desktop/wm/keybindings/key" : "dconf",
}
#
# Helper functions
#
# Run a command on the shell
def run(cmd):
subprocess.Popen(cmd)
# Run a command on the shell and return the
# stripped result
def get(cmd):
try:
return subprocess.check_output(cmd).decode("utf-8").strip()
except:
pass
# Get the PID of the currently active window
def getactive():
xdoid = get(["xdotool", "getactivewindow"])
pidline = [l for l in get(["xprop", "-id", xdoid]).splitlines()\
if "_NET_WM_PID(CARDINAL)" in l]
if pidline:
pid = pidline[0].split("=")[1].strip()
else:
# Something went wrong
print("Warning: Could not obtain PID of current window")
pid = ""
return pid
def readkey(key):
if shortcuts[key] == "gsettings":
return get(["gsettings", "get"] + key.split("/"))
elif shortcuts[key] == "dconf":
return get(["dconf", "read", key])
def writekey(key, val):
if val == "":
val = "['']"
if shortcuts[key] == "gsettings":
run(["gsettings", "set"] + key.split("/") + [val])
elif shortcuts[key] == "dconf":
run(["dconf", "write", key, val])
def resetkey(key):
if shortcuts[key] == "gsettings":
run(["gsettings", "reset"] + key.split("/"))
elif shortcuts[key] == "dconf":
run(["dconf", "reset", key])
# If val == True, disables all shortcuts.
# If val == False, resets all shortcuts.
def setkeys(flag):
for key, val in shortcutmap.items():
if flag == True:
# Read current value again; user may change
# settings, after all!
shortcutmap[key] = readkey(key)
writekey(key, "")
elif flag == False:
if val:
writekey(key, val)
else:
resetkey(key)
#
# Main script
#
# Store current shortcuts in case they are non-default
# Note: if the default is set, dconf returns an empty string!
# Optionally, create a backup script to restore the value in case
# this script crashes at an inopportune time.
shortcutmap = {}
if backupfile:
f = open(os.path.expanduser(backupfile),'w+')
f.write('#!/bin/sh\n')
for key, val in shortcuts.items():
if shortcuts[key] == "gsettings":
shortcutmap[key] = get(["gsettings", "get"] + key.split("/"))
if backupfile:
if shortcutmap[key]:
f.write("gsettings set " + " ".join(key.split("/")) + " " +
shortcutmap[key] + "\n")
else:
f.write("gsettings reset " + " ".join(key.split("/")) + "\n")
elif shortcuts[key] == "dconf":
shortcutmap[key] = get(["dconf", "read", key])
if backupfile:
if shortcutmap[key]:
f.write("dconf write " + key + " " + shortcutmap[key] + "\n")
else:
f.write("dconf reset " + key + "\n")
if backupfile: f.close()
# Check every half second if the window changed form or to a
# matching application.
front1 = None
while True:
time.sleep(0.5)
checkpids = get(["pgrep", "-f", apppattern])
if checkpids:
checkpids = checkpids.splitlines()
activepid = getactive()
#print(activepid)
if activepid:
front2 = True if activepid in checkpids else False
else:
front2 = False
else:
front2 = False
if front2 != front1:
#print("Matches: " + str(flag))
if front2:
setkeys(True)
else:
setkeys(False)
front1 = front2
Notes:
- Note the different key formats for
gsettings
resp.dconf
. gsettings
keys do appear indconf
but changes made there have no effect. As a general rule, add keys found using Jacob's method asgsettings
and those you had to manually track down indconf
as such.- Run the backup file as script in case the shortcuts get messed up, e.g. by the script terminating while shortcuts are disabled.