Программирование на Curses с помощью Python

Автор:

А.М. Кючлинг, Эрик С. Реймонд

Выпуск:

2.04

Что такое проклятия?

Библиотека curses предоставляет независимые от терминала средства рисования экрана и работы с клавиатурой для текстовых терминалов; к таким терминалам относятся VT100, консоль Linux и симуляция терминала, предоставляемая различными программами. Дисплейные терминалы поддерживают различные управляющие коды для выполнения таких распространенных операций, как перемещение курсора, прокрутка экрана и стирание областей. Различные терминалы используют совершенно разные коды и часто имеют свои собственные незначительные особенности.

В мире графических дисплеев можно задаться вопросом: «Зачем это нужно»? Действительно, терминалы с символьными дисплеями - устаревшая технология, но есть ниши, в которых возможность делать с их помощью причудливые вещи все еще ценна. Одна из ниш - это малогабаритные или встроенные Unix, на которых не работает X-сервер. Другая - такие инструменты, как программы установки ОС и конфигураторы ядра, которые должны быть запущены до появления графической поддержки.

Библиотека curses обеспечивает достаточно базовую функциональность, предоставляя программисту абстракцию дисплея, содержащего несколько неперекрывающихся окон с текстом. Содержимое окна можно изменять различными способами - добавлять текст, стирать его, изменять внешний вид - и библиотека curses сама определит, какие управляющие коды нужно послать на терминал, чтобы получить нужный вывод. curses не предоставляет многих концепций пользовательского интерфейса, таких как кнопки, флажки или диалоги; если вам нужны такие возможности, рассмотрите библиотеку пользовательского интерфейса, например Urwid.

Библиотека curses изначально была написана для BSD Unix; в более поздних версиях System V Unix от AT&T было добавлено множество улучшений и новых функций. BSD curses больше не поддерживается, ее заменил ncurses, который является реализацией интерфейса AT&T с открытым исходным кодом. Если вы используете Unix с открытым исходным кодом, например Linux или FreeBSD, в вашей системе почти наверняка используется ncurses. Поскольку большинство современных коммерческих версий Unix основаны на коде System V, все описанные здесь функции, скорее всего, будут доступны. Однако старые версии curses, используемые в некоторых проприетарных Unix, могут поддерживать не все.

Версия Python для Windows не включает модуль curses. Доступна портированная версия под названием UniCurses.

Модуль Python curses

Модуль Python - это довольно простая обертка над функциями C, предоставляемыми curses; если вы уже знакомы с программированием в curses на C, вам будет очень легко перенести эти знания в Python. Самое большое отличие заключается в том, что интерфейс Python упрощает работу, объединяя различные функции C, такие как addstr(), mvaddstr() и mvwaddstr(), в один метод addstr(). Более подробно об этом будет рассказано позже.

Это HOWTO - введение в написание программ в текстовом режиме с помощью curses и Python. Он не пытается быть полным руководством по API curses; для этого смотрите раздел ncurses в руководстве по библиотеке Python и страницы руководства по ncurses на языке C. Тем не менее, это даст вам основные идеи.

Запуск и завершение работы приложения curses

Прежде чем что-либо делать, curses необходимо инициализировать. Для этого вызывается функция initscr(), которая определяет тип терминала, посылает на него необходимые коды настройки и создает различные внутренние структуры данных. В случае успеха initscr() возвращает объект окна, представляющий весь экран; обычно эта функция называется stdscr после имени соответствующей переменной C.

import curses
stdscr = curses.initscr()

Обычно в приложениях curses отключают автоматическое эхо клавиш на экран, чтобы иметь возможность читать клавиши и отображать их только при определенных обстоятельствах. Для этого необходимо вызвать функцию noecho().

curses.noecho()

Приложениям также часто требуется реагировать на нажатие клавиш мгновенно, не требуя нажатия клавиши Enter; это называется режимом cbreak, в отличие от обычного режима буферизованного ввода.

curses.cbreak()

