Skip to content

Instantly share code, notes, and snippets.

@Versatilus
Last active January 9, 2020 08:17
Show Gist options
  • Save Versatilus/850e6b5aadc5785da5bb629101ed3727 to your computer and use it in GitHub Desktop.
Save Versatilus/850e6b5aadc5785da5bb629101ed3727 to your computer and use it in GitHub Desktop.
# -*- 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