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 0000000..9608b4e
Binary files /dev/null and b/constants.pyc differ
diff --git a/disabled.py b/disabled.py
new file mode 100644
index 0000000..6c22879
--- /dev/null
+++ b/disabled.py
@@ -0,0 +1,16 @@
+from Xlib import X
+from constants import KEYSYM_MAP, NORMAL
+from mode import mode
+import normal
+
+def disabled_mode(event, keysym, manager):
+ if keysym in KEYSYM_MAP:
+ character = KEYSYM_MAP.get(keysym, 0)
+
+ if event.type == X.KeyPress:
+ if character == '`':
+ mode(NORMAL)
+ manager.teardown()
+ manager.listen(normal.normal_mode)
+ elif event.type == X.KeyRelease:
+ pass
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..aa3c29f
--- /dev/null
+++ b/main.py
@@ -0,0 +1,76 @@
+from Xlib.display import Display
+from Xlib import X
+from Xlib.ext import record
+from Xlib.protocol import rq
+
+import time
+from constants import KEYSYM_MAP, NORMAL
+
+from mode import mode
+from normal import normal_mode
+
+class ShortcutManager():
+ def __init__(self):
+ self.disp2 = Display()
+
+ def is_inkscape(self):
+ window = self.disp2.get_input_focus().focus
+ cls = window.get_wm_class()
+ return cls and cls[0] == 'inkscape'
+
+ def meta_handler(self, handler):
+ def handle(reply):
+ print('yes')
+ data = reply.data
+ while len(data):
+ event, data = rq.EventField(None).parse_binary_value(
+ data, self.disp.display, None, None)
+
+ if not self.is_inkscape():
+ return
+
+ keysym = self.disp.keycode_to_keysym(event.detail, 0)
+ handler(event, keysym, self)
+
+ return handle
+
+ def listen(self, handler):
+ self.disp = Display()
+ root = self.disp.screen().root
+ self.ctx = self.disp.record_create_context(
+ # datum_flags
+ 0,
+ # clients
+ [record.AllClients],
+ # rangers
+ [{
+ 'core_requests': (0, 0),
+ 'core_replies': (0, 0),
+ 'ext_requests': (0, 0, 0, 0),
+ 'ext_replies': (0, 0, 0, 0),
+ 'delivered_events': (0, 0),
+ 'device_events': (0x02, 0x05),
+ 'errors': (0, 0),
+ 'client_started': False,
+ 'client_died': False,
+ }])
+
+ self.disp.record_enable_context(self.ctx, self.meta_handler(handler))
+ self.disp.record_free_context(self.ctx)
+
+ try:
+ while True:
+ time.sleep(1)
+ # Infinite wait, doesn't do anything as no events are grabbed
+ event = root.display.next_event()
+ except:
+ print('exception!')
+
+ def teardown(self):
+ self.disp.record_disable_context(self.ctx)
+
+sm = ShortcutManager()
+mode('INIT')
+time.sleep(1)
+mode(NORMAL)
+sm.listen(normal_mode)
diff --git a/mode.py b/mode.py
new file mode 100644
index 0000000..7f585b2
--- /dev/null
+++ b/mode.py
@@ -0,0 +1,2 @@
+def mode(name):
+ print(name)
diff --git a/mode.pyc b/mode.pyc
new file mode 100644
index 0000000..bc1bbae
Binary files /dev/null and b/mode.pyc differ
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 = '''
+
+'
+
+ 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"""
+ """
+ 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)