I started writing this post and realized that the nitty-gritty details weren't all that exciting.
So, instead of a play-by-play of how I built this tool, here is a simple overview of some decisions I made, what I did to make it work, and how it could be infinitely better than a short Saturday project.
Alternative title: How I canceled out my projected productivity gains
I use the "find" tool a lot... It's very useful.
I found myself (somewhat) frequently searching single items from a list and got frustrated that I couldn't just copy a whole list of things and then plug it into the Find tool's search bar. Like a multi-search function.
The general workflow I want is;
- Highlight delimited list (comma, tab, space, or new-line separated)
- Press copy hotkey
- Press find hotkey
- Press special paste hotkey
The main thing I wanted to avoid is overwriting my clipboard.
First thought was to build a plugin. This, however, felt like it would be reinventing the wheel. There exist built-in and existing third party find tools that work great. They already make use of fast string searching algorithms and DOM parsing.
Regex seemed like a convenient way to build complex search query in a single line of plain text. Also Firefox (and Chrome) both have regex find-in-page tools.
I wanted to avoid downloading third-party apps onto my computer. It shouldn't be that hard to make a global shortcut for this... right?
The Design
Copy text to clipboard then invoke Python script via a global shortcut to parse and convert a delimited list to a regex query like
/(?:item1|item2|item3)/gmi
The script is straightforward:
import csv
import sys
def convert_to_regex(search_terms: str, delimiter="\n", case_insensitive=True, raw=False) -> str:
"""
Take in a delimited list of search terms and convert to regex
Raw gives complete regex, some regex search functions provide case insensitivity already
"""
if delimiter == "\n":
terms = search_terms.strip().splitlines()
else:
terms = search_terms.strip().split(delimiter)
terms = map(lambda s: s.strip(), terms)
if raw:
r = f"/(?:{'|'.join(terms)})/gm{'i' if case_insensitive else ''}"
else:
r = f"(?:{'|'.join(terms)})"
return r
def get_delimiter(search_terms: str) -> str:
sniffer = csv.Sniffer()
try:
dialect = sniffer.sniff(search_terms)
except csv.Error as e:
return "\n"
return dialect.delimiter
if __name__ == "__main__":
search_terms = " ".join(sys.argv[1:])
r = convert_to_regex(search_terms, delimiter=get_delimiter(search_terms))
sys.stdout.write(r)
Notes on the implementation:
- Just printing the resulting regex adds a trailing
\n
, which messes with some inputs. - Certain regex interpreters ignore the arguments posted at the end of the phrase, and opt for a radio-button menu for things like case insensitivity. Most of the tools I use are like this, so default to using the shorthand version and rely on manual input.
- Python is not the fastest scripting language for this. It shows on execution.
As far as invoking the script from a global shortcut, Mac has an excellent option for binding custom workflows with the Automator application. This is a seriously under-utilized tool, and it makes so many common tasks accessible and, to use a buzz-word, is truly no-code. Strange I don't see many people promoting it.
Here is my configuration for an Automator Quick Action:
After saving the action above, you can easily bind it to whatever (I chose ^⌥⌘V) via System Preferences > Keyboard > Shortcuts > Services.
This next part is, quite frankly, embarrassing and the reason I won't be using this hack going forward. The permissions required for Automator are ridiculous. In order to allow for the Automator script to execute within an application, I have to grant both Automator and the application access to System Events through the "Accessibility" and "Automation" menus. If I was just using this in an internal application, no big deal, but Firefox having such permissions makes me feel uneasy.
Does it work? Yes.
Is it slow? Yes.
Is it dangerously permissive? Yes.
Will I use it going forward? No.
Did I completely cancel out my potential productivity gains over a lifetime of Ctrl-Fing things from a list during my struggle to grant (way too many) permissions to Automator workflows, bind global shortcuts, and write hacky Python code? Oh yeah, you bet.
Do I regret it? Not one bit.
Optimizations
Here are some disorganized thoughts on how this tool could be implemented in a much cleaner fashion.
- Just make a damn plugin. One-click install, built-in hot key binding, exclusive to web applications (where I use it most). If I went this route, I would need to transcribe the guess_delimiter function from Python's csv package. It is really interesting, I suggest you check it out.
- Is there a quicker (custom) string search algorithm for multiple string searches? I bet a in memory cache of sorts would be helpful.
- Python is too slow to execute reliably on a paste. I know I wanted to not overwrite my clipboard, but this could speed up the whole process.