termcap과 terminfo — 터미널마다 다른 제어 코드를 어떻게 숨겼나
TERM, termcap, terminfo, tput의 관계를 통해 터미널 capability database가 왜 필요했고 curses와 현대 TUI의 호환성 계층에 어떻게 연결되는지 정리한다.
앞에서는 TUI 앱이 표준 출력으로 ESC [ ... 형태의 control sequence를 보내 커서를 움직이고 화면을 지울 수 있다는 것을 봤다.
그런데 여기서 역사적으로 큰 문제가 생긴다.
모든 터미널이 같은 sequence와 같은 기능을 지원하는가?
오늘날 Ghostty, Kitty, WezTerm처럼 VT/xterm 계열 호환성이 높은 터미널만 보면 차이가 작아 보이지만, 과거에는 터미널 제조사와 모델마다 기능과 제어 문자열이 크게 달랐다.
애플리케이션이 터미널마다 이렇게 분기한다면 유지보수가 불가능해진다.
1
2
3
4
5
6
7
if (terminal == VT100) {
...
} else if (terminal == ADM3A) {
...
} else if (...) {
...
}
이 문제를 풀기 위해 등장한 핵심 아이디어가 터미널의 기능을 코드에서 분리해 데이터로 기술하는 것이다.
Capability라는 관점
애플리케이션이 실제로 알고 싶은 것은 보통 터미널 모델명이 아니다.
다음 같은 질문이다.
1
2
3
4
5
6
커서를 임의 위치로 이동할 수 있는가?
화면 전체를 지우는 문자열은 무엇인가?
색상을 지원하는가?
몇 개의 색상을 지원하는가?
대체 화면(Alternate Screen)에 들어가는 sequence는 무엇인가?
F1 키는 어떤 바이트 시퀀스로 들어오는가?
이런 기능을 터미널 capability라고 부른다.
그러면 구조를 이렇게 바꿀 수 있다.
1
2
3
4
5
6
7
8
9
애플리케이션
↓
"cursor_address 기능 필요"
↓
Capability Database
↓
현재 터미널에 맞는 제어 시퀀스
↓
터미널
애플리케이션은 하드코딩된 모델별 문자열 대신 capability 이름을 요청한다.
TERM — 지금 어떤 터미널이라고 가정할 것인가
셸에서 다음을 실행해보자.
1
echo "$TERM"
환경에 따라 다음과 같은 값이 나올 수 있다.
1
2
3
xterm-256color
screen-256color
tmux-256color
TERM은 단순한 브랜드 이름이 아니라 어떤 터미널 capability description을 사용할지 선택하는 키에 가깝다.
1
2
3
4
5
$TERM=tmux-256color
↓
terminfo에서 해당 entry 검색
↓
이 터미널이 지원한다고 가정할 capability 집합
따라서 TERM을 무턱대고 바꾸는 것은 좋지 않다.
실제 터미널이 지원하지 않는 capability를 선언하면 프로그램이 잘못된 sequence를 보낼 수 있고, 반대로 너무 보수적인 값이면 사용할 수 있는 기능을 못 쓰게 된다.
termcap — 초기 capability database
초기 Unix/BSD 환경에서는 termcap(terminal capability) 형식이 널리 사용됐다.
핵심 아이디어는 단순하다.
1
2
3
4
5
터미널 이름
+
capability 이름
+
capability 값
을 텍스트 데이터베이스로 관리한다.
예를 들어 개념적으로:
1
2
3
4
vt100:
clear_screen = ESC[H ESC[2J
cursor_move = ...
columns = 80
같은 정보가 들어 있다고 이해하면 된다.
프로그램은 현재 터미널 entry를 읽고 필요한 capability를 가져간다.
이것만으로도 터미널별 제어 코드가 애플리케이션 코드에서 상당 부분 분리된다.
terminfo — capability 표현을 더 구조화
System V 계열에서 발전한 terminfo는 같은 문제를 더 구조적인 형태로 다룬다.
terminfo entry의 capability는 크게 세 종류다.
| 종류 | 의미 | 예 |
|---|---|---|
| Boolean | 기능 존재 여부 | 자동 margin 여부 등 |
| Numeric | 숫자 값 | color 수 등 |
| String | 제어 문자열 | 커서 이동, 화면 지우기 |
즉 terminfo는 단순 escape code 사전이 아니라:
이 터미널이 무엇을 할 수 있고, 그 기능을 어떻게 호출하는가
를 기술하는 데이터베이스다.
오늘날 ncurses 생태계에서는 terminfo가 핵심 capability database 역할을 한다.
실제로 terminfo를 들여다보기 — infocmp
현재 TERM의 terminfo entry를 사람이 읽을 수 있게 출력해볼 수 있다.
1
infocmp
또는 특정 타입:
1
infocmp xterm-256color
출력에는 많은 capability가 등장한다.
대표적으로 개념을 잡기 좋은 것들은:
1
2
3
4
5
6
7
8
colors
clear
cup
bold
setaf
setab
smcup
rmcup
정도다.
이름은 구현/표현 방식에 따라 다소 낯설지만 의미는 대략 다음과 같다.
| capability | 의미 |
|---|---|
colors | 지원 색상 수 |
clear | 화면 지우기 |
cup | 커서 위치 지정 |
bold | 굵은 글씨 모드 |
setaf | 전경색 설정 |
setab | 배경색 설정 |
smcup | 대체 화면 계열 진입 |
rmcup | 대체 화면 계열 종료 |
즉 이전 글에서 직접 ESC[2J를 하드코딩했던 것을 capability 이름으로 찾을 수 있다.
tput — 셸에서 terminfo를 쉽게 사용하기
셸에서 terminfo capability를 사용해보는 가장 쉬운 도구가 tput이다.
화면 지우기:
1
tput clear
커서를 5행 10열 근처로 이동:
1
2
tput cup 5 10
printf 'HELLO'
굵게:
1
2
3
tput bold
printf 'bold text'
tput sgr0
색상 수 확인:
1
tput colors
여기서 중요한 차이는:
1
printf '\e[2J'
은 특정 sequence를 직접 출력한 것이고,
1
tput clear
은 현재 $TERM에 맞는 clear capability를 찾아 출력한 것이다.
구조는 다음과 같다.
1
2
3
4
5
6
7
셸 스크립트
↓ tput clear
terminfo 조회
↓
현재 TERM에 맞는 sequence
↓
터미널
clear 명령도 같은 세계에 있다
clear를 단순히 printf '\e[2J'의 별칭으로 생각하기 쉽지만, 전통적인 Unix 터미널 생태계에서는 현재 터미널 capability를 고려해 적절한 동작을 선택하는 쪽에 가깝다.
즉 이런 도구들이 모두 같은 계층을 공유한다.
1
2
3
4
5
6
7
clear
tput
curses
↓
terminfo
↓
터미널 capability
왜 escape sequence 표준이 있는데 terminfo가 필요한가
여기서 자연스러운 의문이 생긴다.
VT/ANSI sequence가 어느 정도 표준화됐는데 지금도 capability database가 왜 필요한가?
이유는 터미널 기능이 단일 표준 하나로 완전히 끝나지 않았기 때문이다.
터미널마다:
- 지원하는 기능의 범위
- 색상 capability
- function key sequence
- 대체 화면 동작
- private extension
- 오래된 호환성 차이
가 존재할 수 있다.
그리고 애플리케이션은 “이론상 표준에 존재한다”보다 현재 터미널이 무엇을 지원한다고 선언하는가를 알아야 한다.
terminfo는 그 계약을 데이터로 표현한다.
tmux가 들어오면 TERM이 달라지는 이유
이제 tmux의 TERM이 왜 흥미로운지 보인다.
구조를 단순화하면:
1
2
3
4
5
Ghostty
↑ 실제 터미널 capability
tmux
↑ tmux가 클라이언트에게 제공하는 가상 터미널 capability
Neovim
Neovim은 Ghostty와 직접 대화하는 것이 아니다. tmux 안에서 실행되면 Neovim이 보는 상대는 tmux가 제공하는 터미널 인터페이스다.
그래서 내부에서 $TERM이 tmux-256color나 screen-256color처럼 바뀔 수 있다.
1
2
3
4
5
밖의 TERM: xterm-... / ghostty 계열
↓
tmux가 해석하고 다시 표현
↓
안의 TERM: tmux-256color
이것은 단순 환경변수 장난이 아니라 중간에 터미널을 에뮬레이션하는 계층이 하나 더 생겼기 때문이다.
SSH에서도 TERM이 전달되는 이유
SSH 접속에서 PTY를 할당하면 원격 셸/TUI도 어떤 터미널 capability를 기대해야 하는지 알아야 한다.
그래서 로컬 terminal type 정보가 원격 세션의 TERM과 연결된다.
개념적으로:
1
2
3
4
5
6
7
8
9
로컬 터미널 에뮬레이터
↓
SSH 클라이언트
↓ TERM + 터미널 바이트 스트림
SSH 서버
↓
원격 PTY
↓
원격 애플리케이션
원격 애플리케이션은 로컬 GPU나 터미널 에뮬레이터 구현을 직접 아는 것이 아니라, PTY와 TERM이라는 Unix 터미널 계약을 통해 동작한다.
이게 SSH에서도 Neovim/TUI가 그대로 동작할 수 있는 중요한 이유 중 하나다.
terminfo가 모르면 생기는 문제
새로운 터미널 에뮬레이터를 쓰거나 terminfo entry가 없는 오래된 서버에 SSH하면 다음과 같은 문제가 생길 수 있다.
1
2
3
unknown terminal type
Error opening terminal
terminal is not fully functional
로컬에서는 최신 terminfo가 있지만 원격 서버의 데이터베이스에는 해당 $TERM entry가 없는 상황이다.
이때 사람들이 임시로:
1
TERM=xterm-256color
처럼 바꾸기도 하는 이유가 여기 있다.
다만 실제 capability보다 과장하거나 축소된 TERM을 지정하면 미묘한 렌더링 문제가 생길 수 있으므로 근본적으로는 적절한 terminfo entry를 설치하는 편이 낫다.
termcap → terminfo → curses
여기까지의 추상화 상승을 정리하면:
1
2
3
4
5
6
7
8
1. Escape sequence 직접 사용
애플리케이션 → ESC[...] → 터미널
2. Capability database
애플리케이션 → termcap/terminfo → 터미널별 sequence
3. 화면 추상화
애플리케이션 → curses → terminfo → 터미널
terminfo는 아직 UI 프레임워크가 아니다.
“화면에 버튼을 하나 만들어줘” 같은 추상화를 제공하는 것이 아니라:
이 터미널에서 커서를 움직이려면 어떤 문자열을 써야 하는가?
를 알려주는 터미널 capability 추상화 계층이다.
다음 단계인 curses가 이 정보 위에서 화면과 창(Window)이라는 더 높은 추상화를 만든다.
다음 단계 — curses/ncurses
이제 애플리케이션 개발자는 escape sequence와 capability를 직접 다루지 않고 이렇게 말하고 싶어진다.
1
2
3
(10, 20)에 "hello"를 그려라.
이 창(Window)을 갱신해라.
키 하나를 읽어라.
여기서 curses/ncurses가 등장한다.
즉 추상화가:
1
2
3
터미널 capability
↓
화면(Screen) / 창(Window) 추상화
로 한 단계 더 올라간다.
참고
terminfo(5)— terminal capability database- ncurses
curs_terminfo(3X)— terminfo capability lookup API infocmp,tput— terminfo를 관찰하고 사용하는 도구