How the Text Expander Reads Custom Expansions from `~/.you_expand_me.json` in hallelujahIM

The hallelujahIM input method loads user-defined abbreviations from ~/.you_expand_me.json at startup via deserializeJSON, stores them in self.substitutions, and inserts matching expansions as priority candidates during the getCandidates: pipeline.

The hallelujahIM input method for macOS provides a powerful Text Expander feature that lets you define custom abbreviations in a JSON configuration file. Located in the dongyuwei/hallelujahIM repository, this functionality allows you to type short codes like "yem" and instantly receive full phrases like "you expand me" as suggestion candidates. The implementation resides primarily in src/ConversionEngine.mm and operates through a straightforward JSON deserialization and dictionary lookup mechanism.

How the Text Expander Loads Custom Expansions

Locating the Configuration File

When the input method engine initializes, it looks for a file named .you_expand_me.json in the user's home directory. The getUserDefinedSubstitutions method constructs the absolute path using NSHomeDirectory() and returns the deserialized dictionary:

- (NSDictionary *)getUserDefinedSubstitutions {
    NSString *path = [NSString stringWithFormat:@"%@%@", NSHomeDirectory(),
                      @"/.you_expand_me.json"];
    return deserializeJSON(path);
}

Source: src/ConversionEngine.mm, lines 61–64.

This method returns an NSDictionary containing the user-defined abbreviation mappings.

Storing the Substitution Dictionary

During the asynchronous data-initialization phase in loadPreparedData, the engine stores the loaded dictionary in an instance variable:

self.substitutions = [self getUserDefinedSubstitutions];

Source: loadPreparedData, lines 30–34.

This ensures the configuration remains available in memory throughout the IME session, eliminating redundant file I/O during typing.

Applying Expansions During Candidate Generation

When you type a buffer, the engine calls getCandidates: to generate suggestions. After converting the input to lowercase, it performs a dictionary lookup against self.substitutions. If the typed string exists as a key, the corresponding value is injected at the front of the candidate list:

if (self.substitutions && self.substitutions[buffer]) {
    [result addObject:self.substitutions[buffer]];
}

Source: src/ConversionEngine.mm, lines 86–89.

The lowercasing ensures case-insensitive matching—for example, typing "YEM" or "yem" both match the "yem" key in your JSON file. After adding the custom expansion, the engine proceeds with its normal auto-suggestion, spell-check, and pinyin lookup pipelines, returning a deduplicated array of candidates.

Configuration File Format

The ~/.you_expand_me.json file must contain a flat JSON object where keys represent abbreviations and values represent the full expansion text:

{
  "te": "text expander",
  "yem": "you expand me",
  "brb": "be right back"
}

Place this file in your home directory ($HOME/.you_expand_me.json). The keys should be lowercase to match the engine's lowercasing logic, though the values can contain any Unicode text including spaces and punctuation.

Complete Implementation Flow

The following simplified flow illustrates how the three components work together:

// ① Load once at startup in ConversionEngine.mm
self.substitutions = [self getUserDefinedSubstitutions];

// ② During candidate generation when user types "yem"
NSString *buffer = originalInput.lowercaseString; // "yem"
if (self.substitutions && self.substitutions[buffer]) {
    // Adds "you expand me" to the front of suggestions
    [result addObject:self.substitutions[buffer]];
}

End-user experience: Type yem → the IME instantly suggests you expand me as the first candidate, allowing you to insert the full phrase with a single keystroke.

Summary

  • Startup loading: getUserDefinedSubstitutions reads ~/.you_expand_me.json using NSHomeDirectory() and deserializeJSON.
  • Memory storage: The JSON dictionary is cached in self.substitutions during loadPreparedData.
  • Real-time matching: getCandidates: lowercases the input buffer and checks against the substitution dictionary, inserting matches at the top of the suggestion list.
  • Configuration: A flat JSON file with abbreviation keys and expansion values controls the behavior.

Frequently Asked Questions

Where should I place the .you_expand_me.json file?

Place the file in your macOS home directory at ~/.you_expand_me.json (or $HOME/.you_expand_me.json). The engine specifically constructs this path using NSHomeDirectory() during initialization. The file will not be recognized if placed in subdirectories or renamed.

Do I need to restart hallelujahIM after editing the JSON file?

Yes. As documented in the README, you must restart the input method after modifying ~/.you_expand_me.json. The file is read only once during the loadPreparedData initialization phase; changes made while the IME is running require a restart to take effect.

Is text expansion case-sensitive?

No. The engine converts your typed buffer to lowercase before checking against the substitution dictionary. This means typing "YEM", "Yem", or "yem" will all successfully match a key defined as "yem" in your JSON file. However, your JSON keys should be lowercase to ensure consistent behavior.

What happens if my abbreviation matches multiple dictionary entries?

Each abbreviation key must be unique in the JSON file because the implementation uses a simple NSDictionary lookup. If you define duplicate keys, the JSON parser will retain only the last value encountered. The engine does not support multiple expansions for a single abbreviation; it returns exactly one match per key.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →