From 278de2d7f69898b07bdd69edba7d84f9ddf2acab Mon Sep 17 00:00:00 2001 From: Gilles Castel <66gilles99@gmail.com> Date: Fri, 8 Mar 2019 14:16:01 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + Pipfile | 20 +++++ Pipfile.lock | 43 +++++++++++ __init__.py | 0 clipboard.py | 27 +++++++ constants.py | 61 ++++++++++++++++ constants.pyc | Bin 0 -> 1415 bytes disabled.py | 16 ++++ main.py | 76 +++++++++++++++++++ mode.py | 2 + mode.pyc | Bin 0 -> 300 bytes normal.py | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++ press.py | 13 ++++ rofi.py | 30 ++++++++ styles.py | 119 ++++++++++++++++++++++++++++++ testing.py | 34 +++++++++ vim.py | 76 +++++++++++++++++++ 17 files changed, 718 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 __init__.py create mode 100644 clipboard.py create mode 100644 constants.py create mode 100644 constants.pyc create mode 100644 disabled.py create mode 100644 main.py create mode 100644 mode.py create mode 100644 mode.pyc create mode 100644 normal.py create mode 100644 press.py create mode 100644 rofi.py create mode 100644 styles.py create mode 100644 testing.py create mode 100644 vim.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..759bd60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/* +data/**/* diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..3d94c0f --- /dev/null +++ b/Pipfile @@ -0,0 +1,20 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[packages] + +evdev = "*" +xlib = "*" + + +[dev-packages] + + + +[requires] + +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..a22d616 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,43 @@ +{ + "_meta": { + "hash": { + "sha256": "5364a12c57cbeca5c66a33ca1fccbd7cdf27cc83c2279c959a97b0ff1a140903" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "evdev": { + "hashes": [ + "sha256:2dd67291be20e70643e8ef6f2381efc10e0c6e44a32abb3c1db74996ea3b0351" + ], + "index": "pypi", + "version": "==1.1.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "xlib": { + "hashes": [ + "sha256:60b7cd5d90f5d5922d9ce27b61589c07d970796558d417461db7b66e366bc401", + "sha256:8eee67dad83ef4b82bbbfa85d51eeb20c79d12b119fe25aa1d27bd602ff82212" + ], + "index": "pypi", + "version": "==0.21" + } + }, + "develop": {} +} diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clipboard.py b/clipboard.py new file mode 100644 index 0000000..b511313 --- /dev/null +++ b/clipboard.py @@ -0,0 +1,27 @@ +import subprocess + +def copy(string, target=None): + extra_args = [] + if target != None: + extra_args += ['-target', target] + + return subprocess.run( + ['xclip', '-selection', 'c'] + extra_args, + universal_newlines=True, + input=string + ) + +def get(target=None): + extra_args = [] + if target != None: + extra_args += ['-target', target] + + result = subprocess.run( + ['xclip', '-selection', 'c', '-o'] + extra_args, + stdout=subprocess.PIPE, + universal_newlines=True + ) + + # returncode = result.returncode + stdout = result.stdout.strip() + return stdout diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..ae9ad52 --- /dev/null +++ b/constants.py @@ -0,0 +1,61 @@ +KEYSYM_MAP = { + 65307: "ESC", + 32: "SPACE", + 39: "'", + 44: ",", + 45: "-", + 46: ".", + 47: "/", + 48: "0", + 49: "1", + 50: "2", + 51: "3", + 52: "4", + 53: "5", + 54: "6", + 55: "7", + 56: "8", + 57: "9", + 59: ";", + 61: "=", + 91: "[", + 92: "\\", + 93: "]", + 96: "`", + 97: "a", + 98: "b", + 99: "c", + 100: "d", + 101: "e", + 102: "f", + 103: "g", + 104: "h", + 105: "i", + 106: "j", + 107: "k", + 108: "l", + 109: "m", + 110: "n", + 111: "o", + 112: "p", + 113: "q", + 114: "r", + 115: "s", + 116: "t", + 117: "u", + 118: "v", + 119: "w", + 120: "x", + 121: "y", + 122: "z", +} + +TARGET = 'image/x-inkscape-svg' + +NORMAL = '' +VIM = 'Vim' +STYLE = 'Style' +SAVE_STYLE = 'Save style' +OBJECT = 'Object' +SAVE_OBJECT = 'Save object' +DISABLED = 'Disabled' diff --git a/constants.pyc b/constants.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9608b4e195f30dcfc4cdc6fd4f216437f5cb4ea1 GIT binary patch literal 1415 zcmcJNX;;%w5Qg7c0mXga6<1IxWi2407Kn;cKvGcx2yG{nfo_t3;J@&vdOUvCH>q>@ z1tuqXp1Jqj+}xQ-{yNqE^LKrvgnw6(e8ngDkpQ$00!RWGIRZ%%M_ z1+qr0Lkh@vOluKd0#bs<+zPxT#D~WOZ@_DTl;JVUoABBo3XiGYg4?yt(u7fs9CuQ0 z{9(|-z@5)dI@%G7MB~{bsta`=$qaVESFZQ-h0qX|+)H)B7o{Z3m@7!%qc!<>>Rx z_F2?wvvb0OiXXId*e&e&9YGX^tqkXiTYf1FPS{L6j>BV&r!*)OE56Kj9cL6qi{Cp- z?ow_omz|Fn=d%lMoP4aaa|^lbOx&`I)4ArJ`K1{rUgl;Ooi#TO<2@(dIg|M?-ZPq+ zYsT@O({6TR#+mBkW;lEWQ4ggz>s3Ep)(TIOmU_6Mkp!h3ZG?xkJfutYS`Zd$VbIe! gh|lx9T9@65{~{`F5Y;7_X#3B1d$RRNYvSWd3=Ay{3{gM^BSWwT6Hp`>BoD*RAa$HTqK1LN z-2<296;l2fHsxp Lq}qWk1K9-t71}@T literal 0 HcmV?d00001 diff --git a/normal.py b/normal.py new file mode 100644 index 0000000..f63d2be --- /dev/null +++ b/normal.py @@ -0,0 +1,199 @@ +from press import press +from Xlib import X +from evdev import ecodes + +from clipboard import copy +from constants import KEYSYM_MAP, NORMAL, VIM, SAVE_OBJECT, OBJECT, SAVE_STYLE, STYLE, DISABLED + +from mode import mode +from vim import open_vim +import styles +import disabled + +pressed = set() +shift = False + +def normal_mode(event, keysym, manager): + global shift + if event.state & X.ControlMask: + # there are modifiers + # eg. X.ControlMask + # ~or X.ShiftMask~ + return + + if keysym in KEYSYM_MAP: + character = KEYSYM_MAP.get(keysym, 0) + + if event.type == X.KeyPress: + if event.state & X.ShiftMask: + shift = True + + pressed.add(character) + + elif event.type == X.KeyRelease: + + if 'ESC' in pressed: + press(ecodes.KEY_F1) + pressed.clear() + + if character in pressed: + if len(pressed) >= 2: + fire(pressed) + else: + key = pressed.pop() + if key == 'w': + press(ecodes.KEY_F6) # pencil + if key == 'e': + press(ecodes.KEY_F5) # ellipse + if key == 'r': + press(ecodes.KEY_F4) # rectangle + if key == 't': + manager.teardown() + mode(VIM) + compile_latex = shift # shift-t compiles latex + open_vim(compile_latex) + mode(NORMAL) + manager.listen(normal_mode) + if key == 'y': + press(ecodes.KEY_F8) #text + + if key == '`': + manager.teardown() + mode(DISABLED) + manager.listen(disabled.disabled_mode) + + if shift and key == 'a': + mode(SAVE_OBJECT) + manager.teardown() + styles.save_object_mode() + shift = False + mode(NORMAL) + manager.listen(normal_mode) + elif key == 'a': + mode(OBJECT) + manager.teardown() + manager.listen(styles.object_mode) + + if shift and key == 's': + mode(SAVE_STYLE) + manager.teardown() + styles.save_style_mode() + shift = False + manager.listen(normal_mode) + mode(NORMAL) + elif key == 's': + mode(STYLE) + manager.teardown() + manager.listen(styles.style_mode) + + + if key == 'd': + press(ecodes.KEY_F7) # dropper + if key == 'f': + press(ecodes.KEY_F6, [ecodes.KEY_LEFTSHIFT]) # bezier + if key == 'h': + press(ecodes.KEY_H, [ecodes.KEY_LEFTSHIFT]) # flip + + if key == 'z': + press(ecodes.KEY_DELETE) #delete + + if key == 'x': + press(ecodes.KEY_5, [ecodes.KEY_LEFTSHIFT]) # snap + + if key == 'v': + press(ecodes.KEY_V, [ecodes.KEY_LEFTSHIFT]) # flip + + pressed.clear() + + if shift: + shift = False + + if not (event.state & X.ShiftMask): + shift = False + + +def fire(combination): + pt = 1.327 # pixels + w = 0.4 * pt + thick_width = 0.8 * pt + very_thick_width = 1.2 * pt + + style = { + 'stroke-opacity': 1 + } + + if {'s', 'a', 'd', 'g', 'h', 'x', 'e'} & combination: + style['stroke'] = 'black' + style['stroke-width'] = w + style['marker-end'] = 'none' + style['marker-start'] = 'none' + style['stroke-dasharray'] = 'none' + else: + style['stroke'] = 'none' + + if 'g' in combination: + w = thick_width + style['stroke-width'] = w + + if 'h' in combination: + w = very_thick_width + style['stroke-width'] = w + + if 'a' in combination: + style['marker-end'] = f'url(#marker-arrow-{w})' + + if 'x' in combination: + style['marker-start'] = f'url(#marker-arrow-{w})' + style['marker-end'] = f'url(#marker-arrow-{w})' + + if 'd' in combination: + style['stroke-dasharray'] = f'{w},{2*pt}' + + if 'e' in combination: + style['stroke-dasharray'] = f'{3*pt},{3*pt}' + + if 'f' in combination: + style['fill'] = 'black' + style['fill-opacity'] = 0.12 + if 'b' in combination: + style['fill'] = 'black' + style['fill-opacity'] = 1 + if not {'f', 'b'} & combination: + style['fill'] = 'none' + style['fill-opacity'] = 1 + + + if style['fill'] == 'none' and style['stroke'] == 'none': + return + + svg = ''' + + +''' + + if ('marker-end' in style and style['marker-end'] != 'none') or \ + ('marker-start' in style and style['marker-start'] != 'none'): + svg += f''' + + + + + + + +''' + + style_string = ';'.join('{}: {}'.format(key, value) + for key, value in sorted(style.items(), key=lambda x: x[0]) + ) + svg += f'' + + copy(svg, target='image/x-inkscape-svg') + press(ecodes.KEY_V, [ecodes.KEY_LEFTSHIFT, ecodes.KEY_LEFTCTRL]) diff --git a/press.py b/press.py new file mode 100644 index 0000000..9cec61a --- /dev/null +++ b/press.py @@ -0,0 +1,13 @@ +from evdev.uinput import UInput +from evdev import ecodes + +def press(key, modifiers=[], uinput=UInput()): + for mod in modifiers: + uinput.write(ecodes.EV_KEY, mod, 1) + uinput.write(ecodes.EV_KEY, key, 1) + uinput.write(ecodes.EV_KEY, key, 0) + for mod in modifiers: + uinput.write(ecodes.EV_KEY, mod, 0) + + # synchronize ... + uinput.syn() diff --git a/rofi.py b/rofi.py new file mode 100644 index 0000000..101c1c6 --- /dev/null +++ b/rofi.py @@ -0,0 +1,30 @@ +import subprocess + +def rofi(prompt, options, rofi_args=[], fuzzy=True): + optionstr = '\n'.join(option.replace('\n', ' ') for option in options) + args = ['rofi', '-sort', '-no-levenshtein-sort'] + if fuzzy: + args += ['-matching', 'fuzzy'] + args += ['-dmenu', '-p', prompt, '-format', 's', '-i'] + args += rofi_args + args = [str(arg) for arg in args] + + + result = subprocess.run(args, input=optionstr, stdout=subprocess.PIPE, universal_newlines=True) + returncode = result.returncode + stdout = result.stdout.strip() + + selected = stdout.strip() + try: + index = [opt.strip() for opt in options].index(selected) + except ValueError: + index = -1 + + if returncode == 0: + key = 0 + elif returncode == 1: + key = -1 + elif returncode > 9: + key = returncode - 9 + + return key, index, selected diff --git a/styles.py b/styles.py new file mode 100644 index 0000000..4b4bcd6 --- /dev/null +++ b/styles.py @@ -0,0 +1,119 @@ +from evdev import ecodes +from pathlib import Path +from time import sleep +import os +from Xlib import X + +from clipboard import copy, get +from constants import KEYSYM_MAP, TARGET, NORMAL +from press import press +from rofi import rofi +import normal + +from mode import mode + +pressed = [] + +script_path = Path(os.path.realpath(__file__)).parents[0] + +data_dirs = { + 'style': script_path / 'data' / 'styles', + 'object': script_path / 'data' / 'objects', +} + + +def check(what, manager, name): + files = list(data_dirs[what].iterdir()) + names = [f.stem for f in files] + + filtered = list(i for i, n in enumerate(names) if n.startswith(name)) + # print(name,', '.join(n for n in names if n.startswith(name))) + + if len(filtered) == 0: + pressed.clear() + return back_to_normal(manager) + + if len(filtered) == 1: + index = filtered[0] + copy(files[index].read_text(), target=TARGET) + if what == 'style': + press(ecodes.KEY_V, [ecodes.KEY_LEFTCTRL, ecodes.KEY_LEFTSHIFT]) + else: + press(ecodes.KEY_V, [ecodes.KEY_LEFTCTRL]) + + sleep(0.5) # Give the user some time when an object is added. + return back_to_normal(manager) + + +def back_to_normal(manager): + mode(NORMAL) + pressed.clear() + manager.teardown() + manager.listen(normal.normal_mode) + + +def paste_mode(what, event, keysym, manager): + if event.state & X.ControlMask: + # there are modifiers + # eg. X.ControlMask + # ~or X.ShiftMask~ + return + + if keysym in KEYSYM_MAP: + character = KEYSYM_MAP.get(keysym, 0) + + if event.type == X.KeyPress: + if character == 'ESC': + if len(pressed) == 0: + return back_to_normal(manager) + else: + pressed.clear() + else: + pressed.append(character) + return check(what, manager, ''.join(pressed)) + + elif event.type == X.KeyRelease: + pass + +def save_mode(what): + sleep(0.1) + press(ecodes.KEY_C, [ecodes.KEY_LEFTCTRL]) + sleep(0.1) + svg = get(TARGET) + if not 'svg' in svg: + return + + directory = data_dirs[what] + files = list(directory.iterdir()) + names = [f.stem for f in files] + key, index, name = rofi( + 'Save as', + names, + ['-theme', '~/.config/rofi/ribbon.rasi'], + fuzzy=False + ) + + if index != -1: + f = files[index]; + key, index, yn = rofi( + f'Overwrite {name}?', + ['y', 'n'], + ['-theme', '~/.config/rofi/ribbon.rasi', '-auto-select'], + fuzzy=False + ) + if yn == 'n': + return + + (directory / f'{name}.svg').write_text(get(TARGET)) + +def style_mode(event, keysym, manager): + paste_mode('style', event, keysym, manager) + +def object_mode(event, keysym, manager): + paste_mode('object', event, keysym, manager) + +def save_style_mode(): + save_mode('style') + +def save_object_mode(): + save_mode('object') diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..dd6f3f8 --- /dev/null +++ b/testing.py @@ -0,0 +1,34 @@ +from Xlib.display import Display +from Xlib import X +import time +import signal +import sys + +disp=Display() +screen=disp.screen() +root=screen.root + +def handle_event(evt): + print(evt) + +def main(): + inkscapes = [ + w for w in screen.root.query_tree().children + if w.get_wm_class() and w.get_wm_class()[0] == 'inkscape' + ] + + print(inkscapes) + + for inkscape in inkscapes: + inkscape.grab_key(10, X.NONE, True,X.GrabModeAsync, X.GrabModeAsync) + + signal.signal(signal.SIGALRM, lambda a,b:sys.exit(1)) + signal.alarm(10) + # grab_key(62, X.NONE) + while True: + evt=root.display.next_event() + if evt.type in [X.KeyPress, X.KeyRelease]: #ignore X.MappingNotify(=34) + handle_event(evt) + +if __name__ == '__main__': + main() diff --git a/vim.py b/vim.py new file mode 100644 index 0000000..31d5d36 --- /dev/null +++ b/vim.py @@ -0,0 +1,76 @@ +import os +import tempfile +import subprocess +from constants import TARGET +from clipboard import copy +from press import press +from evdev import ecodes + +def open_vim(compile_latex): + f = tempfile.NamedTemporaryFile(mode='w+', delete=False) + + f.write('$$') + f.close() + + subprocess.run([ + 'urxvt', + '-fn', 'xft:Iosevka Term:pixelsize=24', + '-geometry', '60x5', + '-name', 'popup-bottom-center', + '-e', "vim", + "-u", "~/.minimal-tex-vimrc", + f"{f.name}", + ]) + + latex = "" + with open(f.name, 'r') as g: + latex = g.read().strip() + + os.remove(f.name) + + if latex != '$$': + if not compile_latex: + svg = f""" + + {latex} + """ + copy(svg, target=TARGET) + else: + m = tempfile.NamedTemporaryFile(mode='w+', delete=False) + m.write(r""" + \documentclass[12pt,border=12pt]{standalone} + + \usepackage[utf8]{inputenc} + \usepackage[T1]{fontenc} + \usepackage{textcomp} + \usepackage[dutch]{babel} + \usepackage{amsmath, amssymb} + \newcommand{\R}{\mathbb R} + + \begin{document} + """ + latex + r"""\end{document}""") + m.close() + + working_directory = tempfile.gettempdir() + subprocess.run( + ['pdflatex', m.name], + cwd=working_directory, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + subprocess.run( + ['pdf2svg', f'{m.name}.pdf', f'{m.name}.svg'], + cwd=working_directory + ) + + with open(f'{m.name}.svg') as svg: + subprocess.run( + ['xclip', '-selection', 'c', '-target', TARGET], + stdin=svg + ) + + press(ecodes.KEY_V, [ecodes.KEY_LEFTCTRL]) + press(ecodes.KEY_ESC)