Curses программирование с Python

Автор:А.М Кучлинг, Эрик С. Реймонд
Релиз:2.04

Аннотация

В документе описывается использование модуля расширения curses для управления отображениями в текстовом режиме.

Что такое curses?

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

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

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

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

Версия Python для Windows не включает модуль curses. Доступна перенесенная версия под названием UniCurses. Вы также можете попробовать модуль Console, написанный Фредриком Лундом, который не использует тот же API, что и curses, но обеспечивает вывод текста с адресацией курсора и полную поддержку ввода с помощью мыши и клавиатуры.

Python модуль curses

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

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

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

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

import curses
stdscr = curses.initscr()

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

curses.noecho()

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

curses.cbreak()

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

stdscr.keypad(True)

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

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

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

curses.endwin()

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

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

from curses import wrapper

def main(stdscr):
    # Очистить экран
    stdscr.clear()

    # Поднимает ZeroDivisionError когда 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() принимает вызываемый объект и выполняет описанную выше инициализацию, а также инициализирует цвета, если присутствует поддержка цвета. Затем wrapper() запускает предоставленный вами вызываемый объект. После возврата вызываемого объекта wrapper() восстановит исходное состояние терминала. Вызываемый объект вызывается внутри tryexcept, который перехватывает исключения, восстанавливает состояние терминала, а затем повторно вызывает исключение. Поэтому ваш терминал не останется в смешном состоянии при исключении, и вы сможете прочитать сообщение об исключении и трассировку.

Окна и планшеты

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

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

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)
# Эти циклы заполняют pad буквами; addch() объясняется
# в следующем разделе
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# Отображает часть pad в центре экрана.
# (0,0) : координаты верхнего левого угла области pad для отображения.
# (5,5) : координата верхнего левого угла области окна, заполняемой
#         содержимым pad.
# (20, 75) : координата нижнего правого угла области окна, которая должна быть
#          : заполнена pad контентом.
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.getpreferredencoding().

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

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

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

Если вашему приложению вообще не нужен мигающий курсор, вы можете вызвать curs_set(False), чтобы сделать его невидимым. Для совместимости со старыми версиями curses есть функция leaveok(bool), которая является синонимом curs_set(). Когда bool истинно, библиотека 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() вернёт ИСТИНУ, если используемый терминал действительно может отображать цвет. (Примечание: в 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, если такая возможность есть. Если вам посчастливилось иметь такой талантливый терминал, обратитесь к справочным страницам вашей системы для получения дополнительной информации.

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

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

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

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

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

Метод getch() возвращает целое число; если он находится между 0 и 255, он представляет ASCII-код нажатой клавиши. Значения больше 255 — это специальные клавиши, такие как PageUp, 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  # Выходим из цикла while
    elif c == curses.KEY_HOME:
        x = y = 0

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

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

curses.echo()            # Включить отображение символов

# Получить строку из 15 символов с курсором в верхней строке
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)

    # Позвольте пользователю редактировать, пока не будет нажата Ctrl-G.
    box.edit()

    # Получить результирующее содержимое
    message = box.gather()

Дополнительные сведения см. в документации библиотеки на curses.textpad.

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

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

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

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