diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 11 | ||||
-rw-r--r-- | incantation.toml | 5 | ||||
-rw-r--r-- | key_maps.go | 44 | ||||
-rw-r--r-- | keyboard_reader.c | 80 | ||||
-rw-r--r-- | keyboard_reader.h | 6 | ||||
-rw-r--r-- | keyboard_writer.c | 39 | ||||
-rw-r--r-- | keyboard_writer.h | 7 | ||||
-rw-r--r-- | main.go | 115 |
12 files changed, 328 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8671268 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +incantation diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ceb6d5 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: incantation + +incantation: + go build + +clean: + rm incantation + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec2e8ed --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Incantation + +A Linux/X11 based text expander. @@ -0,0 +1,8 @@ +module incantation + +go 1.12 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/sirupsen/logrus v1.4.2 +) @@ -0,0 +1,11 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/incantation.toml b/incantation.toml new file mode 100644 index 0000000..b765f25 --- /dev/null +++ b/incantation.toml @@ -0,0 +1,5 @@ +[bindings] + +[bindings.1] +keypress = ",,hw" +text = "hello world" diff --git a/key_maps.go b/key_maps.go new file mode 100644 index 0000000..699e9ee --- /dev/null +++ b/key_maps.go @@ -0,0 +1,44 @@ +package main + +var x11Map = map[string]string{ + " ": "space", + "!": "exclam", + "@": "at", + "#": "numbersign", + "$": "dollar", + "%": "percent", + "^": "asciicircum", + "&": "ampersand", + "*": "asterisk", + "(": "parenleft", + ")": "parenright", + "~": "asciitilde", + "`": "grave", + "-": "minus", + "_": "underscore", + "=": "equal", + "+": "plus", + "[": "bracketleft", + "]": "bracketright", + "{": "braceleft", + "}": "braceright", + "\\": "backslash", + "|": "bar", + ";": "semicolon", + ":": "colon", + "'": "apostrophe", + "\"": "quotedbl", + ",": "comma", + "<": "less", + ".": "period", + ">": "greater", + "/": "slash", + "?": "question", +} + +var needsShift = []string{ + "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", + "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{", "}", "|", + "A", "S", "D", "F", "G", "H", "J", "K", "L", "L", ":", "\"", + "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", +} diff --git a/keyboard_reader.c b/keyboard_reader.c new file mode 100644 index 0000000..319f702 --- /dev/null +++ b/keyboard_reader.c @@ -0,0 +1,80 @@ +// xkbcat: Logs X11 keypresses, globally. + +#include <X11/XKBlib.h> +#include <X11/extensions/XInput2.h> + +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> + +const char * DEFAULT_DISPLAY = ":0"; +const bool DEFAULT_PRINT_UP = false; + +char * get_key() { + + const char * hostname = DEFAULT_DISPLAY; + bool printKeyUps = DEFAULT_PRINT_UP; + + // Set up X + Display * disp = XOpenDisplay(hostname); + if (NULL == disp) { + fprintf(stderr, "Cannot open X display: %s\n", hostname); + exit(1); + } + + // Test for XInput 2 extension + int xi_opcode; + int queryEvent, queryError; + if (! XQueryExtension(disp, "XInputExtension", &xi_opcode, + &queryEvent, &queryError)) { + // XXX Test version >=2 + fprintf(stderr, "X Input extension not available\n"); return ""; + } + + // Register events + Window root = DefaultRootWindow(disp); + XIEventMask m; + m.deviceid = XIAllMasterDevices; + m.mask_len = XIMaskLen(XI_LASTEVENT); + m.mask = calloc(m.mask_len, sizeof(char)); + XISetMask(m.mask, XI_RawKeyPress); + XISetMask(m.mask, XI_RawKeyRelease); + XISelectEvents(disp, root, &m, 1); + XSync(disp, false); + free(m.mask); + + while (1) { // Forever + XEvent event; + XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie; + XNextEvent(disp, &event); + + if (XGetEventData(disp, cookie) && + cookie->type == GenericEvent && + cookie->extension == xi_opcode) + { + switch (cookie->evtype) + { + case XI_RawKeyRelease: if (!printKeyUps) continue; + case XI_RawKeyPress: { + XIRawEvent *ev = cookie->data; + + // Ask X what it calls that key + KeySym s = XkbKeycodeToKeysym(disp, ev->detail, 0, 0); + if (NoSymbol == s) continue; + char *str = XKeysymToString(s); + if (NULL == str) continue; + + + if (printKeyUps) printf("%s", cookie->evtype == XI_RawKeyPress ? "+" : "-"); + char *ret = malloc(1000); + sprintf(ret, "%s", str); + XCloseDisplay(disp); + return ret; + break; + } + } + } + fflush(stdout); + } +} diff --git a/keyboard_reader.h b/keyboard_reader.h new file mode 100644 index 0000000..4eadc37 --- /dev/null +++ b/keyboard_reader.h @@ -0,0 +1,6 @@ +#ifndef KEYBOARD_READER_H +#define KEYBOARD_READER_H + +char * get_key(); + +#endif diff --git a/keyboard_writer.c b/keyboard_writer.c new file mode 100644 index 0000000..66196ba --- /dev/null +++ b/keyboard_writer.c @@ -0,0 +1,39 @@ +#include "keyboard_writer.h" +#include <stdio.h> +#include <X11/X.h> +#include <X11/Xlib.h> +#include <X11/extensions/XTest.h> + + +int x11_key(char *zh){ + Display *dpy; + dpy = XOpenDisplay(NULL); + + KeySym sym = XStringToKeysym(zh); + KeyCode code = XKeysymToKeycode(dpy,sym); + XTestFakeKeyEvent(dpy, code, True, 1); + XTestFakeKeyEvent(dpy, code, False, 1); + + XFlush( dpy ); + XCloseDisplay( dpy ); + + return 0; +} + +int x11_key_shift(char *zh){ + Display *dpy; + dpy = XOpenDisplay(NULL); + + KeySym sym = XStringToKeysym(zh); + KeyCode code = XKeysymToKeycode(dpy,sym); + XTestFakeKeyEvent(dpy, 50, True, 1); + XTestFakeKeyEvent(dpy, code, True, 1); + XTestFakeKeyEvent(dpy, code, False, 1); + XTestFakeKeyEvent(dpy, 50, False, 1); + + XFlush( dpy ); + XCloseDisplay( dpy ); + + return 0; +} + diff --git a/keyboard_writer.h b/keyboard_writer.h new file mode 100644 index 0000000..995615f --- /dev/null +++ b/keyboard_writer.h @@ -0,0 +1,7 @@ +#ifndef KEYBOARD_WRITER_H +#define KEYBOARD_WRITER_H + +int x11_key(char *zh); +int x11_key_shift(char *zh); + +#endif @@ -0,0 +1,115 @@ +package main + +// #cgo linux LDFLAGS: -lX11 -lXtst -lXi +// #include "keyboard_writer.h" +// #include "keyboard_reader.h" +import "C" + +import ( + "errors" + "fmt" + "github.com/BurntSushi/toml" + "github.com/sirupsen/logrus" + "reflect" + "strings" + "time" +) + +type tomlConfig struct { + Bindings map[string]binding +} + +type binding struct { + Keypress string + Text string +} + +func main() { + // Startup Load the config + config, err := get_config() + if err != nil { + logrus.Println(err) + return + } + for { + expansion := wait_for_autocomplete(config) + logrus.Println(fmt.Sprintf("executing: %s", expansion)) + time.Sleep(100 * time.Millisecond) + for _, v := range expansion { + cs := C.CString(char_to_x11(string(v))) + if stringInSlice(string(v), needsShift) { + C.x11_key_shift(cs) + } else { + C.x11_key(cs) + } + } + } +} + +func get_config() (tomlConfig, error) { + var config tomlConfig + if _, err := toml.DecodeFile("incantation.toml", &config); err != nil { + return config, errors.New("Can't load config file") + } + return config, nil +} + +func wait_for_autocomplete(config tomlConfig) string { + max_length := 10 + buffer := make([]string, max_length) + for { + key_press := x11_to_char(C.GoString(C.get_key())) + // Cut down length + if len(buffer) == max_length { + buffer = buffer[1:] + } + buffer = append(buffer, key_press) + logrus.Println(strings.Join(buffer, ", ")) + // Check for expansion matches, windowing through buffer + for _, binding := range config.Bindings { + // Build kepress string into slice for slice comparison + keypress_slice := make([]string, 0) + for _, v := range binding.Keypress { + keypress_slice = append(keypress_slice, string(v)) + } + for i := 0; i < len(buffer)-len(keypress_slice)+1; i++ { + tmp_buf := buffer[i : i+len(keypress_slice)] + if reflect.DeepEqual(tmp_buf, keypress_slice) { + // Erase the keypresses + for range binding.Keypress { + cs := C.CString("BackSpace") + C.x11_key(cs) + } + return binding.Text + } + } + } + } +} + +func char_to_x11(code string) string { + for k, v := range x11Map { + if k == code { + return v + } + } + return code +} + +func x11_to_char(code string) string { + for k, v := range x11Map { + if v == code { + return k + } + } + return code +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} |