Disable autorepeat under X11

Summary

How to distinguish between "true" Key events and autorepeated ones under X11

See Also

Vogel spiral
illustrates the use of TrueKeyPress and TrueKeyRelease virtual events described below

Introduction

Autorepeat is a functionality of the windowing system that generates multiple Key events when a Key is held down. What events a Tk application gets in that case depends on the windowing system. By default, under X11 an autorepeat sequence looks like this:

P----------^v-----^v-----^v-----R

P: Physical KeyPress (I'll call it true Keypress from now on)
R: Physical KeyRelease (true KeyRelease)
v: KeyPress generated from autorepeat (false KeyPress)
^: KeyRelease generated from autorepeat (false KeyRelease)
-: Time passing

Under Windows an autorepeat sequence looks like this:

P----------v-----v-----v-----R

For Mac, I don't know. If anyone does, please fill in!

While usually autorepeated Key events are just fine, in some cases one might be interested only in true Key events (e.g. games). Under Windows it's easy: act on the first KeyPress you get and ignore all that follow, until a KeyRelease occurs. Under X11 it's more tricky, because you always get two events, a Release and a Press. Since I spent many hours trying to find the best way to distinguish between true and false Key events under X (and bothering people on various mailing listst...), I put the results of my "research" into this page. Several Approaches will be discussed, chose the one that suits best your needs.

xset

The easiest way is to disable autorepeat with xset, a utility to set X user preferences. Once autorepeat is disabled, you will get one KeyPress event when the User presses the key, and one KeyRelease when he releases it. This change affects all clients of the X server, so you'd best [bind] it to <FocusIn> and <FocusOut> events, so other widgets and applications still behave as they should:

bind $w <FocusIn> {toggleAutorepeat off}
bind $w <FocusOut> {toggleAutorepeat on}

proc toggleAutorepeat {state} {
        
        exec xset r $state
}

This way the setting affects all keys, however you can enable/disable autorepeat only for specific keys (see the manpage of xset ). That utility is also available as a Tk package: Xop.

The problem of this apporach is that if your program crashes or gets killed, autorepeat might stay turned off, which results in abnormal behaviour in other applications. So either write BugFree(TM) code, live with that risk, or take a look at the other possibilities.

Compare Serial Numbers

Autorepeated KeyRelease/KeyPress event pairs have identical serial numbers. Notice that it is the subsequent KeyPress serial that matches the previous KeyRelease serial. The 'serial' field of the event can be accessed using the '%#' subtitution in a bind script.

The following drop-in utilities will synthesize TrueKeyPress/TrueKeyRelease events that can be used to ignore auto-generated KeyPress events and react only to real user-generated KeyPress events. $arrayname indicates an array that can be used by the utilities to maintain state.

proc truekeypress {w serial arrayname} {
    upvar $arrayname state
    set state(press) $serial
    if {$serial ne $state(release)} {
        event generate $w <<TrueKeyPress>>
    }
}

proc truekeyrelease {w serial arrayname} {
    upvar $arrayname state
    set state(release) $serial
    after 0 [list apply [list {w arrayname} {
        upvar $arrayname state
        if {$state(release) ne $state(press)} {
            event generate $w <<TrueKeyRelease>>
        }
    } [namespace current]] $w $arrayname]
}

It might be convenient to maintain a "boolean" variable to indicate that a KeyPress is a real user KeyPress. It could initially be set to 1, toggled to 0 whenever a true user KeyPress is detected, and toggled back to 1 when a true user KeyRelease is detected.

The code example that was previously in this section is not recommended because it relies on timing, requiring some guesswork regarding the temporal delay delay between a synthesized KeyRelease and the subsequent synthesized KeyPress:

Do It In C

In C, a solution would make use of the XkbSetDetectableAutoRepeat() [L1 ] from the X11 library. With this function a client can ask the server to get a detectable autorepeat sequence i.e. a "Windows-style" one:

press---press---press---press---release

This makes it very easy to detect the true Key events. The advantage of this approach is that the change applies only to the client that makes the request (usually one application). So even if your program crashes, the others aren't touched.

That's the C code to implement it:

#include <tk.h>
#include <X11/Xutil.h>
#include <X11/Xlib.h>
#include <string.h>
#include <tclInt.h>
#include <stdbool.h>

Bool toggleDetectableAutorepeat (Tcl_Interp*, Bool enable);
Display * getDisplay (Tcl_Interp * interp);
int tdar_Cmd(ClientData cdata, Tcl_Interp  *interp, int objc,  Tcl_Obj * CONST objv[]);


//Bool tkewa_running = false;

int tdar_Cmd(ClientData cdata, Tcl_Interp  *interp, int objc,  Tcl_Obj * CONST objv[]) {
     int ret;
     if (objc != 2) {
         Tcl_SetObjResult(interp, Tcl_NewStringObj("Wrong argc", -1));
         return TCL_ERROR;
     }
     int len = 0;
     char * arg;
     arg = Tcl_GetString(objv[1]);
     if ( strcmp(arg, "start") == 0) {
         ret = toggleDetectableAutorepeat(interp, true);
     } else if (strcmp(arg, "stop") == 0) {
         ret = toggleDetectableAutorepeat(interp, false);
     } else {
         Tcl_SetObjResult(interp, Tcl_NewStringObj("Wrong arg: should be \"w start|stop\"", -1));
         ret = TCL_ERROR;
     }
     return ret;
}

int Tdar_Init(Tcl_Interp *interp)  {
    if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to init TclStubs", -1)); 
        return TCL_ERROR;
    }
    if (Tk_InitStubs(interp, TK_VERSION, 0) == NULL) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to init TkStubs", -1)); 
        return TCL_ERROR;
    }
    Tcl_CreateObjCommand(interp,  "tdar", (Tcl_ObjCmdProc *)tdar_Cmd, NULL, NULL);
    return TCL_OK;
}

