9

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.

  • This is such an outlandish thing to do. How are you going to time your 600 millisecond press? :D +1 for crazy idea. – Wildcard Nov 9 '16 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 Nov 9 '16 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 Nov 11 '16 at 20:39
5

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 &
  • i assume this method requires a terminal window to be in focus while doing key presses? is there a way around this? – kanoko Nov 2 '16 at 18:16
  • @kanoko I've updated the solution. – 林果皞 Nov 4 '16 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 Nov 5 '16 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. – 林果皞 Nov 6 '16 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 Nov 14 '16 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

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

  • 2
    But it doesn't know about time delays :-) – Thomas Dickey Nov 1 '16 at 23:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.