오늘도 밤이야

[UNIX] termcap 라이브러리를 이용한 커서 제어 본문

UNIX

[UNIX] termcap 라이브러리를 이용한 커서 제어

hyeonski 2021. 3. 28. 01:10

프롬프트 환경을 만들게 되면 입력 내용을 수정 및 편집하기 위해 커서를 옮겨주어야 한다. 그러나 기본 설정의 터미널에서 커서를 옮기기 위해 방향키(화살표 키)를 누르면 커서는 옮겨지지 않고 다음과 같은 이상한 문자가 입력된다...

 

왼쪽,  오른쪽 방향키를 눌렀을 때 ^[[D ^[[C가 입력된다....

방향키로 커서를 제어하기 위해서는 터미널 옵션을 바꾸고, 키보드 입력이 있을 때마다 커서를 한땀한땀 바꿔주어야 한다.

먼저 기본 환경 설정이다. getch()처럼 한 글자씩 입력받기 위한 설정과 같다. 

 

[터미널 제어를 통해 한 글자씩 입력 받는 getch() 만들기]

hyeonski.tistory.com/5

 

[C/UNIX] 개행 없이 한 글자씩 입력받는 getch() 만들기 (터미널 제어)

프롬프트 또는 콘솔 환경을 만들기 위해서 키보드 입력을 한 자씩 받아야할 때가 있다. unistd.h의 read함수를 STDIN에서 1바이트만큼 받으면 되지 않을까? 해서 다음과 같은 코드를 실행해보았다. int

hyeonski.tistory.com

 

#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

 

BRL-CAD: src/libcursor/cursor.c File Reference

#include "common.h" #include "cursor.h" #include #include Go to the source code of this file. char * BC   char * UP   char * CS   char * SO   char * SE   char * CE   char * CL   char * HO   char * CM   char * TI   char * DL   char * 

brlcad.org

정해진 위치로 커서 옮기기

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가 이전 내용을 모두 날려버리는 점, 커서가 입력 문자열 이후로도 이동되는 점이 조금 아쉽지만 예제가 너무 길어지는 감이 있어 제외하였다.

 

실제 프롬프트 형식의 프로그램을 만들 경우 문자열을 할당하여 다룰 것이기 때문에 터미널 상의 글자 뿐만 아니라 문자열 내의 문자도 지워주어야 한다. 방법은 자유롭다. 문자열을 먼저 편집하고 터미널의 글자는 날린 뒤 문자열을 다시 출력하는 방법, 터미널 상의 글자와 문자열의 글자를 따로따로 지워주는 방법 등 각자 편한 방법을 택하면 될 것 같다.

 

 

'긴 글 읽어주셔서 감사합니다. 잘못된 정보나 수정 사항, 질문 등은 댓글로 남겨주셔주시면 감사하겠습니다!'

 

 

참고 자료

 

BRL-CAD: src/libcursor/cursor.c File Reference

#include "common.h" #include "cursor.h" #include #include Go to the source code of this file. char * BC   char * UP   char * CS   char * SO   char * SE   char * CE   char * CL   char * HO   char * CM   char * TI   char * DL   char * 

brlcad.org

 

tgoto(3) man page

명칭 tgetent, tgetnum, tgetflag, tgetstr, tgoto, tputs, tparm, __set_ospeed – 터미널과는 독립한 조작 함수 서식 #include char PC; char *BC; char *UP; short ospeed; int tgetent(char *bp, const char *name); int tgetnum(const char *id); int t

nxmnpg.lemoda.net

 

tgetent(3) - Linux man page

tgetent(3) - Linux man page Name tgetent, tgetflag, tgetnum, tgetstr, tgoto, tputs - direct curses interface to the terminfo capability database Synopsis #include #include extern char PC; extern char * UP; extern char * BC; extern unsigned ospeed; int tget

linux.die.net

 

The Termcap Library - The Termcap Library

Go to the first, previous, next, last section, table of contents. The termcap library is the application programmer's interface to the termcap data base. It contains functions for the following purposes: Finding the description of the user's terminal type

www.gnu.org

 

ANSI escape code

1 ANSI escape code 원본 출처 : http://sunyzero.egloos.com/4282610 터미널 테스트 코드  echo -e "\e[33m" echo 문이 실행된 후에는 프롬프트 색상이 모두 노란색으로 바뀜. 이 후 타이핑하는 모든 글자는 다..

keydisk.tistory.com

 

ANSI escape code - Wikipedia

Method using in-band signaling to control the formatting, color, and other output options on video text terminals ANSI escape sequences are a standard for in-band signaling to control cursor location, color, font styling, and other options on video text te

en.wikipedia.org

 

Comments