Преодолевая консольный барьер
Автор: Stephen Bint
Перевод: Юрий Прушинский


Клавиатура, мышь и цветной текст в DOS и Linux консоли

Когда я только начал работать в Линуксе, я столкнулся с тем, что большинство обычных текстовых редакторов совершенно никуда не годятся - минимальная, а то и вообще полное отсутствие поддержки мыши, ни тебе выделения текста клавишей shift, ни меню для открытия файлов. И поэтому я, не долго думая, решил внести свой вклад и исправить существующее положение дел, написав собственный редактор, такой, который мы обычно используем в DOS, но для Linux-консоли. Почему, в конце концов, лучшая ОС не должна иметь редакторов, которые ни в чём не уступают своим кузенам из DOS?!

Итак, я пустился на поиск библиотеки, которая позволила бы мне создать цветной псевдографический интерфейс под обе платформы. Наконец, я обнаружил Slang и curses. Но их возможностей оказалось недостаточно - чтобы перенести интерфейс на множество платформ, пришлось бы ограничить множество функций Линукс-консоли. Ко всему прочему, они оказались настолько громоздкими, что я мог погрязнуть в работе на долгие годы, пытаясь перестроить их для собственных нужд, или связывая модифицированные версии со своими исходниками. Таким образом, полный разочарования, я принялся писать свою собственную библиотеку.

Я задался идеей создать интерфейс, использующий максимальное количество комбинаций c ctrl- alt-, функцию мониторинга состояния клавиш shift, ctrl и alt, нажатия клавиш мыши (и перемещение), а также прямой доступ к экранному массиву в стиле символьно-цветовых пар EGA.[Речь идёт о том, что в текстовом режиме за отображение символа отвечают два байта -- первый содержит ASCII-код отображаемого символа, второй -- цвет. При этом второй байт делится на две половинки: "старшая" это цвет фона, "младшая" -- цвет отображаемого символа. И вот, что ещё -- это относится не только к EGA-режиму, но и к CGA и VGA. Прим.ред.] К тому же хотелось, чтобы она была не очень большая и сложная, чтобы и другие программисты могли включать её в свой код, и вообще без проблем модифицировать и распространять её.

Мышь

Программировать для мыши оказалось относительно просто. В DOS я воспользовался int86 [Хм... Вряд ли int86 является номером прерывания, скорее всего названием пакета, т.к. прерывание, которое отвечает за работу с мышкой в DOS -- это int 33h. Прим.ред.] и Списком Прерываний Ральфа Брауна [Ralf Brown]. В Линуксе я поборолся и, в конце концов, смастерил gpm драйвер для мыши, благо что для него есть приличная документация с примерами программ.

Экран

А вот чтобы решить как выводить на экран цветной текст в Линуксе потребовало значительных усилий. Прочитав статью в Linux Gazette с заголовком "Так Вам нравится цвет!!! (Таинственные символы ^[[)", я был шокирован тем, что в ней говорилось.

В отличие от DOS, где все символы и цвета в виде байт-пар пишутся прямо в видеопамять, в Линуксе экран обновляется путём записи fwrite в stdout! Вместо того, чтобы указывать цвет с каждым символом, необходимо менять цвет всего вывода, если предыдущий символ был другого цвета. А смена цвета вывода означает запись 11-байтной последовательности в stdout.

В результате этого, обновление экрана в Линуксе намного медленней. Я сделал всё, что мог для того, чтобы его ускорить.А именно, я использовал буфер с копией экрана, который обновляется при каждом обновлении экрана. Сравнивая затем изображение с буфером экрана, я обновлял только изменившиеся элементы изображения. Но даже при таком методе, обновление экрана в Линуксе занимает раз в двадцать больше времени чем в DOS.

В современных версиях Линукс уже возможно получить доступ к видеопамяти, если открыть как файл устройство /dev/vcsa (подробности в man vcs). Но лучше этого не делать хотя бы по двум причинам. Первая, это то, что получить доступ к vcs могут только программы, обладающие правами администратора. Вторая причина в том, что поддерживается только набор символов US ASCII. В то время как fwrite поддерживает любой национальный набор символов, что очень важно, так как Линукс это интернациональная вещь от экрана приветствия до своего большого, горячего сердца.

Используя исходники Slang и превосходную программу untic из его состава, я всё-таки придумал как показывать, скрывать и позиционировать текстовый курсор. Untic считывает базу terminfo и переводит её в форму, понятную человеку (База terminfo содержит наборы команд для записи в stdout, использующиеся для управления в любом терминале).

