From 6e64d3a8e4276914de20e22de5e69fee9e4e27bf Mon Sep 17 00:00:00 2001
From: Christian Dietrich <stettberger@dokucode.de>
Date: Sun, 9 Feb 2014 17:29:14 +0100
Subject: [PATCH] Adding keymask feature to disable keybindings temporarily

---
 NEWS                 |  5 +++++
 doc/herbstluftwm.txt |  9 +++++++-
 src/clientlist.c     | 10 ++++++++-
 src/clientlist.h     |  2 +-
 src/key.c            | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/key.h            |  5 +++--
 src/rules.c          | 11 ++++++++++
 src/rules.h          |  2 +-
 8 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/NEWS b/NEWS
index b4a1bf1..15ac92d 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,11 @@ Current git version
     * if swap_monitors_to_get_tag is set to 0, then focus the other monitor if
       the desired tag is shown on another monitor instead of doing nothing
     * new split mode: auto
+    * For each client a keymask can be given. A keymask is a regular expression,
+      that is matched against the string representation of a keybinding. If it matches, 
+      the keybinding is enabled for this client, otherwise not. The default is an empty
+      keymask ("") that matches all keybindings, so no bindings are masked out by default.
+      A keymask is a client attribute, and can be set by a client rule.
 
 Release 0.5.3 on 2013-12-24
 ---------------------------
diff --git a/doc/herbstluftwm.txt b/doc/herbstluftwm.txt
index 4ff6b6d..5cbca5a 100644
--- a/doc/herbstluftwm.txt
+++ b/doc/herbstluftwm.txt
@@ -307,7 +307,7 @@ cycle_all [*--skip-invisible*] ['DIRECTION']::
 cycle_frame ['DIRECTION']::
     Cycles through all frames on the current tag. 'DIRECTION' = 1 means forward,
     'DIRECTION' = -1 means backward, 'DIRECTION' = 0 has no effect. 'DIRECTION'
-    defaults to 1. 
+    defaults to 1.
 
 cycle_layout ['DELTA' ['LAYOUTS' ...]]::
     Cycles the layout algorithm in the current frame by 'DELTA'. 'DELTA'
@@ -1131,6 +1131,13 @@ Each 'CONSEQUENCE' consists of a 'NAME'='VALUE' pair. Valid 'NAMES' are:
     times, which will cause a hook to be emitted for each occurrence of a hook
     consequence.
 
++keymask::
+    Sets the keymask for an client. A keymask is an regular expression that is
+    matched against the string represenation (see list_keybinds). If it matches
+    the keybinding is active when this client is focused, otherwise it is
+    disabled. The default keymask is an empty string (""), which does not
+    disable any keybinding.
+
 A rule's behaviour can be configured by some special 'FLAGS':
 
     * +not+: negates the next 'CONDITION'.
diff --git a/src/clientlist.c b/src/clientlist.c
index 23e7881..78c7f8b 100644
--- a/src/clientlist.c
+++ b/src/clientlist.c
@@ -15,6 +15,7 @@
 #include "rules.h"
 #include "ipc-protocol.h"
 #include "object.h"
+#include "key.h"
 // system
 #include "glib-backports.h"
 #include <assert.h>
@@ -238,6 +239,9 @@ HSClient* manage_client(Window win) {
         client->tag = find_tag(changes.tag_name->str);
     }
 
+    // Reuse the keymask string
+    client->keymask = changes.keymask;
+
     if (!changes.manage) {
         client_changes_free_members(&changes);
         client_destroy(client);
@@ -277,6 +281,7 @@ HSClient* manage_client(Window win) {
     HSAttribute attributes[] = {
         ATTRIBUTE_STRING(   "winid",        client->window_str,     ATTR_READ_ONLY),
         ATTRIBUTE_STRING(   "title",        client->title,          ATTR_READ_ONLY),
+        ATTRIBUTE_STRING(   "keymask",      client->keymask,        ATTR_READ_ONLY),
         ATTRIBUTE_CUSTOM(   "tag",          client_attr_tag,        ATTR_READ_ONLY),
         ATTRIBUTE_INT(      "pid",          client->pid,            ATTR_READ_ONLY),
         ATTRIBUTE_CUSTOM(   "class",        client_attr_class,      ATTR_READ_ONLY),
@@ -392,6 +397,9 @@ void window_unfocus_last() {
         hook_emit_list("focus_changed", "0x0", "", NULL);
         ewmh_update_active_window(None);
         tag_update_each_focus_layer();
+
+        // Enable all keys in the root window
+        key_set_keymask(get_current_monitor()->tag, 0);
     }
     lastfocus = 0;
 }
@@ -435,6 +443,7 @@ void window_focus(Window window) {
     }
     tag_update_focus_layer(get_current_monitor()->tag);
     grab_client_buttons(get_client_from_window(window), true);
+    key_set_keymask(client->tag, client);
     client_set_urgent(client, false);
 }
 
@@ -1040,4 +1049,3 @@ bool client_sendevent(HSClient *client, Atom proto) {
     }
     return exists;
 }
