Is it possible to keystroke special characters in AppleScript?

The Bug:

For certain special characters and symbols, AppleScript will fail to keystroke the actual character, and will instead simply type the letter a.


An Example:

The following AppleScript code:

tell application "System Events" to keystroke "² ³ é ½ ₭"

types the following string of text (no matter what application that I am in):

a a a a a

A Workaround:

One can effectively "type" the text by placing the special characters on the clipboard, and then pasting the clipboard, like so:

set the clipboard to "² ³ é ½ ₭"
tell application "System Events" to keystroke "v" using command down

I avoid using the above method to put text on the clipboard in AppleScript, because it necessarily converts the clipboard to rich text, in the process.

I prefer to employ the method provided here, to ensure that plain text is placed on the clipboard. But, this method converts special characters to mojibake.

For example, using the same string as above, this method puts the following text on the clipboard:

² ³ é ½ ₭

You can manually account for this issue, however, by adding a replace_chars statement for each special character.


The Question:

Is there a way to type special characters via AppleScript, without having to involve the clipboard?

For example, is there some way that I can add the desired special characters to a "bank" or system file somewhere, so that the System Events application is familiar with them?



Solution 1:

It's possible with CoreGraphics (CGEventPost), it's a C API (it doesn't work in a Cocoa-AppleScript), but it's possible to call an executable from an AppleScript.


Here is the method to create the executable with Xcode (Version 8.3.3):

You can do it yourself (you can download Xcode from the App Store), or ask a trusted person to create the executable.

1- Open Xcode, select menu "File" --> "New" --> "Project..." enter image description here


2- Select macOS and Command Line Tool, click on the "Next" button. enter image description here


3- Name it TypeCharacters, select Objective-C as Language, click on the "Next" button. enter image description here


4- Save the project to the desired folder.


5- In the window of Xcode, select the main.m icon, clear the text in the window and paste one of these two codes:

The code need a loop because of a bug, the CGEventKeyboardSetUnicodeString() method truncates a string which exceeds twenty characters, or it's a limit (not documented).

This code use a loop to type a list of characters at each iteration, the list can contain one to twenty characters. (It's fast, 1 seconds to type 2400 characters in a TextEdit's document on my computer.)

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (argc > 1) {
            NSString *theString = [NSString stringWithUTF8String:argv[1]];
            NSUInteger len = [theString length];
            NSUInteger n, i = 0;
            CGEventRef keyEvent = CGEventCreateKeyboardEvent(nil, 0, true);
            unichar uChars[20];
            while (i < len) {
                n = i + 20;
                if (n>len){n=len;}
                [theString getCharacters:uChars range:NSMakeRange(i, n-i)];
                CGEventKeyboardSetUnicodeString(keyEvent, n-i, uChars);
                CGEventPost(kCGHIDEventTap, keyEvent); // key down
                CGEventSetType(keyEvent, kCGEventKeyUp);
                CGEventPost(kCGHIDEventTap, keyEvent); // key up (type 20 characters maximum)
                CGEventSetType(keyEvent, kCGEventKeyDown);
                i = n;
                [NSThread sleepForTimeInterval:0.004]; // wait 4/1000 of second, 0.002 it's OK on my computer, I use 0.004 to be safe, increase it If you still have issues
            }
            CFRelease(keyEvent);
        }
    }
    return 0;
}

This code use a loop to type one character at each iteration (It's slower than the first code).

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (argc > 1) {
            NSString *theString = [NSString stringWithUTF8String:argv[1]];
            UniChar uChar;
            CGEventRef keyEvent = CGEventCreateKeyboardEvent(nil, 0, true);
            for (int i = 0; i < [theString length]; i++)
            {
                uChar = [theString characterAtIndex:i];
                CGEventKeyboardSetUnicodeString(keyEvent, 1, &uChar);
                CGEventPost(kCGHIDEventTap, keyEvent); // key down
                CGEventSetType(keyEvent, kCGEventKeyUp);
                CGEventPost(kCGHIDEventTap, keyEvent); // key up (type the character)
                CGEventSetType(keyEvent, kCGEventKeyDown);
                [NSThread sleepForTimeInterval:0.001]; // wait 1/1000 of second, no need of this line on my computer, I use 0.001 to be safe, increase it If you still have issues
            }
            CFRelease(keyEvent);
        }
    }
    return 0;
}

Note : This code works on macOS Sierra and should work on El Capitan, but not on an older OS.

enter image description here


6- Select menu "Product" --> "Build".


7- Select the TypeCharacters icon in the Product folder, right-click and select the "Show in Finder" menu in the contextual menu.

enter image description here


8- From the Finder, move the "TypeCharacters" file to the desired folder, quit Xcode, that's all.


From an AppleScript, call the executable, like this

set myString to "ùéèà ² ³ é ½ ₭ "
do shell script "'/full path/of/TypeCharacters' " & quoted form of myString

Solution 2:

A workaround (with the clipboard):

1 - You can use a cocoa method from a shell:

set tString to "² ³ é ½ ₭"
my stringToClipboard(tString)
tell application "System Events" to keystroke "v" using command down

on stringToClipboard(t1)
    do shell script "/usr/bin/python -c 'import sys;from AppKit import NSPasteboard, NSPasteboardTypeString; cb=NSPasteboard.generalPasteboard();cb.declareTypes_owner_([NSPasteboardTypeString], None);cb.setString_forType_(sys.argv[1].decode(\"utf8\"), NSPasteboardTypeString)' " & quoted form of t1
end stringToClipboard

2 - Or a Cocoa-AppleScript Applet:

use framework "AppKit"
use scripting additions
set tString to "² ³ é ½ ₭"
my stringToClipboard(tString)
tell application "System Events" to keystroke "v" using command down

on stringToClipboard(t1)
    set ca to current application
    set cb to ca's NSPasteboard's generalPasteboard()
    cb's declareTypes:{ca's NSPasteboardTypeString} owner:(missing value)
    cb's setString:t1 forType:(ca's NSPasteboardTypeString)
end stringToClipboard