Была только небольшая неувязка. В Линуксе, символы для рисования прямоугольников не являются частью набора символов по умолчанию. Значения ASCII, рисующие прямоугольники в DOS, в Линуксе будут рисовать их непонятными смешными буквами до тех пор, пока вы не пошлёте в stdout команду, чтобы переключиться на нужный символьный набор. Но такое временное переключение набора символов нельзя считать выходом из сложившейся ситуации. Я же хотел, чтобы моя библиотека была такой же интернациональной, как и сама система Линукс, поддерживающая интернациональные символьные наборы, но только как это сделать?

Я решил использовать старший бит цветового байта в качестве бита прямоугольника. Программисты, желающие нарисовать прямоугольники, должны будут указывать бит прямоугольника в цвете для каждого символа, который они хотят использовать для рисования прямоугольника. Это подразумевает, что не будет доступен мигающий текст, так как старший бит уже используется для наших целей, но мне повезло - я никогда не любил мигающий текст. [Судя по всему, пользователям редактора Стефана Бинта тоже придётся разлюбить мигающий текст. :-) Прим.ред.]

Клавиатура

Интерпретация сигналов клавиатуры на любой платформе это сущий кошмар. В DOS сканкоды клавиш распределены настолько нелогично, что они с таким же успехом могут обозначать набор случайных чисел. В Линуксе терминал нужно предварительно подготовить, и только затем функциональные клавиши генерируют последовательность байт, которые, в свою очередь, необходимо преобразовать в сканкоды по таблице соответствия.

Мне представлялось практически непостижимым преобразовать значения клавиш из Линукс в DOS и обратно. Поэтому я решил взять чистую функцию какой-нибудь клавиши, которая всегда имеет одно значение, независимо от нажатой ctrl или alt, и меняющей это значение лишь при нажатой shift. Программисты, желающие использовать комбинации ctrl- и alt- для горячих клавиш, к сожалению, должны будут определить статус клавиатуры самостоятельно.

Клавиатура в DOS

Вы должно быть надеетесь, что двухбайтный сканкод будет использовать старший байт для ID клавиши, который никогда не изменяется, а младший байт для ASCII значения, зависящего от нажатой shift, ctrl или alt? К сожалению, из-за необходимости поддерживать совместимость со старыми ХТ клавиатурами старший байт имеет значение в зависимости от того, насколько велико значение младшего байта. Что ещё хуже, разные клавиши реагируют по-разному на ctrl и alt. Чтобы избежать длительных блокировок, я сделал целый клубок тестов с "if", отсеивающих сканкоды с ctrl и alt.

Затем я обнаружил, что нажатие shift влияет на считывание клавиш с панели numlock в DOS, в то время как для Линукс это не так. Пришлось ещё больше усложнить мой фильтр клавиш чтобы решить это недоразумение, и значения numlock давали именно цифры, и ничто иное. Итак, DOS был покорён, и я остался лицом к лицу с ужасами клавиатуры Линукс.

Клавиатура в Linux

В том состоянии, в котором клавиатура работает в Линукс по умолчанию, она далека от того, чтобы быть удобной для использования интерактивной программой. Функция fgetc() не возвращает значения до тех пор, пока не будет нажата клавиша return (в простонародии Enter - прим.перев.). После нажатия return, функция возвращает всю строку целиком, так что номер с перемещением курсора клавишами управления курсора [тавтология :-)] здесь не пройдёт - всё это приводит лишь к отображению символов на экране, а нажатие ctrl-z, ctrl-q и ctrl-s генерируют прерывания. Одним словом, сплошной кошмар ... "на улице Linux"!

У меня ещё была надежда, что я смогу избежать использования fgetc() и перевести клавиатуру в "сырой" режим (чистые сканкоды), но драйвер для мыши [gpm] не позволил мне такой роскоши. Он предоставляет единую функцию для чтения событий как с клавиатуры, так и с мыши, при этом клавиатура использует fgetc(stdin). Есть правда отдельная функция для опроса мыши, но мне так и не удалось заставить её работать.

Но теперь я даже рад, что не сделал этого, потому что понял, что fgetc() получает коды клавиш, которые примерно похожи на разных клавиатурах, где раскладка и, вероятно, даже сканкоды совсем другие. Я заставил себя отказаться от идеи переводить последовательности байт в сканкоды и, оказалось, что это на самом деле легче, чем работать со сканкодами в DOS.

