← 목록
기타 2026-07-11 10KB 읽기 10분

크라우니터미널 네이티브 — 한글 입력(OS 한영 전환 추종) 배선

개요

사장님 확정: crowny-terminal-native(맥·윈 네이티브 PTY 터미널, 클로드코드 실작업용)는 OS의 한영 전환을 그대로 따른다 — 맥에서는 맥 입력소스, 윈도우에서는 윈도우 IME가 조합을 전담한다. 이는 실제 터미널 에뮬레이터 표준 패턴(Terminal.app/iTerm 방식)이며, 크라우니 자체 조합기(한영 로직)는 이 레이어에 관여하지 않는다(그건 브라우저 PPM 앱 쪽 담당). 이번 세션은 맥/윈/리눅스 3개 네이티브 소스를 실사해 IME 조합 경로가 실제로 OS에 위임되고 있는지 확인하고, 맥에서 발견된 실제 버그를 표준 패턴으로 수정했다.

실사 결과

맥 (src/app.m) — 버그 발견 + 수정

기존 구조는 PtyView(컨테이너 NSView)가 자식 NSTextView(_textView, editable=YES)를 갖고, _textView.delegate = self + textView:shouldChangeTextInRange:replacementString: 델리게이트로 모든 텍스트 변경을 가로채 PTY로 리다이렉트하는 방식이었다. 두 가지 문제:

  1. PtyView.keyDown:은 죽은 코드였다. 클릭 시 실제 첫 리스폰더가 되는 건 (진짜
NSTextView인) _textView이지 PtyView가 아니다(PtyViewacceptsFirstResponder를 오버라이드하지 않아 기본값 NO). 즉 화살표/기능키/Ctrl조합을 처리하던 스위치문이 실제로는 한 번도 호출되지 않았다.
  1. shouldChangeTextInRange:replacementString:가 IME 조합(marked text) 변경까지
가로챈다. 한글 조합 중 매 자모 변화가 "텍스트 변경"으로 델리게이트에 들어오고, 이걸 매번 PTY로 즉시 전송 + NO 반환(변경 거부)하면 조합 중간 상태가 커밋되지 않은 채 PTY로 새어나가고, NSTextView 내부의 marked-range 북키핑도 깨진다 — 사실상 한글 조합이 정상 동작할 수 없는 구조였다.

수정: 표준 터미널 패턴 채택. _textViewPtyTextView : NSTextView 서브클래스로 교체하고, 이 클래스가 실제 첫 리스폰더로 남는다(변경 없음 — 애초에 그랬으므로).

  • keyDown: → 화살표/기능키/Ctrl조합만 [ptyOwner handleSpecialKey:event]로 즉시
이스케이프 시퀀스 변환. 매치 안 되면(일반 문자·한글 조합 이벤트 포함) [super keyDown:] 호출 → AppKit의 interpretKeyEvents:가 시스템 IME(한글 2벌식 등) 조합을 정상 처리.
  • insertText:replacementRange:커밋 완료된 텍스트(비-IME 일반 입력 또는 조합
완료된 한글 음절)만 UTF-8로 PTY에 씀. textStorage는 절대 직접 편집하지 않음(실제 화면은 PTY 에코를 받은 renderScreen()이 전담 — 기존 설계 불변 유지).
  • doCommandBySelector: → 개행/탭/삭제(Backspace/Delete)/이동/Escape 등 비-텍스트
편집 명령을 PTY 제어 시퀀스로 변환.
  • setMarkedText:오버라이드하지 않음NSTextView 기본(super) 구현에 위임해
조합 중 밑줄 미리보기가 AppKit 표준 방식으로 자연히 그려진다("최소구현: 마킹 없이 커밋만도 가" 요구를 상회).
  • paste: 오버라이드 — 기본 NSTextView.paste:textStorage에 직접 삽입하므로,
클립보드 문자열을 가로채 PTY로 직접 씀(기존 delegate hack이 처리하던 것을 대체).
  • PtyView.renderScreen()if (_textView.hasMarkedText) return; 가드 추가 — IME
조합 중에는 textStorage 전체 재작성을 보류해 AppKit이 그린 조합 미리보기를 PTY 출력이 지워버리지 않게 보호.

윈도우 (src/app_win.c) — 이미 부분 동작 + 앵커링 보강

WM_CHAR가 이미 WideCharToMultiByte → PTY 기록으로 배선돼 있었고, Windows 기본 동작상 IME 커밋 결과는 DefWindowProc이 자동으로 WM_CHAR로 합성해 전달하므로 기능 자체는 이미 어느 정도 동작하고 있었다(코드에 WM_IME_* 처리가 전혀 없었음에도). 다만:
  • 조합창(candidate/composition box) 위치를 앵커링하는 코드가 없어 기본 위치(대체로
창 좌상단 근처)에 뜬다 — 터미널 커서와 무관한 위치라 사용성이 떨어짐.
  • 커밋 경로가 OS 기본 합성(WM_CHAR)에 암묵적으로 의존해, 명시적 제어가 없었음.
보강: WM_IME_SETCONTEXT/WM_IME_STARTCOMPOSITION에서 updateImeCompositionPos() (신규 헬퍼, ImmSetCompositionWindow/ImmSetCandidateWindow CFS_POINT/CFS_CANDIDATEPOS)로 조합창을 실제 터미널 커서 셀(t->curCol * g_charW, 28 + t->curRow * g_charH, 28=탭바 높이)에 앵커링. WM_IME_COMPOSITION에서 GCS_RESULTSTR만 명시적으로 가로채 ImmGetCompositionStringW로 커밋 문자열을 직접 얻어 UTF-8 변환 후 PTY에 기록하고 return 0(DefWindowProc 미호출 → 기본 WM_CHAR 자동합성과 중복 기록 방지). 조합 진행 중(GCS_COMPSTR만 있고 RESULTSTR 없음)인 경우는 가로채지 않고 기본 처리로 위임해 OS 자체 조합 미리보기 UI를 그대로 사용. WM_INPUTLANGCHANGE는 상태 표시용이라 판단해 구현 보류(선택 사항, 사장님 지시대로).
  • imm.h include + imm32.lib 링크(pragma comment + Makefile.win의 MinGW
MINGW_LIBS-limm32 추가, MSVC cl 링크줄에도 imm32.lib 추가).

리눅스 (src/app_linux.c) — 실사만, 구현 보류

GTK3 + libvte(VteTerminal 위젯) 구조. on_key_press윈도우 레벨에 연결돼 있고 Ctrl+T/Ctrl+W/Ctrl+Shift+[/](탭 이동)/Ctrl+Shift+C/V(클립보드)만 가로채 TRUE를 반환하며, 그 외 모든 키(일반 문자·한글 조합 진행·조합 완료·화살표· 개행·삭제 포함)는 FALSE를 반환해 포커스된 VteTerminal 위젯으로 그대로 전달된다. VteTerminalGtkIMContext(XIM/IBus/Fcitx — 사용자 시스템의 입력 방식 모듈에 따라 자동 선택)를 위젯 내부에 이미 완전히 구현하고 있어, 코드 변경 없이 이미 정상 동작한다 (VTE는 조합 진행 중 미리보기, 커밋, PTY 전달까지 전부 자체 처리). 결론: XIM/ibus 연동은 필요 없음(VTE가 대신함) — 문서화만 하고 구현 보류.

검증

  • 맥 빌드: make app → exit 0, 경고 0, .app 재생성 완료(런타임 미기동 — 데몬/GUI
기동 금지 지침 준수).
  • GUI 없는 하네스(요구사항): /tmp 스크래치패드에 ime_harness.m 작성.
src/app.m-DCROWNY_HARNESS_BUILD로 컴파일(신규 매크로가 main()을 제외 — 운영 빌드는 매크로 미정의라 영향 없음, make app으로 정상 빌드 재확인)해 실제 PtyTextView/PtyView 클래스에 직접 링크. openpty()로 하네스 내부에 PTY 쌍 생성 (데몬 아님, 검증 후 즉시 종료) + cfmakeraw로 라인 discipline 변환 제거, slave fd에서 관측(자식 셸이 실제로 받을 바이트와 동일 관측점). 8개 케이스 전부 PASS(exit 0): ASCII 커밋, 한글 조합 완료 커밋("안녕" → UTF-8 바이트 그대로 PTY 도달), \n\r 변환, doCommandBySelector:(개행/삭제/이동), paste:(클립보드→PTY, textStorage 우회), textStorage 불변(길이 0→0, insertText:가 문서를 건드리지 않음을 확인).
  • ANSI 회귀: 기존 헤드리스 C 하네스(/tmp/ansi_harness.c, SGR/CUP/ED·EL/DECSTBM/
대체화면 25케이스)를 재실행 — 전부 PASS(exit 0). 이번 작업은 키 입력 라우팅만 건드렸고 ANSI 파서/스크린버퍼 로직은 무변경이라 예상대로 회귀 없음.
  • 윈도우 크로스컴파일: x86_64-w64-mingw32-gcc(homebrew) 존재 확인,
make -f Makefile.win cross → exit 0, build/크라우니터미널.exe (PE32+ 64-bit) 정상 생성. 실기동(WM_IME 메시지 흐름)은 실제 Windows 10 1809+ 환경에서만 확인 가능 — 이 세션은 macOS라 크로스컴파일 빌드 검증까지만 수행.

관련 파일

  • /Users/ef/crowny-terminal-native/src/app.mPtyTextView 클래스 신설(NSTextView
서브클래스), PtyView.handleSpecialKey:/renderScreen() 가드/main() 하네스 매크로.
  • /Users/ef/crowny-terminal-native/src/app_win.cupdateImeCompositionPos(),
WM_IME_SETCONTEXT/WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION 케이스, #include <imm.h> + imm32.lib.
  • /Users/ef/crowny-terminal-native/src/app_linux.c — 무변경(실사만).
  • /Users/ef/crowny-terminal-native/Makefile.win-limm32(MinGW) / imm32.lib(MSVC)
링크 추가.
  • /Users/ef/crowny-terminal-native/한선씨/입력라우팅.한선 — 소스(맥/윈/리눅스) ×
키종류(9종) 라우팅 모드 매트릭스를 한선씨로 이관 + 자가검증(10건 PASS, hanseonc_high/crownyc 컴파일·실행 확인).
  • /Users/ef/crowny-terminal-native/README.md — 이번 작업 반영(갱신).
  • 하네스(스크래치패드, 세션 종료 시 정리 대상): ime_harness.m,
ansi_harness.c(기존, 재사용).

잔여 이슈

  • 윈도우 실기동 미검증: 크로스컴파일 빌드는 성공했지만 실제 Windows 환경에서
WM_IME_COMPOSITION/한글 IME 흐름의 육안 확인은 못 했다(맥 세션 한계). 다음 Windows 세션에서 build/크라우니터미널.exe 실행 + 한글 2벌식으로 실측 요망.
  • 맥 IME 후보창 위치: NSTextView 기본 firstRectForCharacterRange:에 의존하는데,
textStorage를 매 PTY 출력마다 통째로 교체하는 설계상 커서가 정확히 터미널 그리드의 시각적 커서 위치와 일치하지 않을 수 있음(대체로 문서 끝 근처라 실사용상 크게 어긋나진 않음, "최소구현" 범위로 판단해 보류).
  • 리눅스: 별도 조치 불요로 판단했지만, 실제 Linux 환경에서 IBus/Fcitx로 실측
검증은 하지 못함(이 세션 = macOS).
  • 병렬로 CrownyBrowser 세션이 별도 작업 중이라는 사전 지시에 따라 그 파일들은 전혀
건드리지 않음(수정 범위 = crowny-terminal-native 내부만 확인).