터미널용 Glyph Protocol 소개
(rapha.land)- 터미널 애플리케이션에서 커스텀 아이콘을 렌더링하려면 패치된 폰트(Nerd Font 등)를 설치해야 하는 기존 문제를 해결하기 위해 새로운 터미널 프로토콜이 등장
- Glyph Protocol은 애플리케이션이 런타임에 벡터 글리프를 터미널에 직접 등록하고, 특정 코드포인트의 렌더링 가능 여부를 질의할 수 있는 구조
- 글리프 데이터는 TrueType의
glyf포맷을 사용하며, 터미널이 이미 보유한 래스터라이저를 그대로 활용해 새로운 의존성 없이 구현 가능 - 등록 대상 코드포인트를 Unicode Private Use Area(PUA) 로 제한해 피싱·시각 위조 공격을 원천 차단
- Rio 터미널에서 첫 구현이 진행 중이며, Bubble Tea·Ratatui·Ink 등 주요 TUI 프레임워크용 예제 코드가 공개된 상태
기존 문제: 패치 폰트 의존성
- 터미널 에디터, 프롬프트, TUI가 아이콘을 제대로 표시하려면 사용자가 Nerd Font이나 Powerline 같은 패치 폰트를 직접 설치해야 함
- 폰트를 설치하지 않으면 아이콘 자리에 tofu(□) 가 표시되며, 패치 폰트는 개당 6~12MB 수준의 대용량
- JetBrainsMono Nerd Font Regular 약 7.8MB, FiraCode Nerd Font Regular 약 10.4MB, 전체 심볼 아카이브는 약 60MB
- 애플리케이션 개발자는 원하는 글리프를 직접 배포할 방법이 없고, 사용자가 올바른 폰트·버전·코드포인트 매핑을 갖추고 있기를 기대할 수밖에 없는 구조
Glyph Protocol 핵심 기능
- 두 가지 핵심 동작 지원
- 커스텀 글리프 등록: 애플리케이션이 Unicode PUA 코드포인트를 선택하고, 벡터 아웃라인을 터미널에 직접 전송해 런타임에 등록
- 코드포인트 질의: 특정 코드포인트가 시스템 폰트에 의해 커버되는지, 세션 내 등록에 의해 커버되는지, 둘 다인지, 아무것도 아닌지를 질의
- 사용자가 Nerd Font을 이미 설치했으면 질의를 통해 글리프 전송을 생략하고, 미설치 상태에서도 애플리케이션이 아웃라인을 직접 전송해 아이콘이 정상 표시
프로토콜 구조
전송 방식 (Transport)
- OSC 대신 APC(Application Program Command) 를 사용
- APC는 애플리케이션 정의 명령을 위해 설계되었으며, 미구현 터미널은 해당 시퀀스를 안전하게 무시 가능
- OSC는 단일 10진수 정수를 명령 식별자로 사용하는 전역 네임스페이스를 공유하므로 충돌 위험이 있으나, APC는 자체 식별 구조로 이 문제가 없음
식별자 (Identifier)
- 모든 Glyph Protocol 메시지에
25a1(U+25A1, WHITE SQUARE) 코드포인트가 접두사로 붙음- 이 문자는 글리프가 없을 때 터미널이 그리는 tofu의 표준 심볼
- 프레이밍 형식:
ESC _ 25a1 ; <verb> [ ; key=value ]* [ ; <payload> ] ESC \ - 4개의 verb:
s(support),q(query),r(register),c(clear)
Support (s): 터미널 지원 여부 확인
- 터미널이 어떤 페이로드 포맷과 프로토콜 버전을 지원하는지 확인하는 용도
- Glyph Protocol 존재 자체를 감지하는 표준적 방법이기도 함
- 응답의
fmt는 비트필드로, 각 비트가 하나의 페이로드 포맷을 의미1=glyf: TrueType 단순 글리프, v1에서 필수2=colrv0: 레이어드 플랫 컬러 글리프(OpenType COLR v0), v1.2에서 추가4=colrv1: 그래디언트·트랜스폼이 포함된 전체 페인트 그래프(OpenType COLR v1), v1.2에서 추가
- 응답이 오면 프로토콜 지원 확인, 타임아웃이면 미지원,
fmt=0이면 프로토콜은 구현했으나 포맷 미지원(완전성을 위한 정의)
Query (q): 코드포인트 렌더링 가능 여부 질의
- 특정 코드포인트가 렌더링 가능한지 질의하고
status값으로 응답0(free): 아무것도 렌더링하지 않음, tofu 표시1(system): 시스템 폰트가 커버2(glossary): 세션 내 등록이 커버3(both): 양쪽 다 커버, 등록이 시스템 폰트를 렌더링 시 덮어씀
- TUI가 시스템에 이미 해당 아이콘이 있으면 등록을 건너뛰고, 없으면 커스텀 코드포인트를 등록해 우아하게 폴백 가능
Register (r): 글리프 등록
- 애플리케이션이 PUA 코드포인트를 선택하고 base64 인코딩된
glyf아웃라인을 전송해 등록 - 주요 파라미터
cp: 대상 코드포인트(hex), 반드시 3개의 Unicode PUA 범위 내여야 함(U+E000–U+F8FF,U+F0000–U+FFFFD,U+100000–U+10FFFD), 범위 밖이면reason=out_of_namespace로 거부fmt: 페이로드 포맷, v1에서는glyf만 정의되며 기본값이므로 대부분 생략 가능upm: units per em, 아웃라인 좌표 공간 정의, 기본값 1000
- 같은
cp에 두 번째r을 보내면 이전 등록을 덮어씀 - 오류 시(비PUA 코드포인트, 잘못된 페이로드, 컴포지트 글리프 등)
status=<nonzero>; reason=<code>로 응답
glyf 포맷 선택 이유
벡터인 이유
- 글리프는 사진이 아니므로 고정 해상도가 없음: 같은 아이콘이 12px 밀도 높은 TUI와 24px HiDPI 디스플레이 모두에서 렌더링 필요
- 래스터 글리프는 특정 해상도에 고정되어 HiDPI에서 흐릿하거나 소형 셀에서 판독 불가
glyf 구체적 선택 이유
- 텍스트를 렌더링하는 모든 터미널에 이미
glyf래스터라이저가 링크되어 있음(FreeType, swash, ttf-parser, fontdue, allsorts 등) - Glyph Protocol 채택 시 터미널 측에 새로운 의존성이 전혀 추가되지 않음
- SVG를 채택하면
resvg를 끌어오거나 새 XML+path 파서를 작성해야 함 - 와이어 크기도 작음: 일반 아이콘이 150~400바이트의
glyf데이터로, 동등한 SVG 대비 2~3배 작음(base64 오버헤드 포함)- 50개 아이콘 등록 시 약 13KB vs 35KB 차이, tmux 파이프나 모바일 SSH 링크에서 체감 가능
glyf 간략 설명
glyf레코드는 글리프를 닫힌 윤곽선(contour)의 집합으로 저장- 각 점은 on-curve 또는 off-curve 메타데이터 1비트를 가짐
- on-curve 두 점 연속 → 직선
- on-curve 사이에 off-curve 점 → 2차 베지어 곡선
- off-curve 두 점 연속 → 중간점에 암묵적 on-curve 점 존재(압축 트릭)
- 좌표는 EM 스퀘어 내 정수 그리드 위치,
upm=1000에서(500, 900)은 절반 너비·90% 높이 - 닫힌 삼각형 약 30바이트, 30개 점 아이콘 약 200바이트
프로토콜이 정의하는 glyf 서브셋
- 단순 글리프만 허용: 컴포지트 글리프, 다른 글리프 참조, 폰트 수준 컨텍스트 불가
- OpenType 스펙에 정의된 표준 플래그 인코딩 사용
- 힌팅 명령 없음: 힌팅은 폰트 전체 제어값 세트를 전제로 하며 여기에는 해당 없음
- 좌표 공간은
upm으로 정의, 기본값 1000, 등록별 오버라이드 가능
컬러·스케일링·저작
glyf아웃라인은 색상 정보가 없으며 현재 전경색으로 렌더링 → Nerd Font 상속 케이스와 동일- 컬러 글리프는 별도 페이로드 포맷
fmt=colrv0/fmt=colrv1로 지원 upm값이 글리프 좌표 공간을 정의하고, 터미널이 렌더 시 셀에 매핑 → 폰트 크기 변경 시 재등록 불필요- 대부분의 개발자는
glyf바이트를 직접 작성하지 않고 SVG에서 빌드 타임에 변환:fonttools의ttx/pens인터페이스 활용 가능,svg2glyf헬퍼도 Rio 레퍼런스 구현과 함께 배포 예정
수명 및 용량
- 각 터미널 세션은 3개 PUA 범위 내 코드포인트로 키잉된 최대 1024개 동시 등록을 담는 glossary 보유
- 등록은 세션 지속 기간 동안 유효
- 1025번째 글리프 등록 시 FIFO 순서로 가장 오래된 등록을 축출 → "glossary full" 오류 없음
- 무음 축출을 허용할 수 없는 애플리케이션은 출력 전 해당 코드포인트를 질의해야 함
실제 예제: 빈 PUA에 아이콘 등록
U+100000(Supplementary PUA-B의 첫 코드포인트)에 스타일라이즈드 아웃라인을 등록하는 전체 파이프라인 예시fontTools를 SVG→glyf변환기로 사용TTGlyphPen으로 아웃라인을 그린 후base64인코딩하여 APC 시퀀스로 전송, 이후 해당 코드포인트 출력- 일반 20포인트 아이콘의
glyf페이로드 약 150바이트, APC 래핑·base64 포함 약 250바이트 - SVG 자산이 있는 개발자를 위해
svg2glyf헬퍼 제공 예정 → 2줄로 등록 완료
대량 등록용 옵션: reply=
- 기본적으로 터미널은 모든
r에 대해 ACK 응답을 보내지만, 100개 글리프를 등록하는 시작 훅에서는 100개의 큐잉된 ACK가 PTY로 흘러나와 셸에 쓰레기로 표시되는 문제 발생 - 3단계 제어
reply=1(기본): 성공·실패 모두 응답, 대화형 단건 등록용reply=2: 실패만 응답, 성공은 무음, 대량 등록에서 오류만 감지할 때 사용reply=0: 아무 응답 없음, fire-and-forget, 시작 훅처럼 응답을 읽을 주체가 없을 때 사용
- 알 수 없는 값은
reply=1로 자동 폴백되므로 향후 확장 시 하위 호환 유지
Clear (c): 등록 해제
- 에디터 종료 시 터미널 기본값 복원, TUI 테마 전환, 디버깅 시 사용
- 단일 슬롯 해제:
cp파라미터로 특정 코드포인트 지정 - 전체 glossary 해제:
cp생략 - 빈 슬롯 해제는 오류가 아닌 no-op,
status=0응답 cp는 PUA 범위 내여야 하며, 범위 밖이면reason=out_of_namespace반환
v1에 의도적으로 포함하지 않은 기능
- 비PUA 코드포인트 등록 불가: 3개의 Unicode PUA 범위로 제한
- 리거처 없음: 단일 코드포인트에만 등록 적용, 시퀀스 키 치환은 v1 범위 밖, 프로그래밍 리거처(
->→⟶)는 이미 OpenType 폰트가 처리 - 세션 간 지속성 없음: 매 실행마다 글리프를 새로 전송, 터미널을 폰트 캐시로 전환하는 것을 방지
- 크로스 애플리케이션 공유 없음: 각 터미널 세션이 자체 glossary 소유, IPC나 데몬 없음
- v1
glyf페이로드에 컬러 글리프 없음: 전경색으로 렌더링, 컬러는 v1.2의colrv0/colrv1로 분리 - 이 기능들은 필요 시 나중에 추가 가능하나, 한번 추가되면 쉽게 제거할 수 없으므로 의도적으로 제외
PUA 제한의 보안적 근거
- PUA 제한은 API 미학이 아니라 프로토콜을 기본 활성화해도 안전하게 만드는 속성
- 임의 코드포인트 등록을 허용하면:
U+0061(a)에o모양 글리프를 등록해bad.com이bod.com으로 보이게 할 수 있음- 셀 버퍼는 여전히
bad.com이므로 복사·붙여넣기 시 바이트는 정직하지만, 사용자가 읽는 것은 거짓 - 모든 터미널 프로그램에 피싱 프리미티브가 생기며, 같은 세션에서 이후 실행되는 프로그램에도 영향 지속
- 셀 버퍼는 여전히
- PUA로 제한하면 이 공격 유형이 기계적으로 불가능: 사용자는 PUA 코드포인트를 타이핑하지 않으며, 파일명·URL·명령·변수명·로그에 PUA 코드포인트가 포함되지 않음
- Nerd Font이 관례로 확립한 신뢰 모델(커스텀 글리프는 예약 범위에만 존재, 실제 텍스트 위에는 불가)을 프로토콜 수준에서 강제
- 추가 보안 속성
- 셀 버퍼가 권위적: 선택·복사·검색·하이퍼링크 감지·셸 히스토리 등은 애플리케이션이 출력한 코드포인트를 반환해야 하며, "보는 것과 복사하는 것이 다른" 함정 생성 불가
- 세션 격리: 두 탭이
U+E0A0에 서로 다른 branch 아이콘을 독립적으로 등록 가능, 한 탭의 등록이 다른 탭의 렌더링에 영향 불가
기존 방식과의 비교
Kitty Image Protocol (KIP) + Unicode Placeholders
- KIP의 Unicode placeholder를 통해 Glyph Protocol을 근사적으로 구현 가능하나, 통합이 까다롭고 placeholder를 구현하는 터미널이 Kitty, Ghostty, Rio뿐
- KIP는 이미지 프로토콜이며 글리프는 이미지가 아님
- 사용별 비용: 화면에 200번 재사용되는 글리프(테이블 테두리, 불릿 마커 등)마다 200개의 이미지 참조를 배치해야 하며 레이아웃·합성 비용 발생. Glyph Protocol은 코드포인트 등록 후 폰트 속도로 렌더링
- 네이티브 해상도 없음:
glyf아웃라인은 픽셀 크기가 없어 폰트 크기 변경 시 자동 적용. KIP는 특정 크기의 비트맵을 전송하므로 크기 변경 시 흐릿해지거나 재업로드 필요, 폰트 크기 변경 감지 수단도 부재 - 전경색 상속: 단색
glyf아웃라인은 셀의 현재 전경색으로 렌더링되어 테마 자동 적용. 이미지는 자체 픽셀이므로 텍스트 컬러링에 참여하지 않음
DEC DECDLD / DRCS
- VT220이 1983년에 도입한 Dynamically Redefinable Character Sets로, 형태상 Glyph Protocol과 유사
- 두 가지 핵심 문제
- 비트맵 방식: 터미널의 현재 셀 크기에 맞춘 픽셀 그리드를 업로드하므로, 폰트 크기 변경·HiDPI·4K 모니터 전환 시 블록 픽셀이 확대되거나 축소됨. 고정 10×20 CRT 시대의 방식으로 현대의 다양한 셀 크기에 부적합
- 네임스페이스 제한 없음: DECDLD는 GL 범위(a, b, c가 있는 영역)에 매핑될 수 있는 문자셋을 덮어쓸 수 있어, 신뢰할 수 없는 프로그램이
a의 렌더링을 재정의 가능 → 현대 터미널이 DECDLD 활성화를 꺼리는 가장 큰 이유
Rio 터미널에서의 구현 현황
- Glyph Protocol은 Rio 터미널의 main 브랜치에서 이미 사용 가능하며, 5월 중 정식 랜딩 예정 → 최초 구현
- 전체 스펙이 릴리스와 함께 공개되며, 글리프 등록·터미널 질의를 위한 예제 코드 포함
- 작동 예제는 raphamorim/glyph-protocol-examples 저장소에서 확인 가능: Bubble Tea, Ratatui, Ink용 샘플 통합 포함
- 프로토콜은 아직 업데이트될 가능성이 있으며, 더 많은 애플리케이션과 터미널이 참여하면서 메시지 형태·질의 응답·에지 케이스가 변경될 수 있음 → 현재 빌드 시 이동 대상으로 취급하고 구현 버전 고정 권장
- 다른 터미널 에뮬레이터들의 채택을 기대하며, 생태계 전체의 이점이 크고 구현 범위는 의도적으로 작게 유지
커뮤니티 오픈 질문
- 폰트 크기 변경 알림이 프로토콜 범위에 포함되어야 하는가?: Glyph Protocol 자체는 아웃라인이 해상도 독립적이므로 이 문제를 회피하나, 이미지와 글리프를 함께 구성하는 TUI는 셀 메트릭 변경을 알 방법이 폴링 외에 없음 →
resize또는metrics-changed알림이 범위 내인지 범위 초과인지에 대한 논의 - 비PUA 등록을 허용하는 책임 있는 방법이 있는가?: PUA 전용 규칙이 기본 안전성을 보장하지만, CJK 입력기가 미커버 한자를 위한 글리프를 전송하거나 언어별 도구가 글리프를 오버라이드하는 사용 사례는 차단됨 → 명시적 사용자 수준 옵트인, 서명된 기능, 신뢰 출처 플래그 등으로 피싱을 재개하지 않으면서 이 사례를 열 수 있는 형태에 대한 의견 요청