Изучив исходники Slang, я наконец выяснил как настроить терминал. Для этого нужно использовать функцию tcsetattr() для установки признаков и значений в структуре управления терминалом. Так я заставил клавиатуру возвращать символы немедленно безо всякого эха и использования ctrl-z, ctrl-q или ctrl-s.

К сожалению, у меня всё ещё не было ни функции kbhit(), ни возможности реагировать на нажатую shift, ctrl или alt. Но поиск в Google дал мне статью в Linux Gazette, называвшуюся "Укрощение клавиатуры в Linux" [К сожалению, перевода этой статьи на нашем сайте пока ещё нет. Прим.ред.], которая и дала мне эти функции с полным исходным текстом.

Последнее испытание

Да, ещё одна проблема осталась нерешённой. Для кого-то она может показаться незначительной, но для меня она была очень важна. Она казалась таким непреодолимым препятствием, что я, признаться, чуть было не сдался.

Вы наверное знаете, что в DOS-редакторах можно выделять текст, удерживая нажатой клавишу shift и используя клавиши перемещения курсора? Так вот в Линуксе сочетания shift-PageUp / shift-PageDown зарезервированы для такой операции как скроллинг в окне терминала. [Этим вопросом часто задаются новички -- "как настроить скроллинг в консоли"? Вот эти две "магических" комбинации клавиш и позволяют сделать это. Правда, в текстовом режиме эта возможность работает до тех пор пока вы не переключились на соседний терминал. Прим.ред.] А это означает, что приложения не получают никаких значений от fgetc() при нажатии shift-PageUp/Down. Ядро просто отбирает эти клавиши себе и ваша программа никогда их не увидит.

Но это, как оказалось, ещё не самое худшее. После нескольких недель напряженного умственного труда, когда вроде бы всё было почти готово, я обнаружил, что если пользователь попытается выделить текст клавишами shift-PageUp, то половина моего любимого цветного экрана самым наглым образом исчезает - прокручивается обратно!

Это был удар. Я понял, что не смогу уже закончить свою библиотеку. Было такое чувство, что я прочитал тысячестраничный роман, и в конце обнаружил что последняя страница вырвана. Я перерыл все страницы в man, облазил всю Сеть - безрезультатно. Но тут неожиданно я заметил, что функция shift_state() которую я взял из статьи, упомянутой ранее, использует в свою очередь некую функцию ioctl() .

Используя apropos ("apropos ioctl"), я обнаружил в страницах справочного руководства упоминание о "console_ioctls". Так я обнаружил, что ioctl() в Линуксе является эквивалентом DOS'овского прерывания. [Функция 44h, прерывание 21h. Прим.ред.] На этой же странице был приведён полный список низкоуровневых системных вызовов, а также предупреждение от разработчика ядра никогда их не использовать, поскольку их поддержка не гарантирована в последующих версиях ядра.

Но мы-то знаем, что можно игнорировать подобные высказывания разработчиков ядра. Таким образом они просто снимают с себя любую ответственность.

Так вот, в этом списке я нашел как изменять функции, связанные с клавишами - включая и PageUP и PageDown. Для этого требовалось заполнить структуру, состоящую из трёх чисел, которые соответствуют ... таблице, клавише и ассоциируемой с нею командой. Проблема только в том, что не было документации, где бы говорилось о том, какие именно числа нужны для отключения скроллингa по shift-PageUp.

Дальнейшие поиски дали пакет kbd, содержащий отличную документацию и несколько утилит для изменения соответствия клавиш. Например, утилитой dumpkeys можно сделать дамп текущего соответствия в stdout. Вот выдержка из того, что выдала мне dumpkeys. Заметьте, что это дало мне только одно из трёх нужных чисел - код клавиши.

keycode 103 = Up              
        alt     keycode 103 = KeyboardSignal  
keycode 104 = Prior
        shift   keycode 104 = Scroll_Backward
keycode 105 = Left            
        alt     keycode 105 = Decr_Console    
keycode 106 = Right           
        alt     keycode 106 = Incr_Console    
keycode 107 = Select          
keycode 108 = Down            
keycode 109 = Next            
        shift   keycode 109 = Scroll_Forward
keycode 110 = Insert          

