# How HALLELUJAH IM Handles the Composition Buffer and Original Buffer States

> Understand how HALLELUJAH IM manages composition and original buffer states. Learn how InputController synchronizes keystrokes and finalized candidates.

- Repository: [dongyuwei/hallelujahim](https://github.com/dongyuwei/hallelujahim)
- Tags: internals
- Published: 2026-02-28

---

**The HALLELUJAH IM engine maintains two distinct mutable string buffers—an original buffer for raw keystrokes and a composition buffer for finalized candidates—synchronizing them through the `InputController` class in `src/InputController.mm` before committing text to the client application.**

The HALLELUJAH input method (dongyuwei/hallelujahim) implements a dual-buffer architecture to manage the complex lifecycle of text composition on macOS. By separating raw user input from converted candidates, the engine enables precise control over pre-edit display, deletion handling, and final text insertion while driving the candidate lookup mechanism.

## Dual-Buffer Architecture

The input method maintains two separate `NSMutableString` properties in `src/InputController.mm`:

| Buffer | Purpose | Implementation Location |
|--------|---------|---------------------------|
| **Original Buffer** (`_originalBuffer`) | Holds raw keystrokes entered by the user to drive candidate lookup and spelling suggestions. | Accessor methods `originalBuffer`, `setOriginalBuffer:`, and `originalBufferAppend:` at lines 70-78. |
| **Composition Buffer** (`_composedBuffer`) | Contains the finalized text (selected candidate) ready for insertion into the client application. | Accessor methods `composedBuffer`, `setComposedBuffer:`, and `appendToComposedBuffer:` at lines 58-68. |

## Buffer Lifecycle and State Management

### Initialization via `reset`

The `reset` method clears both buffers and resets the insertion index when starting a new composition or after committing text:

```objc
- (void)reset {
    [self setComposedBuffer:@""];
    [self setOriginalBuffer:@""];
    _insertionIndex = 0;
    …
}

```

**Source:** `src/InputController.mm` lines 45-52.

### Appending Input to the Original Buffer

When the user types alphabetic characters, `onKeyEvent:` appends the character to the original buffer and updates the candidate list:

```objc
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
    [self originalBufferAppend:characters client:sender];
    [sharedCandidates updateCandidates];
    …
}

```

**Source:** `src/InputController.mm` lines 21-28.

### Candidate Selection and Composition Updates

When a user selects a candidate, the engine copies the candidate text into the composition buffer via `_updateComposedBuffer:` before committing:

```objc
- (void)candidateSelected:(NSAttributedString *)candidateString {
    [self _updateComposedBuffer:candidateString];
    [self commitComposition:_currentClient];
}

```

**Source:** `src/InputController.mm` lines 32-36.

### Committing Compositions

The `commitComposition:` method delivers the composition buffer to the client, falling back to the original buffer if the composition buffer is empty:

```objc
- (void)commitComposition:(id)sender {
    NSString *text = [self composedBuffer];
    if (text == nil || text.length == 0) {
        text = [self originalBuffer];
    }
    …
    [sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
    [self reset];
}

```

**Source:** `src/InputController.mm` lines 12-30.

Pressing **Space** or **Return** triggers this commit path, while **Escape** invokes `cancelComposition` to discard the buffers and insert an empty string.

### Handling Backspace and Synchronization

The `deleteBackward:` method keeps both buffers synchronized by removing the last character from the original buffer and mirroring the truncated string to the composition buffer:

```objc
NSMutableString *originalText = [self originalBuffer];
if (_insertionIndex > 0) {
    --_insertionIndex;
    NSString *convertedString = [originalText substringToIndex:originalText.length - 1];
    [self setComposedBuffer:convertedString];
    [self setOriginalBuffer:convertedString];
    …
}

```

**Source:** `src/InputController.mm` lines 88-99.

## Pre-edit Visualization

The `showPreeditString:` method constructs an attributed string that highlights the original buffer portion of the pre-edit text, sending it to the client via `setMarkedText:`:

```objc
- (void)showPreeditString:(NSString *)input {
    NSDictionary *attrs = [self markForStyle:kTSMHiliteSelectedRawText atRange:NSMakeRange(0, input.length)];
    …
    NSString *originalBuff = [NSString stringWithString:[self originalBuffer]];
    if ([input.lowercaseString hasPrefix:originalBuff.lowercaseString]) {
        attrString = [[NSAttributedString alloc]
            initWithString:[NSString stringWithFormat:@"%@%@", originalBuff,
                            [input substringFromIndex:originalBuff.length]]
            attributes:attrs];
    } else {
        …
    }
    [_currentClient setMarkedText:attrString …];
}

```

**Source:** `src/InputController.mm` lines 82-92.

## Key Implementation Files

- **[`src/InputController.h`](https://github.com/dongyuwei/hallelujahim/blob/main/src/InputController.h)**: Declares the buffer properties (`_originalBuffer`, `_composedBuffer`), state variables, and public method signatures.
- **`src/InputController.mm`**: Implements the complete buffer lifecycle including keystroke handling, candidate UI management, and commit/cancel logic.
- **`src/ConversionEngine.mm`**: Generates candidate lists based on the current content of the original buffer.
- **`src/AnnotationWinController.m`**: Renders optional translation windows after candidate selection.

## Summary

- The **original buffer** (`_originalBuffer`) tracks raw user keystrokes using `originalBufferAppend:` and drives candidate lookups via the conversion engine.
- The **composition buffer** (`_composedBuffer`) stores the finalized candidate text populated by `setComposedBuffer:` and inserted via `commitComposition:`.
- Both buffers synchronize during `deleteBackward:` operations and clear simultaneously via `reset` when compositions end.
- The engine provides visual feedback by highlighting the original buffer portion of pre-edit text through `showPreeditString:` before final insertion.

## Frequently Asked Questions

### What is the difference between the original buffer and composition buffer in HALLELUJAH IM?

The **original buffer** stores raw keystrokes as the user types, functioning as the search key for candidate generation in `ConversionEngine.mm`. The **composition buffer** holds the finalized text—usually a selected candidate—that will be inserted into the application when the user commits the composition via `commitComposition:`.

### How does HALLELUJAH IM handle the Escape key during composition?

When the user presses **Escape**, the `cancelComposition` method discards the current composition buffer, inserts an empty string into the client application, and calls `reset` to clear both buffers and reset the insertion index to zero, effectively canceling the current input session.

### What happens when committing an empty composition buffer?

The `commitComposition:` method includes fallback logic that checks if the composition buffer is `nil` or empty. If so, it uses the original buffer content instead, ensuring that pressing **Space** or **Return** without selecting a candidate still inserts the typed romanization or raw input.

### How are the buffers kept synchronized during backspace operations?

The `deleteBackward:` method in `src/InputController.mm` decrements the `_insertionIndex`, removes the last character from the original buffer, and immediately copies the truncated string to the composition buffer via `setComposedBuffer:`. This ensures both buffers reflect the current deletion state and remain consistent for the pre-edit display.