Yi's Blog

思绪来得快,去得也快

Notes with Dictionary Lookup History

Notes

Learning English is so important after I moved to US. Without sufficient English skills, I can’t communicate clearly with my colleagues, and can’t even have a relaxing and sometimes cheerful daily life. Even more seriously, I feel anxious if I couldn’t fully understand the conversations around me. So as a result, the crisis pushes me learning English. One of the important parts is reading books that others read. During reading, I inevitably encounters unknown words. So I look up dictionaries on my iPhone and MacBook all the time.

One important missing feature of the Dictionary app on macOS for me is saving lookup history. Without the ability to review what I have looked up recently, I end up looking up the same words over and over again. As a programmer, I will solve it with a program for sure.

Before diving into the details, we need to clarify the input and the output of the program. The input of the program will be a word I just looked up in the dictionary app (sometimes in a browser), and a keyboard shortcut which is my intent to save the word for later review. The output of the program will be saving the word with its meaning in a note or file which I can review later easily.

With such a requirement in mind, now we can divide the problem into three sub-problems:

  • Capture the word I just looked up
  • Retrieve the definition of the word
  • Pronounce the word as I often don’t know the correct pronunciation of the new word
  • Save the word and its definition to Notes.app as I use Apple Notes all the times

Capture the word I just looked up

This is actually the trickiest part of the whole program. macOS doesn’t provide a direct API which allows background application to get the current selected word from the current front application. However, there are ways to bypass it.

The main way to achieve this is through macOS’s Accessibility APIs(Here is the documentation: Accessibility Programming Guide for OS X). With such APIs, we will be able to query the states of accessibility properties of all the applications, which includes what is the current selected word. This sounds a little scary, but by default, no application can use these APIs without explicitly whitelisting. You can find the settings at the Security & Privacy section of System Preference.app.

Accessibility

However, not all the applications fill the accessibility properties correctly. For example, Chrome will only populate these properties when it detects some system level accessibility function is enabled (for some performance reason probably? Hey, Safari.app is ok to enable it all the times). So to work with these applications, people usually will send a ⌘Command+c key stroke to copy the current selected word, and you can observe similar behavior from PopClip and Apple’s Speak selected text function. ⌥Option+Esc is a great function on macOS to learn word pronunciations, though it’s not enough as we want to do a little more besides that.

speech

Unfortunately, with above two solutions, there are still incompatible softwares. As PopClip is using the same technique, you can check the list of softwares that won’t work here. For me, as I will mostly use it with Dictionary or Safari or Chrome, the compatibility of the solution is not a problem.

TL;DR, I will simply send a ⌘Command+c stroke to the front application to retrieve the selected word in Dictionary or Safari or Chrome.

Retrieve the definition of the word

This part is fun as well. There are several data sources I can use in preferred order: Dictionary.app, Google Definitions and others.

I really hope Dictionary.app provides an AppleScript API to retrieve the word meanings which I can use directly in my script, however it doesn’t exist today. The easiest way to use the dictionaries with macOS is through Dictionary Service (this is an awesome blog from NSHipster, and besides the blog Mattt also provided a wrapper for the service: DictionaryKit). As I don’t plan to implement a native macOS application just for this tool, I will skip this option.

The second preferred option is the word definition from Google, which is the first search result you will see when you search a word with “definition”. It’s clean and comprehensive.

Google Definition

Though there is no API for that as well, as it’s a good opportunity to show ads when people search word meanings compared to providing a free API. (There are hacky ways to do it as mentioned in Which API allows access to Google’s Dictionary information?, though relying on obfuscated tag IDs seems to be a bad idea.)

Finally, let’s find an easy-to-use API that we can call directly in AppleScript. The query result from WordsAPI seems fairly comprehensive, and the free tier which allows 2,500 requests per day is far more than enough for my own usage. So I end up using the following command to retrieve the definition.

$ curl -s https://wordsapiv1.p.mashape.com/words/<the_word> \
  -H 'X-Mashape-Key: <your https://www.wordsapi.com/ key>'

Pronounce the word

There are several alternatives. The option I mentioned above will use the system’s TTS(Text-to-Speech) engine, which is not very enjoyable. I ended up using an unofficial Google API to download MP3(s) and play them through ffplay.

$/usr/local/bin/ffplay http://ssl.gstatic.com/dictionary/static/sounds/20180430/reminiscent--_us_1.mp3 -autoexit -nodisp

Save the word and its definition to Notes.app

AppleScript can do this easily if you are familiar with it. It’s ok if you are not familiar with it as I don’t know the basics as well. The good part is that it’s fairly easy to read so that we can copy, paste, modify and use in most cases.

The API to manipulate Notes.app is straight-forward, and I learned most of the it from this answer and this tutorial provided in the answer.

The full AppleScript

With all three parts, here is the completed script:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

tell application "System Events"
	set frontmostProcess to first process where it is frontmost
	if the name of frontmostProcess is "WordNote" then
		set visible of frontmostProcess to false
		repeat while (frontmostProcess is frontmost)
			delay 0.2
		end repeat
		set secondFrontmost to name of first process where it is frontmost
	else
		set secondFrontmost to the name of frontmostProcess
	end if
end tell

tell application secondFrontmost to activate

-- Back up clipboard contents:
set savedClipboard to the clipboard

-- Copy selected text to clipboard:
tell application "System Events" to keystroke "c" using {command down}
delay 1 -- Without this, the clipboard may have stale data.

set theSelectedText to the clipboard

-- Restore clipboard:
set the clipboard to savedClipboard

set definitions to do shell script "curl -s https://wordsapiv1.p.mashape.com/words/" & theSelectedText & " -H 'X-Mashape-Key: <your https://www.wordsapi.com/ key>' | /usr/local/bin/gawk '{pattern = \"\\\"definition\\\":\\\"([^\\\"]*)\"; while (match($0, pattern, arr)) {print \"<div><span> - \", arr[1], \"</span></div>\";sub(pattern, \"\");}}'"

do shell script "/usr/local/bin/ffplay http://ssl.gstatic.com/dictionary/static/sounds/20180430/" & theSelectedText & "--_us_1.mp3 -autoexit -nodisp"
do shell script "/usr/local/bin/ffplay http://ssl.gstatic.com/dictionary/static/sounds/20180430/x" & theSelectedText & "--_us_1.mp3 -autoexit -nodisp"

tell application "Notes"
	tell account "iCloud"
		tell folder "Notes"
			set noteBody to body of note "Words"
			set noteBody to noteBody & "<div><i><span>" & theSelectedText & "</span></i></div>" & definitions & "<div><br></div>"
			set body of note "Words" to noteBody
		end tell
	end tell
end tell

How to use it?

First of all, install the dependencies: gawk and ffmpeg.

Secondly, you need to create an application with above script in Automator.app.

Automator.app

Thirdly, whitelist the application in the Security & Privacy section, and allow it to access Notes.app during the first run.

Last but not least, setting up a shortcut for the created application. You need to create a “Quick Action” to launch the application, and then assign the “Quick Action” with a shortcut.

Setting up shortcut

Now you can enjoy the home brewed AppleScript based application to save the words to a note.

But why?

It’s always fun for me to explore what I can do with programs, especially the ones seem challenging to do, for example, using AppleScript and accessibility API to automate some workflows with multiple applications. Thanks Apple for building such a worth playing system level API, and a mostly working perfectly Notes app which I can use to learn English everyday.

Happy Hacking!

PS: As you can see, I didn’t cover all the details in the script, for example the gawk part which extract the explanation from the JSON result. Hopefully I can cover that in a separated blog with a clear explanation.