Terminal Programming

깊이있는 삽질 Ubuntu Korea Community Wiki
이동: 둘러보기, 검색

메뉴[편집]

Basic[편집]

/* menu1.c */
/* getchoice 원형 정리 */
#include <stdio.h>

char *menu[] = {
  "a - add new record",
  "d - delete record",
  "q - quit",
  NULL,
};
* menu 출력하고 사용자의 입력을 받는다.
* main menu 사용하여 getchoice 호출
int getchoice(char *greet, char *choices[])
{
  int chosen = 0;
  int selected;
  char **option;

  do {
    printf("Choice: %s\n", greet);
    option = choices;
    while(*option) {
      printf("%s\n", *option);
      option++;
    }
    selected = getchar();
    option = choices;
    while(*option) {
      if(selected == *option[0]) {
        chosen =1;
        break;
      }
      option++;
    }
    if(!chosen) {
      printf("Incorrect choice, select again\n");
    }
  } while(!chosen);
  return selected;
}

/* main 함수는 menu를 사용하여 getchoice를 호출한다. */
int main()
{
  int choice = 0;

  do
  {
    choice = getchoice("Please select an action", menu);
    printf("You have chosen: %c\n", choice);
  } while (choice != 'q');
  return 0;
}
  • 기본적인 메뉴 프로그램이다.문제가 많다

isatty[편집]

#include <unistd.h>

int isatty(int fildes);
  • isatty는 fildes가 터미널이면 1, 아니면 0을 반환한다.
/* menu2.c */
/* getchoice 원형 정리 */
#include <unistd.h>
#include <stdio.h>

char *menu[] = {
  "a - add new record",
  "d - delete record",
  "q - quit",
  NULL,
};
* menu 출력하고 사용자의 입력을 받는다.
* main menu 사용하여 getchoice 호출
int getchoice(char *greet, char *choices[])
{
  int chosen = 0;
  int selected;
  char **option;

  do {
    printf("Choice: %s\n", greet);
    option = choices;
    while(*option) {
      printf("%s\n", *option);
      option++;
    }
    selected = getchar();
    option = choices;
    while(*option) {
      if(selected == *option[0]) {
        chosen =1;
        break;
      }
      option++;
    }
    if(!chosen) {
      printf("Incorrect choice, select again\n");
    }
  } while(!chosen);
  return selected;
}

int main()
{
  int choice = 0;

  if(!isatty(fileno(stdout))) {
    fprintf(stderr, "You are not a terminal!\n");
    return 1;
  }
  do {
    choice = getchoice("Please select an action", menu);
    printf("You have chosen: %c\n", choice);
  } while (choice != 'q');
  return 0;
}
  • 콘솔이 아닐때 에러를 뱉어준다.
#include <stdio.h>
#include <unistd.h>

char *menu[] = {
  "a - add new record",
  "d - delete record",
  "q - quit",
  NULL,
};

int getchoice(char *greet, char *choices[], FILE *in, FILE *out);
{
  int chosen = 0;
  int selected;
  char **option;

  do {
    fprintf(out, "Choice: %s\n", greet);
    option = choices;
    while(*option) {
      fprintf(out, "%s\n", *option);
      option++;
    }
    do {
      selected = fgetc(in);
    } while (selected == '\n');
    option = choices;
    while(*option) {
      if(selected == *option[0]) {
        chosen = 1;
        break;
      }
      option++;
    }
    if(!chosen) {
      fprintf(out, "Incorrect choice, select again\n");
    }
  } while(!chosen);
  return selected;
}

int main()
{
  int choice = 0;
  FILE *input;
  FILE *output;
  if(!isatty(fileno(stdout))) {
    fprintf(stderr, "You are not a terminal, OK.\n");
  }
  input = fopen("/dev/tty", "r");
  output = fopen("/dev/tty", "w");
  if(!input || !output) {
    fprintf(stderr, "Unable to open /dev/tty\n");
    return 1;
  }
  do {
    choice = getchoice("Please select an action", menu, input, output);
    printf("You have chosen: %c\n", choice);
  } while(choice != 'q');
  return 0;
}
  • 이제 리다이렉션해도 터미널 출력으로만 나오게 된다.

tcgetattr, tgsetattr[편집]

  • 이건 ncurses 라이브러리가 필요. gcc 컴파일시에 -lncurses 옵션을 붙이면 된다.
#include <termios.h>

struct termios {
  tcflag_t c_iflag;
  tcflag_t c_oflag;
  tcflag_t c_cflag;
  tcflag_t c_lflag;
  cc_t c_cc[NCCS];
}