-
diff --git a/src/clientlist.h b/src/clientlist.h
index 9f3947d..c678688 100644
--- a/src/clientlist.h
+++ b/src/clientlist.h
@@ -27,6 +27,7 @@ typedef struct HSClient {
     HSTag*      tag;
     Rectangle   float_size;     // floating size without the window border
     GString*    title;  // or also called window title; this is never NULL
+    GString*    keymask; // keymask applied to mask out keybindins
     bool        urgent;
     bool        fullscreen;
     bool        ewmhfullscreen; // ewmh fullscreen state
@@ -107,4 +108,3 @@ void window_update_border(Window window, unsigned long color);
 unsigned long get_window_border_color(HSClient* client);
 
 #endif
-
diff --git a/src/key.c b/src/key.c
index d6a0c22..1c25929 100644
--- a/src/key.c
+++ b/src/key.c
@@ -11,6 +11,7 @@
 
 #include <stdio.h>
 #include <string.h>
+#include <regex.h>
 #include "glib-backports.h"
 #include <X11/keysym.h>
 #include <X11/XKBlib.h>
@@ -238,8 +239,25 @@ void grab_keybind(KeyBinding* binding, void* useless_pointer) {
         XGrabKey(g_display, keycode, modifiers[i]|binding->modifiers, g_root,
                  True, GrabModeAsync, GrabModeAsync);
     }
+    // mark the keybinding as enabled
+    binding->enabled = true;
 }
 
