From 2843b9d3ce7726dc347db789660128f6258db425 Mon Sep 17 00:00:00 2001 From: Rico Tiongson Date: Thu, 7 Feb 2019 13:14:53 +0800 Subject: [PATCH 1/3] Optimize Regex patterns (#39) * Remove unused device and stamp from swipe_impl * Use constants for regex patterns * Compile regex outside of buffer function to avoid runtime hiccup * Catch dash symbol before event, remove trimming --- src/comfortable-swipe.cpp | 142 ++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/src/comfortable-swipe.cpp b/src/comfortable-swipe.cpp index 4c08ce1..e7643d4 100644 --- a/src/comfortable-swipe.cpp +++ b/src/comfortable-swipe.cpp @@ -56,10 +56,9 @@ extern "C" { /* FORWARD DECLARATIONS */ namespace util { - string join(cstr, string[], int); - string build_gesture_begin(); - string build_gesture_update(); - string build_gesture_end(); + extern const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN; + extern const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN; + extern const char* GESTURE_SWIPE_END_REGEX_PATTERN; map read_config_file(const char*); } @@ -90,7 +89,7 @@ int main(int argc, char** args) { } struct swipe_gesture { - string device, stamp, fingers; + string fingers; string dx, dy, udx, udy; xdo_t* xdo; virtual void on_update() = 0; @@ -207,14 +206,17 @@ namespace service { } namespace service { + + // process regex at compile time + const regex gesture_begin(util::GESTURE_SWIPE_BEGIN_REGEX_PATTERN); + const regex gesture_update(util::GESTURE_SWIPE_UPDATE_REGEX_PATTERN); + const regex gesture_end(util::GESTURE_SWIPE_END_REGEX_PATTERN); + // parses output from libinput debug-events void buffer() { // check first if $user ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); - const regex gesture_begin(util::build_gesture_begin()); - const regex gesture_update(util::build_gesture_update()); - const regex gesture_end(util::build_gesture_end()); string sentence; // read config file auto config = util::read_config_file(conf_filename().data()); @@ -234,25 +236,19 @@ namespace service { auto data = sentence.data(); cmatch matches; if (regex_match(data, matches, gesture_begin)) { - swipe.device = matches[1]; - swipe.stamp = matches[2]; - swipe.fingers = matches[3]; + swipe.fingers = matches[1]; swipe.on_begin(); } else if (regex_match(data, matches, gesture_end)) { - swipe.device = matches[1]; - swipe.stamp = matches[2]; - swipe.fingers = matches[3]; + swipe.fingers = matches[1]; swipe.on_end(); } else if (regex_match(data, matches, gesture_update)) { - swipe.device = matches[1]; - swipe.stamp = matches[2]; - swipe.fingers = matches[3]; - swipe.dx = matches[4]; - swipe.dy = matches[5]; - swipe.udx = matches[6]; - swipe.udy = matches[7]; + swipe.fingers = matches[1]; + swipe.dx = matches[2]; + swipe.dy = matches[3]; + swipe.udx = matches[4]; + swipe.udy = matches[5]; swipe.on_update(); } } @@ -337,50 +333,76 @@ namespace service { namespace util { - string number_regex() { - return "-?\\d+(?:\\.\\d+)"; - } - string join(cstr delim, string arr[], int n) { - string ans = arr[0]; - for (int i = 1; i < n; ++i) { - ans += delim; - ans += arr[i]; - } - return ans; - } + /** + * Regex pattern for the libinput entry for start of swipe. + * Extracts one match for the number of fingers used during the swipe. + * + * eg. event15 GESTURE_SWIPE_BEGIN +34.33s 3 + * ^ + * fingers + */ + const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_BEGIN" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "$" // end of string + ; - string build_gesture_begin() { - string device = "\\s*(\\S+)\\s*"; - string gesture = "\\s*GESTURE_SWIPE_BEGIN\\s*"; - string seconds = "\\s*(\\S+)\\s*"; - string fingers = "\\s*(\\d+)\\s*"; - string arr[] = {device, gesture, seconds, fingers}; - return join("\\s+", arr, 4); - } + /** + * Regex pattern for the libinput entry for the end of swipe. + * Extracts one match for the number of fingers used during the swipe. + * + * eg. event15 GESTURE_SWIPE_END +35.03s 3 + * ^ + * fingers + */ + const char* GESTURE_SWIPE_END_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_END" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "$" // end of string + ; - string build_gesture_update() { - string device = "\\s*(\\S+)\\s*"; - string gesture = "\\s*GESTURE_SWIPE_UPDATE\\s*"; - string seconds = "\\s*(\\S+)\\s*"; - string fingers = "\\s*(\\d+)\\s*"; - string num_1 = "\\s*(" + number_regex() + ")\\s*"; - string num_2 = num_1; - string num_div = num_1 + "/" + num_2; - string num_accel = "\\s*\\(\\s*" + num_div + "\\s*unaccelerated\\s*\\)\\s*"; - string arr[] = {device, gesture, seconds, fingers, num_div, num_accel}; - return join("\\s+", arr, 6); - } + // matches signed decimal numbers (eg. "6.02" "-1.1") + #define CF_NUMBER_REGEX "-?\\d+(?:\\.\\d+)" - string build_gesture_end() { - string device = "\\s*(\\S+)\\s*"; - string gesture = "\\s*GESTURE_SWIPE_END\\s*"; - string seconds = "\\s*(\\S+)\\s*"; - string fingers = "\\s*(\\d+)\\s*"; - string arr[] = {device, gesture, seconds, fingers}; - return join("\\s+", arr, 4); - } + // matches and extracts a space-prefixed signed fraction (eg. "-3.00/ 5.12") + #define CF_NUMBER_DIVISION "\\s*(" CF_NUMBER_REGEX ")/\\s*(" CF_NUMBER_REGEX ")" + + /** + * Regex pattern for the libinput entry for during a swipe. + * Extracts number of fingers used and the speed (normal and accelerated) of the swipe. + * + * eg. event15 GESTURE_SWIPE_UPDATE +34.70s 3 -0.12/ 4.99 (-0.33/13.50 unaccelerated) + * ^ ^ ^ ^ ^ + * fingers dx dy udx udy + */ + const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_UPDATE" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "\\s+" CF_NUMBER_DIVISION // speed (dx/dy) + "\\s+\\(" CF_NUMBER_DIVISION "\\s+unaccelerated\\)" // unaccelerated speed (udx/udy) + "$" // end of string + ; + + // delete macros + #undef CF_NUMBER_DIVISION + #undef CF_NUMBER_EXTRACT + #undef CF_NUMBER_REGEX + /** + * A utility method for reading the config file. + * + * @param filename (const char*) the path of the config file. + */ map read_config_file(const char* filename) { map conf; ifstream fin(filename); From 328ae08ee1e732e1346275765d18572673541ca5 Mon Sep 17 00:00:00 2001 From: Rico Tiongson Date: Thu, 7 Feb 2019 17:39:32 +0800 Subject: [PATCH 2/3] Add `git` to install instructions in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4bfa32..fc211a4 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Comfortable, seamless, and fast 3-finger (and 4-finger) touchpad swipe gestures ## Installation -1. Install libinput and g++ +1. Install git, libinput, and g++ ```bash - sudo apt-get install libinput-tools libxdo-dev g++ + sudo apt-get install git libinput-tools libxdo-dev g++ ``` 2. Clone this repository From e3453c1bbb569ccae23171a59c51413698e3dcfb Mon Sep 17 00:00:00 2001 From: Rico Tiongson Date: Thu, 7 Feb 2019 17:51:47 +0800 Subject: [PATCH 3/3] Perform microoptimizations (#40) * Remove unused device and stamp from swipe_impl * Use constants for regex patterns * Compile regex outside of buffer function to avoid runtime hiccup * Optimize reading of config file * Revert "Optimize reading of config file" This reverts commit 88b85d3941f4936ec9dd300f4bace727c1b6c7f7. * Improve tokenizing of config file * Make sentence string static * Improve README; change default threshold to 20.0 * Add a flag for gesture begin to ignore unneeded update/end * Pre-compute for square of threshold and scale to lessen computation * Compare fingers string with only one digit * Use fgets_unlocked for faster input stream reading * Don't buffer error stream * Set some variables to static * Catch dash symbol before event, remove trimming * Use const char* for conf_filename * Fix error in printing help * Add some test scripts --- README.md | 2 +- src/comfortable-swipe.cpp | 424 +----------------------------------- src/comfortable-swipe.hpp | 439 ++++++++++++++++++++++++++++++++++++++ src/defaults.conf | 7 +- tests/run_tests | 6 + tests/test_regex.cpp | 57 +++++ 6 files changed, 509 insertions(+), 426 deletions(-) create mode 100644 src/comfortable-swipe.hpp create mode 100755 tests/run_tests create mode 100644 tests/test_regex.cpp diff --git a/README.md b/README.md index fc211a4..92024d5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Comfortable swipe makes use of keyboard shortcuts for configurations. The config Property | Description | Default Value | Default Behavior --------- | ----------- | -------------- | ----- -threshold | mouse pixels to activate swipe; higher = less sensitive; floating-point | 0.0 +threshold | mouse pixels to activate swipe; higher = less sensitive; floating-point (Note: Sky is the limit! Can be as large as 1000.0) | 20.0 left3 | 3-finger swipe left | ctrl+shift+Right | switch to right workspace left4 | 4-finger swipe left | ctrl+alt+shift+Right | move window to right workspace right3 | 3-finger swipe right | ctrl+shift+Left | switch to left workspace diff --git a/src/comfortable-swipe.cpp b/src/comfortable-swipe.cpp index e7643d4..18203a1 100644 --- a/src/comfortable-swipe.cpp +++ b/src/comfortable-swipe.cpp @@ -1,76 +1,4 @@ -/* -Comfortable Swipe -by Rico Tiongson - -This program 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. - -This program 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 this program. If not, see . -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#define cstr const string& -#define PROGRAM "/usr/local/bin/comfortable-swipe" -#define CONFIG "/usr/local/share/comfortable-swipe/comfortable-swipe.conf" -using namespace std; - -extern "C" { - // sudo apt install libxdo-dev - #include -} - -/* MASKS FOR GESTURES */ - -#define MSK_THREE_FINGERS 0 -#define MSK_FOUR_FINGERS 1 -#define MSK_NEGATIVE 0 -#define MSK_POSITIVE 2 -#define MSK_HORIZONTAL 0 -#define MSK_VERTICAL 4 - -/* GESTURE MNEMONYMS */ -#define FRESH -1 -#define OPPOSITE (mask ^ MSK_POSITIVE) - -/* FORWARD DECLARATIONS */ - -namespace util { - extern const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN; - extern const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN; - extern const char* GESTURE_SWIPE_END_REGEX_PATTERN; - map read_config_file(const char*); -} - -namespace service { - void buffer(); - void start(); - void stop(); - void restart(); - void autostart(); - void help(); -} - +#include "comfortable-swipe.hpp" /* MAIN DRIVER FUNCTION */ int main(int argc, char** args) { @@ -87,353 +15,3 @@ int main(int argc, char** args) { service::help(); } } - -struct swipe_gesture { - string fingers; - string dx, dy, udx, udy; - xdo_t* xdo; - virtual void on_update() = 0; - virtual void on_begin() = 0; - virtual void on_end() = 0; - swipe_gesture(): xdo(xdo_new(NULL)) {} - ~swipe_gesture() {xdo_free(xdo);} -}; - -const char* const command_map[] = { - "left 3", - "left 4", - "right 3", - "right 4", - "up 3", - "up 4", - "down 3", - "down 4" -}; - -struct swipe_gesture_impl : swipe_gesture { - int screen_num, ix, iy; - float x, y, threshold; - int previous_gesture; - const char** commands; - swipe_gesture_impl( - const float threshold, - const char* left3 /* 000 */, - const char* left4 /* 001 */, - const char* right3 /* 010 */, - const char* right4 /* 011 */, - const char* up3 /* 100 */, - const char* up4 /* 101 */, - const char* down3 /* 110 */, - const char* down4 /* 111 */ - ): swipe_gesture(), threshold(threshold) { - commands = new const char*[8]; - commands[0] = left3; - commands[1] = left4; - commands[2] = right3; - commands[3] = right4; - commands[4] = up3; - commands[5] = up4; - commands[6] = down3; - commands[7] = down4; - } - ~swipe_gesture_impl() { - delete[] commands; - } - void key(const char* cmd) const { - xdo_send_keysequence_window(xdo, CURRENTWINDOW, cmd, 0); - } - void on_begin() override { - xdo_get_mouse_location(xdo, &ix, &iy, &screen_num); - previous_gesture = FRESH; - x = 0; - y = 0; - } - void on_update() override { - x += stof(dx); - y += stof(dy); - // scale threshold to 1/10 when gesture is not fresh - float scale = previous_gesture == FRESH ? - 1.0f : - 0.1f; - if (x*x + y*y > threshold*threshold*(scale*scale)) { - int mask = 0; - if (fingers == "3") mask |= MSK_THREE_FINGERS; else - if (fingers == "4") mask |= MSK_FOUR_FINGERS; - if (abs(x) > abs(y)) { - mask |= MSK_HORIZONTAL; - if (x < 0) mask |= MSK_NEGATIVE; - else mask |= MSK_POSITIVE; - } else { - mask |= MSK_VERTICAL; - if (y < 0) mask |= MSK_NEGATIVE; - else mask |= MSK_POSITIVE; - } - // send command on fresh OR opposite gesture - if (previous_gesture == FRESH or previous_gesture == OPPOSITE) { - x = y = 0; - previous_gesture = mask; - cout << "SWIPE " << command_map[mask] << endl; - key(commands[mask]); - } - } - } - void on_end() override { - } -}; - -// path services -namespace service { - // get the full path of the .conf file - string conf_filename() { - return CONFIG; - } - // get the full path of the .desktop file associated - // with the autostart feature - string autostart_filename() { - static string *filename = NULL; - if (filename == NULL) { - const char* xdg_config = getenv("XDG_CONFIG_HOME"); - string config( - xdg_config == NULL - ? string(getenv("HOME")) + "/.config" - : xdg_config - ); - filename = new string(config - + "/autostart/comfortable-swipe.desktop"); - } - return *filename; - } -} - -namespace service { - - // process regex at compile time - const regex gesture_begin(util::GESTURE_SWIPE_BEGIN_REGEX_PATTERN); - const regex gesture_update(util::GESTURE_SWIPE_UPDATE_REGEX_PATTERN); - const regex gesture_end(util::GESTURE_SWIPE_END_REGEX_PATTERN); - - // parses output from libinput debug-events - void buffer() { - // check first if $user - ios::sync_with_stdio(false); - cin.tie(0); cout.tie(0); - string sentence; - // read config file - auto config = util::read_config_file(conf_filename().data()); - // initialize gesture handler - swipe_gesture_impl swipe( - config.count("threshold") ? stof(config["threshold"]) : 0.0, - config["left3"].c_str(), - config["left4"].c_str(), - config["right3"].c_str(), - config["right4"].c_str(), - config["up3"].c_str(), - config["up4"].c_str(), - config["down3"].c_str(), - config["down4"].c_str() - ); - while (getline(cin, sentence)) { - auto data = sentence.data(); - cmatch matches; - if (regex_match(data, matches, gesture_begin)) { - swipe.fingers = matches[1]; - swipe.on_begin(); - } - else if (regex_match(data, matches, gesture_end)) { - swipe.fingers = matches[1]; - swipe.on_end(); - } - else if (regex_match(data, matches, gesture_update)) { - swipe.fingers = matches[1]; - swipe.dx = matches[2]; - swipe.dy = matches[3]; - swipe.udx = matches[4]; - swipe.udy = matches[5]; - swipe.on_update(); - } - } - } - // starts service - void start() { - int x = system("stdbuf -oL -eL libinput debug-events | " PROGRAM " buffer"); - } - // stops service - void stop() { - // kill all comfortable-swipe, except self - char* buffer = new char[20]; - FILE* pipe = popen("pgrep -f comfortable-swipe", "r"); - if (!pipe) throw std::runtime_error("stop command failed"); - string kill = "kill"; - while (!feof(pipe)) { - if (fgets(buffer, 20, pipe) != NULL) { - int pid = atoi(buffer); - if (pid != getpid()) { - kill += " " + to_string(pid); - } - } - } - int result = system(kill.data()); - delete[] buffer; - pclose(pipe); - } - // stops then starts service - void restart() { - service::stop(); - service::start(); - } - // toggle automatically start application on startup - void autostart() { - string path = autostart_filename(); - if (ifstream(path.data()).good()) { - // file found, delete it - if (remove(path.data()) != 0) - cerr << "Error: failed to switch off autostart. " - << "Maybe the autostart file is in use?" - << endl; - else - cout << "Autostart switched off" << endl; - } else { - // file not found, create it - int result = system(("mkdir -p $(dirname " + path + ")").data()); - ofstream fout(path.data()); - if (result != 0 || !fout.good()) - cerr << "Error: failed to switch on autostart. " - << "Are you sure you have the permissions?" - << endl; - else { - fout << - "[Desktop Entry]\n" - "Type=Application\n" - "Exec=bash -c \"" PROGRAM " start\"\n" - "Hidden=false\n" - "NoDisplay=false\n" - "X-GNOME-Autostart-enabled=true\n" - "Name=Comfortable Swipe\n" - "Comment=3 or 4 touchpad gestures\n"; - cout << "Autostart switched on" << endl; - } - } - } - // shows help - void help() { - puts("comfortable-swipe [start|stop|restart|autostart|buffer|help]"); - puts(""); - puts("start - starts 3/4-finger gesture service"); - puts("stop - stops 3/4-finger gesture service"); - puts("restart - stops then starts 3/4-finger gesture service"); - puts("autostart - automatically run on startup (toggleable)"); - puts("buffer - parses output of libinput debug-events"); - puts("help - shows the help dialog"); - puts(""); - puts((("Configuration file can be found in ") + conf_filename()).data()); - } -} - -/* UTILITY FUNCTIONS */ - -namespace util { - - - /** - * Regex pattern for the libinput entry for start of swipe. - * Extracts one match for the number of fingers used during the swipe. - * - * eg. event15 GESTURE_SWIPE_BEGIN +34.33s 3 - * ^ - * fingers - */ - const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN = - "^" // start of string - "[ -]event\\d+" // event - "\\s+GESTURE_SWIPE_BEGIN" // gesture - "\\s+\\S+" // timestamp - "\\s+(\\d+)" // fingers - "$" // end of string - ; - - /** - * Regex pattern for the libinput entry for the end of swipe. - * Extracts one match for the number of fingers used during the swipe. - * - * eg. event15 GESTURE_SWIPE_END +35.03s 3 - * ^ - * fingers - */ - const char* GESTURE_SWIPE_END_REGEX_PATTERN = - "^" // start of string - "[ -]event\\d+" // event - "\\s+GESTURE_SWIPE_END" // gesture - "\\s+\\S+" // timestamp - "\\s+(\\d+)" // fingers - "$" // end of string - ; - - // matches signed decimal numbers (eg. "6.02" "-1.1") - #define CF_NUMBER_REGEX "-?\\d+(?:\\.\\d+)" - - // matches and extracts a space-prefixed signed fraction (eg. "-3.00/ 5.12") - #define CF_NUMBER_DIVISION "\\s*(" CF_NUMBER_REGEX ")/\\s*(" CF_NUMBER_REGEX ")" - - /** - * Regex pattern for the libinput entry for during a swipe. - * Extracts number of fingers used and the speed (normal and accelerated) of the swipe. - * - * eg. event15 GESTURE_SWIPE_UPDATE +34.70s 3 -0.12/ 4.99 (-0.33/13.50 unaccelerated) - * ^ ^ ^ ^ ^ - * fingers dx dy udx udy - */ - const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN = - "^" // start of string - "[ -]event\\d+" // event - "\\s+GESTURE_SWIPE_UPDATE" // gesture - "\\s+\\S+" // timestamp - "\\s+(\\d+)" // fingers - "\\s+" CF_NUMBER_DIVISION // speed (dx/dy) - "\\s+\\(" CF_NUMBER_DIVISION "\\s+unaccelerated\\)" // unaccelerated speed (udx/udy) - "$" // end of string - ; - - // delete macros - #undef CF_NUMBER_DIVISION - #undef CF_NUMBER_EXTRACT - #undef CF_NUMBER_REGEX - - /** - * A utility method for reading the config file. - * - * @param filename (const char*) the path of the config file. - */ - map read_config_file(const char* filename) { - map conf; - ifstream fin(filename); - if (!fin.is_open()) { - cerr << "file \"" << filename << "\" does not exist!" << endl; - exit(1); - } - string line, key, token, buffer, value; - int line_number = 0; - while (getline(fin, line)) { - ++line_number; - istringstream is(line); - buffer.clear(); - while (is >> token) { - if (token[0] == '#') - break; - buffer += token; - } - if (buffer.empty()) - continue; - auto id = buffer.find('='); - if (id == string::npos) { - cerr << "error in conf file: " << filename << endl; - cerr << "equal sign expected in line " << line_number << endl; - exit(1); - } - key = buffer.substr(0, id); - value = buffer.substr(id + 1); - conf[key] = value; - } - return conf; - } - -} diff --git a/src/comfortable-swipe.hpp b/src/comfortable-swipe.hpp new file mode 100644 index 0000000..9b87052 --- /dev/null +++ b/src/comfortable-swipe.hpp @@ -0,0 +1,439 @@ +/* +Comfortable Swipe +by Rico Tiongson + +This program 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. + +This program 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 this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define cstr const string& +#define PROGRAM "/usr/local/bin/comfortable-swipe" +#define CONFIG "/usr/local/share/comfortable-swipe/comfortable-swipe.conf" +using namespace std; + +extern "C" { + // sudo apt install libxdo-dev + #include +} + +/* MASKS FOR GESTURES */ + +#define MSK_THREE_FINGERS 0 +#define MSK_FOUR_FINGERS 1 +#define MSK_NEGATIVE 0 +#define MSK_POSITIVE 2 +#define MSK_HORIZONTAL 0 +#define MSK_VERTICAL 4 + +/* GESTURE MNEMONYMS */ +#define FRESH -1 +#define OPPOSITE (mask ^ MSK_POSITIVE) + +/* FORWARD DECLARATIONS */ + +namespace util { + extern const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN; + extern const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN; + extern const char* GESTURE_SWIPE_END_REGEX_PATTERN; + map read_config_file(const char*); +} + +namespace service { + void buffer(); + void start(); + void stop(); + void restart(); + void autostart(); + void help(); +} + +struct swipe_gesture { + string fingers; + string dx, dy, udx, udy; + xdo_t* xdo; + virtual void on_update() = 0; + virtual void on_begin() = 0; + virtual void on_end() = 0; + swipe_gesture(): xdo(xdo_new(NULL)) {} + ~swipe_gesture() {xdo_free(xdo);} +}; + +const char* const command_map[] = { + "left 3", + "left 4", + "right 3", + "right 4", + "up 3", + "up 4", + "down 3", + "down 4" +}; + +struct swipe_gesture_impl : swipe_gesture { + int screen_num, ix, iy; + float x, y, threshold_squared; + int previous_gesture; + const char** commands; + swipe_gesture_impl( + const float threshold, + const char* left3 /* 000 */, + const char* left4 /* 001 */, + const char* right3 /* 010 */, + const char* right4 /* 011 */, + const char* up3 /* 100 */, + const char* up4 /* 101 */, + const char* down3 /* 110 */, + const char* down4 /* 111 */ + ): swipe_gesture(), threshold_squared(threshold*threshold) { + commands = new const char*[8]; + commands[0] = left3; + commands[1] = left4; + commands[2] = right3; + commands[3] = right4; + commands[4] = up3; + commands[5] = up4; + commands[6] = down3; + commands[7] = down4; + } + ~swipe_gesture_impl() { + delete[] commands; + } + void key(const char* cmd) const { + xdo_send_keysequence_window(xdo, CURRENTWINDOW, cmd, 0); + } + void on_begin() override { + xdo_get_mouse_location(xdo, &ix, &iy, &screen_num); + previous_gesture = FRESH; + x = 0; + y = 0; + } + void on_update() override { + x += stof(dx); + y += stof(dy); + // scale threshold to 1/10 when gesture is not fresh + float scale = previous_gesture == FRESH + ? 1.00f + : 0.01f; // square root of 1/10 + if (x*x + y*y > threshold_squared*scale) { + int mask = 0; + if (fingers[0] == '3') mask |= MSK_THREE_FINGERS; else + if (fingers[0] == '4') mask |= MSK_FOUR_FINGERS; + if (abs(x) > abs(y)) { + mask |= MSK_HORIZONTAL; + if (x < 0) mask |= MSK_NEGATIVE; + else mask |= MSK_POSITIVE; + } else { + mask |= MSK_VERTICAL; + if (y < 0) mask |= MSK_NEGATIVE; + else mask |= MSK_POSITIVE; + } + // send command on fresh OR opposite gesture + if (previous_gesture == FRESH or previous_gesture == OPPOSITE) { + x = y = 0; + previous_gesture = mask; + cout << "SWIPE " << command_map[mask] << endl; + key(commands[mask]); + } + } + } + void on_end() override { + } +}; + +// path services +namespace service { + // get the full path of the .conf file + const char* conf_filename() { + return CONFIG; + } + // get the full path of the .desktop file associated + // with the autostart feature + string autostart_filename() { + static string *filename = NULL; + if (filename == NULL) { + const char* xdg_config = getenv("XDG_CONFIG_HOME"); + string config( + xdg_config == NULL + ? string(getenv("HOME")) + "/.config" + : xdg_config + ); + filename = new string(config + + "/autostart/comfortable-swipe.desktop"); + } + return *filename; + } +} + +namespace service { + + // process regex at compile time + const regex gesture_begin(util::GESTURE_SWIPE_BEGIN_REGEX_PATTERN); + const regex gesture_update(util::GESTURE_SWIPE_UPDATE_REGEX_PATTERN); + const regex gesture_end(util::GESTURE_SWIPE_END_REGEX_PATTERN); + + // parses output from libinput debug-events + void buffer() { + int c = clock(); + // check first if $user + ios::sync_with_stdio(false); + cin.tie(0); cout.tie(0); + // read config file + auto config = util::read_config_file(conf_filename()); + // initialize gesture handler + swipe_gesture_impl swipe( + config.count("threshold") ? stof(config["threshold"]) : 0.0, + config["left3"].c_str(), + config["left4"].c_str(), + config["right3"].c_str(), + config["right4"].c_str(), + config["up3"].c_str(), + config["up4"].c_str(), + config["down3"].c_str(), + config["down4"].c_str() + ); + // start reading lines from input one by one + static const int MAX_LINE_LENGTH = 256; + static char data[MAX_LINE_LENGTH]; + static cmatch matches; + bool flag_begin = false; + while (fgets_unlocked(data, MAX_LINE_LENGTH, stdin) != NULL) { + if (!flag_begin) { + if (regex_match(data, matches, gesture_begin)) { + swipe.fingers = matches[1]; + swipe.on_begin(); + flag_begin = true; + } + } else { + if (regex_match(data, matches, gesture_update)) { + swipe.fingers = matches[1]; + swipe.dx = matches[2]; + swipe.dy = matches[3]; + swipe.udx = matches[4]; + swipe.udy = matches[5]; + swipe.on_update(); + } else if (regex_match(data, matches, gesture_end)) { + swipe.fingers = matches[1]; + swipe.on_end(); + flag_begin = false; + } + } + } + } + // starts service + void start() { + int x = system("stdbuf -oL -e0 libinput debug-events | " PROGRAM " buffer"); + } + // stops service + void stop() { + // kill all comfortable-swipe, except self + char* buffer = new char[20]; + FILE* pipe = popen("pgrep -f comfortable-swipe", "r"); + if (!pipe) throw std::runtime_error("stop command failed"); + string kill = "kill"; + while (!feof(pipe)) { + if (fgets(buffer, 20, pipe) != NULL) { + int pid = atoi(buffer); + if (pid != getpid()) { + kill += " " + to_string(pid); + } + } + } + int result = system(kill.data()); + delete[] buffer; + pclose(pipe); + } + // stops then starts service + void restart() { + service::stop(); + service::start(); + } + // toggle automatically start application on startup + void autostart() { + cstr path = autostart_filename(); + if (ifstream(path.data()).good()) { + // file found, delete it + if (remove(path.data()) != 0) + cerr << "Error: failed to switch off autostart. " + << "Maybe the autostart file is in use?" + << endl; + else + cout << "Autostart switched off" << endl; + } else { + // file not found, create it + int result = system(("mkdir -p $(dirname " + path + ")").data()); + ofstream fout(path.data()); + if (result != 0 || !fout.good()) + cerr << "Error: failed to switch on autostart. " + << "Are you sure you have the permissions?" + << endl; + else { + fout << + "[Desktop Entry]\n" + "Type=Application\n" + "Exec=bash -c \"" PROGRAM " start\"\n" + "Hidden=false\n" + "NoDisplay=false\n" + "X-GNOME-Autostart-enabled=true\n" + "Name=Comfortable Swipe\n" + "Comment=3 or 4 touchpad gestures\n"; + cout << "Autostart switched on" << endl; + } + } + } + // shows help + void help() { + puts("comfortable-swipe [start|stop|restart|autostart|buffer|help]"); + puts(""); + puts("start - starts 3/4-finger gesture service"); + puts("stop - stops 3/4-finger gesture service"); + puts("restart - stops then starts 3/4-finger gesture service"); + puts("autostart - automatically run on startup (toggleable)"); + puts("buffer - parses output of libinput debug-events"); + puts("help - shows the help dialog"); + puts(""); + printf("Configuration file can be found in %s", conf_filename()); + } +} + +/* UTILITY FUNCTIONS */ + +namespace util { + + + /** + * Regex pattern for the libinput entry for start of swipe. + * Extracts one match for the number of fingers used during the swipe. + * + * eg. event15 GESTURE_SWIPE_BEGIN +34.33s 3 + * ^ + * fingers + */ + const char* GESTURE_SWIPE_BEGIN_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_BEGIN" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "\\s*$" // end of string + ; + + /** + * Regex pattern for the libinput entry for the end of swipe. + * Extracts one match for the number of fingers used during the swipe. + * + * eg. event15 GESTURE_SWIPE_END +35.03s 3 + * ^ + * fingers + */ + const char* GESTURE_SWIPE_END_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_END" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "\\s*$" // end of string + ; + + // matches signed decimal numbers (eg. "6.02" "-1.1") + #define CF_NUMBER_REGEX "-?\\d+(?:\\.\\d+)" + + // matches and extracts a space-prefixed signed fraction (eg. "-3.00/ 5.12") + #define CF_NUMBER_DIVISION "\\s*(" CF_NUMBER_REGEX ")/\\s*(" CF_NUMBER_REGEX ")" + + /** + * Regex pattern for the libinput entry for during a swipe. + * Extracts number of fingers used and the speed (normal and accelerated) of the swipe. + * + * eg. event15 GESTURE_SWIPE_UPDATE +34.70s 3 -0.12/ 4.99 (-0.33/13.50 unaccelerated) + * ^ ^ ^ ^ ^ + * fingers dx dy udx udy + */ + const char* GESTURE_SWIPE_UPDATE_REGEX_PATTERN = + "^" // start of string + "[ -]event\\d+" // event + "\\s+GESTURE_SWIPE_UPDATE" // gesture + "\\s+\\S+" // timestamp + "\\s+(\\d+)" // fingers + "\\s+" CF_NUMBER_DIVISION // speed (dx/dy) + "\\s+\\(" CF_NUMBER_DIVISION "\\s+unaccelerated\\)" // unaccelerated speed (udx/udy) + "\\s*$" // end of string + ; + + // delete macros + #undef CF_NUMBER_DIVISION + #undef CF_NUMBER_EXTRACT + #undef CF_NUMBER_REGEX + + /** + * A utility method for reading the config file. + * + * @param filename (const char*) the path of the config file. + */ + map read_config_file(const char* filename) { + map conf; + ifstream fin(filename); + if (!fin.is_open()) { + cerr << "file \"" << filename << "\" does not exist!" << endl; + exit(1); + } + static string line, token[2]; + int line_number = 0; + while (getline(fin, line)) { + ++line_number; + token[0].clear(); + token[1].clear(); + int length = line.length(); + int equal_flag = 0; + for (int i = 0; i < length; ++i) { + if (line[i] == '#') + break; + if (line[i] == '=') { + if (++equal_flag > 1) { + cerr << "error in conf file " << filename << endl; + cerr << "multiple equal signs in line " << line_number << endl; + exit(1); + } + } else if (!isspace(line[i])) { + token[equal_flag].push_back(line[i]); + } + } + // ignore empty lines + if (equal_flag == 0 && token[0].length() == 0) + continue; + if (equal_flag == 0) { + cerr << "error in conf file: " << filename << endl; + cerr << "equal sign expected in line " << line_number << endl; + exit(1); + } + if (equal_flag == 1 && token[1].length() > 0) { + conf[token[0]] = token[1]; + } + } + return conf; + } + +} diff --git a/src/defaults.conf b/src/defaults.conf index a2f0dc0..9b92261 100644 --- a/src/defaults.conf +++ b/src/defaults.conf @@ -13,8 +13,11 @@ # Threshold # Tweak this value depending on the sensitivity of your mousepad to perform # gestures. A higher value means less sensitive. -# Default: threshold = 0.0 -threshold = 0.0 +# +# (Note: Sky is the limit! Can be as large as 1000.0) +# +# Default: threshold = 20.0 +threshold = 20.0 ############################# # THREE / FOUR FINGER SWIPE # diff --git a/tests/run_tests b/tests/run_tests new file mode 100755 index 0000000..20da97d --- /dev/null +++ b/tests/run_tests @@ -0,0 +1,6 @@ +#!/bin/sh + +DIR=$(dirname $0) +g++ -std=c++11 -O2 $DIR/test_regex.cpp -lxdo -o test.out || exec "Test aborted" +./test.out || rm test.out +rm test.out \ No newline at end of file diff --git a/tests/test_regex.cpp b/tests/test_regex.cpp new file mode 100644 index 0000000..0f35907 --- /dev/null +++ b/tests/test_regex.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include "../src/comfortable-swipe.hpp" + +namespace test { + void gesture_begin_should_match_regex(); + void gesture_update_should_match_regex(); + void gesture_end_should_match_regex(); +} + +int main() { + std::cout << "(1) Testing gesture_begin_should_match_regex()" << std::endl; + test::gesture_begin_should_match_regex(); + + std::cout << "(2) Testing gesture_begin_should_match_regex()" << std::endl; + test::gesture_update_should_match_regex(); + + std::cout << "(3) Testing gesture_begin_should_match_regex()" << std::endl; + test::gesture_end_should_match_regex(); + std::cout << "ALL TEST PASSED" << std::endl; +} + +namespace test { + void gesture_begin_test(const char* data, const char* expected_fingers) { + std::cout << " testing against \"" << data << "\"..."; + std::cmatch matches; + auto result = std::regex_match(data, matches, service::gesture_begin); + assert(result != 0); + assert((string) matches[1] == expected_fingers); + std::cout << "PASSED" << std::endl; + } + void gesture_begin_should_match_regex() { + test::gesture_begin_test(" event15 GESTURE_SWIPE_BEGIN +34.33s 3\n", "3"); + test::gesture_begin_test("-event4 GESTURE_SWIPE_BEGIN +3.12s 4\n", "4"); + test::gesture_begin_test("-event7 GESTURE_SWIPE_BEGIN +4.72s 3\n", "3"); + test::gesture_begin_test(" event9 GESTURE_SWIPE_BEGIN +45.80s 4\n", "4"); + } + void gesture_update_should_match_regex() { + const char* data = " event15 GESTURE_SWIPE_UPDATE +34.70s 3 -0.12/ 4.99 (-0.33/13.50 unaccelerated)\n"; + std::cmatch matches; + auto result = std::regex_match(data, matches, service::gesture_update); + assert(result != 0); + assert((string) matches[1] == "3"); + assert((string) matches[2] == "-0.12"); + assert((string) matches[3] == "4.99"); + assert((string) matches[4] == "-0.33"); + assert((string) matches[5] == "13.50"); + } + void gesture_end_should_match_regex() { + const char* data = " event15 GESTURE_SWIPE_END +35.03s 3\n"; + std::cmatch matches; + auto result = std::regex_match(data, matches, service::gesture_end); + assert(result != 0); + assert((string) matches[1] == "3"); + } +} \ No newline at end of file