/*
* This file is part of espanso.
*
* Copyright (C) 2019 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see .
*/
#include "bridge.h"
#include "fast_xdo.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern "C" { // Needed to avoid C++ compiler name mangling
#include
}
/*
This code uses the X11 Record Extension to receive keyboard
events. Documentation of this library can be found here:
https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html
We will refer to this extension as RE from now on.
*/
/*
This struct is needed to receive events from the RE.
The funny thing is: it's not defined there, it should though.
The only place this is mentioned is the libxnee library,
so check that out if you need a reference.
*/
typedef union {
unsigned char type ;
xEvent event ;
xResourceReq req ;
xGenericReply reply ;
xError error ;
xConnSetupPrefix setup;
} XRecordDatum;
/*
Connections to the X server, RE recommends 2 connections:
one for recording control and one for reading the recorded data.
*/
Display *data_disp = NULL;
Display *ctrl_disp = NULL;
XRecordRange *record_range;
XRecordContext context;
xdo_t * xdo_context;
// Callback invoked when a new key event occur.
void event_callback (XPointer, XRecordInterceptData*);
KeypressCallback keypress_callback;
void * context_instance;
void register_keypress_callback(KeypressCallback callback) {
keypress_callback = callback;
}
int32_t check_x11() {
Display *check_disp = XOpenDisplay(NULL);
if (!check_disp) {
return -1;
}
XCloseDisplay(check_disp);
return 1;
}
int32_t initialize(void * _context_instance) {
setlocale(LC_ALL, "");
context_instance = _context_instance;
/*
Open the connections to the X server.
RE recommends to open 2 connections to the X server:
one for the recording control and one to read the protocol
data.
*/
ctrl_disp = XOpenDisplay(NULL);
data_disp = XOpenDisplay(NULL);
if (!ctrl_disp || !data_disp) { // Display error
return -1;
}
/*
We must set the ctrl_disp to sync mode, or, when we the enable
context in data_disp, there will be a fatal X error.
*/
XSynchronize(ctrl_disp, True);
int dummy;
// Make sure the X RE is installed in this system.
if (!XRecordQueryVersion(ctrl_disp, &dummy, &dummy)) {
return -2;
}
// Make sure the X Keyboard Extension is installed
if (!XkbQueryExtension(ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy)) {
return -3;
}
// Initialize the record range, that is the kind of events we want to track.
record_range = XRecordAllocRange ();
if (!record_range) {
return -4;
}
record_range->device_events.first = KeyPress;
record_range->device_events.last = ButtonPress;
// We want to get the keys from all clients
XRecordClientSpec client_spec;
client_spec = XRecordAllClients;
// Initialize the context
context = XRecordCreateContext(ctrl_disp, 0, &client_spec, 1, &record_range, 1);
if (!context) {
return -5;
}
if (!XRecordEnableContextAsync(data_disp, context, event_callback, NULL)) {
return -6;
}
xdo_context = xdo_new(NULL);
/**
* Note: We might never get a MappingNotify event if the
* modifier and keymap information was never cached in Xlib.
* The next line makes sure that this happens initially.
*/
XKeysymToKeycode(ctrl_disp, XK_F1);
return 1;
}
int32_t eventloop() {
bool running = true;
int ctrl_fd = XConnectionNumber(ctrl_disp);
int data_fd = XConnectionNumber(data_disp);
while (running)
{
fd_set fds;
FD_ZERO(&fds);
FD_SET(ctrl_fd, &fds);
FD_SET(data_fd, &fds);
timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
int retval = select(max(ctrl_fd, data_fd) + 1,
&fds, NULL, NULL, &timeout);
if (FD_ISSET(data_fd, &fds)) {
XRecordProcessReplies(data_disp);
}
if (FD_ISSET(ctrl_fd, &fds)) {
XEvent event;
XNextEvent(ctrl_disp, &event);
if (event.type == MappingNotify) {
XMappingEvent *e = (XMappingEvent *) &event;
if (e->request == MappingKeyboard) {
XRefreshKeyboardMapping(e);
}
}
}
}
return 1;
}
void cleanup() {
XRecordDisableContext(ctrl_disp, context);
XRecordFreeContext(ctrl_disp, context);
XFree (record_range);
XCloseDisplay(data_disp);
XCloseDisplay(ctrl_disp);
xdo_free(xdo_context);
}
void event_callback(XPointer p, XRecordInterceptData *hook)
{
// Make sure the event comes from the X11 server
if (hook->category != XRecordFromServer) {
XRecordFreeData(hook);
return;
}
// Cast the event payload to a XRecordDatum, needed later to access the fields
// This struct was hard to find and understand. Turn's out that all the
// required data are included in the "event" field of this structure.
// The funny thing is that it's not a XEvent as one might expect,
// but a xEvent, a very different beast defined in the Xproto.h header.
// I suggest you to look at that header if you want to understand where the
// upcoming field where taken from.
XRecordDatum *data = (XRecordDatum*) hook->data;
int event_type = data->type;
int key_code = data->event.u.u.detail;
// In order to convert the key_code into the corresponding string,
// we need to synthesize an artificial XKeyEvent, to feed later to the
// XLookupString function.
XKeyEvent event;
event.display = ctrl_disp;
event.window = data->event.u.focus.window;
event.root = XDefaultRootWindow(ctrl_disp);
event.subwindow = None;
event.time = data->event.u.keyButtonPointer.time;
event.x = 1;
event.y = 1;
event.x_root = 1;
event.y_root = 1;
event.same_screen = True;
event.keycode = key_code;
event.state = data->event.u.keyButtonPointer.state;
event.type = KeyPress;
// Extract the corresponding chars.
std::array buffer;
int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL);
switch (event_type) {
case KeyPress:
//printf ("Press %d %d %s\n", key_code, res, buffer.data());
if (res > 0 && key_code != 22) { // Printable character, but not backspace
keypress_callback(context_instance, buffer.data(), buffer.size(), 0, key_code);
}else{ // Modifier key
keypress_callback(context_instance, NULL, 0, 1, key_code);
}
break;
case ButtonPress: // Send also mouse button presses as "other events"
//printf ("Press button %d\n", key_code);
keypress_callback(context_instance, NULL, 0, 2, key_code);
default:
break;
}
XRecordFreeData(hook);
}
void release_all_keys() {
char keys[32];
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard
for (int i = 0; i<32; i++) {
// Only those that show a keypress should be changed
if (keys[i] != 0) {
for (int k = 0; k<8; k++) {
if ((keys[i] & (1 << k)) != 0) { // Bit by bit check
int key_code = i*8 + k;
XTestFakeKeyEvent(xdo_context->xdpy, key_code, false, CurrentTime);
}
}
}
}
}
void send_string(const char * string) {
// It may happen that when an expansion is triggered, some keys are still pressed.
// This causes a problem if the expanded match contains that character, as the injection
// will not be able to register that keypress (as it is already pressed).
// To solve the problem, before an expansion we get which keys are currently pressed
// and inject a key_release event so that they can be further registered.
release_all_keys();
xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 1000);
}
void send_enter() {
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Return", 1000);
}
void fast_release_all_keys() {
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
char keys[32];
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard
for (int i = 0; i<32; i++) {
// Only those that show a keypress should be changed
if (keys[i] != 0) {
for (int k = 0; k<8; k++) {
if ((keys[i] & (1 << k)) != 0) { // Bit by bit check
int key_code = i*8 + k;
fast_send_event(xdo_context, focused, key_code, 0);
}
}
}
}
XFlush(xdo_context->xdpy);
}
void fast_send_string(const char * string, int32_t delay) {
// It may happen that when an expansion is triggered, some keys are still pressed.
// This causes a problem if the expanded match contains that character, as the injection
// will not be able to register that keypress (as it is already pressed).
// To solve the problem, before an expansion we get which keys are currently pressed
// and inject a key_release event so that they can be further registered.
fast_release_all_keys();
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
int actual_delay = 1;
if (delay > 0) {
actual_delay = delay * 1000;
}
fast_enter_text_window(xdo_context, focused, string, actual_delay);
}
void _fast_send_keycode_to_focused_window(int KeyCode, int32_t count, int32_t delay) {
int keycode = XKeysymToKeycode(xdo_context->xdpy, KeyCode);
Window focused;
int revert_to;
XGetInputFocus(xdo_context->xdpy, &focused, &revert_to);
for (int i = 0; i 0) {
usleep(delay * 1000);
XFlush(xdo_context->xdpy);
}
}
XFlush(xdo_context->xdpy);
}
void fast_send_enter() {
_fast_send_keycode_to_focused_window(XK_Return, 1, 0);
}
void delete_string(int32_t count) {
for (int i = 0; ixdpy, win);
snprintf(buffer, size, "%s", title);
XFree(title);
}
xdo_free(x);
return result;
}
int32_t get_active_window_class(char * buffer, int32_t size) {
xdo_t * x = xdo_new(NULL);
if (!x) {
return -1;
}
// Get the active window
Window win;
int ret = xdo_get_active_window(x, &win);
int result = 1;
if (ret) {
fprintf(stderr, "xdo_get_active_window reported an error\n");
result = -2;
}else{
XClassHint hint;
if (XGetClassHint(x->xdpy, win, &hint)) {
snprintf(buffer, size, "%s", hint.res_class);
XFree(hint.res_name);
XFree(hint.res_class);
}
}
xdo_free(x);
return result;
}
int32_t get_active_window_executable(char *buffer, int32_t size) {
xdo_t * x = xdo_new(NULL);
if (!x) {
return -1;
}
// Get the active window
Window win;
int ret = xdo_get_active_window(x, &win);
int result = 1;
if (ret) {
fprintf(stderr, "xdo_get_active_window reported an error\n");
result = -2;
}else{
// Get the window process PID
char *pid_raw = (char*)get_property(x->xdpy,win, XA_CARDINAL, "_NET_WM_PID", NULL);
if (pid_raw == NULL) {
result = -3;
}else{
int pid = pid_raw[0] | pid_raw[1] << 8 | pid_raw[2] << 16 | pid_raw[3] << 24;
// Get the executable path from it
char proc_path[250];
snprintf(proc_path, 250, "/proc/%d/exe", pid);
readlink(proc_path, buffer, size);
XFree(pid_raw);
}
}
xdo_free(x);
return result;
}
int32_t is_current_window_special() {
char class_buffer[250];
int res = get_active_window_class(class_buffer, 250);
if (res > 0) {
if (strstr(class_buffer, "terminal") != NULL) {
return 1;
}else if (strstr(class_buffer, "URxvt") != NULL) { // urxvt terminal
return 4;
}else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm
return 1;
}else if (strstr(class_buffer, "Termite") != NULL) { // Termite
return 1;
}else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole
return 1;
}else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator
return 1;
}else if (strstr(class_buffer, "stterm") != NULL) { // Simple terminal 3
return 2;
}else if (strstr(class_buffer, "St") != NULL) { // Simple terminal
return 1;
}else if (strstr(class_buffer, "st") != NULL) { // Simple terminal 2
return 1;
}else if (strstr(class_buffer, "Alacritty") != NULL) { // Alacritty terminal
return 1;
}else if (strstr(class_buffer, "Emacs") != NULL) { // Emacs
return 3;
}else if (strstr(class_buffer, "yakuake") != NULL) { // Yakuake terminal
return 1;
}else if (strstr(class_buffer, "Tilix") != NULL) { // Tilix terminal
return 1;
}else if (strstr(class_buffer, "kitty") != NULL) { // kitty terminal
return 1;
}
}
return 0;
}