Главная | Новости | Статьи | Книги | Ссылки |
8.13.
Используя написанные функции, реализуйте программу выбора в меню. Выбранную строку выделяйте инверсией фона.
/*#!/bin/cc termio.c -O -o termio -ltermcap
* Смотри man termio, termcap и screen.
* Работа с терминалом в стиле System-V.
* Работа с системой команд терминала через /etc/termcap
* Работа со временем.
* Работа с будильником.
*/
#include <stdio.h> /* standard input/output */
#include <sys/types.h> /* system typedefs */
#include <termio.h> /* terminal input/output */
#include <signal.h> /* signals */
#include <fcntl.h> /* file control */
#include <time.h> /* time structure */
void setsigs(), drawItem(), drawTitle(), prSelects(), printTime();
/* Работа с описанием терминала TERMCAP ---------------------------------*/
extern char *getenv (); /* получить переменную окружения */
extern char *tgetstr (); /* получить строчный описатель /termcap/ */
extern char *tgoto (); /* подставить %-параметры /termcap/ */
static char Tbuf[2048], /* буфер для описания терминала, обычно 1024 */
/* Tbuf[] можно сделать локальной автоматической переменной
* в функции tinit(), чтобы не занимать место */
Strings[256], /* буфер для расшифрованных описателей */
*p; /* вспомогательная перем. */
char *tname; /* название типа терминала */
int COLS, /* число колонок экрана */
LINES; /* число строк экрана */
char *CM; /* описатель: cursor motion */
char *CL; /* описатель: clear screen */
char *CE; /* описатель: clear end of line */
char *SO,
*SE; /* описатели: standout Start и End */
char *BOLD,
*NORM; /* описатели: boldface and NoStandout */
int BSflag; /* можно использовать back space '\b' */
void tinit () { /* Функция настройки на систему команд дисплея */
p = Strings;
/* Прочесть описание терминала в Tbuf */
switch (tgetent (Tbuf, tname = getenv ("TERM"))) {
case -1:
printf ("Нет файла TERMCAP (/etc/termcap).\n");
exit (1);
case 0:
printf ("Терминал %s не описан.\n", tname);
exit (2);
case 1:
break; /* OK */
}
COLS = tgetnum ("co"); /* Прочесть числовые описатели. */
LINES = tgetnum ("li");
CM = tgetstr ("cm", &p); /* Прочесть строчные описатели. */
CL = tgetstr ("cl", &p); /* Описатель дешифруется и заносится */
CE = tgetstr ("ce", &p); /* в массив по адресу p. Затем */
SO = tgetstr ("so", &p); /* указатель p продвигается на */
SE = tgetstr ("se", &p); /* свободное место, а адрес расшиф- */
BOLD = tgetstr ("md", &p); /* рованной строки выдается из ф-ции */
NORM = tgetstr ("me", &p);
BSflag = tgetflag( "bs" ); /* Узнать значение флажка:
1 - есть, 0 - нет */
}
/* Макрос, внесенный в функцию.
Дело в том, что tputs в качестве третьего аргумента
требует имя функции, которую она вызывает в цикле: (*f)(c);
Если подать на вход макрос, вроде putchar,
а не адрес входа в функцию, мы
и не достигнем желанного эффекта,
и получим ругань от компилятора.
*/
void put (c) char c;
{ putchar (c); }
/* очистить экран */
void clearScreen () {
if (CL == NULL) /* Функция tputs() дорасшифровывает описатель */
return; /* (обрабатывая задержки) и выдает его */
tputs (CL, 1, put); /* посимвольно ф-цией put(c) 1 раз */
/* Можно выдать команду не 1 раз, а несколько: например если это */
/* команда сдвига курсора на 1 позицию влево '\b' */
}
/* очистить конец строки, курсор остается на месте */
void clearEOL () { /* clear to the end of line */
if (CE == NULL)
return;
tputs (CE, 1, put);
}
/* позиционировать курсор */
void gotoXY (x, y) { /* y - по вертикали СВЕРХУ-ВНИЗ. */
if (x < 0 || y < 0 || x >= COLS || y >= LINES) {
printf ("Точка (%d,%d) вне экрана\n", x, y);
return;
}
/* CM - описатель, содержащий 2 параметра. Подстановку параметров
* делает функция tgoto() */
tputs (tgoto (CM, x, y), 1, put);
}
/* включить выделение */
void standout () {
if (SO) tputs (SO, 1, put);
}
/* выключить выделение */
void standend () {
if (SE) tputs (SE, 1, put);
/* else normal(); */
}
/* включить жирный шрифт */
void bold () {
if (BOLD) tputs (BOLD, 1, put);
}
/* выключить любой необычный шрифт */
void normal () {
if (NORM) tputs (NORM, 1, put);
else standend();
}
/* Управление драйвером терминала --------------------------------- */
#define ESC '\033'
#define ctrl(c) ((c) & 037 )
int curMode = 0;
int inited = 0;
struct termio old,
new;
int fdtty;
void ttinit () {
/* открыть терминал в режиме "чтение без ожидания" */
fdtty = open ("/dev/tty", O_RDWR | O_NDELAY);
/* узнать текущие режимы драйвера */
ioctl (fdtty, TCGETA, &old);
new = old;
/* input flags */
/* отменить преобразование кода '\r' в '\n' на вводе */
new.c_iflag &= ~ICRNL;
if ((old.c_cflag & CSIZE) == CS8) /* 8-битный код */
new.c_iflag &= ~ISTRIP; /* отменить & 0177 на вводе */
/* output flags */
/* отменить TAB3 - замену табуляций '\t' на пробелы */
/* отменить ONLCR - замену '\n' на пару '\r\n' на выводе */
new.c_oflag &= ~(TAB3 | ONLCR);
/* local flags */
/* выключить режим ICANON, включить CBREAK */
/* выключить эхоотображение набираемых символов */
new.c_lflag &= ~(ICANON | ECHO);
/* control chars */ /* при вводе с клавиш ждать не более ... */
new.c_cc[VMIN] = 1; /* 1 символа и */
new.c_cc[VTIME] = 0; /* 0 секунд */
/* Это соответствует режиму CBREAK */
/* Символы, нажатие которых заставляет драйвер терминала послать сигнал
* либо отредактировать набранную строку. Значение 0 означает,
* что соответствующего символа не будет */
new.c_cc[VINTR] = ctrl ('C'); /* символ, генерящий SIGINT */
new.c_cc[VQUIT] = '\0'; /* символ, генерящий SIGQUIT */
new.c_cc[VERASE] = '\0'; /* забой (отмена последнего символа)*/
new.c_cc[VKILL] = '\0'; /* символ отмены строки */
/* По умолчанию эти кнопки равны: DEL, CTRL/\, BACKSPACE, CTRL/U */
setsigs ();
inited = 1; /* уже инициализировано */
}
void openVisual () { /* open visual mode (включить "экранный" режим) */
if (!inited)
ttinit ();
if (curMode == 1)
return;
/* установить моды драйвера из структуры new */
ioctl (fdtty, TCSETAW, &new);
curMode = 1; /* экранный режим */
}
void closeVisual () { /* canon mode (включить канонический режим) */
if (!inited)
ttinit ();
if (curMode == 0)
return;
ioctl (fdtty, TCSETAW, &old);
curMode = 0; /* канонический режим */
}
/* завершить процесс */
void die (nsig) {
normal();
closeVisual (); /* При завершении программы (в том числе по
* сигналу) мы должны восстановить прежние режимы драйвера,
* чтобы терминал оказался в корректном состоянии. */
gotoXY (0, LINES - 1);
putchar ('\n');
if (nsig)
printf ("Пришел сигнал #%d\n", nsig);
exit (nsig);
}
void setsigs () {
register ns;
/* Перехватывать все сигналы; завершаться по ним. */
/* UNIX имеет 15 стандартных сигналов. */
for (ns = 1; ns <= 15; ns++)
signal (ns, die);
}
/* Работа с меню -------------------------------------------- */
struct menu {
char *m_text; /* выдаваемая строка */
int m_label; /* помечена ли она ? */
} menuText[] = {
/* названия песен Beatles */
{ "Across the Universe", 0 } ,
{ "All I've got to do", 0 } ,
{ "All my loving", 0 } ,
{ "All together now", 0 } ,
{ "All You need is love",0 } ,
{ "And I love her", 0 } ,
{ "And your bird can sing", 0 } ,
{ "Another girl", 0 } ,
{ "Any time at all", 0 } ,
{ "Ask me why", 0 } ,
{ NULL, 0 }
};
#define Y_TOP 6
int nitems; /* количество строк в меню */
int nselected = 0; /* количество выбранных строк */
char title[] =
"ПРОБЕЛ - вниз, ЗАБОЙ - вверх, ESC - выход, \
ENTER - выбрать, TAB - отменить";
# define TIMELINE 1
void main (ac, av) char **av; {
char **line;
register i;
int c;
int n; /* текущая строка */
extern char readkey (); /* forward */
extern char *ttyname (); /* имя терминала */
char *mytty;
extern char *getlogin (); /* имя пользователя */
char *userName = getlogin ();
srand (getpid () + getuid ()); /* инициализировать
* датчик случайных чисел */
/* считаем строки меню */
for (nitems = 0; menuText[nitems].m_text != NULL; nitems++);
/* инициализируем терминал */
tinit (); ttinit();
mytty = ttyname(fdtty);
openVisual ();
again:
clearScreen ();
if (mytty != NULL && userName != NULL) {
gotoXY (0, TIMELINE);
bold ();
printf ("%s", userName);
normal ();
printf (" at %s (%s)", mytty, tname);
}
drawTitle ("", Y_TOP - 4);
drawTitle (title, Y_TOP - 3);
drawTitle ("", Y_TOP - 2);
/* рисуем меню */
for (i = 0; i < nitems; i++) {
drawItem (i, 20, Y_TOP + i, 0);
}
/* цикл перемещений по меню */
for (n=0; ; ) {
printTime (); /* выдаем текущее время */
drawItem (n, 20, Y_TOP + n, 1);
c = getcharacter ();
drawItem (n, 20, Y_TOP + n, 0);
switch (c) {
case ' ':
go_down:
n++;
if (n == nitems)
n = 0;
break;
case '\b': case 0177:
n--;
if (n < 0)
n = nitems - 1;
break;
case ESC:
goto out;
case '\t': /* Unselect item */
if (menuText[n].m_label != 0) {
menuText[n].m_label = 0;
drawItem (n, 20, Y_TOP + n, 0);
nselected--;
prSelects ();
}
goto go_down;
case '\r': /* Select item */
case '\n':
bold ();
drawTitle (menuText[n].m_text, LINES - 2);
/* last but two line */
normal ();
if (menuText[n].m_label == 0) {
menuText[n].m_label = 1;
drawItem (n, 20, Y_TOP + n, 0);
nselected++;
prSelects ();
}
goto go_down;
default:
goto go_down;
}
}
out:
clearScreen ();
gotoXY (COLS / 3, LINES / 2);
bold ();
printf ("Нажми любую кнопку.");
normal ();
/* замусорить экран */
while (!(c = readkey ())) {
/* случайные точки */
gotoXY (rand () % (COLS - 1), rand () % LINES);
putchar ("@.*"[rand () % 3]); /* выдать символ */
fflush (stdout);
}
standout ();
printf ("Нажата кнопка с кодом 0%o\n", c & 0377);
standend ();
if (c == ESC) {
sleep (2); /* подождать 2 секунды */
goto again;
}
die (0); /* успешно завершиться,
* восстановив режимы драйвера */
}
/* Нарисовать строку меню номер i
* в координатах (x,y) с или без выделения
*/
void drawItem (i, x, y, out) {
gotoXY (x, y);
if (out) {
standout ();
bold ();
}
printf ("%c %s ",
menuText[i].m_label ? '-' : ' ', /* помечено или нет */
menuText[i].m_text /* сама строка */
);
if (out) {
standend ();
normal ();
}
}
/* нарисовать центрированную строку в инверсном изображении */
void drawTitle (title, y) char *title; {
register int n;
int length = strlen (title); /* длина строки */
gotoXY (0, y);
/* clearEOL(); */
standout ();
for (n = 0; n < (COLS - length) / 2; n++)
putchar (' ');
printf ("%s", title); n += length;
/* дорисовать инверсией до конца экрана */
for (; n < COLS - 1; n++)
putchar (' ');
standend ();
}
/* выдать общее число выбранных строк */
void prSelects () {
char buffer[30];
if (nselected == 0) {
gotoXY (0, LINES - 1);
clearEOL ();
}
else {
sprintf (buffer, "Выбрано: %d/%d", nselected, nitems);
drawTitle (buffer, LINES - 1);
}
}
/* Работа с будильником -------------------------- */
#define PAUSE 4
int alarmed; /* флаг будильника */
/* реакция на сигнал "будильник" */
void onalarm (nsig) {
alarmed = 1;
}
/* Прочесть символ с клавиатуры, но не позже чем через PAUSE секунд.
* иначе вернуть код 'пробел'.
*/
int getcharacter () {
int c;
fflush(stdout);
/* заказать реакцию на будильник */
signal (SIGALRM, onalarm);
alarmed = 0; /* сбросить флаг */
/* заказать сигнал "будильник" через PAUSE секунд */
alarm (PAUSE);
/* ждать нажатия кнопки.
* Этот оператор завершится либо при нажатии кнопки,
* либо при получении сигнала.
*/
c = getchar ();
/* проверяем флаг */
if (!alarmed) { /* был нажат символ */
alarm (0); /* отменить заказ будильника */
return c;
}
/* был получен сигнал "будильник" */
return ' '; /* продвинуть выбранную строку вниз */
}
/* ---- NDELAY read ----------------------------- */
/* Вернуть 0 если на клавиатуре ничего не нажато,
* иначе вернуть нажатую кнопку
*/
char readkey () {
char c;
int nread;
nread = read (fdtty, &c, 1);
/* обычный read() дожидался бы нажатия кнопки.
* O_NDELAY позволяет не ждать, но вернуть "прочитано 0 символов".
*/
return (nread == 0) ? 0 : c;
}
/* -------- Работа со временем ------------------------ */
void printTime () {
time_t t; /* текущее время */
struct tm *tm;
extern struct tm *localtime ();
char tmbuf[30];
static char *week[7] = { "Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" };
static char *month[12] = { "Янв", "Фев", "Мар", "Апр", "Май", "Июн",
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек" };
time (&t); /* узнать текущее время */
tm = localtime (&t); /* разложить его на компоненты */
sprintf (tmbuf, "%2s %02d:%02d:%02d %02d-%3s-%d",
week[tm -> tm_wday], /* день недели (0..6) */
tm -> tm_hour, /* часы (0..23) */
tm -> tm_min , /* минуты (0..59) */
tm -> tm_sec , /* секунды (0..59) */
tm -> tm_mday, /* число месяца (1..31) */
month[tm -> tm_mon], /* месяц (0..11) */
tm -> tm_year + 1900 /* год */
);
gotoXY (COLS / 2, TIMELINE);
clearEOL ();
gotoXY (COLS - strlen (tmbuf) - 1, TIMELINE);
bold ();
printf ("%s", tmbuf);
normal ();
}
"Сырая" "Каноническая" клавиатура--->ОчередьВвода--*-->ОчередьВвода-->read | файл-устройство драйвер терминала V эхо /dev/tty?? | экран<---ОчередьВывода---<--*--<-----------<---writeЭтот буфер используется для предчтения - вы можете набирать текст на клавиатуре еще до того, как программа запросит его read-ом: этот набранный текст сохранится в буфере и при поступлении запроса будет выдан из буфера. Также, в каноническом режиме ICANON, буфер ввода используется для редактирования введенной строки: забой отменяет последний набранный символ, CTRL/U отменяет всю набранную строку; а также он используется для выполнения некоторых преобразований символов на вводе и выводе**.
Введенная строка попадает в программу (которая запросила данные с клавиатуры при помощи read, gets, putchar) только после того, как вы нажмете кнопку <ENTER> с кодом '\n'. До этого вводимые символы накапливаются в буфере, но в программу не передаются - программа тем временем "спит" в вызове read. Как только будет нажат символ '\n', он сам поступит в буфер, а программа будет разбужена и сможет наконец прочесть из буфера ввода набранный текст.
Для меню, редакторов и других "экранных" программ этот режим неудобен: пришлось бы слишком часто нажимать <ENTER>. В режиме CBREAK нажатая буква немедленно попадает в вашу программу (без ожидания нажатия '\n'). В данном случае буфер драйвера используется только для предчтения, но не для редактирования вводимого текста. Редактирование возлагается на вас - предусмотрите его в своей программе сами!
Заметьте, что код кнопки <ENTER> ("конец ввода") - '\n' - не только "проталкивает" текст в программу, но и сам попадает в буфер драйвера, а затем в вашу программу. Не забывайте его как-то обрабатывать.
В MS DOS функция чтения кнопки в режиме ~ECHO+CBREAK называется getch(). В UNIX аналогично ей будет работать обычный getchar(), если перед его использованием установить нужные режимы драйвера tty вызовом ioctl. По окончании программы режим драйвера надо восстановить (за вас это никто не сделает). Также следует восстанавливать режим драйвера при аварийном завершении программы (по любому сигналу***).
Очереди ввода и вывода используются также для синхронизации скорости работы программы (скажем, скорости наполнения буфера вывода символами, поступающими из программы через вызовы write) и скорости работы устройства (с которой драйвер выбирает символы с другого конца очереди и выдает их на экран); а также для преобразований символов на вводе и выводе. Пример управления всеми режимами есть в приложении.
* Такие буфера носят название "character lists" - clist. Существуют "сырой" (raw) clist, в который попадают ВСЕ символы, вводимые с клавиатуры; и "канонический" clist, в котором хранится отредактированная строка - обработаны забой, отмена строки. Сами специальные символы (редактирования и генерации сигналов) в каноническую очередь не попадают (в режиме ICANON).
** Режимы преобразований, символы редактирования, и.т.п. управляются системным вызовом ioctl. Большой пример на эту тему есть в приложении.
*** Если ваша программа завершилась аварийно и моды терминала остались в "странном" состоянии, то привести терминал в чувство можно командой stty sane
© Copyright А. Богатырев, 1992-95
Си в UNIX
Назад | Содержание | Вперед