int tcgetattr(int fildes, struct termios *termios_p);
int tcsetattr(int fildes, int actions, const struct termios *termios_p);
  • tcgetattr은 fildes가 가리키고 있는 터미널 정보를 가져와 termios_p 구조체에 들어간다.
  • tcsetattr의 actions는 다음중 하나로 지정
  1. TCSANOW - 즉시 값 변경
  2. TCSADRAIN - 출력이 끝났을때 값 변경
  3. TCSAFLUSH - 출력이 끝났을때 값 변경, 유효한 입력이나 read호출에서 리턴되지 않은 입력은 취소.
  • 입력모드 c_iflag에서 사용할 수 있는 매크로는 다음과 같다.
  1. BRKINT - 라인에서 break 조건이 감지되면 인터럽트를 발생
  2. IGNBRK - 라인에서 break 조건을 무시
  3. ICRNL - 입력된 캐리지 리턴을 뉴라인으로 변환
  4. IGNCR - 캐리지 리턴은 무시
  5. INLGR - 입력된 새 라인을 캐리지 리턴으로 변환
  6. IGNPAR - 입력된 문자중 패리티 에러가 있는 문자는 무시
  7. INPCK - 입력 문자들에 대해 패리티 체크를 한다.
  8. PARMRK - 패리티 에러 표시
  9. ISTRIP - 입력되는 모든 문자들을 7비트로 strip
  10. IXOFF - 입력시에 소프트웨어 흐름 제어
  11. IXON - 출력시 소프트웨어 흐름 제어
  • BRKINT나 IGNBRK가 설정되지 않으면 break 조건은 0x00으로 읽힘
  • 출력모드 c_oflag에서 사용할 수 있는 매크로는 다음과 같다.
  1. OPOST - 출력시 처리. 세팅 안하면 밑엣것들 전부 무시.
  2. ONLCR - 출력시 뉴 라인을 캐리지 리턴과 라인피드로 변환
  3. OCRNL - 출력시 캐리지 리턴을 뉴라인으로 변환
  4. ONOCR - 첫문자일땐 캐리지 리턴을 출력하지 않음
  5. ONLRET - 뉴라인을 캐리지 리턴으로 취급
  6. OFILL - 지연을 위해 채움문자로 보냄
  7. OFDEL - 채움 문자로 NULL 대신 DEL을 사용
  8. NLDLY - 새 라인시 딜레이
  9. CRDLY - 캐리지리턴시 딜레이
  10. TABDLY - 탭시 딜레이
  11. BSDLY - 백스페이스 딜레이
  12. VTDLY - 수직탭 딜레이
  13. FFDLY - 폼피드 딜레이
  • 컨트롤 모드 c_cflag에서 사용할 수 있는 매크로는 다음과 같다.
  1. CLOCAL - 상태라인 무시
  2. CREAD - 문자 읽기 가능
  3. CS5 - 문자 보내고 받을때 5비트 사용
  4. CS6 - 문자 보내고 받을때 6비트 사용
  5. CS7 - 문자 보내고 받을때 7비트 사용
  6. CS8 - 문자 보내고 받을때 8비트 사용
  7. CSTOPB - 정지비트 2비트
  8. HUPCL - 전송 끊기, Hang-up 세팅.
  9. PARENB - 패리티 사용
  10. PARODD - 홀수 패리티 사용
  • 로컬 모드 c_lflag에서 사용할 수 있는 매크로는 다음과 같다.
  1. ECHO - 입력되는 문자의 로컬 echo를 가능하게 한다.
  2. ECHOE - erase 문자를 받으면 backspace, space, backspace를 수행.
  3. ECHOK - kill 문자를 받으면 라인을 지운다.
  4. ECHONL - 새 라인 문자도 echo한다.
  5. ICANON - 정규 입력 처리를 가능하게 한다.
  6. IEXTEN - 입력 처리시 특별히 정의한 함수를 사용가능하게
  7. IESIG - 시그널을 가능하게 한다.
  8. NOFLSH - 큐는 플러시 안한다.
  9. TOSTOP - 쓰기시도시 백그라운드 프로세스에 시그널 보냄
  • 제어모드에서 ICANON 세팅여부에 따라 특수제어모드 세팅이 가능
  • ICANON이 세팅되었을때 특수제어모드 c_cc배열의 인덱스는 다음과 같다.
  1. VEOF - EOF문자
  2. VEOL - EOL문자
  3. VERASE - ERASE 문자
  4. VINTR - INTR문자
  5. VKILL - KILL 문자
  6. VQUIT - QUIT 문자
  7. VSUSP - SUSP 문자
  8. VSTART - START 문자
  9. VSTOP - STOP 문자
  • ICANON이 세팅되어 있지 않을 때 특수제어모드 c_cc배열의 인덱스는 다음과 같다.
  1. VINTR - INTR 문자
  2. VMIN - MIN 값
  3. VQUIT - QUIT문자
  4. VSUSP - SUSP문자
  5. VTIME - TIME값
  6. VSTART - START 문자
  7. VSTOP - STOP문자
  • 문자에 대한 설명은 다음과 같다.
  1. INTR - 터미널 드라이버가 터미널과 연결된 프로세스에 SIGINT 시그널을 보내도록 한다.
  2. QUIT - 터미널 드라이버가 터미널과 연결된 프로세스에 SIGQUIT 시그널을 보내도록 한다.
  3. ERASE - 터미널 드라이버가 라인의 마지막 문자를 지우도록 한다.
  4. KILL - 터미널 드라이버가 라인을 전부 지우도록 한다.
  5. EOF - 터미널 드라이버가 라인의 모든 문자들을 응용프로그램의 입력으로 전달한다. 라인이 비었다면 READ호출은 파일의 끝에서 시도한 것처럼 0문자를 리턴
  6. EOL - 라인중단. 뉴라인 문자가 사용된다.
  7. SUSP - 터미널 드라이버가 터미널과 연결된 프로세스에 SIGSUSP시그널을 보내도록 한다.
  8. STOP - 터미널에 출력되지 않도록 흐름정지시킨다. XON/XOFF 흐름제어 제공. 보통 Ctrl-S로 설정
  9. START - STOP 이후에 다시 출력하게 한다. 보통 아스키 XON문자 사용
  • Time과 Min값
  1. Min = 0, Time = 0 - read는 즉시 리턴. 읽을것이 없으면 읽지 않음
  2. Min = 0, Time > 0 - read는 읽어들일 문자가 있거나 Time/10이 경과했을 때 리턴. 정해진 시간동안 문자가 읽혀지지 않는다면 read는 0 리턴
  3. Min > 0, Time = 0 - read는 min개의 문자를 읽을때까지 기다리고, 읽어들인 문자의 갯수 리턴. 파일의 끝이라면 0 리턴
  4. Min > 0, Time > 0 - read가 호출되면 읽어들일 문자를 기다린다. read는 min개의 문자를 읽어들였거나 time/10초만큼 경과했을때 리턴. 이건 ESC키를 눌렀을때 코드값과 기능키 눌렀을때 첫번째 코드값의 차이점을 알아내는데 유용하게 사용될수 있다. 네트워크 통신이나 상위 프로세서는 이러한 타이머 정보를 쉽게 지울수 있다.
  5. ICANON으로 설정하지 않고 min과 time값을 사용하면 프로그램은 입력되는 모든 글자를 처리할 수 있다.

쉘에서 터미널 모드 접근[편집]

  • 터미널의 termios 설정값을 알아보려면 다음의 명령으로 알아볼 수 있다.
$ stty -a
  • 리눅스에서 출력은 다음과 같다.
drake@debian:~$ stty -a
speed 38400 baud; rows 27; columns 93; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>;
swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;
flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany
-imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
drake@debian:~$ 
  • EOF문자가 Ctrl+D고, echo가 설정되어 있음을 알 수 있다.
  • 테스트하다가 이상해지면 다음과 같이 복구할 수 있다.
$ stty sane
  • 쉘스크립트가 한번에 한문자씩 처리하도록 하려면 min을 1로 설정하고 time을 0으로 설정하고 icanon을 빼야 한다.
$ stty -icanon min 1 time 0
  • 비번검사를 한다든지 할땐 프롬프트를 보내기 전에 echo를 끌 수 있다.
$ stty -echo
  • 나중에 다시 가능하게 하려면
$ stty echo

cfgetispeed, cfgetospeed, cfsetispeed, cfsetospeed[편집]

#include <termios.h>

speed_t cfgetispeed(const sturct termios *);
speed_t cfgetospeed(const struct termios *);
int cfsetispeed(struct termios *, speed_t speed);
int cfsetospeed(struct termios *, speed_t speed);
  • tcsetattr로 termios를 만들어 써야 한다.
  • speed는 다음과 같다.
  1. B0 - 접속 끊음
  2. B1200 - 1200baud
  3. B2400 - 2400baud
  4. B9600 - 9600baud
  5. B38400 - 38400baud
  • 표준속도는 38400이 최대, 비표준 속도는 setserial로 사용

tcdrain, tcflow, tcflush[편집]

#include <termios.h>

int tcdrain(int fildes);
int tcflow(int fildes, int flowtype);
int tcflush(int fldes, int in_out_selector);
  • tcdrain은 출력이 다 될때까지 기다리는 함수
  • tcflow는 출력을 중단하거나 재개한다.
  • tcflush는 입력이나 출력을 지워버린다.

비밀번호 입력 프로그램[편집]

/* password.c */
#include <termios.h>
#include <stdio.h>

#define PASSWORD_LEN 32

int main()
{
  struct termios initialrsettings, newrsettings;
  char password[PASSWORD_LEN + 1];
  /* 다음 표준 입력의 현재 설정값을 얻고, 이전에 만들둔 termios 구조체에 그걸 복사한다. */
  tcgetattr(fileno(stdin), &initialrsettings);
  /* 마지막 설정을 복구하기 위해 원래의 설정을 복사하고, newrsettings에서 echo를 끄고 사용자에게 비밀번호를 물어보다. */
  newrsettings = initialrsettings;
  newrsettings.c_lflag &= ~ECHO;

  printf("Enter password: ");
  /* 터미널 속성을 newrsettings으로 설정하고 비번을 읽어들인다. 마지막에는 터미널 속성을 원래의 설정값으로 복구하고 비번을 출력한다. */
  if(tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) {
    fprintf(stderr, "Could not set attributes\n");
  }
  else {
    fgets(password, PASSWORD_LEN, stdin);
    tcsetattr(fileno(stdin), TCSANOW, &initialrsettings);
    fprintf(stdout, "\nYou entered %s\n", password);
  }
  exit(0);
}

문자를 하나씩 읽어들이기[편집]

/* menu4.c */
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
 
char *menu[] = {
  "a - add new record",
  "d - delete record",
  "q - quit",
  NULL,
};
 
int getchoice(char *greet, char *choices[], FILE *in, FILE *out);
{
  int chosen = 0;
  int selected;
  char **option;
 
  do {
    fprintf(out, "Choice: %s\n", greet);
    option = choices;
    while(*option) {
      fprintf(out, "%s\n", *option);
      option++;
    }
    do {
      selected = fgetc(in);
    } while (selected == '\n' || selected == '\r');
    /* 1글자씩 받는 모드에서는 엔터키가 '\r'이 된다 */
    option = choices;
    while(*option) {
      if(selected == *option[0]) {
        chosen = 1;
        break;
      }
      option++;
    }
    if(!chosen) {
      fprintf(out, "Incorrect choice, select again\n");
    }
  } while(!chosen);
  return selected;
}
 
