1P by GN⁺ 18시간전 | ★ favorite | 댓글과 토론
  • 터미널 애플리케이션에서 커스텀 아이콘을 렌더링하려면 패치된 폰트(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+E000U+F8FF, U+F0000U+FFFFD, U+100000U+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에서 빌드 타임에 변환: fonttoolsttx/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.combod.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 입력기가 미커버 한자를 위한 글리프를 전송하거나 언어별 도구가 글리프를 오버라이드하는 사용 사례는 차단됨 → 명시적 사용자 수준 옵트인, 서명된 기능, 신뢰 출처 플래그 등으로 피싱을 재개하지 않으면서 이 사례를 열 수 있는 형태에 대한 의견 요청