+void ungrab_keybind(KeyBinding* binding, void* useless_pointer) {
+    unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask };
+    KeyCode keycode = XKeysymToKeycode(g_display, binding->keysym);
+    if (!keycode) {
+        // ignore unknown keysyms
+        return;
+    }
+    // grab key for each modifier that is ignored (capslock, numlock)
+    for (int i = 0; i < LENGTH(modifiers); i++) {
+        XUngrabKey(g_display, keycode, modifiers[i]|binding->modifiers, g_root);
+    }
+    binding->enabled = false;
+}
+
+
 // update the numlockmask
 // from dwm.c
 void update_numlockmask() {
@@ -361,3 +379,47 @@ void complete_against_modifiers(char* needle, char seperator,
     g_string_free(buf, true);
 }
 
+static void key_set_keymask_helper(KeyBinding* b, regex_t *keymask_regex) {
+    // add keybinding
+    bool enabled = true;
+    if (keymask_regex != NULL) {
+        GString* name = keybinding_to_g_string(b);
+        regmatch_t match;
+        int status = regexec(keymask_regex, name->str, 1, &match, 0);
+        // only accept it, if it matches the entire string
+        if (status == 0
+            && match.rm_so == 0
+            && match.rm_eo == strlen(name->str)) {
+            enabled = true;
+        } else {
+            // Keybinding did not match, therefore we disable it
+            enabled = false;
+        }
+        g_string_free(name, true);
+    }
+
+    if (enabled && !b->enabled) {
+        grab_keybind(b, NULL);
+    } else if(!enabled && b->enabled) {
+        ungrab_keybind(b, NULL);
+    }
+}
+
+void key_set_keymask(HSTag *tag, HSClient *client) {
+    regex_t     keymask_regex;
+    if (client && strlen(client->keymask->str) > 0) {
+        int status = regcomp(&keymask_regex, client->keymask->str, REG_EXTENDED);
+        if (status == 0) {
+            g_list_foreach(g_key_binds, (GFunc)key_set_keymask_helper,
+                           &keymask_regex);
+            return;
+        } else {
+            char buf[ERROR_STRING_BUF_SIZE];
+            regerror(status, &keymask_regex, buf, ERROR_STRING_BUF_SIZE);
+            HSDebug("keymask: Can not parse regex \"%s\" from keymask: %s",
+                    client->keymask->str, buf);
+        }
+    }
+    // Enable all keys again
+    g_list_foreach(g_key_binds, (GFunc)key_set_keymask_helper, 0);
+}
diff --git a/src/key.h b/src/key.h
index e99004d..a69d52f 100644
--- a/src/key.h
+++ b/src/key.h
@@ -9,6 +9,7 @@
 #include <stdbool.h>
 #include <X11/Xlib.h>
 #include "glib-backports.h"
+#include "clientlist.h"
 
 #define KEY_COMBI_SEPARATORS "+-"
 
@@ -17,6 +18,7 @@ typedef struct KeyBinding {
     unsigned int modifiers;
     int     cmd_argc; // number of arguments for command
     char**  cmd_argv; // arguments for command to call
+    bool    enabled;  // Is the keybinding already grabbed
 } KeyBinding;
 
 unsigned int modifiername2mask(const char* name);
@@ -41,11 +43,10 @@ void regrab_keys();
 void grab_keybind(KeyBinding* binding, void* useless_pointer);
 void update_numlockmask();
 unsigned int* get_numlockmask_ptr();
-
+void key_set_keymask(HSTag * tag, HSClient *client);
 void handle_key_press(XEvent* ev);
 
 void key_init();
 void key_destroy();
 
 #endif
-
diff --git a/src/rules.c b/src/rules.c
index f4da9fb..46defad 100644
--- a/src/rules.c
+++ b/src/rules.c
@@ -63,6 +63,7 @@ DECLARE_CONSEQUENCE(consequence_switchtag);
 DECLARE_CONSEQUENCE(consequence_ewmhrequests);
 DECLARE_CONSEQUENCE(consequence_ewmhnotify);
 DECLARE_CONSEQUENCE(consequence_hook);
+DECLARE_CONSEQUENCE(consequence_keymask);
 
 /// GLOBALS ///
 
@@ -91,6 +92,7 @@ static HSConsequenceType g_consequence_types[] = {
     { "ewmhrequests",   consequence_ewmhrequests    },
     { "ewmhnotify",     consequence_ewmhnotify      },
     { "hook",           consequence_hook            },
+    { "keymask",        consequence_keymask         },
 };
 
 static GQueue g_rules = G_QUEUE_INIT; // a list of HSRule* elements
@@ -559,6 +561,7 @@ void client_changes_init(HSClientChanges* changes, HSClient* client) {
     changes->switchtag = false;
     changes->manage = true;
     changes->fullscreen = ewmh_is_fullscreen_set(client->window);
+    changes->keymask = g_string_new("");
 }
 
 void client_changes_free_members(HSClientChanges* changes) {
@@ -821,3 +824,11 @@ void consequence_hook(HSConsequence* cons, HSClient* client,
     g_string_free(winid, true);
 }
 
+void consequence_keymask(HSConsequence* cons,
+                         HSClient* client, HSClientChanges* changes) {
+    if (changes->keymask) {
+        g_string_assign(changes->keymask, cons->value.str);
+    } else {
+        changes->keymask = g_string_new(cons->value.str);
+    }
+}
diff --git a/src/rules.h b/src/rules.h
index 5cdf739..07a6584 100644
--- a/src/rules.h
+++ b/src/rules.h
@@ -63,6 +63,7 @@ typedef struct {
     bool            manage; // whether we should manage it
     bool            fullscreen;
     bool            ewmhnotify; // whether to send ewmh-notifications
+    GString*        keymask; // Which keymask rule should be applied for this client
 } HSClientChanges;
 
 void rules_init();
@@ -83,4 +84,3 @@ int rule_print_all_command(int argc, char** argv, GString* output);
 void complete_against_rule_names(int argc, char** argv, int pos, GString* output);
 
 #endif
-
-- 
1.9.rc1