Терминалы обычно возвращают специальные клавиши, такие как клавиши управления курсором или навигационные клавиши, например Page Up и Home, в виде многобайтовой управляющей последовательности. Хотя вы можете написать свое приложение, чтобы оно ожидало такие последовательности и обрабатывало их соответствующим образом, curses может сделать это за вас, возвращая специальное значение, например curses.KEY_LEFT. Чтобы заставить curses выполнять эту работу, вам придется включить режим клавиатуры.

stdscr.keypad(True)

Завершить работу приложения curses гораздо проще, чем запустить его. Вам нужно будет вызвать:

curses.nocbreak()
stdscr.keypad(False)
curses.echo()

чтобы отменить настройки терминала, дружественного к курсу. Затем вызовите функцию endwin(), чтобы вернуть терминал в исходный режим работы.

curses.endwin()

Частая проблема при отладке приложения на curses - запутать терминал, когда приложение умирает, не восстановив предыдущее состояние терминала. В Python это обычно происходит, когда ваш код содержит ошибки и вызывает не пойманное исключение. Например, клавиши больше не отображаются на экране при их вводе, что затрудняет использование оболочки.

В Python вы можете избежать этих сложностей и значительно упростить отладку, импортировав функцию curses.wrapper() и используя ее следующим образом:

from curses import wrapper

def main(stdscr):
    # Clear screen
    stdscr.clear()

    # This raises ZeroDivisionError when i == 10.
    for i in range(0, 11):
        v = i-10
        stdscr.addstr(i, 0, '10 divided by {} is {}'.format(v, 10/v))

    stdscr.refresh()
    stdscr.getkey()

wrapper(main)

Функция wrapper() принимает объект callable и выполняет инициализацию, описанную выше, а также инициализирует цвета, если присутствует поддержка цветов. Затем wrapper() запускает предоставленный вами вызываемый объект. После возврата вызываемого объекта wrapper() восстановит исходное состояние терминала. Вызываемая переменная находится внутри tryexcept, который перехватывает исключения, восстанавливает состояние терминала, а затем снова вызывает исключение. Таким образом, ваш терминал не будет оставлен в смешном состоянии при исключении, и вы сможете прочитать сообщение об исключении и трассировку.

Окна и накладки

Окна - это базовая абстракция в curses. Объект окна представляет собой прямоугольную область экрана и поддерживает методы для отображения текста, его стирания, ввода строк и так далее.

Объект stdscr, возвращаемый функцией initscr(), - это объект окна, занимающий весь экран. Многим программам может понадобиться только это единственное окно, но вы можете захотеть разделить экран на более мелкие окна, чтобы перерисовывать или очищать их по отдельности. Функция newwin() создает новое окно заданного размера, возвращая объект new window.

begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

Обратите внимание, что система координат, используемая в curses, необычна. Координаты всегда передаются в порядке y,x, а левый верхний угол окна имеет координату (0,0). Это нарушает обычное правило работы с координатами, согласно которому на первом месте стоит координата x. Это досадное отличие от большинства других компьютерных приложений, но оно было частью curses с самого начала его написания, и сейчас уже слишком поздно что-то менять.

Ваше приложение может определить размер экрана, используя переменные curses.LINES и curses.COLS для получения размеров y и x. Тогда законные координаты будут простираться от (0,0) до (curses.LINES - 1, curses.COLS - 1).

Когда вы вызываете метод для отображения или стирания текста, эффект не сразу отображается на экране. Вместо этого вы должны вызвать метод refresh() оконных объектов, чтобы обновить экран.

Это связано с тем, что изначально curses был написан с учетом медленных 300-бодовых терминальных соединений; на таких терминалах минимизация времени, необходимого для перерисовки экрана, была очень важна. Вместо этого curses накапливает изменения на экране и отображает их наиболее эффективным образом, когда вы вызываете refresh(). Например, если ваша программа отображает некоторый текст в окне, а затем очищает окно, нет необходимости отправлять исходный текст, потому что он никогда не будет виден.

