tty — 터미널 입력
에디터나 메뉴처럼 키를 하나씩 받아 반응하는 프로그램을 만들 때 쓴다. 보통 터미널은 사용자가 Enter 를 눌러야 줄을 넘기고, 그 전까지 커널이 글자를 모으고 되비치고 백스페이스도 처리한다(행 버퍼링과 에코). 방향키를 누르는 즉시 커서를 옮기려면 이 편의가 방해가 되므로 raw 모드로 바꿔 “누른 즉시, 되비침 없이” 받는다. 그리고 방향키는 한 바이트가 아니다 — ↑ 는 ESC [ A 세 바이트, Delete 는 ESC [ 3 ~ 네 바이트다. 그 바이트열을 키로 읽어 주는 것이 parse_key 다.
let p option u64 . be tty.parse_key buf 0 .
guard is_some p . else return none .
let code u64 be tty.key_of (some_value p) .
let used u64 be tty.len_of (some_value p) .raw 모드는 사용자의 터미널 설정을 바꾸고, 그 변경은 프로그램이 죽어도 남는다
cap tty 를 든 자만 행사한다 — 권한 없이 tty_raw 를 부르면 컴파일 때 E-CAP-MISSING 이다. 동시에 키 파싱까지 권한 뒤에 두면 실제 터미널 없이는 시험할 수 없게 된다. 그래서 반을 갈랐다 — 순수한 절반이 훨씬 크다. 권한 절반은 raw 모드 · 읽기 · 화면 크기 셋뿐이고, 키 파싱(여기)과 ANSI 조립 · 화면 diff · 표시 폭(term)은 순수하다(37장).키코드 하나로 글자와 특수키를 구별한다. 평범한 바이트는 그 값 그대로(1 … 255, 제어 문자 포함 — Ctrl-C 는 3), 특수키는 1000 위다. 그래서 is_char 는 lt keycode 1000 한 번이다. 튜플이 없으므로 반환은 키코드 × 16 + 길이 한 값에 담는다(길이는 1 … 8). 길이가 필요한 이유는 다음 키가 버퍼의 어디서 시작하는지 알아야 하기 때문이다. 포장 형식이 바뀌어도 코드가 깨지지 않게 key_of · len_of 로 꺼낸다.
| op | 계층 | 하는 일 |
|---|---|---|
key_up · key_down · key_right · key_left | 순수 | 1001 · 1002 · 1003 · 1004 |
key_home · key_end · key_delete · key_pageup · key_pagedown · key_esc | 순수 | 1005 · 1006 · 1007 · 1008 · 1009 · 1010 |
key_unknown | 순수 | 1011 — 알아보지 못한 특수키 |
parse_key buf at | 순수(effects none) | 바이트열 → some (키코드 × 16 + 길이) 또는 none |
key_of · len_of · is_char | 순수 | 포장 값에서 키코드 · 길이 꺼내기 · 평범한 글자인가(키코드를 넣는다) |
tty_raw t on(내장) | cap tty | raw 모드 진입(true) · 복귀(false) |
tty_read t dst(내장) | cap tty | 지금 온 바이트를 버퍼로 → option u64(some 0 = 지금은 없음, 에러가 아니다) |
tty_size t(내장) | cap tty | 화면 크기 → option u64(행 = div v 4294967296, 열 = mod v 4294967296) |
표 50.1 — tty 의 op 과 내장
parse_key 가 알아보는 것 — ESC 아닌 한 바이트(길이 1), 뒤가 없는 ESC(ESC 키, 길이 1), ESC O A–D · H · F 와 ESC [ A–D · H · F(방향키 · Home · End, 길이 3), ESC [ 1 ~ · 3 ~ · 4 ~ · 5 ~ · 6 ~(Home · Delete · End · PageUp · PageDown, 길이 4). 불완전한 시퀀스를 지어내지 않는다 — ESC [ 까지만 왔으면 none 이고 “아마 방향키겠지” 라고 추측하지 않는다. 예외는 하나 — ESC 하나만 오고 뒤가 없으면 ESC 키다(영원히 기다릴 수는 없다). ESC 뒤가 [ 도 O 도 아니면 ESC 키 + 길이 1 로 답한다(Alt 조합을 ESC + 글자로 보내는 터미널에서 다음 호출이 글자를 읽는다).
module keydemo .
use tty .
proc main input t cap tty . input al cap allocator . output u8 . effects alloc . do
let g option mut slice u8 . . be alloc_bytes al capacity 32 .
guard is_some g . else return 1 .
let buf mut slice u8 . be some_value g .
guard tty_raw t true . else return 1 .
var going bool be true .
var last u64 be 0 .
while going . do
let n option u64 . be tty_read t buf .
guard is_some n . else do
set going false .
continue .
end
var off u64 be 0 .
while lt off (some_value n) . do
let p option u64 . be tty.parse_key (subslice buf 0 (some_value n)) off .
guard is_some p . else do
set off (some_value n) .
continue .
end
let code u64 be tty.key_of (some_value p) .
set last code .
if eq code 113 . do set going false . end
set off (add off (tty.len_of (some_value p))) .
end
end
let r bool be tty_raw t false .
return narrow u8 last .
end할당처럼 실패할 수 있는 일은 raw 로 들어가기 전에 끝낸다 — 들어간 뒤 실패해 돌아가면 단말이 raw 로 남는다. 터미널 내장은 cap tty 를 인자로 받는다 — 권한을 op 이 가졌다는 것만으로는 안 되고 쓰는 자리에 건네야 한다. parse_key 는 effects none 이라 터미널 없이 VM · 네이티브 대조로 키 파싱 전부가 검증된다 — 반 가르기의 배당금이다.
반례. raw 모드를 복귀시키지 않는다
tty_raw t true 뒤 중간의 guard … else return 2 로 빠지면 복귀가 없어 셸이 망가진 채 남는다. 언어에 defer 가 없으므로 모든 나가는 길에서 tty_raw t false 를 부르거나, 실패할 수 있는 부분을 raw 진입 전에 끝낸다. 위 예제는 루프가 언제나 아래로 빠져나오게 짜여 있다.반례. none 을 에러로 읽고 끝낸다 · 먹은 길이를 무시하고 1 씩 전진한다
parse_key 의 none 은 대개 “바이트가 더 필요하다” 다 — 읽기 경계가 시퀀스 가운데를 자를 수 있다. 여기서 끝내면 방향키를 누를 때마다 죽는 프로그램이 된다. set off (add off 1) 은 ESC [ A 를 세 개의 키로 읽는다 — 반드시 len_of 만큼 전진한다. 포장 값을 1001 과 직접 비교하면 길이가 섞여 맞지 않는다.주의. 터미널마다 방향키 시퀀스가 달라 ESC [ A 와 ESC O A 둘 다 알아본다. 그래도 못 잡는 시퀀스는 none 이다 — 조용히 다른 키로 답하지 않는다. 한글을 입력하면 parse_key 가 바이트 하나씩 돌려주므로 코드포인트는 utf8 로 조립한다. is_char 의 “글자” 는 “특수키가 아니다” 라는 뜻이지 출력할 수 있다는 뜻이 아니다. raw 모드에서 Ctrl-C 는 시그널이 아니라 바이트 3 으로 온다. 화면 크기 변화(SIGWINCH)는 알려 주지 않는다 — 필요하면 매 프레임 tty_size 를 다시 묻는다. 마우스 보고 · bracketed paste · 터미널 질의 응답 파싱은 짓지 않았다.