Write your own IME on macOS - 1. Create project

On macOS, it is not so difficult to write an Input Method Engine (IME). Unlike Linux or the other Unix-like OS, macOS provides an official framework, InputMethodKit, to help you write an Input Method Engine.

An IME project is in fact a normal Cocoa app located in a special directory, with a special bundle identifier, some special declarations in plist manifest file, and some special classes/objects in the project. This post may help you initiate your own IME project. Let’s go!

Create the project

First, open XCode, create a new App project:

Then, type your IME name. Here I choose the typical name “HelloWorld”.

Notice that there must be an inputmethod identifier in the bundle identifier. I choose to append a suffix to my organization identifier. However, you should also be able to do it in another way.

Add some magic

In this section, I’ll add some magic to to turn the app into an IME.

Plist magic

In the manifest file Info.plist, there are magic to declare some information of your IME:

Key Type
InputMethodConnectionName String
InputMethodServerControllerClass String
tsInputMethodCharacterRepertoireKey Array

The InputMethodConnectionName will be the name of your IME, recognized by macOS.

The InputMethodServerControllerClass is the name of the Input Method controller class in your IME, which is in charge of communicating with macOS, exchanging the input events and submitting the candidate selected by the user.

The tsInputMethodCharacterRepertoireKey can help you declare the category of your IME. It will depend where your IME will show up in the Input Source panel of the macOS system preference. As an array, there can be multiple items. Thus, an IME for several languages is possible. For example, Squirrel has a zh-Hans string in the array, so it shows up in the Chinese, Simplified language as follow:

If all is set, we can have an Info.plist like this:

I choose InokiHelloWorldIME as the connection name. There will be an Input Method controller class named InokiHelloWorldController in my project, in charge of events. And I added zh-Hans, zh-Hant and Latn for the category.

Class magic

We need to create our Input Method controller class, inheriting IMKInputController class from Input Method Kit.

Add these codes to the implementation(.m) file, so that we can observe what happens when we are “using”(not actually) the IME:

1
2
3
4
5
6
7
8
9
- (void)activateServer:(id)sender
{
NSLog(@"Server activated for %@", [sender bundleIdentifier]);
}

- (void)deactivateServer:(id)sender
{
NSLog(@"Server deactivated for %@", [sender bundleIdentifier]);
}

This method is using one of the controller API to receive input event from macOS:

1
2
3
4
5
- (BOOL)inputText:(NSString*)string client:(id)sender
{
NSLog(@"Controller received: %@", string);
return NO;
}

There are in fact 3 different APIs to handle the input event, such as:

1
2
3
- (BOOL)inputText:(NSString*)string key:(NSInteger)keyCode modifiers:(NSUInteger)flags client:(id)sender;
- (BOOL)inputText:(NSString*)string client:(id)sender;
- (BOOL)handleEvent:(NSEvent*)event client:(id)sender;

For more details, you can take a look at the declaration of IMKInputController in InputMethodKit/IMKInputController.h.

With all these done, macOS should be able to find your Input Controller, thanks to the runtime information in Objective-C.

Main Code magic

Finally, we need a minimum main function like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Cocoa/Cocoa.h>
#import <InputMethodKit/InputMethodKit.h>

NSString* connectionName = @"InokiHelloWorldIME";
IMKServer* server;

int main(int argc, char * argv[]) {
@autoreleasepool{
server = [[IMKServer alloc] initWithName:(NSString*)connectionName
bundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]];

[[NSApplication sharedApplication] run];
}
return 0;
}

In which we create an instance of IMKServer and make it alive during all the life cycle of our app. The server is also registered to macOS, so that the OS knows which process it should communicate.

Run it

Finally, we can build it and copy it into /Library/Input Methods.

For debug or running, this post (in Chinese) is a good example.

Here, I concentrate more on the potential and undocumented bug (ok, it might be a feature) as follows.

Oups, you may be isolated in a sandbox

You may suffer an error like:

1
[IMKServer _createConnection]: *Failed* to register NSConnection name=xxxxx

Same to me. I took several hours to dive into macOS and debug such error.

I found that the reason: IMKServer needs an NSConnection to communicate with macOS. However, by default, the app is sandboxized (by the <project-name.entitlements>) since a version of XCode.

So, the solutions are various:

  • Remove the entitlements file;
  • Allow NSConnection from sandbox;
  • Disable the App sandbox in the entitlements file.

Then, your IME should be good to go.

Conclusion

In this post, there is a brief description for creating an IME project. I show an issue that may generally exist related to the sandbox stuff. Hope this can help you. In the next post, I’ll try to show up the event handle in an IME on macOS.