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.

  1. Turning off system shortcuts globally impedes my overall productivity with the system.
  2. 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

  1. The script needs xdotool:

    sudo apt-get install xdotool
    
  2. Copy the script into an empty file, save it as disable_shortcuts.py

  3. In the head of the script, replace in the line:

    app = "gedit"
    

    "gedit" by your application, meaning: the process name that owns the window.

  4. Test-run the script by the command:

    python3 /path/to/disable_shortcuts.py
    
  5. 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:

  1. Open a terminal window, run the command dconf watch /
  2. Open System Settings > "Keyboard" > "Shortcuts" > "System"
  3. Re-set the shortcut to itself. In the terminal, you can see the gsettings key that belongs to the shortcut:

    enter image description here

  4. 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

enter image description here

re- enabling the shortcut if another window gets focus

enter image description here


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

  1. Like the first script, xdotool needs to be installed:

    sudo apt-get install xdotool
    
  2. Copy the script into an empty file, save it as disable_shortcuts.py

  3. In the head of the script, replace in the line:

    appdir = "/opt"
    

    "/opt" by the directory your applications are.

  4. Test-run the script by the command:

    python3 /path/to/disable_shortcuts.py
    
  5. 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:

  1. Honors changes the user makes while the script is running.
  2. Does not reset values the user had set to the defaults.
  3. Stores a backup of the settings in case the script quits while shortcuts are disabled.
  4. Handles gsettings and dconf shortcuts. (This may have been a non-issue.)

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 in dconf but changes made there have no effect. As a general rule, add keys found using Jacob's method as gsettings and those you had to manually track down in dconf 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.