На практике явное указание curses перерисовать окно не сильно усложняет программирование с помощью curses. Большинство программ начинают бурную деятельность, а затем приостанавливаются в ожидании нажатия клавиши или какого-либо другого действия со стороны пользователя. Все, что вам нужно сделать, - это убедиться, что экран был перерисован, прежде чем сделать паузу в ожидании пользовательского ввода, предварительно вызвав метод stdscr.refresh() или refresh() какого-нибудь другого соответствующего окна.

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

pad = curses.newpad(100, 100)
# These loops fill the pad with letters; addch() is
# explained in the next section
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# Displays a section of the pad in the middle of the screen.
# (0,0) : coordinate of upper-left corner of pad area to display.
# (5,5) : coordinate of upper-left corner of window area to be filled
#         with pad content.
# (20, 75) : coordinate of lower-right corner of window area to be
#          : filled with pad content.
pad.refresh( 0,0, 5,5, 20,75)

Вызов refresh() отображает участок пэда в прямоугольнике, простирающемся от координаты (5,5) до координаты (20,75) на экране; левый верхний угол отображаемого участка - это координата (0,0) на пэде. За исключением этого различия, пэды в точности похожи на обычные окна и поддерживают те же методы.

Если у вас на экране несколько окон и планшетов, есть более эффективный способ обновления экрана и предотвращения раздражающего мерцания экрана при обновлении каждой его части. refresh() фактически делает две вещи:

  1. Вызывает метод noutrefresh() каждого окна для обновления базовой структуры данных, представляющей желаемое состояние экрана.

  2. Вызывает функцию doupdate() для изменения физического экрана в соответствии с желаемым состоянием, записанным в структуре данных.

Вместо этого вы можете вызвать noutrefresh() в нескольких окнах для обновления структуры данных, а затем вызвать doupdate() для обновления экрана.

Отображение текста

С точки зрения программиста на C, curses иногда может выглядеть как запутанный лабиринт функций, все из которых неуловимо отличаются друг от друга. Например, addstr() выводит строку в текущем положении курсора в окне stdscr, а mvaddstr() сначала перемещается к заданной координате y,x, а затем выводит строку. waddstr() аналогичен addstr(), но позволяет указать окно, которое будет использоваться, вместо того чтобы использовать stdscr по умолчанию. mvwaddstr() позволяет указать и окно, и координату.

К счастью, интерфейс Python скрывает все эти детали. stdscr - это объект окна, как и любой другой, а методы, такие как addstr(), принимают несколько форм аргументов. Обычно существует четыре различных формы.

Форма

Описание

str или ch

Вывести строку str или символ ch в текущей позиции

str или ch, attr

Вывести строку str или символ ch, используя атрибут attr в текущей позиции

y, x, str или ch

Перемещение в позицию y,x в окне и отображение str или ch.

y, x, str или ch, attr

Перемещение в позицию y,x внутри окна и отображение str или ch, используя атрибут attr.

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

Метод addstr() принимает строку Python или байтстринг в качестве отображаемого значения. Содержимое байт-строки отправляется на терминал как есть. Строки кодируются в байты с помощью значения атрибута encoding окна; по умолчанию используется системная кодировка, возвращаемая locale.getencoding().

Методы addch() принимают символ, который может быть либо строкой длины 1, либо байтовой строкой длины 1, либо целым числом.

Для символов расширения предусмотрены константы, которые представляют собой целые числа больше 255. Например, ACS_PLMINUS - это символ +/-, а ACS_ULCORNER - левый верхний угол рамки (удобно для рисования границ). Вы также можете использовать соответствующий символ Unicode.

Windows запоминает, где был оставлен курсор после последней операции, поэтому если вы не укажете координаты y,x, строка или символ будут отображаться там, где была оставлена последняя операция. Вы также можете перемещать курсор с помощью метода move(y,x). Поскольку некоторые терминалы всегда отображают мигающий курсор, вы можете позаботиться о том, чтобы курсор был расположен в таком месте, где он не будет отвлекать внимание; мигающий курсор в каком-то случайном месте может сбить с толку.

Если вашему приложению вообще не нужен мигающий курсор, вы можете вызвать функцию curs_set(False), чтобы сделать его невидимым. Для совместимости со старыми версиями curses существует функция leaveok(bool), которая является синонимом curs_set(). Если значение bool равно true, библиотека curses попытается подавить мигающий курсор, и вам не нужно будет беспокоиться о том, чтобы оставлять его в странных местах.

Атрибуты и цвет

Символы могут отображаться по-разному. Строки состояния в текстовых приложениях обычно отображаются в виде обратного видео, или в программе просмотра текста может потребоваться выделить определенные слова. curses поддерживает это, позволяя вам указать атрибут для каждой ячейки на экране.

Атрибут - это целое число, каждый бит которого представляет собой отдельный атрибут. Вы можете попытаться отобразить текст с несколькими установленными битами атрибутов, но curses не гарантирует, что все возможные комбинации будут доступны, или что все они будут визуально различимы. Это зависит от возможностей используемого терминала, поэтому безопаснее всего придерживаться наиболее часто используемых атрибутов, перечисленных здесь.

Атрибут

Описание

A_BLINK

Мигающий текст

A_BOLD

Очень яркий или жирный текст

A_DIM

Полуяркий текст

A_REVERSE

Текст обратного видео

A_STANDOUT

Лучший режим подсветки из всех существующих

A_UNDERLINE

Подчеркнутый текст

Так, чтобы отобразить строку состояния обратного видео в верхней строке экрана, вы можете написать код:

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

Библиотека curses также поддерживает цвет на тех терминалах, которые его предоставляют. Самым распространенным таким терминалом, вероятно, является консоль Linux, а затем цветные терминалы xterms.

Чтобы использовать цвет, вы должны вызвать функцию start_color() вскоре после вызова initscr(), чтобы инициализировать набор цветов по умолчанию (функция curses.wrapper() делает это автоматически). После этого функция has_colors() возвращает значение TRUE, если используемый терминал действительно может отображать цвет. (Примечание: в curses используется американское написание „color“, а не канадское/британское „color“. Если вы привыкли к британскому написанию, то ради этих функций вам придется смириться с неправильным написанием).

В библиотеке curses хранится конечное число пар цветов, содержащих цвет переднего плана (или текста) и цвет фона. Вы можете получить значение атрибута, соответствующее паре цветов, с помощью функции color_pair(); это значение может быть побитово-исполнено с другими атрибутами, такими как A_REVERSE, но, опять же, не гарантируется, что такие комбинации будут работать на всех терминалах.

Пример, в котором строка текста отображается с использованием цветовой пары 1:

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

Как я уже говорил, цветовая пара состоит из цвета переднего плана и фона. Функция init_pair(n, f, b) изменяет определение цветовой пары n на цвет переднего плана f и цвет фона b. Цветовая пара 0 жестко привязана к белому на черном и не может быть изменена.

Цвета пронумерованы, и start_color() инициализирует 8 основных цветов при активации цветового режима. К ним относятся: 0:черный, 1:красный, 2:зеленый, 3:желтый, 4:голубой, 5:пурпурный, 6:голубой и 7:белый. Модуль curses определяет именованные константы для каждого из этих цветов: curses.COLOR_BLACK, curses.COLOR_RED и так далее.

Давайте соберем все это вместе. Чтобы изменить цвет 1 на красный текст на белом фоне, нужно вызвать:

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

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

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

Очень причудливые терминалы могут изменять определения фактических цветов в соответствии с заданным значением RGB. Это позволяет изменить цвет 1, который обычно является красным, на пурпурный, синий или любой другой, который вам нравится. К сожалению, консоль Linux этого не поддерживает, поэтому я не могу попробовать и не могу привести никаких примеров. Вы можете проверить, может ли ваш терминал делать это, вызвав команду can_change_color(), которая возвращает True, если такая возможность есть. Если вам повезло, и у вас такой талантливый терминал, обратитесь к man-страницам вашей системы за дополнительной информацией.

Ввод данных пользователем

Библиотека curses на языке C предлагает только очень простые механизмы ввода. Модуль curses в Python добавляет базовый виджет ввода текста. (В других библиотеках, таких как Urwid, есть более обширные коллекции виджетов).

