/* * 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*); int error_callback(Display *display, XErrorEvent *error); KeypressCallback keypress_callback; X11ErrorCallback x11_error_callback; void * context_instance; void register_keypress_callback(KeypressCallback callback) { keypress_callback = callback; } void register_error_callback(X11ErrorCallback callback) { x11_error_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); // Setup a custom error handler XSetErrorHandler(&error_callback); /** * 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); } int error_callback(Display *display, XErrorEvent *error) { x11_error_callback(context_instance, error->error_code, error->request_code, error->minor_code); return 0; } 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; }