Skip to content

Instantly share code, notes, and snippets.

@alexboche
Last active June 6, 2019 11:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexboche/cd598235aeb4fb04d7403857e721ea1c to your computer and use it in GitHub Desktop.
Save alexboche/cd598235aeb4fb04d7403857e721ea1c to your computer and use it in GitHub Desktop.
# 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