Существует два метода получения ввода из окна:

  • getch() обновляет экран, а затем ждет, пока пользователь нажмет клавишу, отображая ее, если ранее была вызвана echo(). Дополнительно можно указать координату, на которую следует переместить курсор перед паузой.

  • getkey() делает то же самое, но преобразует целое число в строку. Отдельные символы возвращаются в виде 1-символьных строк, а специальные клавиши, такие как функциональные клавиши, возвращают более длинные строки, содержащие имя клавиши, например KEY_UP или ^G.

Можно не ждать пользователя, используя метод nodelay() окна. После nodelay(True), getch() и getkey() для окна становятся неблокирующими. Чтобы сигнализировать о том, что вход не готов, getch() возвращает curses.ERR (значение -1), а getkey() вызывает исключение. Есть также функция halfdelay(), с помощью которой можно (фактически) установить таймер на каждый getch(); если в течение заданной задержки (измеряемой десятыми долями секунды) вход не становится доступным, curses поднимает исключение.

Метод getch() возвращает целое число; если оно находится в диапазоне от 0 до 255, то представляет собой ASCII-код нажатой клавиши. Значения, превышающие 255, представляют собой специальные клавиши, такие как Page Up, Home или клавиши управления курсором. Вы можете сравнить возвращаемое значение с константами, такими как curses.KEY_PPAGE, curses.KEY_HOME или curses.KEY_LEFT. Главный цикл вашей программы может выглядеть примерно так:

while True:
    c = stdscr.getch()
    if c == ord('p'):
        PrintDocument()
    elif c == ord('q'):
        break  # Exit the while loop
    elif c == curses.KEY_HOME:
        x = y = 0

Модуль curses.ascii предоставляет функции принадлежности к классу ASCII, принимающие аргументы в виде целого числа или строки из 1 символа; они могут быть полезны при написании более читабельных тестов для таких циклов. Он также предоставляет функции преобразования, которые принимают целочисленные или 1-символьные строковые аргументы и возвращают один и тот же тип. Например, curses.ascii.ctrl() возвращает управляющий символ, соответствующий его аргументу.

Существует также метод извлечения всей строки, getstr(). Он используется нечасто, поскольку его функциональность весьма ограничена; единственными доступными клавишами редактирования являются клавиша backspace и клавиша Enter, которая завершает строку. При желании можно ограничиться фиксированным количеством символов:

curses.echo()            # Enable echoing of characters

# Get a 15-character string, with the cursor on the top line
s = stdscr.getstr(0,0, 15)

Модуль curses.textpad предоставляет текстовое поле, поддерживающее набор привязок клавиш, подобный Emacs. Различные методы класса Textbox поддерживают редактирование с проверкой ввода и сбор результатов редактирования как с пробелами, так и без них. Вот пример:

import curses
from curses.textpad import Textbox, rectangle

def main(stdscr):
    stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")

    editwin = curses.newwin(5,30, 2,1)
    rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
    stdscr.refresh()

    box = Textbox(editwin)

    # Let the user edit until Ctrl-G is struck.
    box.edit()

    # Get resulting contents
    message = box.gather()

Более подробную информацию см. в документации по библиотеке curses.textpad.

Дополнительная информация

В этом HOWTO не рассматриваются некоторые продвинутые темы, такие как чтение содержимого экрана или захват событий мыши из экземпляра xterm, но страница библиотеки Python для модуля curses теперь достаточно полная. Вам следует просмотреть ее дальше.

Если вы сомневаетесь в подробном поведении функций curses, обратитесь к страницам руководства для вашей реализации curses, будь то ncurses или проприетарная версия Unix. На страницах руководства будут описаны все причуды и приведены полные списки всех доступных вам функций, атрибутов и символов ACS_*.

Поскольку API curses очень велик, некоторые функции не поддерживаются в интерфейсе Python. Часто это происходит не потому, что их сложно реализовать, а потому, что они еще никому не понадобились. Кроме того, Python пока не поддерживает библиотеку меню, связанную с ncurses. Патчи, добавляющие поддержку этих функций, будут приветствоваться; смотрите the Python Developer’s Guide, чтобы узнать больше об отправке патчей для Python.