13

I would like to remap the keys on my number pad so that they behave differently depending on how long the key is pressed. Here's an example:

If I hold the Numpad 9 key down for less than 300ms it will send the "previous tab" key command Ctrl+Tab

If I hold the Numpad 9 key down for 300-599ms it will send the "new tab" key command Ctrl+T

If I hold the Numpad 9 key down for 600-899ms it will send the "close tab/window" key command Ctrl+W

If I hold the Numpad 9 key down for more than 899ms, it does nothing in case I missed the time window I wanted.

On Windows I could do this with AutoHotKey and on OS X I could do this with ControllerMate, but I cannot find a tool on UNIX/Linux that allows key remapping based on how long a key is held.

If you are aware of a tool that can solve my problem, please make sure to provide a script or code sample that demonstrates the conditional key hold duration behavior I described above. It doesn't need to be the full code to solve my example, but it should be enough for me to repurpose it for my example.

3
  • This is such an outlandish thing to do. How are you going to time your 600 millisecond press? :D +1 for crazy idea.
    – Wildcard
    Commented Nov 9, 2016 at 9:57
  • Just to add some spice to your life, you should add a time window from 347 to 350 ms that will force-shutdown your computer. ;)
    – Wildcard
    Commented Nov 9, 2016 at 9:58
  • @Wildcard I actually use the number pad on my Razer Naga for this and when I first implemented the idea with AutoHotKey on Windows I used 300-400ms time windows, but now that I've been using this system for a while, I use use time windows about 200ms apart, and I can get the desired time window about 99% of the time. It's very similar to the way you would communicate with morse code.
    – kanoko
    Commented Nov 11, 2016 at 20:39

3 Answers 3

10

I just wrote this in C:

#include <stdio.h>
#include <curses.h>
#include <time.h> //time(0)
#include <sys/time.h>                // gettimeofday()
#include <stdlib.h>

void waitFor (unsigned int secs) {
    //credit: http://stackoverflow.com/a/3930477/1074998
    unsigned int retTime = time(0) + secs;   // Get finishing time.
    while (time(0) < retTime);               // Loop until it arrives.
}

int
main(void) {

    struct timeval t0, t1, t2, t3;
    double elapsedTime;

    clock_t elapsed_t = 0;
    int c = 0x35;

    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);

    halfdelay(5); //increae the number if not working //adjust below `if (elapsedTime <= 0.n)` if this changed
    printf("\nSTART again\n");

    elapsed_t = 0;
    gettimeofday(&t0, NULL);

    float diff;

    int first = 1;
    int atleast_one = 0;

      while( getch() == c) { //while repeating same char, else(ffff ffff in my system) break

            int atleast_one = 1;

            if (first == 1) {
                gettimeofday(&t1, NULL);
                first = 0;
            }

            //printf("DEBUG 1 %x!\n", c);
            gettimeofday(&t2, NULL);
            elapsedTime = (t2.tv_sec - t1.tv_sec) + ((t2.tv_usec - t1.tv_usec)/1000000.0); 

            if (elapsedTime > 1) { //hit max time

                printf("Hit Max, quit now. %f\n", elapsedTime);
                system("gnome-terminal");
                //waitFor(4);

                int cdd;
                while ((cdd = getch()) != '\n' && cdd != EOF);
                endwin();

                exit(0);
            }

            if(halfdelay(1) == ERR) { //increae the number if not working
                //printf("DEBUG 2\n");
                //waitFor(4);
                break; 
                }
            else {
                //printf("DEBUG 3\n");
                }
        }

    if (atleast_one == 0) {
            //gettimeofday(&t1, NULL);
            t1 = t0;
    }

    gettimeofday(&t3, NULL);
    elapsedTime = (t3.tv_sec - t1.tv_sec) + ((t3.tv_usec - t1.tv_usec)/1000000.0); 
    printf("Normal quit %f\n", elapsedTime);
    if (elapsedTime > 0.6) { //this number based on halfdelay above
        system("gedit &");
        //system("xdotool key shift+left &");
        //system("mplayer -vo caca -quiet 'video.mp4' &");
        //waitFor(4);
    }
    else if (elapsedTime <= 0.6) {
        system("xdotool key ctrl+shift+t &");
        //waitFor(4);
    }

    int cdd;
    while ( (cdd = getch() ) != '\n' && cdd != EOF);
    endwin();
    return 0; 

}

Use showkey -a to get the bind keycode:

xb@dnxb:/tmp$ sudo showkey -a

Press any keys - Ctrl-D will terminate this program

^[[24~   27 0033 0x1b #pressed F12
         91 0133 0x5b
         50 0062 0x32
         52 0064 0x34
        126 0176 0x7e
5        53 0065 0x35 #pressed Numpad 5, 5 is the keycode used in `bind`
^C        3 0003 0x03
^D        4 0004 0x04
xb@dnxb:/tmp$ 

Put the bind keycode 5 and its command(e.g. run /tmp/.a.out) in ~/.bashrc:

bind '"5":"/tmp/a.out\n"'

Note that relevant keycode need to change in the source code too (the hex value can get from sudo showkey -a above too):

int c = 0x35;

Compile with (output to /tmp/a.out in my example):

cc filename.c -lcurses

Demonstration:

Numpad 5, short press open new tab, medium press open gedit, and long press open gnome-terminal.

enter image description here

This is not direct applicable in any window on gnome desktop manager, but i think it should give you some idea how (hard) to implement it. It work in Virtual Console(Ctrl+Alt+N) too, and work in some terminal emulator (e.g. konsole, gnome-terminal, xterm).

p/s: I'm not a c programmer, so forgive me if this code is not optimized.

[UPDATE]

The previous answer only work in shell and required focus, so i think parse the /dev/input/eventX is the solution to work in entire X session.

I don't want to reinvent the wheel. I play around with evtest utility and modified the bottom part of evtest.c with my own code:

int onHold = 0;
struct timeval t0;
double elapsedTime;
int hitMax = 0;

while (1) {
    rd = read(fd, ev, sizeof(struct input_event) * 64);

    if (rd < (int) sizeof(struct input_event)) {
        perror("\nevtest: error reading");
        return 1;
    }

    system("echo 'running' >/tmp/l_is_running 2>/tmp/l_isrunning_E &");
    for (i = 0; i < rd / sizeof(struct input_event); i++) {

        //system("date >/tmp/l_date 2>/tmp/l_dateE &");

        if (ev[i].type == EV_KEY) {
            if ( (ev[i].code == 76) ) {

                if (!onHold) {
                    onHold = 1;
                    t0 = ev[i].time;
                    hitMax = 0;
                }
                if (!hitMax) { //to avoid hitMax still do the time checking instruction, you can remove hitMax checking if you think it's overkill, but still hitMax itself is necessary to avoid every (max) 2 seconds will repeatly system();
                    elapsedTime = (ev[i].time.tv_sec - t0.tv_sec) + ((ev[i].time.tv_usec - t0.tv_usec)/1000000.0);
                    printf("elapsedTime: %f\n", elapsedTime);
                    if (elapsedTime > 2) {
                        hitMax = 1;
                        printf("perform max time action\n");
                        system("su - xiaobai -c 'export DISPLAY=:0; gedit &'");
                    }
                }

                if (ev[i].value == 0)  {
                    printf("reseted ...... %d\n", ev[i].value);
                    onHold = 0;
                    if (!hitMax) {
                        if (elapsedTime > 1) { //just ensure lower than max 2 seconds
                            system("su - xiaobai -c 'export DISPLAY=:0; gnome-terminal &'");
                        } else if (elapsedTime > 0.5) { 
                            system("su - xiaobai -c \"export DISPLAY=:0; vlc '/home/xiaobai/Downloads/videos/test/Pokémon Red_Blue_Yellow Gym Leader Battle Theme Remix-CbJTkx7QUJU.mp4' &\"");
                        } else if  (elapsedTime > 0.2) {
                            system("su - xiaobai -c 'export DISPLAY=:0; nautilus &'");
                        }
                    } else { //else's max system() already perform
                        hitMax = 0;
                    }
                }
            }
        }
    }
}

Note that you should change the username (xiaobai is my username) part. And also the if ( (ev[i].code == 76) ) { is my Numpad 5 keycode, you might need to manually print the ev[i].code to double confirm. And of course you should change the video path too :)

Compile and test it directly with (the `` part is in order to get the correct /dev/input/eventN):

$ gcc /home/put_your_path/my_long_press.c -o /home/put_your_path/my_long_press; sudo /home/put_your_path/my_long_press `ls -la /dev/input/by-path/* | grep kbd |  echo "/dev/input/""$(awk -F'/' '{print $NF}')" ` &

Note that /by-id/ doesn't work in Fedora 24, so i change it to /by-path/. Kali no such problem.

My desktop manager is gdm3:

$ cat /etc/X11/default-display-manager 
/usr/sbin/gdm3

So, i put this line in /etc/gdm3/PostLogin/Default to run this command as root on gdm startup (/etc/X11/Xsession.d/* doesn't work):

/home/put_your_path/my_long_press `ls -la /dev/input/by-id/* | grep kbd |  echo "/dev/input/""$(awk -F'/' '{print $NF}')" 2>/tmp/l_gdm` 2>/tmp/l_gdmE &

For unknown reason /etc/gdm/PostLogin/Default doesn't work on Fedora 24' gdm which give me "Permission denied" when check /tmp/l_gdmE log. Manually run no problem though.

Demonstration:

Numpad 5, instant-press (<=0.2 second) will be ignored, short-press (0.2 to 0.5 second) open nautilus, medium-press (0.5 to 1 second) open vlc to play video, long-press (1 to 2 seconds) open gnome-terminal, and timeout-press (2 seconds) open gedit.

enter image description here

I uploaded the full code(only one file) here.

[UPDATE again]

[1] Added multiple keys flow and fixed notify-send failed by define DBUS_SESSION_BUS_ADDRESS. [2] Added XDG_CURRENT_DESKTOP and GNOME_DESKTOP_SESSION_ID to ensure konsole use gnome theme gui (Change it if you're not using gnome).

I updated my code here.

Note that this code doesn't handle for combination keys flow, e.g. Ctrl+t.

UPDATE:

There's multiple device interfaces which the /dev/input/by-path/XXX-eventN entries sequence is random. So I change the command in /etc/gdm3/PostLogin/Default as below (Chesen is my keyboard name, for your case, you should changed it to grep Razer instead):

/your_path/my_long_press "$(cat /proc/bus/input/devices | grep -i Chesen -A 4 | grep -P '^(?=.*sysrq)(?=.*leds)' |  tr ' ' '\n' | ls /dev/input/`grep event`)" 2>/tmp/l_gdmE &

You can try the eventN extract from cat /proc/bus/input/devices | grep -i Razer -A 4:

$ cat /proc/bus/input/devices | grep -i Razer -A 4
N: Name="Razer Razer Naga Chroma"
P: Phys=usb-0000:00:14.0-1.3/input0
S: Sysfs=/devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.3/3-1.3:1.0/0003:1532:0053.0003/input/input6
U: Uniq=
H: Handlers=mouse2 event5 
--
N: Name="Razer Razer Naga Chroma"
P: Phys=usb-0000:00:14.0-1.3/input1
S: Sysfs=/devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.3/3-1.3:1.1/0003:1532:0053.0004/input/input7
U: Uniq=
H: Handlers=sysrq kbd event6 
--
N: Name="Razer Razer Naga Chroma"
P: Phys=usb-0000:00:14.0-1.3/input2
S: Sysfs=/devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.3/3-1.3:1.2/0003:1532:0053.0005/input/input8
U: Uniq=
H: Handlers=sysrq kbd leds event7 
$ 

In this example above, only sudo cat /dev/input/event7 will print bizarre output when click the 12 digits on Razer mouse, which has the pattern "sysrq kbd leds event7" to use in grep -P '^(?=.*sysrq)(?=.*leds)' above (your pattern might vary). sudo cat /dev/input/event6 will print bizarre output only when click the middle up/down key. While sudo cat /dev/input/event5 will print bizarre output when move your mouse and scrolling the wheel.

[Update: Support Replug keyboard cable to reload the program]

The following should be self-explanation:

$ lsusb #to know my keyboard is idVendor 0a81 and idProduct 0101
...
Bus 001 Device 003: ID 0a81:0101 Chesen Electronics Corp. Keyboard

$ cat /etc/udev/rules.d/52-hole-keyboard.rules #add this line with your idVendor and idProduct above in custom udev rules file
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0a81", ATTR{idProduct}=="0101", MODE="0666", GROUP="plugdev", RUN+="/bin/bash -c 'echo 1 > /tmp/chesen_plugged'"

$ cat /usr/local/bin/inotifyChesenPlugged #A long run listener script to listen for modification of /tmp/chesen_plugged #Ensures `inotifywait` has been installed first.
touch /tmp/chesen_plugged
while inotifywait -q -e modify /tmp/chesen_plugged >/dev/null; do
        killall -9 my_long_press
        /usr/local/bin/startLongPress &
done

$ cat /usr/local/bin/startLongPress #the executable script run the long press executable #Change with your pattern as explained above.
#!/bin/bash
<YOUR_DIR>/my_long_press "$(cat /proc/bus/input/devices | grep -i Chesen -A 4 | grep -P '^(?=.*sysrq)(?=.*leds)' |  tr ' ' '\n' | ls /dev/input/`grep event`)" 2>/tmp/l_gdmE) & disown

$ cat /etc/gdm3/PostLogin/Default #the executable startup script run listener and long press script
/usr/local/bin/inotifyChesenPlugged &
/usr/local/bin/startLongPress &
16
  • i assume this method requires a terminal window to be in focus while doing key presses? is there a way around this?
    – kanoko
    Commented Nov 2, 2016 at 18:16
  • @kanoko I've updated the solution.
    – 林果皞
    Commented Nov 4, 2016 at 3:33
  • thx, i really appreciate the effort you put into this. I will try this out. do you think this solution will have a noticeable impact on cpu usage if i set it up with 12 different hotkeys?
    – kanoko
    Commented Nov 5, 2016 at 17:21
  • @kanoko I've updated the code again to play around with multiple keys. IMHO i don't think it's noticeable impact on cpu because 10+ if-else is too subtle, and it only run the checking after read(fd, ev, sizeof(struct input_event) * 64); statement, i.e. it only run the if-else every key press, while i also added if (currCode >= 59) && (currCode <= 81) to limit the range before if-else.
    – 林果皞
    Commented Nov 6, 2016 at 0:23
  • 1
    you are amazing!!! thank you so much for all of your help. if you ever get a chance to try this with an MMO numpad mouse like Razer Naga, i swear it will change your life. I can show you my key mappings if you are interested.
    – kanoko
    Commented Nov 14, 2016 at 22:47
1

You might find a tool that works with a particular set of programs, but there will be no globally-usable tool because time-related behavior is done in applications in X, rather than by the windowing system.

0
0

did you check the Xmodmap ?

xmodmap is a utility for modifying keymaps and pointer button mappings in Xorg

https://wiki.archlinux.org/index.php/Xmodmap

1
  • 3
    But it doesn't know about time delays :-) Commented Nov 1, 2016 at 23:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .