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

크라우니 입력도구 v1 — 구현 완료 (2026-07-11)

개요

사장님 확정 스펙(2026-07-11-크라우니-입력도구-스펙.md)에 따라 터미널 탭에서 실측된 Cmd 조합키 무동작·캐럿/I빔 부재 결함을 해소하고, 캐럿·선택·클립보드를 지원하는 공용 입력 컴포넌트(입력코어.한선)를 신설했다. 한영 OS추종 세션(같은 날 완료, crowny-browser.m 충돌 회피 완료 확인 후 착수) 이후 진행.

실측 결함 진단 (근본원인)

  1. Cmd+V/C/X/A 무동작: crowny-browser.m 메뉴바 Edit 메뉴(native/crowny-browser.m
:6443~6446 부근)가 cut:/copy:/paste:/selectAll:nil-target(첫 리스폰더 체인 디스패치)으로 이미 갖고 있었다. CrownyAppView가 앱탭 활성화 시 첫 리스폰더가 되는데(makeFirstResponder:t.appView, :5223 부근) 이 4개 액션 메서드를 구현하지 않아 ⌘C/V/X/A 키 이퀴벌런트가 performKeyEquivalent 단계에서 메뉴에 먼저 소비되고 keyDown 까지 오지도 못한 채 조용히 무시됐다(진단 그대로 확인 — 별도의 keyDown 기반 Cmd 가로채기는 애초에 도달 불가능해 무의미하다고 판단, 표준 Cocoa 관례대로 4개 액션 메서드를 구현하는 것이 유일하게 유효한 수정).
  1. 캐럿/I빔 부재: 애초에 /tmp/입력상태.psv에 캐럿 개념 자체가 없었다(값 문자열만
존재) — 렌더도 클릭 처리도 캐럿 위치를 몰랐다.

기존 터미널 학습 (스펙 5항 — 설계 반영)

  • crowny-terminal-native/src/app.m (PTY 백엔드 네이티브 터미널): 진짜 NSTextView
품고 있어 캐럿/선택을 AppKit이 대신 처리한다. 자체 커서는 역상(reverse-video) 블록 셀(isCursor → bg를 밝은 회색으로 반전, :906~910)이며 ANSI CSI ?25h/l(모드25)로 가시성만 토글한다 — CrownyBrowser의 PPM/cmd-IR 캔버스에는 대응하는 네이티브 텍스트뷰가 없어 이 접근을 그대로 못 쓴다(직접 캐럿바+선택하이라이트를 그려야 하는 이유).
  • crowny-terminal/web/index.html (웹, xterm.js): 테마 객체가 cursor/cursorAccent/
selectionBackground(반투명 accent 색, 예: rgba(46,204,113,.25)) 키를 갖고 cursorBlink:true 옵션을 쓴다 — 얇은 바 캐럿 + 반투명 틴트 선택 하이라이트 관례. 이번 구현의 캐럿바(2px 수직바)·선택 하이라이트(옅은 톤 사각형) 설계가 이 관례와 가장 가깝다. 단 크라우니 VM의 _프림_사각형은 알파블렌딩이 없어(불투명 RGB만) 반투명 틴트 대신 미리 섞인 톤 색상(라이트: rgb(250,224,204), 다크: rgb(70,58,28))으로 근사했다.

구현

1. 입력코어.한선 (신규 — /Users/ef/CrownyBrowser/src/입력코어.한선)

키입력처리.한선(FOCUS|KEY|BS, /tmp/입력상태.psv 이름|값 2필드)의 KEYCMD 계약을 그대로 두고(그 파일은 무수정 — mouseDown의 FOCUS 기록 경로가 계속 사용), 상태 파일을 이름|값|캐럿|선앞|선뒤 5필드로 확장하는 새 처리기. 캐럿/선택 필드가 없는 줄은 캐럿=값끝· 선택없음으로 기본 해석되어 완전 하위호환.

명령: FOCUS|이름, KEY|c(캐럿 위치 삽입, 선택 있으면 치환), BS(캐럿 앞 삭제/선택 삭제), LEFT/RIGHT(선택 있으면 그 경계로 접기), HOME/END, SELALL, PASTE|텍스트, COPY/CUT(표준출력 SEL|<선택텍스트>), CLICKCARET|localX,h(근사 글자폭 스캔으로 캐럿 인덱스 역산, 크기=h/3·최소8).

2. crowny-browser.m (CrownyAppView)

  • copy:/cut:/paste:/selectAll: 4개 표준 액션 메서드 구현 — Edit 메뉴가 이제
정상적으로 첫 리스폰더(CrownyAppView)까지 도달해 처리된다. paste:는 NSPasteboard → NFC 정규화 → PASTE 명령. copy:/cut:COPY/CUT 명령의 stdout SEL| 라인을 파싱해 NSPasteboard에 씀.
  • appRunInputCoreCmd() — 입력코어.toau 호출 헬퍼(appRunKeyCmd와 동일 NFC 파일 계약).
  • injectKey:키입력처리.toau 대신 입력코어.toau로 라우팅되도록 변경(캐럿이
항상 끝에 있는 한 결과 동일 — 하위호환). "BS" 값은 입력코어의 별도 최상위 BS 모드로 매핑(키입력처리 관례와의 차이를 여기서 흡수).
  • keyDown:에 ←/→/Home/End 캐럿 내비게이션 추가 — 부수 수정: 기존엔 이 키들이
raw 비인쇄 문자(예: 0xF702)로 그냥 injectKey되어 버퍼에 오염 문자가 삽입되는 잠재 버그가 있었다(이번에 함께 수정).
  • mouseDown:에 CLICKCARET 전처리 추가 — appInputHitRect()(히트맵.psv 역순 스캔,
이벤트루프.한선의 히트테스트와 동일 레이아웃)로 클릭이 "입력" 종류 히트영역에 맞았는지 rect를 구해 로컬X·높이를 입력코어에 전달. actionSh(터미널 등)와 폴백 분기 둘 다 이후 각자 FOCUS를 재기록하지만(멱등, 무해) 좌표를 아는 건 여기뿐이라 캐럿 배치는 반드시 이 시점에 선행해야 한다.
  • resetCursorRects 오버라이드 — 히트맵의 "입력" 종류 rect를 뷰 좌표로 역스케일해
addCursorRect:cursor:[NSCursor IBeamCursor]. mouseDown의 캔버스→뷰 역스케일과 대칭인 정스케일 공식 사용. renderAndReload 완료 시 invalidateCursorRectsForView: 호출로 매 렌더마다 갱신.
  • 캐럿 깜빡 — 0.5s NSTimer(캐럿타이머), /tmp/캐럿상태.psv에 on/off 기록 후
재렌더. viewDidMoveToWindow에서 시작/정지(activateTabAtIndex가 매번 비활성 탭뷰를 컨테이너에서 통째로 removeFromSuperview 하므로, 이 훅이 "탭 활성화=창에 붙음/비활성화=창에서 떨어짐"과 정확히 일치 — 별도 활성탭 추적 불요). 매 틱마다 appHasFocusedInput()로 포커스 유무 확인 후 없으면 재렌더 스킵(0.1s 비용 회피).

3. 화면.한선 (렌더 확장 — 기존 서명 유지)

  • _입력상태읽기(키)가 확장 필드(캐럿|선앞|선뒤)를 절단해 "값"만 반환하도록 수정
(하위호환 — 2필드 줄은 기존과 동일 동작). _입력상태원시읽기(신규, 절단 없음)로 캐럿/선택 파싱.
  • _입력캐럿읽기/_입력선앞읽기/_입력선뒤읽기/_캐럿켜짐(신규) — 확장 필드 없으면
캐럿=값끝·선택없음 기본값.
  • _선택하이라이트/_캐럿바(신규, 화면.한선+터미널앱.한선 공용) — 포커스 필드가
아니면 아무것도 안 그림. 입력칸()(공용, 흰 배경)과 어두운입력칸()(터미널 전용, 다크)에 각각 배선. 함수 시그니처 변경 없음(내부에서 상태를 직접 읽으므로 기존 호출부 무수정).

4. 터미널상태.한선 / 한글조합기.한선 / 키입력처리.한선 (방어적 수정)

/tmp/입력상태.psv를 읽는 세 곳(_psv읽기/_상태읽기×2)이 모두 값 뒤 확장 필드를 절단하지 않으면, 입력코어가 5필드로 승격한 줄을 읽을 때 캐럿/선택 숫자가 명령/조합 텍스트에 섞여 들어간다(실측 재현 — 아래 검증 참조). 세 함수 모두 화면.한선과 동일한 "첫 | 뒤 나머지에서 다음 | 앞까지만" 절단 로직을 추가해 방어. 순수 2필드 줄(기존 동작)은 절단 지점이 없어 무변화.

⚠️ 알려진 제약: 한글조합기.한선의 _상태갱신(쓰기 측)은 여전히 2필드로만 기록한다 — 한글 조합 중에는 그 필드의 캐럿이 매 자모마다 "끝"으로 재설정된다(조합은 원래도 끝에서만 진행되므로 기능 저하는 아니지만, 조합 도중 캐럿을 중간으로 옮겨둔 상태를 보존하지 못함). v1 범위 밖으로 문서화만 하고 다음 라운드로 이관.

검증 (헤드리스 E2E)

  1. 입력코어 단독 KEYCMD 시퀀스FOCUS|터미널입력KEY|가 KEY|나 KEY|다
LEFT LEFT(캐럿 3→1) → KEY|X(중간삽입 → "가X나다") → SELALL(0,4) → PASTE|짱(선택치환 → "짱", 캐럿1) — 매 단계 /tmp/입력상태.psv 필드 정합 확인, 전부 기대값과 일치. COPY(선택없음→SEL|, 전체선택→SEL|짱abcCUT(값 비움+ SEL|반환)·BS(선택삭제/캐럿삭제/빈값 3분기)·HOME/END·CLICKCARET(0→0, 9999→끝 클램프, 중간값→가까운 글자경계)까지 전부 검증.
  1. 하위호환: 2필드 레거시 줄(레거시필드|안녕하세요, 캐럿 필드 없음)에 LEFT
실행 → 캐럿=4(=len-1, 정확히 "끝에서 시작"으로 해석됨) 확인, 이어 KEY|! → "안녕하세!요"(끝에서 한 칸 앞 삽입) 정상.
  1. 렌더 — 캐럿 깜빡 픽셀 diff: /tmp/캐럿상태.psv on/off 두 프레임을 터미널
렌더 파이프라인으로 각각 생성, Python으로 PPM 픽셀 비교 → 차이 48px, bbox x=207,208, y=597,620 — 캐럿바만 토글되고 나머지 화면은 완전 동일함을 확인.
  1. 렌더 — 선택 하이라이트 픽셀 diff: 선택있음(2,4) vs 선택없음(4,4) 프레임 비교
→ 714px 차이, bbox가 선택범위 텍스트 위치와 일치, 색상 샘플이 지정한 배경색 계열(다크 골드 틴트) 확인.
  1. 회귀 스모크: 계산기(계산기렌더.sh)·메모(뷰어렌더.sh) 둘 다 exit0 + 유효
PPM(P6 헤더) 확인 — 무캐럿 상태에서 안 깨짐.
  1. 터미널 실행 E2E: FOCUS(마우스클릭 시뮬)→CLICKCARET 생략(끝 삽입 경로)→
echo 입력도구완성을 글자 단위 KEY| 커맨드로 타이핑(입력코어 경유, 기존 키입력처리 아님)→"실행" 액션 → /tmp/터미널상태.psv 스크롤백에 `$ echo 입력도구완성 + 출력 입력도구완성` 정확히 기록, 입력값 정상 초기화 확인. 이 과정에서 터미널상태.한선의 값-절단 수정이 실제로 파이프(|) 오염을 막는지도 함께 재현·확인(수정 전 재현 시 sh: 17: command not found 형태로 캐럿숫자가 셸 파이프로 오인되는 것을 직접 목격 → 수정 후 정상).
  1. 빌드: make web exit0(신규 경고 0 — 기존 무관 경고 2건만), `make public
universal crownyonly 3변종 전부 exit0. otool -L` WebKit 링크수 재확인: crownyonly=0, public/universal/main=1(회귀 없음).
  1. 3곳 배치: 스크래치 임시경로 경유 mv(제자리 cp 금지 규칙 준수) → `native/
CrownyBrowser, CrownyAI.app/Contents/MacOS/CrownyBrowser`, CrownyBrowser.app/Contents/MacOS/CrownyBrowser SHA1 전부 동일: 8f4e2426dace203451d8f33281e951815be7e521.
  1. 자가치유 컴파일 배선: 뷰어렌더.sh(메모)·터미널렌더.sh·터미널액션.sh
toau 자가치유 목록에 입력코어 추가(기존 키입력처리가 있던 곳마다 동일하게) — /tmp/입력코어.toau 삭제 후 각 스크립트 단독 실행으로 자동 재컴파일 확인.

크라우니코드 보고

  • lookup-first 조회: crownycode-learn.sh search "캐럿 선택 입력코어" /
"커서 캐럿 텍스트편집" → 둘 다 MISS(0건, 학습DB/패턴DB/의미어색인 전부 무매치).
  • 생성: 입력코어.한선 전체 신규 작성(1건, 캐럿/선택/버퍼 통합 엔진).
  • 학습: crownycode-learn.sh add 입력코어캐럿선택버퍼 "<전체코드>" → 학습완료.
  • 크라우니코드: lookup HIT 0건(재사용) / MISS 1건(생성) / learn 추가 1건.

하위호환 확인 요약

  • 키입력처리.한선 — 무수정, FOCUS 경로 계속 사용(mouseDown).
  • 화면.한선 입력칸() / 터미널앱.한선 어두운입력칸() — 함수 시그니처 무변경.
  • /tmp/입력상태.psv — 2필드 줄(값만)은 캐럿=끝·선택없음으로 자동 해석, 어떤
기존 소비자도 깨지지 않음(값-절단 방어 4곳 적용).
  • 계산기/메모 등 캐럿 없는 기존 입력칸 — 렌더 스모크로 무회귀 확인.

수정/신규 파일

  • /Users/ef/CrownyBrowser/src/입력코어.한선 (신규)
  • /Users/ef/CrownyBrowser/native/crowny-browser.m (CrownyAppView: 상수·헬퍼·
copy/cut/paste/selectAll·keyDown 내비·mouseDown CLICKCARET·resetCursorRects· 캐럿타이머·injectKey 라우팅)
  • /Users/ef/CrownyBrowser/src/화면.한선 (입력상태 파싱 하위호환화 + 캐럿/선택
읽기·렌더 헬퍼 + 입력칸() 배선)
  • /Users/ef/CrownyBrowser/src/터미널앱.한선 (어두운입력칸() 배선)
  • /Users/ef/CrownyBrowser/src/터미널상태.한선, `/Users/ef/CrownyBrowser/src/
한글조합기.한선, /Users/ef/CrownyBrowser/src/키입력처리.한선` (값-절단 방어)
  • /Users/ef/CrownyBrowser/native/뷰어렌더.sh, 터미널렌더.sh, 터미널액션.sh
(입력코어 자가치유 배선 추가)
  • 배포: native/CrownyBrowser, CrownyAI.app/Contents/MacOS/CrownyBrowser,
CrownyBrowser.app/Contents/MacOS/CrownyBrowser (SHA1 동일 배치)

잔여 이슈 / 다음 단계

  1. GUI 실측(사용자 몫): 실제 클릭으로 캐럿이 정확한 글자 사이에 놓이는지,
Cmd+V/C/X/A가 실제 pasteboard와 오가는지, I빔 커서가 hover 시 정확히 뜨는지는 헤드리스로 재현 불가 — CrownyBrowser 앱을 열어 터미널 탭에서 직접 확인 필요.
  1. 한글 조합 중 캐럿 보존 안 됨(위 "알려진 제약" 참조) — 조합 완료 후에는
정상, 조합 도중 캐럿을 중간으로 옮기는 시나리오만 제약.
  1. Shift+화살표 드래그 선택 미구현 — v1 명령 목록(LEFT/RIGHT/HOME/END/SELALL)에
명시되지 않아 범위 밖으로 뒀다. 필요 시 입력코어에 SHIFTLEFT/SHIFTRIGHT 명령과 mouseDragged: 핸들러 추가로 확장 가능(설계상 여지 있음 — 선앞/선뒤 필드가 이미 존재).
  1. 분할뷰(split) 모드에서 캐럿 타이머 2개 동시 구동 — 두 탭이 동시에 창에
붙으면 /tmp/캐럿상태.psv(전역 파일)를 두 타이머가 각자 토글해 깜빡임 위상이 충돌할 수 있음(기능 손상 아님, 사소한 시각 흠). 필요 시 탭별 캐럿상태 파일 분리로 해결.
  1. 적용 2단계(메모 등 기존 입력칸 앱 전환): 이미 입력칸()이 범용적으로
캐럿/선택을 그리므로 추가 작업 없이 자동 적용됨(메모 등 다른 서비스도 포커스+캐럿상태만 맞으면 즉시 캐럿이 보인다 — 렌더 스모크로 무회귀 확인됨). 다만 그 서비스들의 렌더 스크립트 자가치유 목록에 입력코어가 없으면(계산기 등, 원래 키입력처리도 없었음) /tmp/입력코어.toau가 다른 서비스(메모/터미널) 실행으로 이미 빌드돼있어야 동작 — 기존에도 동일한 암묵적 전역 공유 의존성이 있었으므로 신규 회귀는 아님. 견고화하려면 각 서비스 렌더sh에도 입력코어 추가.
  1. 적용 3단계(문서/프리젠/스프레드 작성기): 아직 미착수(해당 앱 자체가 미존재
— INVENTORY.md 확인 필요). 이 v1 컴포넌트(입력코어.한선 + 화면.한선 캐럿/ 선택 렌더)를 그대로 재사용하면 되도록 설계했음(공용 _선택하이라이트/_캐럿바, 입력칸() 시그니처 무변경).