int main()
{
  int choice = 0;
  FILE *input;
  FILE *output;
  struct termios initial_settings, new_settings;

  if(!isatty(fileno(stdout))) {
    fprintf(stderr, "You are not a terminal, OK.\n");
  }
  input = fopen("/dev/tty", "r");
  output = fopen("/dev/tty", "w");
  if(!input || !output) {
    fprintf(stderr, "Unable to open /dev/tty\n");
    return 1;
  }

  /* 문자를 한글자씩 받기 위해 터미널의 특성을 변경 */
  tcgetattr(fileno(input), &initial_settings);
  new_seettings = initial_settings;
  new_settings.c_lflag &= ~ICANON;
  new_settings.c_lflag &= ~ECHO;
  new_settings.c_cc[VMIN] =1;
  new_settings.c_cc[VTIME] = 0;
  new_settings.c_lflag &= ~ISIG;
  if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
    fprintf(stderr, "Could not set attributes\n");
  }

  do {
    choice = getchoice("Please select an action", menu, input, output);
    printf("You have chosen: %c\n", choice);
  } while(choice != 'q');

  /* 문자를 한글자씩 받으려고 터미널 특성 변경했던걸 복구 */
  tcsetattr(fileno(input), TCSANOW, &initial_settings);
  return 0;
}
  • 이제 좀 즉시 처리되는 형태가 된것 같다.

터미널 형태 식별[편집]

  • 터미널은 여러 종류가 있다. 터미널 에뮬레이터에서 어떤 방식으로 에뮬레이트할건지 정해본적이 있을 거다. vt100이나 vt220같은것들..
  • 다음과 같이 입력하면 터미널의 종류를 식별할 수 있다.
drake@debian:~$ echo $TERM
linux
drake@debian:~$
  • 얼마전까지는 xterm이었으나, 많은 터미널 에뮬레이터가 linux를 지원해주는 추세라 옮겨가고 있다.
  • 예전엔 프로그래머가 터미널의 종류에 따라 다 신경써야 했지만, terminfo라는게 생겨서, 아예 그 안에 웬만한건 다 들어있다.
  • terminfo에 대한 정보는, 원래는 /usr/lib/terminfo/v/vt100 이런 경로에 있었지만, 현재는 /usr/share/terminfo/v/vt100 이런 경로로 정의되어 있고, 컴파일된 파일로 제공된다.
  • 속도때문인지 /lib/share/terminfo/v/vt100 과 같이, 기본 경로에 제공되기도 한다.
  • 추후 원본 파일을 볼 수 있는 방법을 찾아 기술하도록 함

setupterm[편집]

  • 마찬가지로, ncurses 라이브러리를 사용한다. gcc 컴파일시 -lncurses 옵션을 붙여서 컴파일해야 한다.
#include <term.h>

int setupterm(char *term, int fildes, int *errret);
  • setupterm은 터미널 형태를 변수 term으로 세팅한다. term이 널 포인터라면 기본값을 사용한다. fildes는 터미널용 File Descriptor다. 함수의 결과는 errret가 널이 아닐 경우 errret가 가리키는 정수 변수에 저장된다. 그 값은 다음과 같다.
  1. -1 - terminfo 데이터베이스가 없음
  2. 0 - terminfo 데이터베이스에 해당하는게 없음
  3. 1 - 성공
  • setupterm함수는 성공시 OK, 실패시 ERR을 반환한다.
/* badterm.c */
#include <stdio.h>
#include <nterm.h>
#include <ncurses.h>

int main()
{
  setupterm("unlisted", fileno(stdout), (int *)0);
  printf("Done.\n");
  return 0;
}
  • 여기서 눈여겨보아야 할 것은, Done이 출력되지 않는다는 점이다.

tigetflag, tigetnum, tigetstr[편집]

#include <term.h>

int tigetflag(char *capname);
int tigetnum(char *capname);
char *tigetstr(char *capname);
  • ini 파일좀 다뤄봤다면 어느정도 이해가 빠를거다.
  • tigetflag는 플래그를 반환. 실패시 -1 반환
  • tigetnum은 정수 반환. 실패시 -2 반환
  • tigetstr은 문자열 반환. 실패시 (char *) -1 반환
/* sizeterm.c */
#include <stdio.h>
#include <term.h>
#include <ncurses.h>

int main()
{
  int nrows, ncolumns;

  setupterm(NULL, fileno(stdout), (int *)0);
  nrows = tigetnum("lines");
  ncolumns = tigetnum("cols");
  printf("This terminal has %d columns and %d rows\n", ncolumns, nrows);
  return 0;
}
  • 이 어플리케이션은 터미널 크기를 반환한다.

tparm[편집]

#include <term.h>

char *tparm(char *cap, long p1, long p2, ..., long p9);
  • terminfo의 각 항을 바꾼다. 거의 쓰이지 않음.존나 쓸까말까 고민했는데, 걍 있다는거 정도는 알아야 된다고 생각

putp, tputs[편집]

#include <term.h>

int putp(char *const str);
int tputs(char *const str, int affcnt, int (*putfunc)(int));
  • putp는 터미널 제어 문자열을 취해서 stdout으로 보낸다. 성공시 OK를, 실패시 ERR 반환.개같은 putfunc 어떻게 설명하라고
char *cursor;
char *esc_sequence;
cursor = tigetstr("cup");
esc_sequence = tparm(cursor, 5, 30);
putp(esc_sequence);
  • 아래로 다섯칸, 오른쪽으로 30칸 이동한다.
/* screenmenu.c */
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <term.h>
#inlcude <curses.h>

static FILE *output stream = (FILE *)0;

char *menu[] = {
  "a - add new record",
  "d - delete record",
  "q - quit",
  NULL,
};

int char_to_terminal(int char_to_write)
{
  if (output_stream) putc(char_to_write, output_stream);
  return 0;
}

int getchoice(char *greet, char *choices[], FILE *in, FILE *out);
{
  int chosen = 0;
  int selected;
  int screenrow, screencol = 10;

  char **option;
  char *cursor, *clear;

  output_stream = out;

  setupterm(NULL, fileno(out), (int *)0);
  cursor = tigetstr("cup");
  clear = tigetstr("clear");

  screenrow = 4;
  tputs(clear, 1, char_to_terminal);
  tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
  fprintf(out, "Choice: %s", greet);
  screenrow += 2;
  option = choices;
  while(*option) {
    tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
    fprintf(out, "%s", *option);
    screenrow++;
    option++;
  }

  do {
    selected = fgetc(in);
    option = choices;
    while(*option) {
      if(selected == *option[0]) {
        chosen = 1;
        break;
      }
      option++;
    }
    if(!chosen) {
      tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
      fprintf(out, "Incorrect choice, select again\n");
    }
  } while(!chosen);
  tputs(clear, 1, char_to_terminal);
  return selected;
}

int main()
{
  int choice = 0;
  FILE *input;
  FILE *output;
  struct termios initial_settings, new_settings;
 
  if(!isatty(fileno(stdout))) {
    fprintf(stderr, "You are not a terminal, OK.\n");
  }
  input = fopen("/dev/tty", "r");
  output = fopen("/dev/tty", "w");
  if(!input || !output) {
    fprintf(stderr, "Unable to open /dev/tty\n");
    return 1;
  }
 
  /* 문자를 한글자씩 받기 위해 터미널의 특성을 변경 */
  tcgetattr(fileno(input), &initial_settings);
  new_seettings = initial_settings;
  new_settings.c_lflag &= ~ICANON;
  new_settings.c_lflag &= ~ECHO;
  new_settings.c_cc[VMIN] =1;
  new_settings.c_cc[VTIME] = 0;
  new_settings.c_lflag &= ~ISIG;
  if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
    fprintf(stderr, "Could not set attributes\n");
  }
 
  do {
    choice = getchoice("Please select an action", menu, input, output);
    printf("You have chosen: %c\n", choice);
    sleep(1); /* 선택된 화면이 너무 빠르게 지나가버리기 때문에, 1초간 대기하여 사람이 볼 수 있을 정도로 지나가게 함.. */
  } while(choice != 'q');
 
  /* 문자를 한글자씩 받으려고 터미널 특성 변경했던걸 복구 */
  tcsetattr(fileno(input), TCSANOW, &initial_settings);
  return 0;
}

