Last active
June 6, 2019 11:04
-
-
Save alexboche/cd598235aeb4fb04d7403857e721ea1c 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
# Author: Alex Boche | |
from dragonfly import Key, Pause, AppContext, Window | |
import pyperclip | |
import re | |
from castervoice.lib import context | |
from castervoice.lib.ccr.core.punctuation import text_punc_dict, double_text_punc_dict | |
from castervoice.lib.alphanumeric import caster_alphabet | |
text_punc_dict.update(caster_alphabet) | |
character_dict = text_punc_dict | |
character_list = character_dict.values() | |
contexts = { | |
"texstudio": AppContext(executable="texstudio"), | |
"lyx": AppContext(executable="lyx") | |
} | |
def get_application(): | |
window = Window.get_foreground() | |
# Check all contexts. Return the name of the first one that matches or | |
# "standard" if none matched. | |
for name, context in contexts.items(): | |
if context.matches(window.executable, window.title, window.handle): | |
return name | |
return "standard" | |
def get_start_end_position(text, phrase, direction, occurrence_number): | |
# def get_start_end_position(text, phrase, direction): | |
if phrase in character_list: | |
pattern = re.escape(phrase) | |
else: | |
# avoid e.g. matching 'and' in 'land' but allow e.g. matching 'and' in 'hello.and' | |
# for matching purposes use lowercase | |
# PROBLEM: this will not match words in class names like "Class" in "ClassName" | |
# PROBLEM: it's not matching the right one when you have two occurrences of the same word in a row | |
pattern = '(?:[^A-Za-z]|\A)({})(?:[^A-Za-z]|\Z)'.format(phrase.lower()) # must get group 1 | |
if not re.search(pattern, text.lower()): | |
# replaced phase not found | |
print("'{}' not found".format(phrase)) | |
return | |
match_iter = re.finditer(pattern, text.lower()) | |
if phrase in character_list: # consider changing this to if len(phrase) == 1 or something | |
match_index_list = [(m.start(), m.end()) for m in match_iter] | |
else: | |
match_index_list = [(m.start(1), m.end(1)) for m in match_iter] # first group | |
if direction == "left": | |
try: | |
match = match_index_list[-1*occurrence_number] # count from the right | |
except IndexError: | |
print("There aren't that many occurrences of '{}'".format(phrase)) | |
return | |
if direction == "right": | |
try: | |
match = match_index_list[occurrence_number - 1] # count from the left | |
except IndexError: | |
print("There aren't that many occurrences of '{}'".format(phrase)) | |
return | |
left_index, right_index = match | |
return (left_index, right_index) | |
copy_pause_time_dict = {"standard": "10", "texstudio": "70", "lyx": "60"} | |
paste_pause_time_dict = {"standard": "0", "texstudio": "100", "lyx": "20"} | |
def text_manipulation_copy(application): | |
# the wait time can also be modified up or down further by going into context.read_selected_without_altering_clipboard | |
# and changing the sleep time which is apparently slightly different than the pause time. | |
# the sleep time is set to a positive number, so can be reduced | |
# here I am using "wait time" to mean the sum of the sleep and pause time right after pressing control c | |
err, selected_text = context.read_selected_without_altering_clipboard(pause_time=copy_pause_time_dict[application]) | |
if err != 0: | |
# I'm not discriminating between err = 1 and err = 2 | |
print("failed to copy text") | |
return | |
return selected_text | |
def text_manipulation_paste(text, application): | |
context.paste_string_without_altering_clipboard(text, pause_time=copy_pause_time_dict[application]) | |
def select_text_and_return_it(direction, number_of_lines_to_search, application): | |
if direction == "left": | |
Key("s-home, s-up:%d, s-home" %number_of_lines_to_search).execute() | |
if direction == "right": | |
Key("s-end, s-down:%d, s-end" %number_of_lines_to_search).execute() | |
selected_text = text_manipulation_copy(application) | |
if selected_text == None: | |
# failed to copy | |
return | |
return selected_text | |
def deal_with_phrase_not_found(selected_text, application, direction): | |
# Approach 1: unselect text by pressing left and then right, works in Tex studio | |
if application == "texstudio": | |
Key("left, right").execute() # unselect text | |
if direction == "right": | |
Key("left:%d" %len(selected_text)).execute() | |
# Approach 2: unselect text by pressing opposite arrow key, does not work in Tex studio | |
else: | |
if direction == "left": | |
Key("right").execute() | |
if direction == "right": | |
Key("left").execute() | |
def replace_phrase_with_phrase(text, replaced_phrase, replacement_phrase, direction, occurrence_number): | |
match_index = get_start_end_position(text, replaced_phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
return | |
return text[: left_index] + replacement_phrase + text[right_index:] | |
def copypaste_replace_phrase_with_phrase(replaced_phrase, replacement_phrase, direction, number_of_lines_to_search, occurrence_number): | |
application = get_application() | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
replaced_phrase = str(replaced_phrase) | |
replacement_phrase = str(replacement_phrase) | |
new_text = replace_phrase_with_phrase(selected_text, replaced_phrase, replacement_phrase, direction, occurrence_number) | |
if not new_text: | |
# replaced_phrase not found | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
text_manipulation_paste(new_text, application) | |
if number_of_lines_to_search < 20: | |
# only put the cursor back in the right spot if the number of lines to search is fairly small | |
if direction == "right": | |
offset = len(new_text) | |
Key("left:%d" %offset).execute() | |
def remove_phrase_from_text(text, phrase, direction, occurrence_number): | |
match_index = get_start_end_position(text, phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
return | |
# if the "phrase" is punctuation, just remove it, but otherwise remove an extra space adjacent to the phrase | |
if phrase in character_list: | |
return text[: left_index] + text[right_index:] | |
else: | |
if left_index == 0: | |
# the phrase is at the beginning of the line | |
return text[right_index:] # todo: consider removing extra space | |
else: | |
return text[: left_index - 1] + text[right_index:] | |
def copypaste_remove_phrase_from_text(phrase, direction, number_of_lines_to_search, occurrence_number): | |
application = get_application() | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
phrase = str(phrase) | |
new_text = remove_phrase_from_text(selected_text, phrase, direction, occurrence_number) | |
if not new_text: | |
# phrase not found | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
text_manipulation_paste(new_text, application) | |
if direction == "right": | |
offset = len(new_text) | |
Key("left:%d" %offset).execute() | |
def move_until_phrase(direction, before_after, phrase, number_of_lines_to_search, occurrence_number): | |
application = get_application() | |
if not before_after: | |
# default to whatever is closest to the cursor | |
if direction == "left": | |
before_after = "after" | |
if direction == "right": | |
before_after = "before" | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
phrase = str(phrase) | |
match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
# phrase not found | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
if application == "texstudio": | |
# Approach 1: Unselect text by pressing left and then right. A little slower but works in Texstudio | |
Key("left, right").execute() # unselect text | |
if direction == "left": | |
# cursor is at the left side of the previously selected text | |
if before_after == "before": | |
selected_text_to_the_left_of_phrase = selected_text[:left_index] | |
multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n") | |
offset = left_index - multiline_offset_correction | |
if before_after == "after": | |
selected_text_to_the_left_of_phrase = selected_text[:right_index] | |
multiline_offset_correction = selected_text_to_the_left_of_phrase.count("\r\n") | |
offset = right_index - multiline_offset_correction | |
Key("right:%d" %offset).execute() | |
if direction == "right": | |
# cursor is at the left side of the previously selected text | |
if before_after == "before": | |
selected_text_to_the_right_of_phrase = selected_text[left_index :] | |
if before_after == "after": | |
selected_text_to_the_right_of_phrase = selected_text[right_index :] | |
multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") | |
if before_after == "before": | |
offset = len(selected_text) - left_index - multiline_offset_correction | |
if before_after == "after": | |
offset = len(selected_text) - right_index - multiline_offset_correction | |
Key("left:%d" %offset).execute() | |
else: | |
# Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio | |
if right_index < round(len(selected_text))/2: | |
# it's faster to approach phrase from the left | |
Key("left").execute() # unselect text and place cursor on the left side of selection | |
if before_after == "before": | |
offset_correction = selected_text[: left_index].count("\r\n") | |
offset = left_index - offset_correction | |
if before_after == "after": | |
offset_correction = selected_text[: right_index].count("\r\n") | |
offset = right_index - offset_correction | |
Key("right:%d" %offset).execute() | |
else: | |
# it's faster to approach phrase from the right | |
Key("right").execute() # unselect text and place cursor on the right side of selection | |
if before_after == "before": | |
offset_correction = selected_text[left_index :].count("\r\n") | |
offset = len(selected_text) - left_index - offset_correction | |
if before_after == "after": | |
offset_correction = selected_text[right_index :].count("\r\n") | |
offset = len(selected_text) - right_index - offset_correction | |
Key("left:%d" %offset).execute() | |
def select_phrase(phrase, direction, number_of_lines_to_search, occurrence_number): | |
application = get_application() | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
phrase = str(phrase) | |
match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
# phrase not found | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
# Approach 1: paste the selected text over itself rather than simply unselecting. A little slower but works Texstudio | |
# todo: change this so that it unselects by pressing left and then right rather than pasting over the top | |
if application == "texstudio": | |
text_manipulation_paste(selected_text, application) # yes, this is kind of redundant but it gets the proper pause time | |
multiline_movement_correction = selected_text[right_index :].count("\r\n") | |
movement_offset = len(selected_text) - right_index - multiline_movement_correction | |
Key("left:%d" %movement_offset).execute() | |
multiline_selection_correction = selected_text[left_index : right_index].count("\r\n") | |
selection_offset = len(selected_text[left_index : right_index]) - multiline_selection_correction | |
Key("s-left:%d" %selection_offset).execute() | |
# Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio | |
else: | |
if right_index < round(len(selected_text))/2: | |
# it's faster to approach phrase from the left | |
Key("left").execute() # unselect text and place cursor on the left side of selection | |
multiline_movement_offset_correction = selected_text[: left_index].count("\r\n") | |
movement_offset = left_index - multiline_movement_offset_correction | |
# move to the left side of the phrase | |
Key("right:%d" %movement_offset).execute() | |
# select phrase | |
multiline_selection_offset_correction = selected_text[left_index : right_index].count("\r\n") | |
selection_offset = len(phrase) - multiline_selection_offset_correction | |
Key("s-right:%d" %selection_offset).execute() | |
else: | |
# it's faster to approach phrase from the right | |
Key("right").execute() # unselect text and place cursor on the right side of selection | |
multiline_movement_offset_correction = selected_text[left_index :].count("\r\n") | |
movement_offset = len(selected_text) - left_index - multiline_movement_offset_correction | |
# move to the left side of the phrase | |
Key("left:%d" %movement_offset).execute() | |
# select phrase | |
multiline_selection_offset_correction = selected_text[left_index : right_index].count("\r\n") | |
selection_offset = len(phrase) - multiline_selection_offset_correction | |
Key("s-right:%d" %selection_offset).execute() | |
def select_until_phrase(direction, phrase, before_after, number_of_lines_to_search, occurrence_number): | |
application = get_application() | |
if not before_after: | |
# default to select all the way through the phrase not just up until it | |
if direction == "left": | |
before_after = "before" | |
if direction == "right": | |
before_after = "after" | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
phrase = str(phrase) | |
match_index = get_start_end_position(selected_text, phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
# phrase not found | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
# Approach 1: paste the selected text over itself rather than simply unselecting. A little slower but works Texstudio | |
# todo: change this so that it unselects by pressing left and then right rather than pasting over the top | |
if application == "texstudio": | |
text_manipulation_paste(selected_text, application) # yes, this is kind of redundant but it gets the proper pause time | |
if direction == "left": | |
if before_after == "before": | |
selected_text_to_the_right_of_phrase = selected_text[left_index :] | |
multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") | |
offset = len(selected_text) - left_index - multiline_offset_correction | |
if before_after == "after": | |
selected_text_to_the_right_of_phrase = selected_text[right_index :] | |
multiline_offset_correction = selected_text_to_the_right_of_phrase.count("\r\n") | |
offset = len(selected_text) - right_index - multiline_offset_correction | |
Key("s-left:%d" %offset).execute() | |
if direction == "right": | |
multiline_movement_correction = selected_text.count("\r\n") | |
movement_offset = len(selected_text) - multiline_movement_correction | |
if before_after == "before": | |
multiline_selection_correction = selected_text[: left_index].count("\r\n") | |
selection_offset = left_index - multiline_movement_correction | |
if before_after == "after": | |
multiline_selection_correction = selected_text[: right_index].count("\r\n") | |
selection_offset = right_index | |
# move cursor to original position | |
Key("left:%d" %movement_offset).execute() | |
# select text | |
Key("s-right:%d" %selection_offset).execute() | |
# Approach 2: unselect using arrow keys rather than pasting over the existing text. (a little faster) does not work texstudio | |
else: | |
if direction == "left": | |
Key("right").execute() # unselect text and move to left side of selection | |
if before_after == "before": | |
multiline_correction = selected_text[left_index :].count("\r\n") | |
offset = len(selected_text) - left_index - multiline_correction | |
if before_after == "after": | |
multiline_correction = selected_text[right_index :].count("\r\n") | |
offset = len(selected_text) - right_index - multiline_correction | |
Key("s-left:%d" %offset).execute() | |
if direction == "right": | |
Key("left").execute() # unselect text and move to the right side of selection | |
if before_after == "before": | |
multiline_correction = selected_text[: left_index].count("\r\n") | |
offset = left_index - multiline_correction | |
if before_after == "after": | |
multiline_correction = selected_text[: right_index].count("\r\n") | |
offset = right_index - multiline_correction | |
Key("s-right:%d" %offset).execute() | |
def delete_until_phrase(text, phrase, direction, before_after, occurrence_number): | |
match_index = get_start_end_position(text, phrase, direction, occurrence_number) | |
if match_index: | |
left_index, right_index = match_index | |
else: | |
return | |
# the spacing below may need to be tweaked | |
if direction == "left": | |
if before_after == "before": | |
# if text[-1] == " ": | |
# return text[: left_index] + " " | |
return text[: left_index] | |
else: # todo: handle before-and-after defaults better | |
if text[-1] == " ": | |
return text[: right_index] + " " | |
else: | |
return text[: right_index] | |
if direction == "right": | |
if before_after == "after": | |
return text[right_index :] | |
else: | |
if text[0] == " ": | |
return " " + text[left_index :] | |
else: | |
return text[left_index :] | |
def copypaste_delete_until_phrase(direction, phrase, number_of_lines_to_search, before_after, occurrence_number): | |
application = get_application() | |
if not before_after: | |
# default to delete all the way through the phrase not just up until it | |
if direction == "left": | |
before_after = "before" | |
if direction == "right": | |
before_after = "after" | |
selected_text = select_text_and_return_it(direction, number_of_lines_to_search, application) | |
print("selected_text: {}".format(selected_text)) | |
phrase = str(phrase) | |
new_text = delete_until_phrase(selected_text, phrase, direction, before_after, occurrence_number) | |
print("new_text: {}".format(new_text)) | |
if new_text is None: | |
# do NOT use `if not new_text` because that will pick up the case where new_text="" which | |
# occurs if the phrase is at the beginning of the line | |
# phrase not found | |
# deal_with_phrase_not_found(selected_text, temp_for_previous_clipboard_item, application, direction) | |
deal_with_phrase_not_found(selected_text, application, direction) | |
return | |
if new_text == "": | |
# phrase is at the beginning of the line | |
Key("del").execute() | |
return | |
else: | |
text_manipulation_paste(new_text, application) | |
if direction == "right": | |
offset = len(new_text) | |
Key("left:%d" %offset).execute() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment