Special Character Insert Script

Mac computers have various methods by which the user can insert special characters; these include keyboard shortcuts, the character viewer, and text replacement. Most users need nothing more.

I find the character viewer a bit difficult to use, and I have trouble remembering keyboard shortcuts and text-replacement characters for infrequently-used characters. So, I wrote the script included below.

To test this script, simply open and run it in Script Editor. Adding a new character to the script requires its decimal ID, which can easily be found online, and the site I use (because it’s easily searched) is linked below. There is no requirement as to the name of the special character. This script should work with any application that has an Edit > Paste menu item.

https://kellykjones.tripod.com/webtools/ascii_utf8_table.html

use framework "Foundation"
use scripting additions

on main()
	set characterData to {{"dash - em", 8212}, {"dash - en", 8211}, {"divide", 247}, {"double angle - left", 171}, {"double angle - right", 187}, {"double quote - left", 8220}, {"double quote - right", 8221}, {"ellipsis", 8230}, {"not equal to", 8800}, {"plus or minus", 177}, {"single quote - left", 8216}, {"single quote - right", 8217}, {"space - nonbreaking", 160}}
	
	set characterData to sortTheList(characterData) -- this can be removed if sorting is not desired
	set characterSettingPlist to "com.peavine.CharacterInsert"
	set dialogDefault to item 1 of readPlist(characterSettingPlist, "")
	
	set characterDescriptions to {}
	repeat with aList in characterData
		set end of characterDescriptions to item 1 of aList
	end repeat
	
	set theCharacters to (choose from list characterDescriptions default items dialogDefault with prompt "Select one or more characters to insert" with title "Character Insert" with multiple selections allowed)
	if theCharacters = false then error number -128
	
	set theIDs to {}
	repeat with i from 1 to (count theCharacters)
		repeat with j from 1 to (count characterData)
			if item i of theCharacters = item 1 of item j of characterData then
				set end of theIDs to item 2 of item j of characterData
				exit repeat
			end if
		end repeat
	end repeat
	
	set the clipboard to character id theIDs
	
	tell application "System Events"
		set appName to (name of first process whose frontmost is true)
		tell process appName
			set frontmost to true
			click menu item "Paste" of menu "Edit" of menu bar 1
		end tell
	end tell
	
	writePlist(characterSettingPlist, {theCharacters})
end main

on sortTheList(theList)
	repeat with i from (count theList) to 2 by -1
		repeat with j from 1 to i - 1
			if item 1 of item j of theList > item 1 of item (j + 1) of theList then
				set {item j of theList, item (j + 1) of theList} to {item (j + 1) of theList, item j of theList}
			end if
		end repeat
	end repeat
	return theList
end sortTheList

on readPlist(thePlist, theDefault)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	theDefaults's registerDefaults:{theKey:theDefault}
	return theKey of theDefaults as list
end readPlist

on writePlist(thePlist, theList)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	set theKey of theDefaults to theList
end writePlist

main()

One thing to keep in mind is that some characters can have more than one ID. And you can probably simplify things a bit by using a record rather than list of lists.

So something like:

set characterData to {|dash - em|:{8212}, |dash - en|:{8211}} -- etc
set characterData to current application's NSDictionary's dictionaryWithDictionary:characterData
set characterDescriptions to characterData's allKeys() as list

set theCharacters to (choose from list characterDescriptions with prompt "Select one or more characters to insert" with title "Character Insert" with multiple selections allowed)
if theCharacters = false then error number -128

set theIDs to {}
set theValues to (characterData's objectsForKeys:theCharacters notFoundMarker:{}) as list
repeat with aValue in theValues
	set theIDs to theIDs & aValue
end repeat

I’m not quite sure why you’re bothering with defaults, given the list doesn’t change.

Thanks Shane, I’ll rewrite my script as you suggest. The script will be much cleaner without the clumsy repeat loop and sort handler, and this will be a good learning experience, as I’ve never used NSDictionary before.

As far as the default setting, its purpose is to remember the character or characters selected in the dialog from one running of the script to the next. Thus, for example, if I insert left and right double quotes once in a document, there is a reasonable probability that I will insert them again in that document. So, just for me, this is a desirable feature, and I don’t know what this has to do with the list not changing. :frowning:

Nothing – I scanned the code in too much haste.

This revised version of my script is based in part on the suggestion made by Shane in post 2 above. This revision eliminates two repeat loops and a slow sort handler, and streamlines the plist handlers.

-- Revised 2021.05.06

use framework "Foundation"
use scripting additions

on main()
	set characterData to {|dash - em|:8212, |dash - en|:8211, |division sign|:247, |double angle - left|:171, |double angle - right|:187, |double quote - left|:8220, |double quote - right|:8221, |ellipsis symbol|:8230, |not equal to|:8800, |plus or minus|:177, |single quote - left|:8216, |single quote - right|:8217, |space - nonbreaking|:160}
	
	set characterData to current application's NSDictionary's dictionaryWithDictionary:characterData
	set characterDescriptions to (characterData's allKeys()'s sortedArrayUsingSelector:"caseInsensitiveCompare:") as list
	
	set plistPath to POSIX path of ((path to preferences as text) & "CharacterInsert.plist")
	
	set dialogDefault to readPlist(plistPath)
	set theCharacters to (choose from list characterDescriptions with prompt "Select one or more characters to insert" default items dialogDefault with title "Character Insert" with multiple selections allowed)
	if theCharacters = false then error number -128
	
	set theIDs to (characterData's objectsForKeys:theCharacters notFoundMarker:{}) as list
	set the clipboard to character id theIDs
	
	tell application "System Events"
		set appName to (name of first process whose frontmost is true)
		tell process appName
			set frontmost to true
			click menu item "Paste" of menu "Edit" of menu bar 1
		end tell
	end tell
	
	writePlist(plistPath, theCharacters)
end main

on readPlist(thePath)
	set theArray to current application's NSArray's arrayWithContentsOfFile:thePath
	return theArray as list
end readPlist

on writePlist(thePath, theList)
	set theArray to current application's NSArray's arrayWithArray:theList
	theArray's writeToFile:thePath atomically:true
end writePlist

main()

Here’s a version without using a dictionary either, using Cocoa to provide the names of the characters. It does no sorting, relying on your entry order.

use AppleScript version "2.5" -- 10.11 or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set theChars to current application's NSString's stringWithString:"–—“”‘’≠±…" -- edit to suit

set characterSettingPlist to "com.peavine.CharacterInsert"
set dialogDefault to item 1 of readPlist(characterSettingPlist, "")
-- convert string to list of char names
set theNames to theChars's stringByApplyingTransform:(current application's NSStringTransformToUnicodeName) |reverse|:false
set theNames to theNames's substringWithRange:{3, (theNames's |length|()) - 4}
set theNames to theNames's lowercaseString()
set theList to (theNames's componentsSeparatedByString:"}\\n{") as list

set theCharacters to (choose from list theList with prompt "Select one or more characters to insert" with title "Character Insert" default items dialogDefault with multiple selections allowed)
if theCharacters = false then error number -128

writePlist(characterSettingPlist, {theCharacters})
-- convert names back to string
set theNames to (current application's NSArray's arrayWithArray:theCharacters)'s componentsJoinedByString:"}\\N{"
set theNames to current application's NSString's stringWithFormat_("\\N{%@}", theNames)
set theNames to theNames's uppercaseString()
set theString to theNames's stringByApplyingTransform:(current application's NSStringTransformToUnicodeName) |reverse|:true
-- put on clipboard
set thePasteboard to current application's NSPasteboard's generalPasteboard()
thePasteboard's clearContents()
thePasteboard's writeObjects:{theString}

on readPlist(thePlist, theDefault)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	theDefaults's registerDefaults:{theKey:theDefault}
	return theKey of theDefaults as list
end readPlist

on writePlist(thePlist, theList)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	set theKey of theDefaults to theList
end writePlist

Works great Shane. :slight_smile:

A few comments:

  • The words, use AppleScript, needs to be added to the beginning of the script.

  • The list of character names in the dialog contains a “}” after the last entry. This is eliminated by removing the space at the end of theChars string.

  • The script places the character on the clipboard but does not insert it in the active document. If a user desires that, they can simply simply copy over the section from my script beginning with: tell application “System Events”. I assume there’s not a better way to do this.

  • Adding new characters to theChars string is drop-dead simple–simply place the cursor in the string where the character should be inserted; open Character Viewer; and double click on the desired character. There are a few characters that cannot be added in this manner–such as a nonbreaking space–but a copy and paste will do that.

That “space” was actually a no-break space, but it looks like the forum software converted it to a standard space. I’ll edit it out.