NCURSES?
이를 설명하기 위해선 터미널 단말 시대로 가게 됩니다. 옛날에는 컴퓨터 하나에 아래 그림과 같이 컴퓨터에 접속하는 다수의 터미널이 접속하는 방식으로 제작이 되었습니다.
이런 특정 터미널에 특정한 바이트를 보내서 터미널을 설정할 수 있었습니다. 예를 들어, 터미널 에뮬레이터(e.g. bash)에서 echo "^[[0;31;40mIn Color"
를 치게 되면 글자가 빨간색으로 변경이 되는 것을 알 수 있습니다. 여기서 ^[
는 Ctrl+V+ESC를 통해서 출력을 할 수 있습니다. 이것을 escape sequence라고 이야기 합니다.
하지만 이러한 쿼리를 기억하는 것은 너무 복잡하여 curses라는 좀 더 쉬운 라이브러리가 나왔습니다. 이러한 curses가 좀 더 발전해서 나온 것이 바로 ncurses가 됩니다.
이런 ncurses는 터미널 기능이 wrapper만 하는 것이 아니라 텍스트 모드에서의 UI를 만들 수 있도록 만들어줍니다.
Tutorial
가장 기본적인 ncurse 프로그램을 만들어보도록 하겠습니다. 아래의 코드를 작성하시고 gcc -o sample sampel.c -lncurses
를 치면 컴파일이 가능합니다.
#include <ncurses.h>
#include <unistd.h>
int func1(){
initscr();
mvprintw(0, 0, "Hello, World"); // 화면의 0행, 0열부터 Hello, World를 출력합니다.
refresh(); // 화면에 출력하도록 합니다.
sleep(1);
endwin();
return 0;
}
int main(){
return func2();
}
이렇게 하면 화면에 1초 동안 “Hello, World”를 보여주고 꺼지는 역할을 하게 됩니다. 여기서 괄목할 만한 사항이 있습니다. 바로 refresh()
라는 함수인데, 이 함수는 아까 전에 요청한 mvprintw(0, 0, "Hello, World");
를 실제로 화면에 뿌려주는 역할을 합니다. 이 함수가 없는 경우에 방금 출력한 내용은 보이지 않게 됩니다.
따라서 반드시 어떤 출력을 수행하였으면 refresh()
를 수행해주도록 해야 합니다. 만약 어떤 특정 창에 대해서 refresh()
를 수행하고자 하는 경우에는 wrefresh(WINDOW *)
를 실행해주시면 됩니다.
좀 더 동적인 코드를 확인하도록 하겠습니다.
#include <ncurses.h>
#include <unistd.h>
int timer(){
int row = 10, col = 10;
initscr();
noecho(); // 입력을 자동으로 화면에 출력하지 않도록 합니다.
curs_set(FALSE); // cursor를 보이지 않게 합니다.
keypad(stdscr, TRUE);
while(1){
int input = getch();
clear();
switch(input){
case KEY_UP:
mvprintw(--row, col, "A"); // real moving in your screen
continue;
case KEY_DOWN:
mvprintw(++row, col, "A");
continue;
case KEY_LEFT:
mvprintw(row, --col, "A");
continue;
case KEY_RIGHT:
mvprintw(row, ++col, "A");
continue;
}
if(input == 'q') break;
}
endwin();
return 0;
}
int main(){
return timer();
}
위 코드는 터미널 상의 10행 10열에서 시작해서 키 입력에 따라 움직이는 ‘A’ 문자를 보여주고 있습니다. 먼저 initscr()
은 ncurses의 자료 구조를 초기화하고 적절한 terminfo 파일을 읽는 데 사용이 됩니다. 만약에 하나 이상의 단말기에 출력하고자 하는 경우에는 newterm(...)
을 사용할 수 있습니다.
그리고 keypad(stdscr, TRUE)
는 표준 스크린에서의 입력을 받도록 한다는 것을 의미합니다. 그리고 TRUE
는 사용자가 기능키(화살표 같은 것)를 사용할 수 있도록 해주도록 합니다.
다음으로 getch()
가 있습니다. 이는 윈도우에서 conio.h
를 넣어서 사용한 getch()
와 유사합니다. 하지만 이 getch()
는 ncurses에 종속되어 있기 때문에 이러한 특징을 잘 활용을 하면 kbhit()
도 만들 수 있습니다.
static inline bool
kbhit(void){
int ch;
bool ret;
nodelay(stdscr, TRUE);
ch = getch();
if ( ch == ERR ) {
ret = false;
} else {
ret = true;
ungetch(ch); // 마지막에 받은 문자를 버퍼에 다시 넣어서 다음 getch()가 받을 수 있도록 합니다.
}
nodelay(stdscr, FALSE);
return ret;
}
이렇게 하여 실행을 하면 화살표 키를 입력함에 따라 A
가 움직이는 것을 알 수 있습니다. 그런데 여기서 조금 이상한 것이 있습니다. 방금 전 모든 출력을 진행을 하면 refresh()
가 실행이 되어야 하는 데 여기서는 없는 것을 알 수 있습니다. 그럼에도 불구하고 정상적으로 화면에 값이 출력이 됨을 알 수 있습니다.
이러한 이유는 getch()
가 refresh()
를 묵시적으로 가지고 있기 때문입니다. getch()
는 엄밀히 따지면 wgetch(stdscr)
의 wrapper라고 볼 수 있습니다. 그리고 wgetch(stdscr)
안에는 wrefresh(stdscr)
이 들어가 있습니다. 이는 refresh()
가 wrapping하는 함수라고 할 수 있습니다. 결과적으로, 실질적으로 소스 코드에 refresh()
가 존재하지 않음에도 불구하고 화면이 출력되는 것을 알 수 있습니다.
이러한 ncurse를 잘 활용하면 Terminal에서의 다양한 GUI 프로그램을 만들 수 있습니다.