Если направить этот вывод в текстовый файл, то потом его можно подредактировать и загрузить с помощью loadkeys, и таким образом изменить соответствие. Немного поэкспериментировав, я выяснил, что можно вообще удалить из файла всё, кроме тех соответствий, которые нужно изменить, поэтому я оставил всего две строки:

shift   keycode 104 = Scroll_Backward 
shift   keycode 109 = Scroll_Forward

и изменил значения так, что функции клавиш при нажатой shift не меняются:

shift      keycode 104 = Prior
shift   keycode 109 = Next

Назвав этот файл kmap, я выполнил "loadkeys kmap". Затем запустил свою тестовую программу и получил желаемый результат - скроллинг больше не работал. Теперь я знал, что это возможно. Изучение исходников loadkeys показало, что она использует те же ioctl для смены функций, что я обнаружил, но я по прежнему не знал какие числа нужно использовать.

Мне ничего не осталось, как вновь включить свою смекалку. Оказалось, что у loadkeys есть опция -m, генерирующая исходный файл, содержащий таблицу из 256 значений. Я запустил "loadkeys -m kmap" и получил таблицу, в которой было 254 нулевых и два не нулевых значения. Подсчитывая эти элементы, выяснилось, что не нулевые элементы имели номера 104 и 109 - то есть коды клавиш в моём файле kmap. Должно быть значения в таблице это значения команд "Prior" и "Next".

Ещё я заметил, что у этой таблицы был номер. Я поменял "shift" на "control" в одной из строк файла kmap, и получил уже две таблицы, соответственно, одну для shift, а другую для control. В обоих случаях номером таблицы с shift была "1". Таким образом, вместе с фактическими значениями в таблице я получил свои три числа.

Получилось, что для того чтобы отключить скроллинг и сделать shift-PageUp/Down обычными комбинациями клавиш, надо сохранить существующие значения, затем изменить их, а при выходе опять восстановить.

Если вы вдруг захотите отключить какую-нибудь клавишу, например, клавишу переключения консоли, то вам нужно будет повторить этот фокус с "loadkeys -m" и выяснить нужные для этого номера.

Данная функция сохраняет функцию клавиши, а затем изменяет её на то значение, которое вы ставите ей в соответствие (написано для gcc):

(текст всех листингов)

#include <sys/ioctl.h>
#include <linux/kd.h>
#include <linux/keyboard.h>
#include <stdio.h>

int set_kb_entry( unsigned short table, unsigned short keycode, 
                  unsigned short value, unsigned short *oldvalue ) {

   struct kbentry ke;

   ke.kb_table = table;
   ke.kb_index = keycode;

/* Берём старое значение, выдаем ошибку если таблица или код клавиши неверны */
   if( ioctl( fileno(stdin), KDGKBENT, &ke ) )
      return -1;

/* Если oldvalue ptr не равен NULL,  сохраняем старое значение, чтобы затем восстановить его */
   if( oldvalue ) *oldvalue = ke.kb_value;

/* Новое действие для данной клавиши */
   ke.kb_value = value; 

/*  Делаем дело, возвращаем ошибку если значение неверно */
   if( ioctl( fileno(stdin), KDSKBENT, &ke ) )
      return -1;

   return 0;
   }

Пример использования вышеприведенной функции для отключения скроллинга и восстановления его при выходе:

#include <stdlib.h>

/* Здесь будут храниться старые значения клавиш */
unsigned short scroll_forward = 0;
unsigned short scroll_backward = 0;

/* Волшебные числа, найденные с помощью dumpkeys и loadkeys -m */
#define SHIFT_TABLE          1
#define PAGE_UP_KEYCODE    104
#define PAGE_DOWN_KEYCODE  109
#define PAGE_UP_ACTION     0x0118 /* Prior */
#define PAGE_DOWN_ACTION   0x0119 /* Next  */


/* Восстанавливаем  обычные значения для shift-PageUp и shift-PageDown */
static void restore_scrollback() {

   if( scroll_backward )
      set_kb_entry( SHIFT_TABLE, PAGE_UP_KEYCODE, 
                    scroll_backward, 0 );

   if( scroll_forward )
      set_kb_entry( SHIFT_TABLE, PAGE_DOWN_KEYCODE, 
                    scroll_forward, 0 );
   }


/* Освобождаем  shift-PageUp и shift-PageDown для обычных функций */
int disable_scrollback() {

   if( set_kb_entry( SHIFT_TABLE, PAGE_UP_KEYCODE, 
                     PAGE_UP_ACTION, &scroll_backward ) )
      return -1;

   if( set_kb_entry( SHIFT_TABLE, PAGE_DOWN_KEYCODE, 
                     PAGE_DOWN_ACTION, &scroll_forward ) )
      return -1;

   atexit( restore_scrollback );

   return 0;
   }

