Hi, in the post I want to show how to convert your keyboard in a trackpad.
With the IPhones it is possible to convert the keyboard to a trackpad. Why? No idea but it is funny 😃. I will also turn my keyboard from my laptop into a trackpad.
I will implement it here on Linux and use the X11 libraries for it. I will read out the keyboard as a Linux device, because it saves me lines of code.
First we have to understand how the positions of a display are regulated. A display or screen is like a coordinate system. On the top left you find the position (0; 0) and on the bottom right the position (max. width; max. height). I made a small sketch to show how it works exactly.
Implementation
Now we can already build the code, step by step. The implementation is done in C/C++. First we have to import some libraries. Then we create a few global variables, which we will use several times later in the code. We create an X11 display and a root window. After that we create the maximum width and height as global variables. At last we need the step sizes.
#include <iostream>
#include <X11/Xlib.h>
#include <linux/input.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
Display *dpy; //X11 Display
Window root_window; //Root-Window
int src_width = 0; //Max. width
int src_height = 0; // Max. height
int step_x = 0; //Stepwise x-direction
int step_y = 0; //Stepwise y-direction
We need the variables later for the calculations and as memory. Next, we create a struct that contains the newly determined position of the cursor. The position is a point in the coordinate system, as default values our struct gets (0; 0).
struct CurPos
{
int x = 0;
int y = 0;
};
Now we can use the X11 library to create a display that tells us the screen resolution. With the resolution we can calculate the step size in the code.
void initScreen()
{
dpy = XOpenDisplay(nullptr);
int s_num = XDefaultScreen(dpy);
Screen *screen = ScreenOfDisplay(dpy, s_num);
src_width = screen->width;
src_height = screen->height;
XCloseDisplay(dpy);
dpy = nullptr;
step_x = int((src_width / 14));
step_y = int((src_height / 5));
}
First we open a display and pass a nullptr as ID to open the display from the environment variables. From the display we now read the screen number to query our screen later. From the determined screen, we can now query the height and width. We store the values in the two global variables src_width and src_height. After that we can close the display and set the reference to a nullptr. To determine the step widths, we can simply count the keys in a row as well as the rows of keys. Now we divide the width by the number of keys in a row and the height by the number of rows.
To translate the keys into cursor positions, we need to create another method.
CurPos calcPos(int key)
{
CurPos pos = CurPos();
pos.x = step_x * (key % 14);
if(key < 15 && key > 2) //Has to be optimized to your setup
{
pos.y = step_y * 0;
}
else if(key < 29 && key >=15)//Has to be optimized to your setup
{
pos.y = step_y * 1;
}
else if(key < 44 && key >= 30)//Has to be optimized to your setup
{
pos.y = step_y * 2;
}
else if(key < 56 && key >= 45)//Has to be optimized to your setup
{
pos.y = step_y * 3;
}
else if(key >= 57)//Has to be optimized to your setup
{
pos.y = step_y * 4;
}
printf("CursorPos: %dx%d\n", pos.x, pos.y);
return pos;
}
Here in the code snippet, we do nothing but create a struct. The struct gets the and values assigned to it. To calculate the coordinate I made a simple modulus expansion.
The formula is:
is our step size in direction, our key and the number of keys per row.
To determine the coordinate, we need to work with constraints. These arise in the test run, I'll get to them later. The important thing is that we multiply the step size in direction by the row number. The struct, with the result values, we simply return and can use that later in the other functions.
Now we come to the reading of the keys themselves. Under Linux you have the possibility to reach your keyboard under /dev/input/event0
or /dev/input/by-path/platform-i8042-serio-0-event-kbd
. For this, however, root rights are needed later! Nevertheless, we have the advantage that we can tap the keyboard strokes with a simple FileDescriptor.
void keylog(const char *device)
{
int fd = open(device, O_RDONLY);
struct input_event ev{};
while (true)
{
read(fd, &ev, sizeof(struct input_event));
if(ev.type == 1)
{
printf("key %i state %i\n", ev.code, ev.value);
moveCursor(ev.code);
if(ev.code == 104)
{
break;
}
}
}
}
What we do here exactly is that we first open our keyboard. The keyboard is passed as the passing parameter. We just do a readonly operation and read in the data, but here we don't have ASCII values but structs well in binary format.
All this, we have to store in a struct that is suitable for the events. First we have to query if a key was pressed. For this we have the event type 1. If this is the case, we have to query what the keycode and the value is. The value is not directly the key value, rather in which state the current key is.
- Value = 0: Key was released.
- Value = 1: Key pressed.
- Value = 2: Key still pressed (autorepeat).
We pass the keycodes to the function moveCursor, which later sets the cursor to the respective position. As termination condition, I have selected the key KEY_PAGEUP (picture above) at the arrow keys. The whole thing is executed in a continuous loop.
The Inputevent as Struct: https://www.kernel.org/doc/Documentation/input/input.txt
The keycodes as well as the event types: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h#L65
Now we can implement the moveCursor method.
void moveCursor(int key)
{
printf("Screen: %dx%d\n", src_width, src_height);
printf("Steps: %dx%d\n", step_x, step_y);
CurPos pos = calcPos(key);
dpy = XOpenDisplay(nullptr);
root_window = XRootWindow(dpy, 0);
XSelectInput(dpy, root_window, KeyReleaseMask);
XWarpPointer(dpy, None, root_window, 0, 0, 0, 0, pos.x, pos.y);
XSync(dpy, False);
XCloseDisplay(dpy);
dpy = nullptr;
}
This method, calls the calcPos method with the current key and stores the position in the Struct we defined between. Next, we create a new display as we just did and select our desired screen. With the method XWarpPointer, we can place our cursor anywhere on the screen. With XSync we flush the output buffer and wait until the X-Server has processed the changes. At the end, we close the display again and set it back to a nullptr.
Now we only have to create the main method.
int main(int argc, char** argv)
{
if(argc < 2) {
printf("usage: %s \n", argv[0]);
return 1;
}
initScreen();
keylog(argv[1]);
}
Here we first query for arguments, if this is not the case, we output the message that a device is required. Otherwise we call the method initScreen to determine the resolution and the step sizes. Finally, we call the method keylog and pass our device here.
If we now compile the whole thing, it looks like this:
#include <iostream>
#include <X11/Xlib.h>
#include <linux/input.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
Display *dpy;
Window root_window;
int src_width = 0;
int src_height = 0;
int step_x = 0;
int step_y = 0;
struct CurPos
{
int x = 0;
int y = 0;
};
void initScreen()
{
dpy = XOpenDisplay(nullptr);
int s_num = XDefaultScreen(dpy);
Screen *screen = ScreenOfDisplay(dpy, s_num);
src_width = screen->width;
src_height = screen->height;
XCloseDisplay(dpy);
dpy = nullptr;
step_x = int((src_width / 14));
step_y = int((src_height / 5));
}
CurPos calcPos(int key)
{
CurPos pos = CurPos();
pos.x = step_x * (key % 14);
if(key < 15 && key > 2)
{
pos.y = step_y * 0;
}
else if(key < 29 && key >=15)
{
pos.y = step_y * 1;
}
else if(key < 44 && key >= 30)
{
pos.y = step_y * 2;
}
else if(key < 56 && key >= 45)
{
pos.y = step_y * 3;
}
else if(key >= 57)
{
pos.y = step_y * 4;
}
//pos.y = step_y * (key % 6);
printf("CursorPos: %dx%d\n", pos.x, pos.y);
return pos;
}
void moveCursor(int key)
{
printf("Screen: %dx%d\n", src_width, src_height);
printf("Steps: %dx%d\n", step_x, step_y);
CurPos pos = calcPos(key);
dpy = XOpenDisplay(0);
root_window = XRootWindow(dpy, 0);
XSelectInput(dpy, root_window, KeyReleaseMask);
XWarpPointer(dpy, None, root_window, 0, 0, 0, 0, pos.x, pos.y);
XSync(dpy, False);
XCloseDisplay(dpy);
dpy = nullptr;
}
void keylog(const char *device)
{
int fd = open(device, O_RDONLY);
struct input_event ev{};
while (true)
{
read(fd, &ev, sizeof(struct input_event));
if(ev.type == 1)
{
printf("key %i state %i\n", ev.code, ev.value);
moveCursor(ev.code);
if(ev.code == 104)
{
break;
}
}
}
}
int main(int argc, char** argv)
{
if(argc < 2) {
printf("usage: %s \n", argv[0]);
return 1;
}
initScreen();
keylog(argv[1]);
return 0;
}
To compile the program, we still have to tell g++ to include the X11 libraries. To do this, we simply specify -lX11
as a parameter.
g++ main.cpp -o program -lX11
Now we can start it with...
./program /dev/input/by-path/platform-i8042-serio-0-event-kbd
and have converted the keyboard into a trackpad. In the recording we see how "smooth" that works 😜
In order for the new trackpad to work quite well, we need to do a few optimizations. Just start the program and go through all the keys. This will allow us to adjust the keycodes in the calculation.
As always, the code is available on Github.
I hope you enjoyed it and see you again later 😄