Как получить коды стрелок и т.п. в программе с терминалом под *nix

Навеяно вот этим вопросом...

Как написать для терминала в linux (unix) программу на Си (или С++), которая будет вызывать функцию getchar() и получать вместе с кодами обычных символов еще какие-нибудь коды при нажатии на клавиатуре стрелок и функциональных клавиш (F1, F2 ... F12 и т.п.)?


Ответы (2 шт):

Автор решения: avp

Как вызывать с такой целью именно 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() может помочь при подобной обработке вводимого текста.

→ Ссылка
Автор решения: Serge3leo

Как написать для терминала в linux (unix) программу на Си

Как бы, как всем нам известно, есть такой ужас-ужас данный нам в ощущениях: каждый разработчик эмулятора терминала, так же как и ранее, каждый разработчик терминала, норовит сделать свой собственный уникальный набор Esc-последовательностей.

И разработчики клавиатур, тоже, посильно, принимают участие в этой оргии.

Плюс возможности настройки этих эмуляторов терминала ввиду предыдущих пунктов, чаще это эмуляция эмулятора, с той или иной целью, но...

Сложившийся способ борьбы:

  1. Коллективное поддержание базы "терминалов" terminfo (был ещё termcap, но я его уже давно не видел);
  2. Задание переменной окружения TERM и её наследрвание при локальных или сетевых взаимодействиях;
  3. Использование той или иной библиотеки работы с терминалом: 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 и т.п.

→ Ссылка