입력[편집]

kbhit[편집]

  • MSDOS에서 겜같은거 만들때 꼭 쓰던놈
  • 현재 눌려진 키가 뭔지 확인하는 기능을 한다.
/* kbhit.c */
/* 표준 헤더파일과 터미널 설정을 위해 2개의 구조체를 정의 */
#include <stdio.h>
#include <termios.h>
#include <term.h>
#include <curses.h>
#include <unistd.h>

static struct termios initial_settings, new_settings;
static int peek_character = -1;

void init_keyboard();
void close_keyboard();
int kbhit();
int readch();

/* main함수는 터미널을 설정하기 위해 init_keyboard를 호출하고 나서 1초에 1번씩 kbhit을 실행한다. 'q'를 입력하면 close_keyboard가 호출되어 터미널을 정상적으로 설정한 후에 프로그램을 종료한다. */
int main()
{
  int ch = 0;

  init_keyboard();
  while(ch != 'q') {
    printf("looping\n");
    sleep(1);
    if(kbhit()) {
      ch = readch();
      printf("You hit %c\n", ch);
    }
  }
  close_keyboard();
  return 0;
}

/* init_keyboard와 close_keyboard는 프로그램의 시작과 끝에서 터미널을 세팅한다. */
void init_keyboard()
{
  tcgetattr(0, &initial_settings);
  new_settings.c_lflag &= ~ICANON;
  new_settings.c_lflag &= ~ECHO;
  new_settings.c_lflag &= ~ISIG;
  new_settings.c_cc[VMIN] = 1;
  new_settings.c_cc[VTIME] = 0;
  tcsetattr(0, TCSANOW, &new_settings);
}

void close_keyboard()
{
  tcsetattr(0, TCSANOW, &initial_settings);
}

/* kbhit함수가 키보드가 눌렸는지 검사한다. */
int kbhit()
{
  char ch;
  int nread;

  if(peek)character != -1)
    return 1;
  new_settings.c_cc[VMIN] = 0;
  tcsetattr(0, TCSANOW, &new_settings);
  nread = read(0, &ch, 1);
  new_settings.c_cc[VMIN] = 1;
  tcsetattr(0, TCSANOW, &new_settings);

  if(nread == 1) {
    peek_character = ch;
    return 1;
  }
  return 0;
}

/* 눌린 문자는 다음번 readch함수가 읽어들이고, 다음 루프를 위해 peek_character를 -1로 설정 */
int readch()
{
  char ch;

  if(peek_character != -1) {
    ch = peek_character;
    peek_character = -1;
    return ch;
  }
  read(0, &ch, 1);
  return ch;
}

ncurses[편집]

Hello world[편집]

/* curses_hello.c */
#include <unistd.h>
#include <curses.h>

int main() {
  initscr(); /* Init curses */
  move(5, 15);
  printw("%s", "Hello World");
  refresh();

  sleep(2);
  endwin(); /* de-init curses */
  return 0;
}

초기화와 종료[편집]

#include <curses.h>

WINDOW *initscr(void);
int endwin(void);
  • initscr은 한번만 호출되어야 함. 성공시 stdscr 구조체 반환, 실패하면 에러메세지를 내고 종료
  • endwin은 성공시 OK 실패시 ERR 반환.
  • clearok(stdscr, 1)이랑 refresh를 호출해서 재개가능하다.
  • curses는 WINDOW구조체 내부에 직접 접근하는 방법을 제공하지 않는다.

출력[편집]

#include <curses.h>

int addch(const chtype char_to_add);
int addchstr(chtype *const string_to_add);
int printw(char *format, ...);
int refresh(void);
int box(WINDOW *win_ptr, chtype vertical_char, chtype horizontal_char);
int insch(chtype char_to_insert);
int insertln(void);
int delch(void);
int deleteln(void);
int beep(void);
int flash(void);
  • curses에는 이상한 데이터 타입으로 chtype이 있다. unsigned long 타입임.
  • addch, addchstr은 현재위치에 출력, printw는 printw처럼 쓰면 된다.
  • refresh는 화면갱신. 성공하면 OK, 에러는 ERR 반환. box는 윈도우 둘레에 상자를 그림.
  • insch는 문자 삽입하고 오른쪽에 문자들을 밀어버린다.
  • delete는 문자지우기. beep는 소리내기, flash함수는 깜빡이기.

읽기[편집]

#include <curses.h>

chtype inch(void);
int instr(char *string);
int innstr(char *string, int number_of_characters);
  • inch는 현재 커서가 가리키는 좌표와 문자를 반환
  • instr, innstr은 읽어들인 문자를 char 배열에 기록한다.

화면 지우기[편집]

#include <curses.h>

int erase(void);
int clear(void);
int clrtobot(void);
int clrtoeol(void);
  • erase, clear는 화면 전체를 공백으로 채운다. clear는 리프레시가 추가된것.
  • clrtobot는 커서 위치부터 화면 끝까지(세로), clrtoeol은 커서 위치부터 줄 끝까지(가로) 지운다.

화면 이동[편집]

#include <curses.h>

int move(int new_y, int new_x);
int leaveok(WINDIW *window_ptr, bool leave_flag);
  • move는 지정한 위치로 이동시킨다. 좌측상단 구석은 0,0 이다.
  • leaveok는 화면갱신후 물리커서를 어디에 둘건지 제어한다. default는 false. Win32API의 Active Window 개념이라고 보면 쉬울듯

attribute[편집]

#include <curses.h>

int attron(chtype attribute);
int attroff(chtype attribute);
int attrset(chtype attribute);
int standout(void);
int standend(void);
  • 이런 속성이 있다.
  1. A_BLINK - 깜빡임
  2. A_BOLD - 볼드체
  3. A_DIM - 어두운색
  4. A_REVERSE - 역상. Reverse 이런것
  5. A_STANDOUT - 밝은색
  6. A_UNDERLINE - 밑줄
  • attrset은 curses의 속성을 설정
  • attron, attroff는 지정 속성을 켜거나 끈다.
/* moveadd.c */
#include <unistd.h>
#include <curses.h>

int main() {
  const char witch_one[] = " First Witch ";
  const char witch_two[] = " Second Witch ";
  const char *scan_ptr;

  initscr();
  /* 세개의 텍스트 집합이 간격을 두고 화면상에 출력된다. 테스트 속성도 on, off한다. */
  move(5, 15);
  attron(A_BOLD);
  printw("%s", "Macbeth");
  attroff(A_BOLD);
  refresh();
  sleep(1);

  move(8, 15);
  attron(A_DIM);
  printw("%s", "Thunder and Lightning");
  attroff(A_DIM);
  refresh();
  sleep(1);

  move(10, 10);
  printw("%s", "when shall we three meet again");
  move(11, 23);
  printw("%s", "In thunder, ligning, or in rain?");
  move(13, 10);
  printw("%s", "When the hurlyburly's done,");
  move(14,23);
  printw("%s", "When the battle's lost and won.");
  refresh();
  sleep(1);

  attron(A_DIM);
  scan_ptr = witch_one + strlen(witch_one);
  while(scan_ptr != which_two) {
    move(13, 10);
    insch(*scan_ptr--);
  }
  attroff(A_DIM);

  refresh();
  sleep(1);

  endwin();
  return 0;
}

키보드[편집]

#include <curses.h>

int getch(void);
int getstr(char *string);
int getnstr(char *string, int number_of_characters);
int scanw(char *format, ...);
  • getch는 getchar, getstr랑 getnstr은 gets, scanw는 scanf랑 사용법이 같다.
#include <unistd.h>
#include <curses.h>
#include <string.h>

#define PW_LEN 32
#define NAME_LEN 256

int main() {
  char name[NAME_LEN];
  char password[PW_LEN];
  char *real_password = "xyzzy";
  int i = 0;

  initscr();

  move(5, 10);
  printw("%s", "Please login: ");

  move(7, 10);
  printw("%s", "User name: ");
  getstr(name);

  move(9, 10);
  printw("%s", "Password: ");
  refresh();

  /* 비번이 화면에 보이면 망하니까 안보이게 하자. */
  cbreak();
  noecho();

  memset(password, '\0', sizeof(password));
  while(i < PW_LEN) {
    password[i] = getch();
    move(9, 20 + i);
    addch('*');
    refresh();
    if(password[i] = '\n') break;
    if(strcmp(password, real_password) == 0) break;
    i++;
  } /* while */

  /* 키보드를 다시 원래대로 복구하고 메세지 출력 */
  echo();
  nocbreak();

  move(11, 10);
  if(strcmp(password, real_password) == 0) printw("%s", "Correct");
  else printw("%s", "Wrong");
  refresh();
  sleep(3);

  endwin();
  return 0;
}

다중 윈도우[편집]

#include <curses.h>

WINDOW *newwin(int num_of_lines, int num_of_cols, int start_y, int start_x);
int delwin(WINDOW *window_to_delete);
  • newwin 함수는 새로운 윈도우를 생성한다. 윈도우의 좌측 상단은 (start_x, start_y)가 되고, 'num_of_lines'와 'num_of_cols'만큼의 크기를 가진다. 성공하면 윈도우에 대한 포인터를 반환하고, 실패하면 null을 반환한다.
  • 윈도우가 영역을 벗어나면 실패한다.
  • delwin은 newwin으로 만들어진 윈도우를 없애버린다.
#include <curses.h>

int waddch(WINDOW *window_pointer, const char char);
int mvwaddch(WINDOW *window_pointer, int y, int x, const chtype char);
int wprintw(WINDOW *window_pointer, char *format, ...);
int mvwprintw(WINDOW *window_pointer, int y, int x, char *format, ...);
int mvwin(WINDOW *window_to_move, int new_y, int new_x);
int wrefresh(WINDOW *window_ptr);
int wclear(WINDOW *window_ptr);
int werase(WINDOW *window_ptr);
int touchwin(WINDOW *window_ptr);
int scrollok(WINDOW *window_ptr, bool scroll_flag);
int scroll(WINDOW *window_ptr);
  • waddch, mvwaddch, wprintw, mvwprintw, wrefresh, wclear, werase는 각각 addch, mvaddch, printw, mvprintw, refresh, clear, erase의 윈도우 버전이라고 생각하면 된다.
  • mvwin은 윈도우 위치를 변경한다. 범위를 벗어나면 실패.
  • touchwin은 Active Window를 선택
  • scrollok에 scroll_flag에 true를 세우면 스크롤이 가능하다.
/* multiw1.c */
#include <unistd.h>
#include <curses.h>

int main()
{
  WINDOW *new_window_ptr;
  WINDOW *popup_window_ptr;
  int x_loop;
  int y_loop;
  char a_letter = 'a';

  initscr();

  /* 기본 윈도우를 문자로 채우고 refresh. */
  move(5,5);
  printw("%s", "Testing multiple windows");
  refresh();

  for(x_loop = 0; x_loop < COLS -1; x_loop++) {
    for(y_loop = 0; y_loop < LINES -1; y_loop++) {
      mvaddch(stdscr, y_loop, x_loop, a_letter);
      a_letter++;
      if(a_letter > 'z') a_letter = 'a';
    }
  }

  refresh();
  sleep(2);

  /* 10x20 크기의 새로운 윈도우를 만들고 텍스트를 조금 넣어본다. */
  new_window_ptr = newwin(10, 20, 5, 5);
  mvprintw(new_window_ptr, 2, 2, "%s", "Hello World");
  mvprintw(new_window_ptr, 5, 2, "%s", "Notice how very long lines wrap inside the window");

  wrefresh(new_window_ptr);
  sleep(2);

  /* 배경윈도우의 내용을 바꾸고 화면을 리플레시할 때 new_window_ptr이 가리키는 윈도우는 숨겨진다. */
  a_letter = '0';
  for(x_loop = 0; x_loop < COLS -1; x_loop++) {
    for(y_loop = 0; y_loop < LINES -1; y_loop++) {
      mvaddch(stdscr, y_loop, x_loop, a_letter);
      a_letter++;
      if(a_letter > '9') a_letter = '0';
    }
  }

  refresh();
  sleep(2);

  /* 새로운 윈도우를 전혀 변경하지 않았으므로, 새로운 윈도우를 리프레시해도 아무것도 변하지 않는다. */
  wrefresh(new_window_ptr);
  sleep(2);

  /* 윈도우를 touch해서 Active Window라고 하고 리프레시하면 짠 */
  touchwin(new_window_ptr);
  wrefresh(new_window_ptr);
  sleep(2);

  /* 윈도우를 하나 만들어 겹치게 한다. */
  popup_window_ptr = newwin(10, 20, 8, 8);
  box(popup_window_ptr, '|', '-');
  mvwprintw(popup_window_ptr, 5, 2, "%s", "Pop Up Window!");
  wrefresh(popup_window_ptr);
  sleep(2);

  /* 좀더 뭔가 해보고 윈도우를 날려보자. */
  touchwin(new_window_ptr);
  wrefresh(new_window_ptr);
  sleep(2);

  wclear(new_window_ptr);
  wrefresh(new_window_ptr);
  sleep(2);

  delwin(new_window_ptr);
  touchwin(popup_window_ptr);
  wrefresh(popup_window_ptr);
  sleep(2);

  delwin(popup_window_ptr);

  touchwin(stdscr);
  refresh();
  sleep(2);

  endwin();
  return 0;
}

보조윈도우[편집]

#include <curses.h>

WINDOW *subwin(WINDOW *parent, int num_of_lines, int num_of_cols, int start_y, int start_x);
  • 보조윈도우는 윈도우 안에 들어가는 윈도우다. MFC 기본 프로그램을 보면 창 안에 창이 여러개 들어가는걸 보여준다. 이것도 비슷한 개념이다.
  • newwin처럼 쓰면 된다.
/* subscl.c */
#include <unistd.h>
#include <curses.h>

int main()
{
  WINDOW *sub_window_ptr;
  int x_loop;
  int y_loop;
  int counter;
  char a_letter = '1';

  initscr();

  for(x_loop = 0; x_loop < COLS -1; x_loop++) {
    for(y_loop = 0; y_loop < LINES -1; y_loop++) {
      mvaddch(stdscr, y_loop, x_loop, a_letter);
      a_letter++;
      if(a_letter > '9') a_letter = '1';
    }
  }

  /* 스크롤 할 보조윈도우를 만들고, 리프레시하기전에 한번 만진다. */
  sub_window_ptr = subwin(stdscr, 10, 20, 10, 10);
  scrollok(sub_window_ptr, 1);

  touchwin(stdscr);
  refresh();
  sleep(1);

  /* 보조 윈도우의 내용을 지우고 보조 윈도우에 텍스트를 출력하고 리프레시한다. 스크롤하는 텍스트는 루프에 의해 만들어짐ㅋ */
  werase(sub_window_ptr);
  mvwprintw(sub_window_ptr, 2, 0, "%s", "This window will now scroll");
  wrefresh(sub_window_ptr);
  sleep(1);

  for(counter = 1; counter < 10; counter++) {
    wprintw(sub_window_ptr, "%s", "This text is both wrapping and scrolling.");
    wrefresh(sub_window_ptr);
    scroll(1);
  }

  /* 루프가 끝나면 보조윈도우를 제거하고 기본화면을 리프레시한다. */
  delwin(sub_window_ptr);

  touchwin(stdscr);
  refresh();
  sleep(1);

  endwin();
  return 0;
}

키패드[편집]

#include <curses.h>

int keypad(WINDOW *window_ptr, bool keypad_on);
  • keypad 함수에서 keypad_on을 true로 세우면 Keypad mode가 세팅되고, curses는 시퀀스키를 처리할 수 있다.
  • Keypad mode에서는 제한사항이 몇가지가 있었다.요즘 컴터중에 텍스트 처리 한다고 느려지는건 ARM걸리는 ARM밖에 없지
  1. 이스케이프 시퀀스를 인식하는건 타이밍에 의존하게 되는 경우도 있는데, 예전에 느린 네트워크에서는 MTU때문에 오동작하던 때가 있었다.
  2. 이스케이프 시퀀스의 처리는 일반적인 처리보다 많은 연산을 필요로 하는데, 요즘 PC에서는 신경쓰지 않아도 된다.
  3. 중복 이스케이프 시퀀스 처리는 불가능하다. 하지만 그거야 raw data를 건들지 않으면 거의 발생하지 않을 문제다.
/* keypad.c */
#include <curses.h>

#define LOCAL_ESCAPE_KEY 27

int main()
{
  int key;

  initscr();
  crmode();
  keypad(stdscr, TRUE);

  /* echo off하고 약간 메세지를 출력한다. 프로그램은 키입력을 기다려서 'q'면 종료하고, 에러가 나지 않았으면 뭐가 눌렸는지 대충 출력해본다. */
  noecho();

  clear();
  mvprintw(5, 5, "Key pad demonstration. Press 'q' to quit.");
  move(7, 5);
  refresh();

  key = getch();
  while(key != ERR && key != 'q') {
    move(7, 5);
    clrtoeol();

    if((key >= 'A' && key <= 'Z') || (key >= 'a' && key <= 'z')) {
      printw("Key was %c", (char)key);
    }
    else {
      switch(key) {
      case LOCAL_ESCAPE_KEY: printw("%s", "Escape key"); break;
      case KEY_END: printw("%s", "END key"); break;
      case KEY_BEG: printw("%s", "BEGINNING key"); break;
      case KEY_RIGHT: printw("%s", "RIGHT key"); break;
      case KEY_LEFT: printw("%s", "LEFT key"); break;
      case KEY_UP: printw("%s", "UP key"); break;
      case KEY_DOWN: printw("%s", "DOWN key"); break;
      default: printw("Unmatched - %d", key); break;
      } /* switch */
    } /* else */

    refresh();
    key = getch();
  } /* while */

  endwin();
  return 0;
}

색깔[편집]

  • 원래 curses는 흑백이었다.
#include <curses.h>

bool has_colors(void);
int start_color(void);

int init_pair(short pair_number, int foreground, int background);
int COLOR_PAIR(int pair_number);
int pair_content(short pair_number, short *foreground, short *background);
  • has_color는 색을 지원하는 터미널인지 확인하고 지원되면 true, 지원안되거나 에러면 false를 반환한다.
  • start_color는 제대로 초기화되면 OK, 실패하면 ERR을 반환한다.
  • start_color가 초기화되면 COLOR_PAIRS 변수에터미널이 제공할 수 있는 최대한의 색상 페어를 저장한다.
  • 색깔을 쓰려면 그 색상 페어를 초기화해야 한다.
  • 녹색 배경에 빨간 문자색을 색상페어로 지정하려면 다음과 같이 하면 된다.
init_pair(1, COLOR_RED, COLOR_GREEN);
  • 그리고 COLOR_PAIR로 제어할 수 있다.
wattron(window_ptr, COLOR_PAIR(1));
  • 이렇게 하면 init_pair로 지정한 색으로 세팅된다.
  • 다음처럼 조합도 가능하다.
wattron(window_ptr, COLOR_PAIR(1) | A_BOLD);
  • 잘 와닿지 않으면 다음을 보자.
/* color.c */
#include <unistd.h>
#include <stdio.h>
#include <curses.h>

int main() {
  int i;

  initscr();
  if(!has_colors()) {
    endwin();
    fprintf(stderr, "Error - no color support on this terminal\n");
    return 1;
  }

  if(start_color() != OK) {
    endwin();
    fprintf(stderr, "Error - could not initialize colors\n");
    return 2;
  }

  /* 색상 페어로 7개의 페어를 만들어서 보여준다. */
  clear();
  mvprintw(5, 5, "There are %d COLORS, and %d COLOR_PAIRS available", COLORS, COLOR_PAIRS);
  refresh();

  init_pair(1, COLOR_RED, COLOR_BLACK);
  init_pair(2, COLOR_RED, COLOR_GREEN);
  init_pair(3, COLOR_GREEN, COLOR_RED);
  init_pair(4, COLOR_YELLOW, COLOR_BLUE);
  init_pair(5, COLOR_BLACK, COLOR_WHITE);
  init_pair(6, COLOR_MAGENTA, COLOR_BLUE);
  init_pair(7, COLOR_CYAN, COLOR_WHITE);

  for(i = 1; i <= 7; i++) {
    attroff(A_BOLD);
    attrset(COLOR_PAIR(i));
    mvprintw(5 + i, 5, "Color pair %d", i);
    attrset(COLOR_PAIR(i) | A_BOLD);
    mvprintw(5 + i, 25, "Bold color pair %d", i);
    refresh();
    sleep(1);
  }

  endwin();
  return 0;
}

패드 윈도우[편집]

#include <curses.h>

WINDOW *newpad(int number_of_lines, int number_of_columns);
int prefresh(WINDOW *pad_ptr, int pad_row, int pad_column, int screen_row_min, int screen_col_min, int screen_row_max, int screen_col_max);
  • newpad는 newwin이랑 사용법이 같다. 삭제도 delwin으로 가능하다.
/* pad.c */
/* 패드를 초기화하고 패드를 만든다. 그리고 패드에 문자를 채워넣는다. 패드는 보통의 터미널보다 크다. */
#include <unistd.h>
#include <curses.h>

int main()
{
  WINDOW *pad_ptr;
  int x, y;
  int pad_lines;
  int pad_cols;
  char disp_char;

 initscr();

  pad_lines = LINES + 50;
  pad_cols = COLS + 50;

  pad_ptr = newpad(pad_lines, pad_cols);

  disp_char = 'a';
  for (x = 0; x< pad_lines; x++) {
    for (y = 0; y < pad_cols; y++) {
      mvwaddch(pad_ptr, x, y, disp_char);
      if (disp_char == 'z') disp_char = 'a';
      else disp_char++;
    }
  }

  /* 이제 패드의 영역을 화면상의 특정위치로 쓴다. */
  prefresh(pad_ptr, 5, 7, 2, 2, 9, 9);
  sleep(1);

  prefresh(pad_ptr, LINES +5, COLS +7, 5, 5, 21, 19);
  sleep(1);

  delwin(pad_ptr);
  endwin();
  return 0;
}

종합[편집]

/* cdapp.c */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <curses.h>

#define MAX_STRING (80)   /* 제일 긴 문자열 */
#define MAX_ENTRY (1024)  /* 제일 긴 데이터베이스 엔트리 */

#define MESSAGE_LINE  6   /* 기타 메세지 표현할 라인 */
#define ERROR_LINE   22   /* 에러를 표시할 라인 */
#define Q_LINE       20   /* 질문을 표시할 라인 */
#define PROMPT_LINE  18   /* 입력 프롬프트를 표시할 라인 */

/* 몇가지 전역변수를 정의한다. current_cd는 현재 CD의 제목을 저장, 초기화는 null로, 선택된 CD가 없다는걸 보여준다. current_cat는 현재 CD의 카탈로그 번호를 기록하는데 사용 */
static char current_cd[MAX_STRING] = "\0";
static char current_cat[MAX_STRING];

/* 몇몇 파일 이름을 정의한다. 프로그램을 간단하게 하기 위해 파일 이름을 고정시킨다. */
const char *title_file = "title.cdb";
const char *tracks_file = "tracks.cdb";
const char *temp_file="cdb.tmp";

/* 프로그램에서 필요한 함수 원형 */
void clear_all_screen(void);
void get_return(void);
int get_confirm(void);
int getchoice(char *greet, char *choices[]);
void draw_menu(char *options[], int highlight, int start_row, int start_col);
void insert_title(char *cdtitle);
void get_string(char *string);
void add_record(void);
void count_cds(void);
void find_cd(void);
void list_tracks(void);
void remove_tracks(void);
void remove_cd(void);
void update_cd(void);

/* 메뉴를 출력하기 위한 문자열 배열. 첫번째 문자는 메뉴가 선택됐을때 반환될 문자고, 나머지 텍스트는 출력될 문자다. 현재 선택된 CD가 있을 경우에는 extended_menu에 있는 메뉴를 출력한다. */
char *main_menu[] = {
  "aadd new CD",
  "ffind CD",
  "ccount CDs and tracks in the catalog",
  "qquit",
  0,
};

char *extended_menu[] = {
  "aadd new CD",
  "ffind CD",
  "ccount CDs and tracks in the catalog",
  "llist tracks on current CD",
  "rremove current CD",
  "uupdate track information",
  "qquit",
  0,
};

/* main은 메뉴를 선택하고 q를 누르면 종료한다. */
int main()
{
  int choice;
  initscr();
  do {
    choice = getchoice("Options:", current_cd[0] ? extended_menu : main_menu);
    switch(choice) {
    case 'q':
      break;
    case 'a':
      add_record();
      break;
    case 'c':
      count_cds();
      break;
    case 'f':
      find_cd();
      break;
    case 'l':
      list_tracks();
      break;
    case 'r':
      remove_cd();
      break;
    case 'u':
      update_cd();
      break;
    }
  } while (choice != 'q');
  endwin();
  return 0;
}

/* getchoice 함수는 main에서 호출되는 함수고, greet랑 choices가 인자로 전달된다. choices는 main_menu나 extended_menu에 대한 포인터다. */
int getchoice(char *greet, char *choices[])
{
  static int selected_row = 0;
  int max_row = 0;
  int start_screenrow = MESSAGE_LINE, start_screencol = 10;
  char **option;
  int selected;
  int key = 0;

  option = choices;
  while(*option) {
    max_row++;
    option++;
  }

  if(selected_row >= max_row)
    selected_row = 0;
  clear_all_screen();
  mvprintw(start_screenrow - 2, start_screencol, greet);

  keypad(stdscr, TRUE);
  cbreak();
  noecho();

  key = 0;
  while (key != 'q' && key != KEY_ENTER && key != '\n') {
    if(key == KEY_UP) {
      if(selected_row == 0)
        selected_row = max_row -1;
      else
        selected_row--;
    }
    if(key == KEY_DOWN) {
      if(selected_row == (max_row -1))
        selected_row = 0;
      else
        selected_row++;
    }

    selected = *choices[selected_row];
    draw_menu(choices, selected_row, start_screenrow, start_screencol);
    key = getch();
  }

  keypad(stdscr, FALSE);
  nocbreak();
  echo();

  if(key == 'q')
    selected = 'q';

  return (selected);
}

/* getchoice 함수 내부에서 호출된 clear_all_screen, draw_menu 함수 */
void draw_menu(char *options[], int current_highlight, int start_row, int start_col)
{
  int current_row = 0;
  char **option_ptr;
  char *txt_ptr;

  option_ptr = options;

  while (*option_ptr) {
    if(current_row == current_highlight) {
      mvaddch(start_row + current_row, start_col - 3, ACS_BULLET);
      mvaddch(start_row + current_row, start_col + 40, ACS_BULLET);
    } else {
      mvaddch(start_row + current_row, start_col -3, ' ');
      mvaddch(start_row + current_row, start_col + 40, ' ');
    }

    txt_ptr = options[current_row];
    txt_ptr++;
    mvprintw(start_row + current_row, start_col, "%s", txt_ptr);
    current_row++;
    option_ptr++;
  }

  mvprintw(start_row + current_row + 3, start_col, "Move highlight then press Enter.");

  refresh();
}

/* clear_all_screen은 화면을 지우고 제목을 다시 적는다. CD가 선택되면 해당 정보가 출력된다. */
void clear_all_screen()
{
  clear();
  mvprintw(2, Q_LINE, "%s", "CD Database Application");
  if(current_cd[0]) {
    mvprintw(ERROR_LINE, 0, "Current CD: %s: %s\n", current_cat, current_cd);
  }
  refresh();
}

/* 새로 CD레코드를 추가하는 함수 */
void add_record()
{
  char catalog_number[MAX_STRING];
  char cd_title[MAX_STRING];
  char cd_type[MAX_STRING];
  char cd_artist[MAX_STRING];
  char cd_entry[MAX_STRING];

  int screenrow = MESSAGE_LINE;
  int screencol = 10;

  clear_all_screen();
  mvprintw(screenrow, screencol, "Enter new CD details");
  screenrow += 2;

  mvprintw(screenrow, screencol, "Catalog Number: ");
  get_string(catalog_number);
  screenrow ++;

  mvprintw(screenrow, screencol, "    CD Title: ");
  get_string(cd_title);
  screenrow ++;

  mvprintw(screenrow, screencol, "    CD Type: ");
  get_string(cd_type);
  screenrow ++;

  mvprintw(screenrow, screencol, "    Artist: ");
  get_string(cd_artist);
  screenrow ++;

  mvprintw(15, 5, "About to add this new entry:");
  sprintf(cd_entry, "%s, %s, %s, %s", catalog_number, cd_title, cd_type, cd_artist);
  mvprintw(17, 5, "%s", cd_entry);
  refresh();

  move(PROMPT_LINE, 0);
  if(get_confirm()) {
    insert_title(cd_entry);
    strcpy(current_cd, cd_title);
    strcpy(current_cat, catalog_number);
  }
}

/* get_string 함수는 현재 화면 위치에서 문자열을 읽는다. */
void get_string(char *string)
{
  int len;

  wgetnstr(stdscr, string, MAX_STRING);
  len = strlen(string);
  if(len > 0 && string[len - 1] == '\n')
    string[len - 1] = '\0';
}

/* get_confirm 함수는 사용자의 의사를 확인한다. */
int get_confirm()
{
  int confirmed = 0;
  char first_char = 'N';

  mvprintw(Q_LINE, 5, "Are you sure? ");
  clrtoeol();
  refresh();

  cbreak();
  first_char = getch();
  if(first_char == 'Y' || first_char == 'y') {
    confirmed = 1;
  }
  nocbreak();

  if(!confirmed) {
    mvprintw(Q_LINE, 1, "    Cancelled");
    clrtoeol();
    refresh();
    sleep(1);
  }
  return confirmed;
}

/* insert_title 함수는 타이틀을 추가한다. 타이틀 문자열을 타이틀 파일의 마지막에 추가하는 방식을 사용. */
void insert_title(char *cdtitle)
{
  FILE *fp = fopen(title_file, "a");
  if(!fp) {
    mvprintw(ERROR_LINE, 0, "cannot open CD titles database");
  } else {
    fprintf(fp, "%s\n", cdtitle);
    fclose(fp);
  }
}

/* 화면 창을 표현하기 위한 전역 상수 몇개를 선언한다. */
#define BOXED_LINES   11
#define BOXED_ROWS    60
#define BOX_LINE_POS   8
#define BOX_ROW_POS    2

/* update_cd는 CD 트랙을 수정한다. */
void update_cd()
{
  FILE *tracks_fp;
  char track_name[MAX_STRING];
  int len;
  int track = 1;
  int screen_line = 1;
  WINDOW *box_window_ptr;
  WINDOW *sub_window_ptr;
  clear_all_screen();
  mvprintw(PROMPT_LINE, 0, "Re-entering tracks for CD. ");
  if(!get_confirm())
    return;
  move(PROMPT_LINE, 0);
  clrtoeol();
  remove_tracks();
  mvprintw(MESSAGE_LINE, 0, "Enter a blank line to finish");

  tracks_fp = fopen(tracks_file, "a");

  box_window_ptr = subwin(stdscr, BOXED_LINES + 2, BOXED_ROWS + 2, BOX_LINE_POS - 1, BOX_ROW_POS - 1);
  if(!box_window_ptr)
    return;
  box(box_window_ptr, ACS_VLINE, ACS_HLINE);

  sub_window_ptr = subwin(stdscr, BOXED_LINES, BOXED_ROWS, BOX_LINE_POS, BOX_ROW_POS);
  if(!sub_window_ptr)
    return;
  scrollok(sub_window_ptr, TRUE);
  werase(sub_window_ptr);
  touchwin(stdscr);

  do {
    mvwprintw(sub_window_ptr, screen_line++, BOX_ROW_POS + 2, "Track %d: ", track);
    clrtoeol();
    refresh();
    wgetnstr(sub_window_ptr, track_name, MAX_STRING);
    len = strlen(track_name);
    if(len > 0 && track_name[len - 1] == '\n')
      track_name[len - 1] = '\0';

    if(*track_name)
       fprintf(tracks_fp, "%s, %d, %s\n", current_cat, track, track_name);
    track++;
    if(screen_line > BOXED_LINES - 1) {
      /* time to start scrolling */
      scroll(sub_window_ptr);
      screen_line--;
    }
  } while(*track_name);
  delwin(sub_window_ptr);

  fclose(tracks_fp);
}

