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 ();
}
Напишите программу, выдающую файл на экран порциями по 20 строк и ожидающую нажатия клавиши. Усложнения:
Напишите функции включения и выключения режима эхо-отображения набираемых на
клавиатуре символов (
).
). В обычном режиме строка, набранная на клавиатуре, сначала попадает в некоторый буфер в драйвере терминала
.
Этот буфер используется для предчтения - вы можете набирать текст на клавиатуре еще
-ом: этот набранный текст сохранится в буфере
и при поступлении запроса будет выдан из буфера. Также, в каноническом режиме
,
буфер ввода используется для редактирования введенной строки: забой отменяет последний набранный символ,
отменяет всю набранную строку; а также он используется
для выполнения некоторых преобразований символов на вводе и выводе
.
Введенная строка попадает в программу (которая запросила данные с клавиатуры при
помощи read, gets, putchar) только после того, как вы нажмете кнопку <ENTER> с кодом
'\n'. До этого вводимые символы накапливаются в буфере, но в программу не передаются
- программа тем временем "спит" в вызове read. Как только будет нажат символ '\n',
он сам поступит в буфер, а программа будет разбужена и сможет наконец прочесть из
буфера ввода набранный текст.
Для меню, редакторов и других "экранных" программ этот режим неудобен: пришлось
бы слишком часто нажимать <ENTER>. В режиме CBREAK нажатая буква немедленно попадает
в вашу программу (без ожидания нажатия '\n'). В данном случае буфер драйвера используется только для предчтения, но не для редактирования вводимого текста. Редактирование возлагается на вас - предусмотрите его в своей программе сами!
Очереди ввода и вывода используются также для синхронизации скорости работы
программы (скажем, скорости наполнения буфера вывода символами, поступающими из программы через вызовы write) и скорости работы устройства (с которой драйвер выбирает
символы с другого конца очереди и выдает их на экран); а также для преобразований
символов на вводе и выводе. Пример управления всеми режимами есть в приложении.