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

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

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


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

Автор решения: 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() может помочь при подобной обработке вводимого текста.

→ Ссылка