453 lines
12 KiB
C++
453 lines
12 KiB
C++
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "bridge.h"
|
|
|
|
#include <locale.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <array>
|
|
#include <string.h>
|
|
|
|
#include <X11/Xlibint.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/cursorfont.h>
|
|
#include <X11/keysymdef.h>
|
|
#include <X11/keysym.h>
|
|
#include <X11/extensions/record.h>
|
|
#include <X11/extensions/XTest.h>
|
|
#include <X11/XKBlib.h>
|
|
#include <X11/Xatom.h>
|
|
extern "C" { // Needed to avoid C++ compiler name mangling
|
|
#include <xdo.h>
|
|
}
|
|
|
|
/*
|
|
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 = KeyRelease;
|
|
|
|
// 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<char, 10> buffer;
|
|
int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL);
|
|
|
|
switch (event_type) {
|
|
case KeyPress:
|
|
//printf ("%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;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
XRecordFreeData(hook);
|
|
}
|
|
|
|
void send_string(const char * string) {
|
|
xdo_enter_text_window(xdo_context, CURRENTWINDOW, string, 12000);
|
|
}
|
|
|
|
void delete_string(int32_t count) {
|
|
for (int i = 0; i<count; i++) {
|
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "BackSpace", 8000);
|
|
}
|
|
}
|
|
|
|
void trigger_paste() {
|
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+v", 8000);
|
|
}
|
|
|
|
void trigger_terminal_paste() {
|
|
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000);
|
|
}
|
|
|
|
// SYSTEM MODULE
|
|
|
|
// Function taken from the wmlib tool source code
|
|
char *get_property(Display *disp, Window win,
|
|
Atom xa_prop_type, char *prop_name, unsigned long *size)
|
|
{
|
|
unsigned long ret_nitems, ret_bytes_after, tmp_size;
|
|
Atom xa_prop_name, xa_ret_type;
|
|
unsigned char *ret_prop;
|
|
int ret_format;
|
|
char *ret;
|
|
int size_in_byte;
|
|
|
|
xa_prop_name = XInternAtom(disp, prop_name, False);
|
|
|
|
if (XGetWindowProperty(disp, win, xa_prop_name, 0, 4096 / 4, False,
|
|
xa_prop_type, &xa_ret_type, &ret_format, &ret_nitems,
|
|
&ret_bytes_after, &ret_prop) != Success)
|
|
return NULL;
|
|
|
|
if (xa_ret_type != xa_prop_type)
|
|
{
|
|
XFree(ret_prop);
|
|
return NULL;
|
|
}
|
|
|
|
switch(ret_format) {
|
|
case 8: size_in_byte = sizeof(char); break;
|
|
case 16: size_in_byte = sizeof(short); break;
|
|
case 32: size_in_byte = sizeof(long); break;
|
|
}
|
|
|
|
tmp_size = size_in_byte * ret_nitems;
|
|
ret = (char*) malloc(tmp_size + 1);
|
|
memcpy(ret, ret_prop, tmp_size);
|
|
ret[tmp_size] = '\0';
|
|
|
|
if (size) *size = tmp_size;
|
|
|
|
XFree(ret_prop);
|
|
return ret;
|
|
}
|
|
|
|
// Function taken from Window Management Library for Ruby
|
|
char *xwm_get_win_title(Display *disp, Window win)
|
|
{
|
|
char *wname = (char*)get_property(disp,win, XA_STRING, "WM_NAME", NULL);
|
|
char *nwname = (char*)get_property(disp,win, XInternAtom(disp,
|
|
"UTF8_STRING", False), "_NET_WM_NAME", NULL);
|
|
|
|
return nwname ? nwname : (wname ? wname : NULL);
|
|
}
|
|
|
|
int32_t get_active_window_name(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{
|
|
char * title = xwm_get_win_title(x->xdpy, 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_terminal() {
|
|
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) { // Manjaro terminal
|
|
return 1;
|
|
}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;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
} |