Last active
January 9, 2020 08:17
-
-
Save Versatilus/850e6b5aadc5785da5bb629101ed3727 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# Author: Eric Lewis Paulson (Versatilus) | |
# License: Public Domain | |
# except`url_fix` adapted from werkzeug | |
# see https://github.com/pallets/werkzeug/blob/master/LICENSE.rst | |
# Version: 20200109-001525 (-8 UTC) | |
r""" | |
A simple set of voice commands for mangling text with the clipboard. | |
Currently it will: | |
* normalize escaped Windows file paths for string literals | |
* normalize unescaped Windows file paths | |
* normalize POSIX (non-Windows) file paths | |
* normalize simple URLs | |
* pass text unaltered | |
** NEW: pluralize words for a speech grammar, ie: "file" becomes "(file | files)" | |
** NEW: Now able to generate quoted and unquoted lists. Feel free to experiment! | |
** NEW: Import into and export out of the Caster multi clipboard, emulating it if | |
it isn't there. | |
The following actions are available: | |
* copy selection | |
* cut selection | |
* type to the input focus | |
* paste to the input focus | |
* print to the console | |
* export to the clipboard | |
The following are valid targets/secondary actions: | |
* export to the clipboard | |
* print to the console | |
* type to the input focus (focus default) | |
* paste to the input focus | |
Example: | |
Given the selected string: "C:/Example/path/.caster/data//\\\\/transformers.toml" | |
Spoken command | |
"copy Windows path" | |
copies the selected text to the clipboard as | |
"C:\\Example\\path\\.caster\\data\\transformers.toml" | |
which can then be converted to a POSIX path and pasted with spoken command | |
"paste POSIX path" | |
resulting in the following text pasted to the input focus | |
"C:/Example/path/.caster/data/transformers.toml" | |
The following compound operation takes the clipboard contents, converts it | |
in place to an unescaped Windows path, and prints it to the console. | |
Spoken command | |
"export simple Windows path and print to the console" | |
Uses the clipboard contents | |
"C:\\Example\\path\\.caster\\data\\transformers.toml" | |
which converts it to the unescaped Windows path | |
"C:\Example\path\.caster\data\transformers.toml" | |
and also prints the text to the console. | |
Read the source code for phrasing options and beware of possible ambiguous commands. | |
""" | |
from __future__ import print_function, unicode_literals | |
import sys | |
import time | |
from functools import partial | |
from os.path import normpath | |
from dragonfly import (Choice, Clipboard, Function, Grammar, IntegerRef, | |
MappingRule) | |
from future import standard_library | |
standard_library.install_aliases() | |
try: | |
from castervoice.lib.actions import Key, Text | |
except ImportError: | |
from dragonfly import Key, Text | |
try: | |
import regex as re | |
except ImportError: | |
import re | |
try: | |
from castervoice.lib.clipboard import Clipboard | |
from castervoice.lib import navigation, settings, utilities | |
CASTER = True | |
except ImportError: | |
print("Failed to import Caster. Emulating Caster multiclipboard.") | |
CASTER = False | |
try: | |
navigation._CLIP.has_key("fail") | |
except NameError: | |
class navigation(object): | |
_CLIP = {} | |
KEYPRESS_WAIT = 1.0 | |
ON_WINDOWS = sys.platform.startswith("win") | |
grammar = Grammar("clipboard Swiss Army knife") | |
# This function was blatantly adapted from werkzeug via Stack Overflow. | |
def url_fix(s): | |
"""Sometimes you get an URL by a user that just isn't a real | |
URL because it contains unsafe characters like ' ' and so on. This | |
function can fix some of the problems in a similar way browsers | |
handle data entered by the user: | |
>>> url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)') | |
'http://de.wikipedia.org/wiki/Elf%20%28Begriffskl%C3%A4rung%29' | |
""" | |
from urllib.parse import urlsplit, urlunsplit, quote, quote_plus | |
scheme, netloc, path, qs, anchor = urlsplit(s) | |
path = quote(path, '/%') | |
qs = quote_plus(qs, ':&=') | |
return urlunsplit((scheme, netloc, path, qs, anchor)) | |
def normalize_windows_path(input_text): | |
output_text = normpath(input_text) | |
if ON_WINDOWS: | |
output_text = re.sub(r"([\\]+)", r"\\\\", output_text) | |
else: | |
output_text = re.sub(r"([/]+)", r"\\\\", output_text) | |
return output_text | |
def normalize_simple_windows_path(input_text): | |
output_text = normpath(input_text) | |
if not ON_WINDOWS: | |
output_text = re.sub(r"([/]+)", r"\\", output_text) | |
return output_text | |
def normalize_posix_path(input_text): | |
output_text = normpath(input_text) | |
if ON_WINDOWS: | |
output_text = re.sub(r"([\\]+)", r"/", output_text) | |
return output_text | |
def normalize_url(input_text): | |
output_text = re.sub(r"([\\]+)", r"/", input_text) | |
output_text = re.sub(r"([/]+)", r"/", output_text) | |
return url_fix(output_text.replace("\n", "").replace("\r", "")) | |
def normalize_list(input_text, normalizer=lambda x: x, enclosure='""'): | |
output_list = input_text.split("\n") | |
output_list = [ | |
'{1}{0}{2}'.format( | |
normalizer(line.strip()), enclosure[:1], enclosure[1:]).strip() | |
for line in output_list | |
] | |
return "\n".join(output_list) | |
def make_plural(input_text): | |
# ignore possessive | |
if input_text.endswith("'s"): | |
return input_text | |
elif input_text.endswith("s"): | |
return "({0} | {0}es)".format(input_text) | |
return "({0} | {0}s)".format(input_text) | |
ACTIONS = { | |
"copy": 1, | |
"type": 2, | |
"print": 3, | |
"paste": 4, | |
"export": 5, | |
"cut": 6, | |
} | |
TRANSFORMERS = { | |
"Windows path": normalize_windows_path, | |
"POSIX path": normalize_posix_path, | |
"text": lambda x: x, | |
"normalized URL": normalize_url, | |
"(plain|simple) Windows path": normalize_simple_windows_path, | |
"pluralized word": make_plural, | |
} | |
TARGETS = { | |
"[and export][to [the]] clipboard": 1, | |
"[and print][to [the]] console": 2, | |
"[and type][to [the]] focus": 3, | |
"[and paste][to [the]] focus": 4, | |
} | |
def manipulate_clipboard(action, | |
transformer, | |
target, | |
copy_action=Key("c-c"), | |
cut_action=Key("c-x"), | |
paste_action=Key("c-v")): | |
original_text = Clipboard.get_system_text() | |
output_text = original_text | |
if action == 1: | |
copy_action.execute() | |
time.sleep(KEYPRESS_WAIT) | |
output_text = Clipboard.get_system_text() | |
if action == 6: | |
cut_action.execute() | |
time.sleep(1.0) | |
output_text = Clipboard.get_system_text() | |
output_text = transformer(output_text) | |
if action == 2 or target == 3: | |
Text(output_text).execute() | |
if action == 3 or target == 2: | |
print(output_text) | |
if action == 4 or target == 4: | |
Clipboard.set_system_text(output_text) | |
paste_action.execute() | |
time.sleep(KEYPRESS_WAIT) | |
if action == 5 or target == 1 or (action in [1, 6] and target == 0): | |
Clipboard.set_system_text(output_text) | |
else: | |
Clipboard.set_system_text(original_text) | |
def composer(function_one, function_two, **kwargs): | |
"""Combine functions to operate on lists. I feel like there is probably a better way to do this.""" | |
action = kwargs.pop("action") | |
transformer = kwargs.pop("transformer") | |
enclosure = kwargs.pop("enclosure") | |
target = kwargs.pop("target") | |
function_one(action, | |
partial( | |
function_two, normalizer=transformer, | |
enclosure=enclosure), target) | |
class ClipboardUtilities(MappingRule): | |
mapping = { | |
"<action> <transformer> [<target>]": | |
Function(manipulate_clipboard), | |
"<action> [<enclosure>] <transformer> list [<target>]": | |
Function( | |
composer, | |
function_one=manipulate_clipboard, | |
function_two=normalize_list) | |
} | |
extras = [ | |
Choice("action", ACTIONS), | |
Choice("transformer", TRANSFORMERS, default=lambda x: x), | |
Choice("target", TARGETS, default=0), | |
Choice("enclosure", { | |
"quoted": '""', | |
"unquoted": "" | |
}, default=""), | |
] | |
def access_multi_clipboard(slot, mode=0): | |
success = False | |
slot = str(slot) | |
if mode == 0: | |
Clipboard.set_system_text(navigation._CLIP.get(slot, "")) | |
elif mode == 1: | |
navigation._CLIP[slot] = Clipboard.get_system_text() | |
if CASTER: | |
utilities.save_json_file( | |
navigation._CLIP, | |
settings.SETTINGS["paths"]["SAVED_CLIPBOARD_PATH"]) | |
elif mode == 2: | |
if slot == "0": | |
if len(navigation._CLIP.keys()) == 0: | |
print("\nThe multi clipboard is empty!\n") | |
else: | |
print("\multi clipboard contents:") | |
for k, v in sorted(navigation._CLIP.items(), key=lambda x: int(x[0])): | |
print("slot {}:\n{}\n".format(k, v)) | |
else: | |
if not slot in navigation._CLIP: | |
print("\The multi clipboard slot {} is empty!\n".format(slot)) | |
else: | |
print("\multi clipboard slot {}:\n{}\n".format( | |
slot, navigation._CLIP[slot])) | |
return success | |
class MultiClipboardUtilities(MappingRule): | |
mapping = { | |
"<mode> [(caster|multi)] clipboard [slot] [<slot>]": Function(access_multi_clipboard), | |
} | |
extras = [ | |
Choice("mode", { | |
"import [to]": 1, | |
"export [from]": 0, | |
"print": 2 | |
}), | |
IntegerRef("slot", 0, 500, default=0), | |
] | |
grammar.add_rule(ClipboardUtilities()) | |
grammar.add_rule(MultiClipboardUtilities()) | |
grammar.load() | |
def unload(): | |
global grammar | |
if grammar: | |
grammar.unload() | |
grammar = None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment