Как получить коды стрелок и т.п. в программе с терминалом под *nix
Навеяно вот этим вопросом...
Как написать для терминала в linux (unix) программу на Си (или С++), которая будет вызывать функцию getchar() и получать вместе с кодами обычных символов еще какие-нибудь коды при нажатии на клавиатуре стрелок и функциональных клавиш (F1, F2 ... F12 и т.п.)?
Ответы (2 шт):
Как вызывать с такой целью именно getchar(), откровенно говоря, не знаю (если только воспользоваться #define?).
Можно написать ее аналог, скажем get_fchar() (с тем же прототипом), которая для стрелок и т.п. будет возвращать код больший 255.
При нажатии на стрелку и т.п. в терминале, клавиатура посылает на stdin программы несколько символов (так называемую esc-последовательность), которые мы будем искать в заранее составленной таблице. Если нашли, то вернем индекс строки таблицы плюс 255 (это и будет желаемый код функциональной клавиши). Если же прочитанная последовательность отсутствует в таблице, то мы будем возвращать коды составляющих ее символов в ходе последующих вызовов функции.
Понятно, что для полноценного использования такой функции в программе типа текстового редактора надо переводить терминал в raw -echo режим и не забывать, что в нем вывод '\n' не переводит автоматически курсор в начало строки.
Например, вот:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct f_code {
const char *seq;
const char *code;
};
static struct f_code
def_seq[] =
{
{"\x1b[A", "UP"},
{"\x1b[B", "DOWN"},
{"\x1b[C", "RIGHT"},
{"\x1b[D", "LEFT"},
{"\x1b[E", "CENTER"},
{"\x1b[H", "HOME"},
{"\x1b[F", "END"},
{"\x1bOP", "F1"},
{"\x1bOQ", "F2"},
{"\x1bOR", "F3"},
{"\x1bOS", "F4"},
{"\x1b[15~", "F5"},
{"\x1b[17~", "F6"},
{"\x1b[18~", "F7"},
{"\x1b[19~", "F8"},
{"\x1b[2~", "INS"},
{"\x1b[3~", "DEL"},
{"\x1b[5~", "PG_UP"},
{"\x1b[6~", "PG_DOWN"},
{"\x1b[20~", "F9"},
{"\x1b[21~", "F10"}, // ??? mapped to terminal emulator action in my Linux Mint 19.3
{"\x1b[23~", "F11"}, // ??? gnome-terminal TERM=xterm-256color, can't to see esc-sequence
{"\x1b[24~", "F12"},
{0, 0}
},
*fseq = def_seq;
struct f_code *
set_fctab (struct f_code *p)
{
struct f_code *r = fseq;
fseq = p;
return r;
}
struct f_code *
def_fctab ()
{
fseq = def_seq;
return fseq;
}
/*
search `s` in fseq[].seq
returns:
index + 256 if complete match
0 if not matched
n > 0 number of uncompeleted matches
*/
int
match_seq (char *s)
{
int n = 0;
for (int i = 0; fseq[i].seq; i++) {
int j = 0;
const char *t = fseq[i].seq;
while (s[j] && t[j] && s[j] == t[j])
j++;
if (s[j] == 0) {
if (t[j] == 0)
return i + 256;
else
n++;
}
}
return n;
}
// well, ^@ can't be into the sequence
int
imatch_seq (int *s)
{
int n = 0;
for (int i = 0; fseq[i].seq; i++) {
int j = 0;
const char *t = fseq[i].seq;
while (s[j] && t[j] && s[j] == t[j])
j++;
if (s[j] == 0) {
if (t[j] == 0)
return i + 256;
else
n++;
}
}
return n;
}
const char *
get_fcode (int i)
{
if (i < 256 || i - 256 > sizeof(fseq) / sizeof(fseq[0]) - 1) {
static char buf[2];
buf[0] = i;
return (const char *)buf;
}
return fseq[i - 256].code;
}
// Returns 8-bit char or functional key as 256 + code
int
get_fchar()
{
static int buf[8];
static int b_cnt = 0;
static int b_pos = 0;
static int rchar = 0, has_rchar = 0;
int c;
ret_queue:
if (b_cnt) {
c = buf[b_pos++];
b_cnt--;
if (!b_cnt)
b_pos = 0; // ready for store new sequence
return c;
}
c = has_rchar ? rchar : getchar();
has_rchar = 0;
if (c != 0x1b)
return c;
buf[b_cnt++] = c; // ^[
buf[b_cnt] = 0;
int rc;
for (;;) {
buf[b_cnt++] = getchar();
buf[b_cnt] = 0;
rc = imatch_seq(buf);
if (rc > 255)
break;
if (rc == 0) {
rchar = buf[--b_cnt];
has_rchar = 1;
goto ret_queue;
}
}
b_cnt = 0;
b_pos = 0;
has_rchar = 0;
return rc;
}
int
main (int ac, char *av[])
{
for (int j = 0; fseq[j].seq; j++) {
const char *str = fseq[j].seq;
printf("%-10s ", fseq[j].code);
for (int i = 0; str[i]; i++)
if (str[i] < ' ')
putchar('^'), putchar('@' + str[i]);
else
putchar(str[i]);
puts("");
}
puts("\ntest get_fchar() (^D for end)");
int c;
while ((c = get_fchar()) != EOF) {
if (c > 255)
puts(get_fcode(c));
else
printf("'%c'\n", c);
if (c == 4)
break;
}
system("stty sane"); // for auto restore after `stty raw -echo; ./a.out`
return puts("\nEnd") == EOF;
}
В main пример использования get_fchar() с печатью названий функциональных клавиш и т.п.
Также обратите внимание на функции set_fctab() (она позволяет сменить таблицу кодов клавиш (точнее, esc-последовательностей)) и def_fctab() (устанавливает таблицу по-умолчанию).
Кстати, все эти функции можно использовать для преобразования любых клавиатурных последовательностей в коды (при установке соответствующей таблицы). Функция match_seq() может помочь при подобной обработке вводимого текста.
Как написать для терминала в linux (unix) программу на Си
Как бы, как всем нам известно, есть такой ужас-ужас данный нам в ощущениях: каждый разработчик эмулятора терминала, так же как и ранее, каждый разработчик терминала, норовит сделать свой собственный уникальный набор Esc-последовательностей.
И разработчики клавиатур, тоже, посильно, принимают участие в этой оргии.
Плюс возможности настройки этих эмуляторов терминала ввиду предыдущих пунктов, чаще это эмуляция эмулятора, с той или иной целью, но...
Сложившийся способ борьбы:
- Коллективное поддержание базы "терминалов"
terminfo(был ещёtermcap, но я его уже давно не видел); - Задание переменной окружения
TERMи её наследрвание при локальных или сетевых взаимодействиях; - Использование той или иной библиотеки работы с терминалом:
curses,ncurses,readline, ...
Curses, по-моему, даже попадал в одну из частей POSIX. В стандарт Linux Standard Base (LSB) входит C интерфейс ncurses (расширение SVR4/POSIX curses), он же входит в macOS и FreeBSD (в Solaris и AIX, опционален, но поддерживается производителем)
Ну, и, что б дважды не вставать, пусть будут коды от мышки и ввод кириллицы посимвольно, а не побайтно.
// IMHO, для кириллицы проще curses с wchar_t/wint_t и setlocale()
#define _XOPEN_SOURCE_EXTENDED (1)
#include <locale.h>
#include <ncurses.h> // Требует ключа `-lncursesw` или `-lncurses`.
// `pkg-config ncursesw --cflags`
// `pkg-config ncursesw --libs`
// Вариант <curses.h> и `-lcurses`.
#include <wchar.h>
#include <wctype.h>
void show_wchar();
int main() {
setlocale(LC_ALL, ""); // Объясняем за кириллицу, если есть
initscr(); // Стартуем ncurses/curses
scrollok(stdscr, TRUE); // Автоматическая прокрутка экрана
show_wchar();
keypad(stdscr, TRUE); // Распознаём кнопки
mmask_t mousemask_setting = ALL_MOUSE_EVENTS;
auto mm_success = mousemask(mousemask_setting, nullptr);
if (!mm_success) {
printw("FAIL: mousemask\n");
} else if (mm_success == mousemask_setting) {
printw("SUCCESS: mousemask\n");
} else {
printw("Partial success: mousemask %x of %x\n",
mm_success, mousemask_setting);
}
for(;; refresh()) {
wint_t wc;
int kc = get_wch(&wc); // Получаем символ/кнопку
switch (kc) {
case KEY_CODE_YES:
switch (wc) { // Можем сравнивать с константами
case KEY_F(1):
case KEY_NPAGE:
case KEY_ENTER:
printw("%ls ", L"Приз");
break;
case KEY_MOUSE: {
MEVENT event;
if (OK != getmouse(&event)) {
printw("%ls ", L"Ошибка");
} else {
printw("%3d %3d %d ", event.x, event.y, event.bstate);
}
}
}
printw("Key: %s\n", keyname(wc)); // А можем получить имя
break;
case OK:
if (iswcntrl(wc)) { // Ctrl-кнопка - это другое :)
printw(" - Ctrl: %s\n", unctrl(wc));
} else { // Либо "обычный" wchar_t
printw(" - %lc 0x%x\n", wc, wc);
}
break;
default:
printw("ERR or unknown kc=%d\n", kc);
}
}
}
// Но надо иметь ввиду, что wchar_t не всегда (не везде) Unicode, может
// зависеть от текущего региона (ну, если не определён символ
// __STDC_ISO_10646__).
void show_wchar() {
#ifdef __STDC_ISO_10646__
printw("__STDC_ISO_10646__ = %ld\n", __STDC_ISO_10646__);
printw("%ls\n", L"INT (Ctrl-C) для завершения");
#else
const char* yo[] = { "\u0451" };
wchar_t wyo[2];
mbstate_t state = {};
if (1 == mbsrtowcs(wyo, yo, 2, &state) &&
L'\U00000451' == wyo[0]) {
printw("%ls\n", L"Регион Unicode");
printw("%ls\n", L"INT (Ctrl-C) для завершения");
} else {
printw("Independent locale\n");
printw("INT (Ctrl-C) for exit\n");
}
#endif
}
Можно с другой машины зайти, по сети ли, эмуляторами последовательных портов ли, и получать клавиши, включая клавишу ESC - отдельно от других. ?
(или С++)
В ncurses входит не очень популярный C++ интерфейс: libncurses++. Но для демонстрации ввода он не особо полезен, только main() можно убрать, он больше наоборот, норовить скрыть кухню низкого уровня в деле меню, списков, диалогов и прочих widgets.
Кроме того, если конкретно для консоли Linux, то есть ещё альтернативный специализированный путь (скан-коды и все дела), для особых ценителей, таких, как реализация GUI и т.п.