Display * getDisplay (Tcl_Interp * interp) {
    Display * display;
    display = Tk_Display(Tk_MainWindow(interp));
    return display;
}
int toggleDetectableAutorepeat (Tcl_Interp* interp, Bool enable) {
    Display * display;
    display = getDisplay(interp);
    if(display == NULL) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Error while getting display connection to X server", -1));
        return TCL_ERROR;
    }
    Bool supported;
    Bool result = XkbSetDetectableAutoRepeat(display, enable, &supported);
    XFlush(display);
    if(!supported || enable != result) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Could not set detectable autorepeat", -1));
        return TCL_ERROR;
    } else {
        return TCL_OK;
    }
}

Compile and load!

Note that the DetectableAutoRepeat setting applies to all widgets and toplevels of the application that changed it, however this should not cause problems: autorepeat still works and usually where that functionality is used, only KeyPress events are considered. If for some reaon you want/need DetectableAutorepeat to be set only for certain widgets or toplevels, you can always bind to <FocusIn> and <FocusOut>.

Consuming auto-repeat keys based on idle/busy status

In an attempt to solve this problem via a different route, I experimented with throwing away auto-repeat generated key presses that we have insufficient time to process.

If you're happy to accept the possibility of scheduling other "after idle" events in between your auto-repeat keys then we can use the idle event loop as a means to detect whether we are busy, and the %t time stamp on Release/Press events to distinguish between auto-repeated keys and manually pressed keys. The two together allow us to consume auto-repeat generated key presses and prevent "run-on" in an application that takes some time to process a specific key press.

Bindtags allows us to arbitrarily fix this for any window. As an example:

namespace eval ::auto_repeat {
    set release_time 0
    set key_idle 1

    # Key press. Consume any auto-repeat keys (detected as simultaneous
    # KeyRelease and KeyPress events) when we're busy so we don't
    # start accumulating a large queue.
    proc AutoRepeatPress {k t} {
        variable release_time
        variable key_idle

        if {$t == $release_time && !$key_idle} {
            return -code break;
        }

        set key_idle 0
        after idle {set ::auto_repeat::key_idle 1}
    }

    # Key release. If we've had an idle event loop process since
    # the last press then we're safe to assume auto-repeat is not
    # swamping the application
    proc AutoRepeatRelease {k t} {
        variable release_time
        variable key_idle

        if {$key_idle} {
            set release_time 0
        } else {
            set release_time $t
        }
    }

    bind AutoRepeat <Any-KeyPress>   {::auto_repeat::AutoRepeatPress   %K %t}
    bind AutoRepeat <Any-KeyRelease> {::auto_repeat::AutoRepeatRelease %K %t}
}


# Applies a correction to window $w to remove excess auto-repeated key
# events if processing is failing to keep up.
proc AutoRepeatCorrect {w} {
    bindtags $w [linsert [bindtags $w] 0 AutoRepeat]
}

pack [label .laggy -text "Laggy" -width 20 -height 10 -bg red] -fill both
bind .laggy <Key-a> {puts a; exec sleep 0.2}
bind .laggy <Key-b> {puts b; exec sleep 1}
bind .laggy <Key-c> {puts c; exec sleep 2}
bind .laggy <Any-Enter> {focus %W}

pack [label .fixed -text "Fixed" -width 20 -height 10 -bg blue] -fill both
bind .fixed <Key-a> {puts A; exec sleep 0.2}
bind .fixed <Key-b> {puts B; exec sleep 1}
bind .fixed <Key-c> {puts C; exec sleep 2}
bind .fixed <Any-Enter> {focus %W}

AutoRepeatCorrect .fixed

Clearly this relies on the Press/Release semantics of Unix and would not apply to Windows event methods.