Возвращение джедая

Итак, с победой в руках я вырвался наконец из мрачных подземелий консоли Линукс. Я сделал возможным для программистов создавать консольные приложения, которые ведут себя одинаково и в DOS и в Linux, и, я думаю, гарантировал себе место в легенде.

И знаете, что самое интересное? Я так и не написал этот текстовый редактор! Не смог, потому что я бездомный, и за время пока у меня был доступ к компьютеру я смог сделать лишь всё вышеописанное. Может теперь всё дело за вами?

Линукс это пока ещё девственная территория, которую колонизируют люди из Африки или Индии. Они тоже бедные и у них нет дорогих компьютеров, на которых можно запустить Х (да уж, особенно если почитать статьи LinuxGazette где индусы и африканцы пишут и смотрят видео, то консоль у них кроме как xterm представить трудно :) - прим.перев.), и поэтому им необходимы консольные приложения. А теперь даже те из вас у кого не установлен Линукс смогут им помочь.

Линукс ещё нужны пионеры, которые смогут создать инфраструктуру до того, как сюда придёт первая волна переселенцев. Ведь им понадобятся конфигурационные утилиты для таких приложений как например Apache, или таких фильтров как grep. Ещё им понадобится хороший текстовый редактор с меню "вырезать-копировать-вставить", доступное через правый клик мышки.

Программистам, которые будут создавать эти приложения, понадобятся библиотеки элементов управления, и особенно диалог "Открыть/Сохранить файл". Ещё им пригодится хороший класс строковых массивов с функциями вырезать-копировать-вставить.

Хороший редактор не обязательно должен иметь много функций, но должна быть простая возможность добавлять функции в его меню. [Взгляните на Far под оффтопиком. Прим.ред.] Причем настолько простая, что любой дурак смог бы написать функцию на С++ ["И только дурак захочет её написать..." Шутка! Вернее, цитата шутки. :-) Прим.ред.], которая имеет в своих параметрах указатель на этот редактор в качестве аргумента, и добавляется в его меню одной строчкой в main(). Программисты смогут обмениваться этими функциями друг с другом, и таким образом получится мощный редактор.

Готовы ли вы стать одним из этих пионеров? Если никто не отважится, то боюсь, Линукс умрёт, и мы окажемся беззащитными игрушками в руках Зла. Я очень надеюсь что вы поднимите мой упавший штандарт. Возможно, именно вы можете оказаться нашей последней и единственной надеждой. Удачи.

И да пребудут с вами Исходники.

[Умф... Немного патетично, но автор проделал неплохую работу. Хм... Интересно, что же такое курят английские бездомные, если их пробивает на такой спич? ;-) Прим.ред.]

ctio.zip (41.7kb)
ctio.tar (150kb)

Ссылки

Slang, автор - John E. Davis. Slang написан очень качественно и хорошо, поэтому его очень легко использовать. При его помощи я узнал как инициализировать клавиатуру, а также большинство команд для работы с экраном. Кроме этого много полезных команд я узнал при помощи программы untic, входящей в состав Slang. Но самое замечательное в Slang то, что именно с его помощью можно запустить Midnight Commander в терминале telnet. Думаю те, кому хоть раз приходилось удаленно восстанавливать веб-сервер, со мной согласятся.

"Так Вам нравится цвет!!! (Таинственные символы ^[[)" Автор - Pradeep Padala (LG #65). В этой статье я познакомился с принципом работы консоли и экрана в Линукс.

Укрощение клавиатуры в Linux Автор - Petar Marinov (LG #76). Мои функции shift_status() и key_awaits() являются просто модифицированными версиями shift_state() и kbhit() взятыми из этой статьи.

Ralf Brown, Святой покровитель всех DOS-программистов

Стивен (Stephen) простой бездомный англичанин, живёт в палатке в лесу. Питается из консервных банок и курит окурки, найденные на дороге. Хотя он работал какое-то время программистом на С, но предпочитает называть себя "профессиональным любителем". [Класс! Кто сказал, что хиппи вымерли?! :-) Прим.ред.]


Copyright (c) 2003, Stephen Bint. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 86 of Linux Gazette, January 2003


Вернуться на главную страницу