• Home
  • About
    • 검은색 잉크 블로그 photo

      검은색 잉크 블로그

      github GIST → https://gist.github.com/BlaCkinkGJ입니다.

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

[유닉스] ncurse

11 Apr 2019

Reading time ~3 minutes

NCURSES?

이를 설명하기 위해선 터미널 단말 시대로 가게 됩니다. 옛날에는 컴퓨터 하나에 아래 그림과 같이 컴퓨터에 접속하는 다수의 터미널이 접속하는 방식으로 제작이 되었습니다.

terminal

이런 특정 터미널에 특정한 바이트를 보내서 터미널을 설정할 수 있었습니다. 예를 들어, 터미널 에뮬레이터(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 프로그램을 만들 수 있습니다.



UNIX Share Tweet +1