일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- MAN
- 터미널제어
- multiplexing
- virtual 상속
- 터미널 제어
- UNIX
- Assembly
- kqueue
- 클래스 상속
- 레지스터
- 커서 제어
- 서버 프로그래밍
- kevent
- 멀티플렉싱
- nonblock
- 명령어
- lseek
- 가상 상속
- canonical
- 가상상속
- 어셈블리어
- getch
- pipe buffer
- cursor
- termcap
- termios
- 프롬프트
- opcode
- 터미널 커서
- IPC
- Today
- Total
오늘도 밤이야
[UNIX] termcap 라이브러리를 이용한 커서 제어 본문
프롬프트 환경을 만들게 되면 입력 내용을 수정 및 편집하기 위해 커서를 옮겨주어야 한다. 그러나 기본 설정의 터미널에서 커서를 옮기기 위해 방향키(화살표 키)를 누르면 커서는 옮겨지지 않고 다음과 같은 이상한 문자가 입력된다...
방향키로 커서를 제어하기 위해서는 터미널 옵션을 바꾸고, 키보드 입력이 있을 때마다 커서를 한땀한땀 바꿔주어야 한다.
먼저 기본 환경 설정이다. getch()처럼 한 글자씩 입력받기 위한 설정과 같다.
[터미널 제어를 통해 한 글자씩 입력 받는 getch() 만들기]
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
/* 터미널 옵션 제어 */
struct termios term;
tcgetattr(STDIN_FILENO, &term);
term.c_lflag &= ~ICANON;
term.c_lflag &= ~ECHO;
term.c_cc[VMIN] = 1;
term.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &term);
int c = 0;
while (read(0, &c, sizeof(c)) > 0)
{
printf("keycode: %d\n", c);
c = 0; // flush buffer
}
}
read()를 sizeof(int)만큼 읽었는데, 이는 방향키의 키값을 받기 위해서이다. OS마다 다르지만, 64bit macOS/Linux 환경에서 방향키의 경우 4바이트만큼은 읽어주어야 한다. (ctrl+up같은 다른 특수키의 경우 더 큰 자료형으로 읽어야할 때도 있다.)
다 읽고 사용한 뒤에는 c를 0으로 초기화하여 버퍼를 비워준다. 일반 ASCII 범위의 문자들은 1바이트이기 때문에 비워주지 않으면 키값이 깨진다.
실행하면 다음과 같은 키값을 얻을 수 있다. a, b, c, d, backspace까진 ASCII 코드와 같고, 뒤에 추가로 입력된 4479771와 4414235는 각각 왼쪽, 오른쪽 방향키이다.
termcap 라이브러리 세팅
커서 제어를 위해서는 termcap 라이브러리를 사용해야 한다. 라이브러리 사용을 위한 세팅은 tgetent() 함수로 시작된다.
#include <termcap.h>
char *env = getenv("TERM");
if (env == NULL)
env = "xterm";
tgetent(NULL, env); // xterm 설정 사용
char *cm = tgetstr("cm", NULL); //cursor motion
char *ce = tgetstr("ce", NULL); //clear line from cursor
termcap 라이브러리를 사용할 경우 컴파일 시 -lncurses 옵션을 붙여주어야 한다.
getenv()를 활용하여 TERM 설정을 가져온다. (macOS 환경에서) 대부분 기본값은 xterm이고 xterm 설정을 사용할 것이기 때문에 env = "xterm"으로 줘도 무관하다.
tgetent()를 통해 xterm 설정을 사용하도록 초기화하고, tgetstr()을 통해 사용할 수 있는 루틴을 가져온다. man페이지와 여러 자료에 정확한 용어가 쓰여있지만, 영어가 약해서...루틴이라고 해두겠다. 이번 예제에서는 커서를 양옆으로 옮기고, backspace로 입력을 지울 것이기에 cursor motion에 해당하는 "cm"과 현재 커서 위치에서 한 줄의 끝까지 지워주는 "ce"를 가져왔다.
더 많은 루틴(?)은 아래 링크에서 찾아볼 수 있다. 정확히 termcap 라이브러리를 사용하는 것 같진 않은데, 비슷한 내용을 담고 있어 라이브러리 사용에 도움이 되었다.
brlcad.org/docs/doxygen-r64112/dd/dfa/cursor_8c.xhtml
정해진 위치로 커서 옮기기
termcap 라이브러리에서는 tputs()함수로 동작 실행을, tgoto() 함수로 동작 실행에 필요한 명령을 받을 수 있다. (5, 5) 위치로 커서를 옮기는 동작은 다음과 같이 할 수 있다.
#include <unistd.h>
#include <termcap.h>
int putchar_tc(int tc)
{
write(1, &tc, 1);
return (0);
}
int main(void)
{
tgetent(NULL, "xterm");
char *cm = tgetstr("cm", NULL);
tputs(tgoto(cm, 5, 5), 1, putchar_tc);
}
tgoto(cm, 5, 5)를 통해 (5, 5) 위치로 이동하는(cm - cursor motion) 명령을 tputs에 전달하여 실행한다. 터미널 창의 가장 왼쪽 위가 (0, 0)이다.
ce - clear line from cursor 명령의 경우 tgoto 없이 바로 tputs(ce, 1, putchar_tc)의 형식으로 사용해주면 된다.
키보드 입력을 받아 커서 옮기기
다음은 키보드 입력을 받아 커서를 옮겨주어 프롬프트 형태의 프로그램을 만드는 예제이다.
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <termcap.h>
# define BACKSPACE 127
# define LEFT_ARROW 4479771
# define RIGHT_ARROW 4414235
int nbr_length(int n)
{
int i = 0;
if (n <= 0)
i++;
while (n != 0)
{
n /= 10;
i++;
}
return (i);
}
void get_cursor_position(int *col, int *rows)
{
int a = 0;
int i = 1;
char buf[255];
int ret;
int temp;
write(0, "\033[6n", 4); //report cursor location
ret = read(0, buf, 254);
buf[ret] = '\0';
while (buf[i])
{
if (buf[i] >= '0' && buf[i] <= '9')
{
if (a == 0)
*rows = atoi(&buf[i]) - 1;
else
{
temp = atoi(&buf[i]);
*col = temp - 1;
}
a++;
i += nbr_length(temp) - 1;
}
i++;
}
}
int putchar_tc(int tc)
{
write(1, &tc, 1);
return (0);
}
void move_cursor_left(int *col, int *row, char *cm)
{
if (*col == 0)
return ;
--(*col);
tputs(tgoto(cm, *col, *row), 1, putchar_tc);
}
void move_cursor_right(int *col, int *row, char *cm)
{
++(*col);
tputs(tgoto(cm, *col, *row), 1, putchar_tc);
}
void delete_end(int *col, int *row, char *cm, char *ce)
{
if (*col != 0)
--(*col);
tputs(tgoto(cm, *col, *row), 1, putchar_tc);
tputs(ce, 1, putchar_tc);
}
int main(void)
{
/* change term settings */
struct termios term;
tcgetattr(STDIN_FILENO, &term);
term.c_lflag &= ~ICANON;
term.c_lflag &= ~ECHO;
term.c_cc[VMIN] = 1;
term.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &term);
/* init termcap */
tgetent(NULL, "xterm");
char *cm = tgetstr("cm", NULL); //cursor motion
char *ce = tgetstr("ce", NULL); //clear line from cursor
int c = 0;
int row;
int col;
while (read(0, &c, sizeof(c)) > 0)
{
get_cursor_position(&col, &row);
if (c == LEFT_ARROW)
move_cursor_left(&col, &row, cm);
else if (c == RIGHT_ARROW)
move_cursor_right(&col, &row, cm);
else if (c == BACKSPACE)
delete_end(&col, &row, cm, ce);
else
{
col++;
write(0, &c, 1);
}
c = 0; //flush buffer
}
}
완벽한 예제를 만들기에는 코드가 너무 길어져서... 조금 타협을 봤다.
먼저 get_cursor_position() 함수를 보자. "\033[6n"이라는 문자열을 STDIN에 출력하고 다시 읽는데, 이는 ANSI 이스케이프 문자로 현재 커서의 위치를 STDIN에 출력해준다. [1;3R처럼 [row;colR의 형태로 출력되어, 출력된 위치를 파싱해 현재 커서 위치를 저장할 수 있다. 이때 출력된 커서의 위치는 실제 사용할 커서의 좌표보다 1씩 크다. 좌표가 (0, 0)이라면 [1;1R로 출력되기 때문에, 파싱할 때 1을 뺀 값을 저장해야한다. 여기서 저장된 커서 위치를 기준으로 연산하면서 옮겨줄 커서의 위치를 알 수 있다.
양쪽 화살표와 backspace가 아닌 경우 일반 문자로 취급하여 STDIN에 출력해주도록 했고, 양쪽 화살표 입력 시 col 값을 증감시킨 후 그 위치로 커서를 옮겨주었다. backspace는 col을 하나 줄이고 그 뒤의 문자는 날려버리는 식으로 작성했다.
backspace가 이전 내용을 모두 날려버리는 점, 커서가 입력 문자열 이후로도 이동되는 점이 조금 아쉽지만 예제가 너무 길어지는 감이 있어 제외하였다.
실제 프롬프트 형식의 프로그램을 만들 경우 문자열을 할당하여 다룰 것이기 때문에 터미널 상의 글자 뿐만 아니라 문자열 내의 문자도 지워주어야 한다. 방법은 자유롭다. 문자열을 먼저 편집하고 터미널의 글자는 날린 뒤 문자열을 다시 출력하는 방법, 터미널 상의 글자와 문자열의 글자를 따로따로 지워주는 방법 등 각자 편한 방법을 택하면 될 것 같다.
'긴 글 읽어주셔서 감사합니다. 잘못된 정보나 수정 사항, 질문 등은 댓글로 남겨주셔주시면 감사하겠습니다!'
참고 자료
'UNIX' 카테고리의 다른 글
[UNIX] kqueue man page (macOS - BSD) 번역 (0) | 2021.07.11 |
---|---|
[UNIX] I/O Multiplexing을 위한 kqueue 사용법 (2) | 2021.07.11 |
[UNIX] pipe(2) 더 잘 알고 활용하기 (0) | 2021.07.10 |
[UNIX] 개행 없이 한 글자씩 입력받는 getch() 만들기 (터미널 제어) (7) | 2021.03.27 |