# UNIM 코어 엔진 세부 기능 명세
> `src/`는 UNIM 프로젝트의 **핵심 라이브러리 크레이트**입니다.
> 한글 자모 조합, 키보드 레이아웃 매핑, 입력 상태 관리, 한자 변환 등
> 모든 입력 처리 로직이 이 디렉토리에 집약되어 있습니다.
> 프론트엔드(GTK/Qt/XIM/Wayland)와 데몬은 이 크레이트의 소비자입니다.
---
## 1. 아키텍처 개요
### 1.1 모듈 계층 구조
```
src/
├── lib.rs # 크레이트 루트 — 모듈 선언
├── input_engine.rs # ★ 최상위 엔진 — 키 입력 진입점
├── config.rs # 설정 구조체 (YAML 직렬화)
├── keycode.rs # 키코드 추상화 (evdev/X11 → KeyCode)
├── status.rs # 상태 파일 공유 (~/.cache/unim/status)
├── logging.rs # 통합 로깅 (unim_log! 매크로)
├── hangul/ # 한글 조합 서브시스템
│ ├── mod.rs # 모듈 선언 + re-exports
│ ├── jamo.rs # 자모 열거형 (Cho/Jung/Jong/JamoEnum)
│ ├── char.rs # 유니코드 음절 조합 (HangulChar)
│ ├── composer.rs # 조합기 트레이트 + 기본 구현 (BaseHangulComposer)
│ ├── composer_with_2bul.rs 두벌식 조합 (도깨비불 현상 포함)
│ ├── composer_with_3bul.rs 세벌식 조합
│ ├── input_context.rs # 조합 세션 관리 (HangulInputContext)
│ └── hanja.rs # 한자 사전 (빌드 시 임베드)
├── keystroke/ # 키보드 레이아웃 매핑
│ ├── mod.rs # JSON 키맵 로드 + 변환 유틸리티
│ ├── keyboard_map.rs # 영어→자모 매핑 생성
│ ├── keystrokes_to_korean.rs 키스트로크 → 한글 변환
│ ├── korean_to_keystrokes.rs 한글 → 키스트로크 역변환
│ └── keymap/ # JSON 키맵 데이터 (9개)
│ ├── en_qwerty.json QWERTY
│ ├── en_dvorak.json Dvorak
│ ├── en_colemak.json Colemak
│ ├── en_colemak_dh.json Colemak-DH
│ ├── en_workman.json Workman
│ ├── ko_2bulstd.json 두벌식 표준
│ ├── ko_3bul390.json 세벌식 390
│ ├── ko_3bul391.json 세벌식 최종
│ └── ko_3bul_noshift.json 세벌식 순아래
└── data/
└── hanja.txt # 한자 사전 원본 (libhangul 호환)
```
### 1.2 코드 규모
| 영역 | 파일 수 | 코드 행 | 바이트 |
|------|---------|---------|--------|
| 엔진 + 설정 + 상태 | 4 | ~1,970 | 62KB |
| 한글 조합 (`hangul/`) | 8 | ~3,100 | 144KB |
| 키스트로크 매핑 (`keystroke/`) | 4+9 JSON | ~620 | 59KB |
| 로깅 | 1 | 71 | 2KB |
| **합계** | **~26** | **~5,800** | **~267KB** |
### 1.3 의존 흐름
```mermaid
graph TD
IE["InputEngine
input_engine.rs"]
HIC["HangulInputContext
input_context.rs"]
HC["HangulComposer
composer.rs (trait)"]
HC2["HangulComposer2Bul
composer_with_2bul.rs"]
HC3["HangulComposer3Bul
composer_with_3bul.rs"]
HCH["HangulChar
char.rs"]
JM["Jamo (Cho/Jung/Jong)
jamo.rs"]
KM["KeyboardMap / EnglishKeymap
keyboard_map.rs"]
JSON["JSON Keymaps
keymap/*.json"]
KC["KeyCode / ModifierState
keycode.rs"]
CFG["Config
config.rs"]
HJ["HanjaDictionary
hanja.rs"]
ST["Status
status.rs"]
IE --> HIC
IE --> KM
IE --> KC
IE --> CFG
IE --> HJ
IE --> ST
HIC --> HC
HC --> HC2
HC --> HC3
HC2 --> HCH
HC3 --> HCH
HCH --> JM
KM --> JSON
KM --> JM
```
---
## 2. InputEngine — 키 입력의 심장
### 2.1 구조체
```rust
pub struct InputEngine {
input_category: InputCategory, // 현재 모드 (Korean/English)
korean_context: HangulInputContext, // 한글 조합 세션
commit_buffer: String, // 확정된 텍스트 버퍼
preedit_cache: String, // 조합 중 텍스트 캐시
keyboard_map: Option>, // 영문→자모 매핑
english_keymap: EnglishKeymap, // 영어 레이아웃 키맵
english_layout: EnglishLayout, // 영어 레이아웃 설정
korean_layout: KoreanLayout, // 한국어 레이아웃 설정
toggle_keys: Vec, // 한/영 전환키 (설정 기반)
hanja_dict: Arc, // 한자 사전 (공유)
hanja_candidates: Vec, // 현재 한자 후보
hanja_mode: bool, // 한자 선택 모드
hanja_target: String, // 한자 변환 대상 음절
}
```
### 2.2 InputResult — 7가지 결과 패턴
| 팩토리 메서드 | consumed | preedit_changed | commit_changed | 사용 시점 |
|--------------|----------|-----------------|----------------|-----------|
| `not_consumed()` | ✗ | ✗ | ✗ | 엔진이 처리하지 않음 → 앱으로 전달 |
| `consumed()` | ✓ | ✗ | ✗ | 키 소비만 (한/영 전환 등) |
| `preedit_updated()` | ✓ | ✓ | ✗ | 조합 중 갱신 (자모 입력, Backspace) |
| `committed()` | ✓ | ✓ | ✓ | 문자 확정 (음절 완성, Space 등) |
| `committed_passthrough()` | ✗ | ✓ | ✓ | 조합 커밋 + 키 통과 (Enter, Tab, Escape) |
| `hanja_candidates()` | ✓ | ✗ | ✗ | 한자 후보 준비됨 |
| `special_char_candidates()` | ✓ | ✗ | ✗ | 특수문자 후보 준비됨 (한자 없을 때) |
> [!IMPORTANT]
> `committed_passthrough()`는 UNIM의 핵심 설계 중 하나입니다.
> Enter/Tab/Escape 같은 특수키가 조합을 확정시키되, **키 자체는 애플리케이션에 전달**되어야 합니다.
> `consumed=false`이므로 프론트엔드는 커밋을 처리한 뒤 키를 그대로 애플리케이션에 전달합니다.
### 2.3 키 처리 파이프라인 (`press_key`)
```mermaid
flowchart TD
START["press_key(keycode, modifier, config)"]
MOD{"수정자 키만?
(Shift/Ctrl/Alt 등)"}
CTRL{"Ctrl/Alt/Super
눌림?"}
COMPOSE_CHECK{"조합 중?"}
TOGGLE{"한/영 키?\n(설정: toggle_keys)"}
CATEGORY{"input_category?"}
KOREAN["process_korean_key()"]
ENGLISH["process_english_key()"]
START --> MOD
MOD -->|Yes| NC["not_consumed()"]
MOD -->|No| CTRL
CTRL -->|Yes| COMPOSE_CHECK
COMPOSE_CHECK -->|"Yes → flush"| CM1["committed()"]
COMPOSE_CHECK -->|No| NC
CTRL -->|No| TOGGLE
TOGGLE -->|Yes| T1["toggle + consumed/committed()"]
TOGGLE -->|No| CATEGORY
CATEGORY -->|Korean| KOREAN
CATEGORY -->|English| ENGLISH
```
### 2.4 한국어 키 처리 (`process_korean_key`)
```mermaid
flowchart TD
K["process_korean_key(keycode, modifier)"]
HANJA{"Hanja 키?\n(설정: hanja_keys)"}
BS{"Backspace?"}
ENTER{"Enter/Tab/
Escape?"}
SPACE{"Space?"}
CHAR{"문자 키?
(영어 키맵 조회)"}
JAMO{"자모 매핑
존재?"}
SPECIAL{"Special 자모?"}
COMPOSING{"조합 중?"}
K --> HANJA
HANJA -->|Yes| HC["start_hanja_conversion()"]
HANJA -->|No| BS
BS -->|Yes, 조합 중| PU["preedit_updated()"]
BS -->|Yes, 미조합| NC["not_consumed()"]
BS -->|No| ENTER
ENTER -->|Yes, 조합 중| CP["committed_passthrough()"]
ENTER -->|Yes, 미조합| NC
ENTER -->|No| SPACE
SPACE -->|Yes| CM["committed()"]
SPACE -->|No| CHAR
CHAR -->|Some(c)| JAMO
CHAR -->|None, 조합 중| CP2["committed_passthrough()"]
CHAR -->|None, 미조합| NC2["not_consumed()"]
JAMO -->|Yes| SPECIAL
JAMO -->|No| SYM["기호: flush + committed()"]
SPECIAL -->|Yes| SYM2["Special 문자 커밋"]
SPECIAL -->|No| PROC["process_jamo → preedit/committed"]
```
> [!NOTE]
> **CapsLock 처리 차이**: 한국어 모드에서는 CapsLock을 **무시**합니다.
> Shift만 적용되어 쌍자음(ㄲ, ㄸ 등)을 입력할 수 있습니다.
> 영어 모드에서는 `Shift XOR CapsLock` 로직으로 표준 대소문자 전환을 지원합니다.
### 2.5 영어 키 처리 (`process_english_key`)
```
1. lower_char = english_keymap.get_char(keycode, false)
2. is_alpha = lower_char가 알파벳?
3. shifted:
→ 알파벳: Shift XOR CapsLock (둘 다 켜면 소문자)
→ 숫자/기호: Shift만 적용
4. ch = english_keymap.get_char(keycode, shifted)
5. ch → commit_buffer → committed()
6. None → not_consumed()
```
### 2.6 레이아웃 핫스왑
```rust
pub fn set_korean_layout(&mut self, layout: KoreanLayout) {
// 1. 조합 중이면 flush
// 2. keyboard_map 재생성 (영어↔한국어 매핑)
// 3. ComposerType 결정 (TwoBul/ThreeBul)
// 4. HangulInputContext 재생성
}
pub fn set_english_layout(&mut self, layout: EnglishLayout) {
// 1. 조합 중이면 flush
// 2. keyboard_map 재생성
// 3. english_keymap 재생성 (JSON 기반)
}
```
---
## 3. 설정 시스템 (`config.rs`)
### 3.1 구조 계층
```yaml
# ~/.config/unim/config.yaml
engine:
default_category: Korean # InputCategory
mode_sharing: Global # ModeSharingMode
korean:
layout: Dubeolsik # KoreanLayout
english:
layout: Qwerty # EnglishLayout
toggle_keys: # Vec
- Korean
- RightAlt
hanja_keys: # Vec
- Hanja
- F9
auto_typefix: # AutoTypeFixConfig
enabled: true
time_window_ms: 2000
kor_syllable_threshold: 3
eng_word_min_length: 4
forward: true # 영→한 오타 교정 (ASCII 흐름 중 한글 음절 감지)
reverse: true # 한→영 오타 교정 (한글 preedit 중 영단어 감지)
skip_on_english_word: true # 사전에 있는 영단어면 교정 보류
skip_on_complete_syllable: true # 이미 완성된 한글 음절이면 forward 차단
rollback_detection: true # (4315dce) 롤백 관측 기반 Blacklist 자동 등록
tentative_expiry_hours: 1 # (4315dce, aeab5f5) Tentative 엔트리 Inactive 전환까지의 시간
observation_timeout_secs: 10 # (aeab5f5) 롤백 관측 pending flag 유지 시간(초)
```
```rust
pub struct Config {
pub engine: EngineConfig, // config_path, last_mtime 포함
}
pub struct EngineConfig {
pub default_category: InputCategory,
pub mode_sharing: ModeSharingMode,
pub korean: KoreanConfig, // { layout: KoreanLayout }
pub english: EnglishConfig, // { layout: EnglishLayout }
pub toggle_keys: Vec, // 한/영 전환키 목록 (기본: ["Korean", "RightAlt"])
pub hanja_keys: Vec, // 한자/특수문자키 목록 (기본: ["Hanja", "F9"])
pub auto_typefix: AutoTypeFixConfig, // 자동 오타 교정 설정 (§3.1.1)
}
```
#### 3.1.1 `AutoTypeFixConfig`
자동 오타 교정(AutoTypeFix) 서브 구조. 범위 상수는 `config.rs`의
`AUTO_TYPEFIX_*_MIN/MAX`로 노출되며 `Config::clamp_ranges()`가
로드/세트 시점에 강제한다.
| 필드 | 타입 | 기본값 | 범위 | 역할 |
|------|------|-------|------|------|
| `enabled` | bool | true | — | 마스터 스위치 |
| `time_window_ms` | u32 | 2000 | 500..=5000 | 키 입력 버퍼 유지 시간(ms) |
| `kor_syllable_threshold` | u8 | 3 | 2..=6 | forward 트리거 최소 한글 음절 수 |
| `eng_word_min_length` | u8 | 4 | 3..=8 | reverse 트리거 최소 영단어 길이 |
| `forward` | bool | true | — | 영→한 교정 활성 (영어 모드인데 한글로 변환되는 ASCII 흐름 감지) |
| `reverse` | bool | true | — | 한→영 교정 활성 (한글 모드 preedit이 사전상 영단어일 때) |
| `skip_on_english_word` | bool | true | — | forward에서 한글 변환 결과를 영어 사전에 질의해 매치되면 보류 |
| `skip_on_complete_syllable` | bool | true | — | forward에서 이미 완성된 한글 음절이 있으면 트리거 차단 |
| `rollback_detection` | bool | true | — | **(4315dce)** 롤백 관측 기반 Blacklist 자동 등록 활성 |
| `tentative_expiry_hours` | u16 | 1 | 1..=12 | **(4315dce, aeab5f5)** Tentative 엔트리의 Inactive 자동 전환 시간. 초기 구현에서 `tentative_expiry_days`(1..=90)였으나 aeab5f5에서 `hours`/1..=12로 개명·축소. 레코드는 보존, 억제 효과만 해제 |
| `observation_timeout_secs` | u8 | 10 | 5..=15 | **(aeab5f5)** 롤백 관측 pending flag 유지 시간(초). BS/모드 전환 관찰이 이 시간 내에 재시도로 이어져야 Tentative 등록 |
> [!NOTE]
> `forward`/`reverse` 방향 판정의 IME 관측 관점은
> [`IME_BEHAVIOR.md §9`](../IME_BEHAVIOR.md#9-autotypefix-억제-사전-blacklist--ime-레벨-관측-동작)을 참조한다.
### 3.2 열거형 정의
#### InputCategory
| 값 | 설명 |
|----|------|
| `Korean` (기본) | 한국어 입력 모드 |
| `English` | 영어 입력 모드 |
#### ModeSharingMode
| 값 | 설명 |
|----|------|
| `Global` (기본) | 모든 컨텍스트가 동일한 모드 공유 |
| `PerApp` | 각 InputContext가 독립 모드 유지 |
| `PerWindow` | 각 창이 독립 모드 유지 |
#### KoreanLayout
| 값 | repr | 세벌식? | 설명 |
|----|------|---------|------|
| `Dubeolsik` (기본) | 0 | ✗ | 두벌식 표준 |
| `Sebeolsik390` | 1 | ✓ | 세벌식 390 |
| `Sebeolsik391` | 2 | ✓ | 세벌식 최종 |
| `SebeolsikNoShift` | 3 | ✓ | 세벌식 순아래 |
#### EnglishLayout
| 값 | repr | keymap 파일 | 설명 |
|----|------|-------------|------|
| `Qwerty` (기본) | 0 | `en_qwerty` | 표준 QWERTY |
| `Dvorak` | 1 | `en_dvorak` | Dvorak |
| `Colemak` | 2 | `en_colemak` | Colemak |
| `ColemakDh` | 3 | `en_colemak_dh` | Colemak-DH (인체공학 개선) |
| `Workman` | 4 | `en_workman` | Workman |
### 3.3 설정 관리 메서드
| 메서드 | 기능 |
|--------|------|
| `Config::new()` | 기본값 생성 |
| `Config::default_config_path()` | `~/.config/unim/config.yaml` |
| `Config::load_from_default_path()` | 파일 로드 (없으면 기본값 + 파일 생성) |
| `Config::save_to_default_path()` | 파일 저장 |
| `Config::reload_if_changed()` | 파일 수정 시간 비교, 변경 시 리로드 (Throttling) |
| `Config::ensure_config_file()` | 파일 없으면 기본값으로 생성 |
> [!NOTE]
> `reload_if_changed()`는 **매 키 입력마다** 호출되므로, 파일 시스템 접근을 최소화하기 위해
> 내부적으로 `last_mtime` 캐시와 비교하여 실제 변경 시에만 파싱합니다.
---
## 4. 한글 조합 서브시스템 (`hangul/`)
### 4.1 계층 설계 — Strategy 패턴
```
┌─────────────────────────────┐
│ HangulComposer (trait) │
│ │
│ add_jamo() → Option │
│ remove_jamo() │
│ compose_korean() │
│ force_compose_korean() │
│ is_compose() │
│ compose_cho/jung/jong() │
└──────────┬──────────────────┘
│ impl
┌──────────────┴──────────────┐
│ │
┌─────────┴─────────┐ ┌─────────┴─────────┐
│ HangulComposer2Bul│ │ HangulComposer3Bul│
│ │ │ │
│ • 도깨비불 현상 │ │ • 초/중/종 분리키 │
│ • 초→종 자동변환 │ │ • 초성 조합 │
│ • 겹받침 분리 │ │ • 순서 위반 감지 │
└─────────┬─────────┘ └─────────┬─────────┘
│ delegate │ delegate
└──────────┬─────────────────┘
│
┌─────────┴─────────┐
│ BaseHangulComposer │
│ │
│ • jamo_queue │
│ • combined_jamo │
│ • current_korean │
│ • last_jamo_queue │
└─────────┬─────────┘
│ uses
┌─────────┴─────────┐
│ HangulChar │
│ │
│ • cho/jung/jong │
│ • to_syllable() │
│ • 유니코드 연산 │
└───────────────────┘
```
### 4.2 HangulInputContext — 조합 세션 관리
```rust
pub struct HangulInputContext {
composer_type: ComposerType, // TwoBul / ThreeBul
composer: Box, // 동적 디스패치
preedit: String, // 현재 조합 중 문자열
committed: String, // 확정된 문자열
}
```
#### 주요 메서드
| 메서드 | 설명 |
|--------|------|
| `process_jamo(jamo)` | 자모 입력 → 조합 → preedit/committed 갱신 |
| `backspace()` | 마지막 자모 제거 |
| `commit()` | 현재 조합 강제 확정 |
| `get_preedit()` / `get_committed()` | 버퍼 조회 |
| `is_composing()` | 조합 중 여부 |
| `set_composer_type()` | 조합기 전환 (조합 중이면 먼저 확정) |
#### 조합 흐름 예시 (두벌식: "한글" 입력)
| 입력 | jamo_queue | preedit | committed | 설명 |
|------|-----------|---------|-----------|------|
| ㅎ | [Cho(H)] | ㅎ | | 초성 입력 |
| ㅏ | [Cho(H), Jung(A)] | 하 | | 중성 결합 → 음절 |
| ㄴ | [Cho(H), Jung(A), Jong(N)] | 한 | | 종성 결합 |
| ㄱ | [Cho(G)] | ㄱ | 한 | 도깨비불: "한" 확정, "ㄱ" 새 음절 |
| ㅡ | [Cho(G), Jung(Eu)] | 그 | 한 | 중성 결합 |
| ㄹ | [Cho(G), Jung(Eu), Jong(R)] | 글 | 한 | 종성 결합 |
### 4.3 도깨비불 현상 (두벌식 핵심)
한국어 두벌식 입력의 가장 중요한 특성은 **도깨비불 현상**입니다.
```
"한" + 'ㄱ' (초성) 입력 시:
↓
종성 ㄴ과 구별 불가 (두벌식에서는 초성과 종성이 같은 키)
↓
"한ㄱ"으로 일단 조합 유지
↓
다음 입력이 중성이면: "한" 확정 + "ㄱ"을 새 음절의 초성으로 분리
다음 입력이 초성이면: 겹받침 가능 여부 확인 → 불가능하면 새 음절
```
```rust
// composer_with_2bul.rs
fn handle_dokkaebi_effect(&mut self, jamo: JamoEnum) -> Option