/* 다음은 remove_cd */
void remove_cd()
{
  FILE *titles_fp, *temp_fp;
  char entry[MAX_ENTRY];
  int cat_length;

  if(current_cd[0] == '\0')
    return;

  clear_all_screen();
  mvprintw(PROMPT_LINE, 0, "About to remove CD %s: %s. ", current_cat, current_cd);
  if(!get_confirm())
    return;

  cat_length = strlen(current_cat);

  /* 타이틀 파일을 임시파일로 복사한다 */
  titles_fp = fopen(title_file, "r");
  temp_fp = fopen(temp_file, "w");
  while(fgets(entry, MAX_ENTRY, titles_fp)) {
    /* 카타로그 넘버랑 복사된 엔트리랑 체크 */
    if(strncmp(current_cat, entry, cat_length) != 0)
      fputs(entry, temp_fp);
  }
  fclose(titles_fp);
  fclose(temp_fp);

  /* 타이틀 파일을 삭제하고, 임시파일을 타이틀파일로 덮어쓴다. */
  unlink(title_file);
  rename(temp_file, title_file);

  /* 트랙 파일도 마찬가지 작업을 한다. */
  remove_tracks();

  /* 현재 CD는 없는 CD라고 입력해둔다. */
  current_cd[0] = '\0';
}

/* remove_tracks는 트랙을 제거한다. update_cd랑 remove_cd에서 호출됨. */
void remove_tracks()
{
  FILE *tracks_fp, *temp_fp;
  char entry[MAX_ENTRY];
  int cat_length;

  if(current_cd[0] == '\0')
    return;

  cat_length = strlen(current_cat);

  tracks_fp = fopen(tracks_file, "r");
  temp_fp = fopen(temp_file, "w");

  while(fgets(entry, MAX_ENTRY, tracks_fp)) {
    /* 카타로그 넘버랑 복사된 엔트리랑 체크 */
    if(strncmp(current_cat, entry, cat_length) != 0)
      fputs(entry, temp_fp);
  }
  fclose(tracks_fp);
  fclose(temp_fp);

  unlink(tracks_file);
  rename(temp_file, tracks_file);
}

/* count_cds는 디비를 뒤져서 타이틀과 트랙의 갯수를 구한다. */
void count_cds()
{
  FILE *titles_fp, *tracks_fp;
  char entry[MAX_ENTRY];
  int titles = 0;
  int tracks = 0;

  titles_fp = fopen(title_file, "r");
  if(titles_fp) {
    while(fgets(entry, MAX_ENTRY, titles_fp))
      titles++;
    fclose(titles_fp);
  }
  tracks_fp = fopen(tracks_file, "r");
  if(tracks_fp) {
    while(fgets(entry, MAX_ENTRY, tracks_fp))
      tracks++;
    fclose(tracks_fp);
  }
  mvprintw(ERROR_LINE, 0, "Database contains %d titles, with a total of %d tracks.", titles, tracks);
  get_return();
}

/* 트랙 목록 검색해서 CD 타이틀을 찾을 수 있다 */
void find_cd()
{
  char match[MAX_STRING], entry[MAX_ENTRY];
  FILE *titles_fp;
  int count = 0;
  char *found, *title, *catalog;

  mvprintw(Q_LINE, 0, "Enter a string to search for in CD titles: ");
  get_string(match);
  titles_fp = fopen(title_file, "r");
  if(titles_fp) {
    while(fgets(entry, MAX_ENTRY, titles_fp)) {

      /* 이전 카다록 스킵 */
      catalog = entry;
      if(found = strstr(catalog, ",")) {
        *found = 0;
        title = found + 1;

        /* 콤마 지우기 */
        if(found = strstr(title, ",")) {
          *found = '\0';
          if(found = strstr(title, match)) {
            count++;
            strcpy(current_cd, title);
            strcpy(current_cat, catalog);
          }
        }
      }
    }
    fclose(titles_fp);
  }
  if(count != 1) {
    if(count == 0)
      mvprintw(ERROR_LINE, 0, "Sorry, no matching CD found. ");
    if(count > 1) {
      mvprintw(ERROR_LINE, 0, "Sorry, match is ambiguous: %d CDs found. ", count);
    }
    current_cd[0] = '\0';
    get_return();
  }
}

void list_tracks()
{
    FILE *tracks_fp;
    char entry[MAX_ENTRY];
    int cat_length;
    int lines_op = 0;
    WINDOW *track_pad_ptr;
    int tracks = 0;
    int key;
    int first_line = 0;

    if (current_cd[0] == '\0') {
      mvprintw(ERROR_LINE, 0, "You must select a CD first. ", stdout);
      get_return();
      return;
    }

    clear_all_screen();
    cat_length = strlen(current_cat);

    /* 현재 CD의 카운트 */
    tracks_fp = fopen(tracks_file, "r");
    if(!tracks_fp)
      return;
    while(fgets(entry, MAX_ENTRY, tracks_fp)) {
      if(strncmp(current_cat, entry, cat_length) == 0)
        tracks++;
    }
    fclose(tracks_fp);

    /* 새 패드를 만든다. */
    track_pad_ptr = newpad(tracks + 1 + BOXED_LINES, BOXED_ROWS + 1);
    if(!track_pad_ptr)
      return;
    tracks_fp = fopen(tracks_file, "r");
    if(!tracks_fp)
      return;

    mvprintw(4, 0, "CD Track Listening\n");

    /* 트랙 정보를 패드에 작성 */
    while(fgets(entry, MAX_ENTRY, tracks_fp)) {
      /* 카다록 넘버랑 나머지를 출력 */
      if(strncmp(current_cat, entry, cat_length) == 0) {
        mvwprintw(track_pad_ptr, lines_op++, 0, "%s", entry + cat_length + 1);
      }
    }
    fclose(tracks_fp);

    if(lines_op > BOXED_LINES) {
      mvprintw(MESSAGE_LINE, 0, "Cursor keys to scroll, Enter or q to exit");
    } else {
      mvprintw(MESSAGE_LINE, 0, "Enter or q to exit");
    }
    wrefresh(stdscr);
    keypad(stdscr, TRUE);
    cbreak();
    noecho();

    key = 0;
    while(key != 'q' && key != KEY_ENTER && key != '\n') {
      if(key == KEY_UP) {
        if(first_line > 0)
          first_line--;
      }
      if(key == KEY_DOWN) {
        if(first_line + BOXED_LINES + 1 < tracks)
          first_line++;
      }
      /* 이제 패드에서 적절한 부분을 떼서 그린다. */
      prefresh(track_pad_ptr, first_line, 0, BOX_LINE_POS, BOX_ROW_POS, BOX_LINE_POS + BOXED_LINES, BOX_ROW_POS + BOXED_ROWS);
      /* wrefresh(stdscr); */
      key = getch();
    }
    delwin(track_pad_ptr);
    keypad(stdscr, FALSE);
    nocbreak();
    echo();
  }

/* get_return 함수는 리턴문자가 입력될때까지 프롬프트를 출력 */
void get_return()
{
  int ch;

  mvprintw(23, 0, "%s", " Press Enter ");
  refresh();
  while((ch = getchar()) != '\n' && ch != EOF);
}