PHP-ncurses FAQ

v. 0.1 (alexey rybak, raa no-spam-plz phpclub d0t net)

Что можно и чего нельзя при помощи ncurses-модуля php?

Ncurses - это библиотека языка Си, позволяющая разрабатывать приложения c псевдографическим пользовательским интерфейсом - c текстовыми меню, окнами, диалогами и прочими забавными штуками (примерами подобных приложений служат такие известные *NIX-программы как mc, vim и многие другие). Ncurses-модуль для PHP представляет собой интерфейс к функциям этой библиотеки, с помощью которого вы можете реализовать такие же приложения на языке PHP. Несмотря на то, что возможности ncurses чрезвычайно широки, эта библиотека в-основном служит самым базовым ("низкоуровневым") инструментом для создания программ с подобным интерфейсом. В свою очередь в PHP-модуль вошла лишь часть API ncurses, без готовых функций для работы с формами и меню, несколько усложнив таким образом жизнь программисту. Сложно сказать, плохо это или хорошо, так как существует мнение, что и "родная" реализация этих функций в ncurses оставляет желать лучшего, но для разработчика это означает лишь одно: в наших руках имеются только функции для отрисовки окон, вывода текста в любую область окна (экрана), чтения пользовательского ввода и настройки различных режимов терминала. Никакого API, позволяющего, например, одной строчкой кода создать окно-диалог с вопросом и несколькими кнопками для ответа, переключиться в режим диалога с пользователем и вернуть код нажатой кнопки у нас пока нет. Тем не менее, подобные вещи прекрасно реализуются при помощи этого модуля, пусть и с чуть бОльшим усилием.

Как инициализировать псевдографический режим и как из него правильно выйти?

Для этого служат функции ncurses_init() и ncurses_end(). При завершении работы вы обязаны вызвать ncurses_end(), в противном случае вы рискуете сбить настройки терминала для сеанса, в котором выполнялась ваша программа. Если по какой-то причине это всё же произошло, вам может помочь команда инициализации терминала (например, reset в unix-подобных операционных системах). Следует также помнить о том, что вообще говоря использование nsurses некорректно, если вывод программы не "направлен" на интерактивный терминал, поэтому хорошим стилем будет специальная проверка дескриптора вывода при помощи вызова posix_isatty(STDOUT) перед инициализацией ncurses.

Как определить размер окна терминала?

Необходимо создать окно с нулевыми параметрами ширины и высоты (в этом случае окно будет иметь полноэкранный размер) и просто получить этот размер: $window = ncurses_newwin(0, 0, 0, 0); ncurses_getmaxyx($window, $rows, $cols);

Как обработать ввод с клавиатуры?

Для обработки пользовательского ввода служат функции getch и wgetch. В режиме echo вводимые символы будут отображаться на экране, в режиме noecho - нет. Если вам нужно автоматическое отображение вводимых символов, то корректнее использовать wgetch: эта функция принимает первым аргументом ресурс окна и заботится о том, чтобы отображаемые символы не выходили за его пределы. Однако, режим echo неудобен - в первую очередь из-за того, что практически отсутсвует поддержка перемещения курсора при нажатии специализированных клавиш. Есть и менее важные но также неприятные недостатки: например, если у окна есть рамка, то wgetch не обращает на неё никакого внимания и бессовестно затирает её при вводе достаточно длинного текста. Поэтому скорее всего в случаях, когда необходимо отображение вводимой информации, вам придется обрабатывать getch/wgetch в режиме noecho и самостоятельно заботится и о корректном отображении символов в нужной области окна, и о перемещении курсора.

Почему среди констант нет некоторых распространённых кодов клавиш?

Действительно, среди констант нет кодов таких важных клавиш, как, например, Esc или Enter. Это связано с тем, что единообразного набора кодов для абсолютно всех терминалов не существует. Но для большинства случаев код нажатия Esc равен 27, а Enter - 13. На некоторых терминалах нажатие Enter соответствует последовательности \r\n (10,13). Поскольку код 10 обычно в других ситуациях при наборе на клавиатуре не встречается, то часто он также считается признаком нажатого Enter.

Можно ли сделать чтение ввода с клавиатуры "неблокирующим"?

Нам необходим неблокирующий ввод, когда нужно лишь проверить, была ли нажата пользователем какая-нибудь клавиша. Для этого существуют функции ncurses_timeout и ncurses_halfdelay. Первая, ncurses_timeout - наиболее продвинутая, может перевести чтение в три режима в зависимости от значения аргумента: В halfdelayed режиме getch будет ожидать появления данных не бесконечно долго, а лишь некоторое время. Если по истечении этого времени данные так и не поступили, вызов getch вернет -1. Функция ncurses_halfdelay похожа на функцию ncurses_timeout за тем исключением, что в качестве аргумента принимает время блокировки в десятых долях секунды, а при нулевом аргументе возвращает чтение в полностью блокирующий режим. Возможности ncurses_timeout шире, чем у ncurses_halfdelay, но есть одно досадное недоразумение. Ncurses_timeout влияет только на getch, в то время как ncurses_halfdelay - и на getch, и на wgetch. В оригинальной библиотеке ncurses есть функция wtimeout - аналог timeout для окна, но на момент написания данного документа в php-модуле аналогичной функции не было.

Как обрабатывать escape-последовательности?

Самостоятельно лучше их вообще не обрабатывать. Для этого у ncurses есть очень удобный режим - keypad. После вызова инициализирующей функции ncurses_keypad($window, TRUE) функция wgetch будет корректно обрабатывать нажатие различных функциональных клавиш, возвращая для них свой уникальный псевдо-код (см. определения констант NCURSES_KEY_* в документации к модулю). Даже если вам не нужно обрабатывать нажатие этих клавиш, но ваша программа, например, завершает работу при нажатии Esc - вам следует использовать keypad-инициализацию. Если вы случайно забыли это сделать, то выход из программы будет происходить далеко не только при нажатии клавиши Esc ;) Для того, чтобы подстраховаться на случай ошибок, при обработке нажатия Esc можно насильно очищать буфер ввода при помощи ncurses_flushinp, чтобы не оставлять буфер "грязным".

С обработкой Esc связана ещё одна особенность ncurses. Поскольку Esc может быть началом последовательности для функциональной клавиши, ncurses при нажатии Esc запустит односекундный таймер. В случае, если остаток получен в течение этого времени, возвратится код функциональной клавиши. Но если была нажата только Esc, ваша программа будет ждать ровно секунду, прежде чем возвратит код этой клавиши. Поэтому часто выход из многих ncurses-приложений рекомендуется производить не просто по Esc, а по двойному Esc. В этом случае ncurses прерывает чтение, возвращая код Esc, и выход из программы происходит без задержки.

Что происходит, если я меняю размер окна терминала во время выполнения приложения?

Опытным путем установлено, что getch при изменении размера окна терминала возвращает значения 410 или -1 (следует помнить, что в halfdelay режиме -1 также означает, что буфер пуст). Для проверки, действительно ли изменились размеры окна, следует использовать функцию ncurses_getmaxyx.

Как спрятать курсор?

Скажите ncurses_curs_set(0). Или ncurses_curs_set(1) чтобы он, наоборот, появился.

Как работать с перекрывающимися окнами?

К сожалению, если ваше приложение создает много окон, перекрывающих друг друга, вы сами должны позаботиться о том, чтобы происходило восстановление первоначального вида "нижних" окон при переключении между окнами или удалении "верхних". Для решения этой проблемы можно использовать несколько подходов. Первый и наиболее корректный метод состоит в том, чтобы воспользоваться panel-функциями ncurses (соответствующий пример приведен ниже в этом FAQ). Другой способ заключается в том, чтобы сделать дамп терминала в текстовый файл и затем восстановить его при помощи ncurses_scr_* функций. Наконец, вы можете хранить где-то в памяти все данные, однозначно определяющие содержимое всех окон, и при необходимости просто явно вызывать процедуры перерисовки нужных окон.

Как удобнее отлаживать ncurses-приложения и обрабатывать ошибки?

Как только вы инициализировали ncurses, любой вывод на экран должен быть корректно отображен. С отладкой всё довольно просто: достаточно привести отладочные вызовы к такому виду, чтобы вывод был читабелен. Для этого либо перенаправляют поток ошибок (если ваша любимая функция отладки - error_log), либо пишут собственную alert-функцию, открывающую новое ncurses-окно и показывающую всю отладочную информацию. От вызовов другой популярной отладочной функции var_dump придется отказаться, заменив var_dump($foo), например, на error_log(var_export($foo,TRUE))) или my_ncurses_alert(var_export($foo,TRUE))). Вариант с окном-предупреждением весьма красив, и можно было бы аналогично поступить и для автоматического оповещения пользователя об ошибках, если бы несколько проблем: Поэтому, если мы хотим наиболее корректно отладить приложение, нам необходимо направлять всю отладочную информацию на стандартный поток ошибок при помощи error_log, а его перенаправлять при отладке в отдельный файл. Но при эксплуатации готового скрипта вряд ли пользователь будет перенаправлять поток ошибок и анализировать его - ему-то как раз было бы полезнее максимум ошибок показывать при помощи ncurses-окон. Поэтому одним из возможных компромиссных решений может быть следующее. При написании и отладке программы вывод ошибок идет в перенаправленный поток ошибок, а когда программа уже почти готова, все нужные вызовы error_log можно заменить на trigger_error и установить собственный "оконный" обработчик ошибок (Это лишь один из вариантов, наверняка существуют и более изощренные методы решения описанных проблем). Основной минус все тот же: в случае фатальной ошибки, приводящей к немедленному завершению работы скрипта, при очистке терминала вы "потеряете" сообщение об ошибке вместе со всем, что было отображено на экране в режиме ncurses. Но вероятность фатальной ошибки (это синтаксические ошибки, вызов неизвестных функций и т.д.) на этапе эксплуатации готового скрипта невелика.

примеры:

1)
shterm1> ./your_script 2>errlog.txt
shterm2> tail -f errlog.txt

2)
function my_error_handler($errno, $errstr, $errfile, $errline) {
   return dialog(
       array(
           'message' => 'ERROR #'.$errno.': '.$errstr.'(FILE:'.$errfile.',LINE:'.$errline.')',
           'buttons' => array('OK')
       )
   );
}
set_error_handler('my_error_handler');

пример реализации функции dialog см. ниже

Где мне найти пример реализации диалогового окна?


#!/usr/local/bin/php -q

<? 

define('XCURSES_KEY_LF',    13);
define('XCURSES_KEY_CR',    10);
define('XCURSES_KEY_ESC',   27);

###############################################################################################
function dialog($params) {
###############################################################################################
    if (empty($params) || !is_array($params) ) {
        trigger_error('params must be non-empty array');
        return NULL;
    }

    $message = isset($params['message']) ? $params['message'] : ''; 

    $buttons = (!empty($params['buttons']) && is_array($params['buttons'])) ? 
        $params['buttons'] : array('OK');  
    $n_buttons = count($buttons);
    for ($i=0;$i<$n_buttons;$i++) {
        $buttons[$i] = ' '.$buttons[$i].' ';
    }
 
    $parent_rows = (isset($params['rows']) && ($params['rows']>0)) ? 
        (int)$params['rows'] : 25;
    $parent_cols = (isset($params['cols']) && ($params['cols']>0)) ? 
        (int)$params['cols'] : 80;

    if (empty($message) || empty($buttons) || $parent_rows<=0 || $parent_cols<=0) {
        trigger_error('wrong params');
        return NULL;
    }

    $message_lines = split("\n",$message);
    $message_width = 0;
    $n_message_lines = count($message_lines);
    for ($i=0; $i<$n_message_lines; $i++) {
        $message_width = max(strlen($message_lines[$i]),$message_width);
    }

    $buttons_delim = '  ';
    $buttons_delim_len = strlen($buttons_delim);
    $buttons_len = strlen(implode($buttons_delim, $buttons));

    $width  = 4 + max($buttons_len + 2*$buttons_delim_len,$message_width);
    $height = 4 + $n_message_lines;
    $dlg_y = ($parent_rows > $height) ? 
	(($parent_rows - $height) >> 1) : 1;
    $dlg_x = ($parent_cols > $width) ? 
        (($parent_cols - $width) >> 1) : 1;

    $window = ncurses_newwin($height, $width, $dlg_y, $dlg_x);
    if (empty($window)) {
        trigger_error('unable to create window');
        return NULL;
    }

    ncurses_wborder($window, 0,0, 0,0, 0,0, 0,0);
    $i_x = 0;
    $i_y = 0;
    for ($i = 0; $i<$n_message_lines; $i++) {
        $i_y = 1 + $i;
        $i_x = 1 + (($width - 2 - strlen($message_lines[$i])) >> 1);
        ncurses_mvwaddstr($window, $i_y, $i_x, rtrim($message_lines[$i]));
    }

    $buttons_data = array();
    $buttons_shift_x = 1 + (($width - 1 - $buttons_len) >> 1);
    $buttons_shift_y = 2 + $n_message_lines;
    $i_title = '';
    $i_x = $buttons_shift_x;
    for ($i = 0; $i < $n_buttons; $i++) {
        $i_title = $buttons[$i];
        $buttons_data[] = array(
            'x' => $i_x, 
            's' => $i_title
        );
        if (0 == $i) ncurses_wattron($window, NCURSES_A_REVERSE);
        ncurses_mvwaddstr($window, $buttons_shift_y, $i_x, $i_title);
        if (0 == $i) ncurses_wattroff($window, NCURSES_A_REVERSE);
        $i_x += strlen($i_title) + $buttons_delim_len;
    }

    ncurses_wrefresh($window);
    ncurses_keypad($window,TRUE);  
    ncurses_curs_set(0);
    ncurses_noecho();

    $result = -1;
    $do_loop = 1;
    $move = 0;
    $current = 0;

    while ($do_loop) {
        $key = ncurses_wgetch($window);
        $move = 0;
        switch ($key) {
            case NCURSES_KEY_LEFT:
                if ($current > 0) $move = -1;
                break;
            case NCURSES_KEY_RIGHT:
                if ($current < $n_buttons-1) $move = 1;
                break;
            case XCURSES_KEY_LF:
            case XCURSES_KEY_CR:
                $result = $current;
                $do_loop = 0;
                break;
            case XCURSES_KEY_ESC:
		$do_loop = 0;
                break;
        }
        
        if (0 == $do_loop) {
            ncurses_flushinp();
        } elseif ($move) {
            ncurses_mvwaddstr($window, $buttons_shift_y, 
                $buttons_data[$current]['x'], $buttons_data[$current]['s']);
            $current += $move;
            ncurses_wattron($window, NCURSES_A_REVERSE);
            ncurses_mvwaddstr($window, $buttons_shift_y, 
                $buttons_data[$current]['x'], $buttons_data[$current]['s']);
            ncurses_wattroff($window, NCURSES_A_REVERSE);
            ncurses_wrefresh($window);
        }
    }

    ncurses_delwin($window);

    return $result;
}

###################################################################################################
function my_error_handler($errno, $errstr, $errfile, $errline) {
###################################################################################################
    global $errors;
    $errors[] = 'ERROR #'.$errno.': '.$errstr.' ('.$errfile.', line '.$errline.')';
    return TRUE;
}

###################################################################################################
# main
###################################################################################################

if (!posix_isatty(STDOUT)) {
    trigger_error('wrong terminal');
    exit;
}

ncurses_init();
error_reporting(E_ALL);
$errors = array();
set_error_handler('my_error_handler');

$main_window = ncurses_newwin(0, 0, 0, 0);
ncurses_getmaxyx($main_window, $rows, $cols);
ncurses_keypad($main_window,TRUE);

$message = < < < EOM
Mr. Treehorn draws a lot of water in this town. You don't draw shit, Lebowski. 
Now we got a nice, quiet little beach community here, and I aim to keep it 
nice and quiet. So let me make something plain. I don't like 
you sucking around, bothering our citizens, Lebowski. 
I don't like your jerk-off name. I don't like your jerk-off face. 
I don't like your jerk-off behavior, and I don't like you, jerk-off. 
Do I make myself clear?
EOM;
$result = dialog(
    array (
        'message' => $message,
        'rows' => -1,
        'cols' => -1, 
	'buttons' => array ('Yes','No','I\'m sorry, I wasn\'t listening')
    )
);

ncurses_end();

if (!empty($errors)) {
    print join("\n",$errors)."\n";
}

?>

Где мне найти пример использования panel-функций?

#!/usr/local/bin/php -q

<? 

define('XCURSES_KEY_LF',    13);
define('XCURSES_KEY_CR',    10);
define('XCURSES_KEY_ESC',   27);

if (!posix_isatty(STDOUT)) {
    trigger_error('wrong terminal');
    exit;
}

ncurses_init();

$quotes = <<<EOT
----------------------------------------------
Usage: 
    ESC - exit
    any other key - browse windows
----------------------------------------------

We will encourage you to develop
the three great virtues of a programmer:
laziness, impatience, and hubris (Larry Wall)

A good programmer is someone who looks
both ways before crossing a one-way
street (Doug Linder)

Managing senior programmers is like
herding cats (Dave Platt)
EOT;

$lines = explode("\n",$quotes);
$n_lines = count($lines);
$window_width = 0;
for ($i=0; $i<$n_lines; $i++) {
    $window_width = max($window_width,strlen($lines[$i]));
}
$window_width += 4;

$x_coords = array(10,14,18);
$y_coords = array(10,12,8);
for ($i=0; $i<3; $i++) {
    $windows[$i] = ncurses_newwin(4+$n_lines, $window_width, $y_coords[$i], $x_coords[$i]);
    ncurses_wborder($windows[$i], 0,0, 0,0, 0,0, 0,0);
    ncurses_wattron($windows[$i], NCURSES_A_REVERSE);
    ncurses_mvwaddstr($windows[$i], 0, 2, ' window #'.$i.' ');
    ncurses_wattroff($windows[$i], NCURSES_A_REVERSE);
    for ($j=0; $j<$n_lines; $j++) {
        ncurses_mvwaddstr($windows[$i], 2+$j, 2, $lines[$j]);
    }
    ncurses_wrefresh($windows[$i]);
    $panels[$i] = ncurses_new_panel($windows[$i]);
}

ncurses_update_panels();

ncurses_curs_set(0);
ncurses_noecho();
$i = 0;
$k = NULL;
while(XCURSES_KEY_ESC != $k) {
    $k = ncurses_getch();
    ncurses_top_panel($panels[$i%3]);
    ncurses_update_panels();
    ncurses_doupdate();
    $i++;
}

ncurses_end();

?>

Где мне найти пример реализации окна для ввода значений парамеров?


#!/usr/local/bin/php -q

<? 

define('XCURSES_KEY_LF',                 13);
define('XCURSES_KEY_CR',                 10);
define('XCURSES_KEY_ESC',                27);
define('XCURSES_KEY_PRINTABLE_MIN',      32);
define('XCURSES_KEY_PRINTABLE_MAX',     127);

###############################################################################################
function dlg_input($params = array()) {
###############################################################################################
    $title = isset($params['title']) ? $params['title'] : NULL;
    $max_length = isset($params['max_len']) ? (int)$params['max_len'] : 10;        
    $dlg_rows = isset($params['dlg_cols']) ? (int)$params['dlg_cols'] : 3;
    $dlg_cols = isset($params['dlg_cols']) ? (int)$params['dlg_cols'] : 40;
    $parent_cols = isset($params['cols']) ? (int)$params['cols'] : NULL;
    $parent_rows = isset($params['rows']) ? (int)$params['rows'] : NULL;

    $dlg_x = (int)(($parent_cols - $dlg_cols)/2);
    if($dlg_x<0) $dlg_x = 0;
    $dlg_y = (int)(($parent_rows - $dlg_rows)/2);
    if($dlg_y<0) $dlg_y = 0;
   
    if ($max_length<=0 || $dlg_rows<=0 || $dlg_cols<=0) {
        trigger_error('wrong params');
        return NULL;
    }
 
    $dlg_window = ncurses_newwin($dlg_rows, $dlg_cols, $dlg_y, $dlg_x);
    
    if (empty($dlg_window)) { 
        return NULL;
    }
    
    ncurses_wborder($dlg_window, 0, 0, 0, 0, 0, 0, 0, 0);
    if ($title) {
        ncurses_wattron($dlg_window, NCURSES_A_REVERSE);
        ncurses_mvwaddstr($dlg_window, 0, 2,' '.$title.' ');
        ncurses_wattroff($dlg_window, NCURSES_A_REVERSE);
    }
    ncurses_curs_set(1);
    ncurses_wmove($dlg_window, 2, 2);
    ncurses_wrefresh($dlg_window);
    
    $do_getch = 1;
    $input_val = '';
    $input_char = '';
    $input_len = 0;
    $cursor_x = 2;
    $cursor_y = 1;
    ncurses_wmove($dlg_window, $cursor_y, $cursor_x);
    ncurses_noecho();
    ncurses_keypad($dlg_window,TRUE);
    
    while($do_getch){
        $key_code = ncurses_wgetch($dlg_window);
        if (($key_code == XCURSES_KEY_CR) || ($key_code == XCURSES_KEY_LF)) {
    	    $do_getch = 0;
        } elseif ($key_code == NCURSES_KEY_BACKSPACE) {
            if($input_len>0) {
	        $input_len--;
	        $input_val = substr($input_val,0,$input_len);
	        $cursor_x--;
	        ncurses_mvwaddstr($dlg_window, $cursor_y, $cursor_x,' ');
	        ncurses_wmove($dlg_window, $cursor_y, $cursor_x);
            }
        } elseif ($key_code < XCURSES_KEY_PRINTABLE_MIN || $key_code > XCURSES_KEY_PRINTABLE_MAX) {
    	    continue;
        } elseif($input_len<$max_length) {
    	    $input_val .= $input_char = chr($key_code);
    	    $input_len++;
    	    $cursor_x++;
    	    ncurses_waddstr($dlg_window, $input_char);
        }
    }
    
    ncurses_delwin($dlg_window);
    
    return $input_val;
}

###################################################################################################
# main
###################################################################################################

if (!posix_isatty(STDOUT)) {
    trigger_error('wrong terminal');
    exit;
}

error_reporting(E_ALL);
ncurses_init();
$main_window = ncurses_newwin(0, 0, 0, 0);
ncurses_getmaxyx($main_window, $rows, $cols);

$input = dlg_input(
    array(
        'title' => 'sample input',
        'rows' => $rows, 
        'cols' => $cols
    )
);

ncurses_end();
echo 'Input was: '.$input."\n";

?>

Где мне найти пример реализации простого меню?

#!/usr/local/bin/php -q

<? 

define('XCURSES_KEY_LF',    13);
define('XCURSES_KEY_CR',    10);
define('XCURSES_KEY_ESC',   27);

###################################################################################################
function menu_select($params) {
###################################################################################################
    if(!is_array($params) || empty($params)) {
        trigger_error('wrong params');
        return NULL;
    }

    $menu = isset($params['items']) ? $params['items'] : NULL;
    $rows = isset($params['rows']) ? (int)$params['rows'] : 0;
    $cols = isset($params['cols']) ? (int)$params['cols'] : 0;
    $selected = isset($params['selected']) ? (int)$params['selected'] : 0;
    $centered = empty($params['centered']) ? 0 : 1;
    $y_menu = isset($params['y']) ? (int)$params['y'] : 0;
    $x_menu = isset($params['x']) ? (int)$params['x'] : 0;
   
    if(!is_array($menu) || empty($menu) || $rows<=0 || $cols<=0 || $y_menu<0 || $x_menu<0) {
        trigger_error('wrong params');
        return NULL;
    }

    $keys = array_keys($menu);
    $values = array();

    $current = 0;
    $width = 0;
    $height = count($menu) + 2;

    foreach ($menu as $value) {
        $width = max($width, strlen($value));
    }

    $i = 0;
    foreach ($menu as $k => $v) {
        $values[$i] = ' '.$v.str_repeat(' ',1 + $width - strlen($v));
        if ($k == $selected) $current = $i;
        $i++;
    }

    $width += 4;

    if ($centered) {
        $y_menu = ($rows - $height) >> 1;
        $x_menu = ($cols - $width) >> 1;
    }

    $window = ncurses_newwin($height, $width, $y_menu, $x_menu);
    if (empty($window)) {
        trigger_error('unable to create window');
        return NULL;
    }

    ncurses_wborder($window, 0,0, 0,0, 0,0, 0,0);

    for ($a = 0; $a < count($values); $a++) {
        if ($a == $current) ncurses_wattron($window, NCURSES_A_REVERSE);
        ncurses_mvwaddstr($window, 1 + $a, 1, $values[$a]);
        if ($a == $current) ncurses_wattroff($window, NCURSES_A_REVERSE);
    }

    ncurses_wrefresh($window);
    ncurses_keypad($window,TRUE);
    ncurses_curs_set(0);

    do {
        $key = ncurses_wgetch($window);
        $move = 0;
        switch ($key) {
            case NCURSES_KEY_UP :
                if ($current > 0) $move = -1;
                break;
            case NCURSES_KEY_DOWN :
                if ($current < count($values) - 1) $move = 1;
                break;
            case XCURSES_KEY_LF:
            case XCURSES_KEY_CR :
                $result = $keys[$current];
                break;
            case XCURSES_KEY_ESC :
                ncurses_flushinp();
                $result = '';
                break;
        }

        if ($move) {
            ncurses_mvwaddstr($window, 1 + $current, 1, $values[$current]);
            $current += $move;
            ncurses_wattron($window, NCURSES_A_REVERSE);
            ncurses_mvwaddstr($window, 1 + $current, 1, $values[$current]);
            ncurses_wattroff($window, NCURSES_A_REVERSE);
            ncurses_wrefresh($window);
        }
    } while (!isset($result));

    ncurses_delwin($window);
    return $result;
}

###################################################################################################
# main
###################################################################################################

if (!posix_isatty(STDOUT)) {
    trigger_error('wrong terminal');
    exit;
}

ncurses_init();
error_reporting(E_ALL);

$main_window = ncurses_newwin(0, 0, 0, 0);
ncurses_getmaxyx($main_window, $rows, $cols);

$result = menu_select(
    array (
        'items' => array (
            'dude'    => 'The Dude',
            'walter'  => 'Walter Sobchak',
            'donny'   => 'Donny',
            'maude'   => 'Maude Lebowski',
            'jesus'   => 'Jesus Quintana',
            'smokey'  => 'Smokey'
         ),
        'rows' => $rows,
        'cols' => $cols,
        'selected' => 3,
        'centered' => 1
    )
);

ncurses_end();

echo 'Result is:'.$result."\n";

?>

Где мне найти пример реализации listbox (checkbox-списка)?

#!/usr/local/bin/php -q

<? 

define('XCURSES_KEY_LF',    13);
define('XCURSES_KEY_CR',    10);
define('XCURSES_KEY_ESC',   27);
define('XCURSES_KEY_SPACE',  32);

###############################################################################################
function menu_check_list($params) {
###############################################################################################
    if(!is_array($params) || empty($params)) {
        trigger_error('wrong_params');
        return NULL;
    }

    $menu = isset($params['items']) ? $params['items'] : NULL;
    $rows = isset($params['rows']) ? (int)$params['rows'] : 0;
    $cols = isset($params['cols']) ? (int)$params['cols'] : 0;
    $centered = empty($params['centered']) ? 0 : 1;
    $y_menu = isset($params['y']) ? (int)$params['y'] : 0;
    $x_menu = isset($params['x']) ? (int)$params['x'] : 0;

    if(!is_array($menu) || empty($menu) || $rows<=0 || $cols<=0 || $y_menu<0 || $x_menu<0) {
        trigger_error('wrong params');
        return NULL;
    }

    $keys = array_keys($menu);
    $n_menu = count($keys);

    $items = array();
    $checked = array();
    $current = 0;
    $width = 0;
    $height = $n_menu + 2;
    $i = 0;
    $k = NULL;
    $i_checked = NULL;

    for($i=0; $i<$n_menu; $i++) {
        $k = $keys[$i];
        $i_checked = (isset($menu[$k][1]) && $menu[$k][1] == 1) ? 1 : 0;
        $items[$i] = ' ['. ($i_checked ? '*' : ' ').'] '.$menu[$k][0];
        $width = max($width, strlen($items[$i]));
        $checked[$i] = $i_checked;
    }

    for ($i=0; $i<$n_menu; $i++) {
        $items[$i] = $items[$i].str_repeat(' ', 2 + $width - strlen($items[$i]));
    }

    $width += 4;

    if ($centered) {
        $r = ($rows - $height) >> 1;
        $c = ($cols - $width) >> 1;
    }

    $window = ncurses_newwin($height, $width, $r, $c);
    if (empty($window)) {
        trigger_error('unable to create window');
        return NULL;
    }

    ncurses_wborder($window, 0,0, 0,0, 0,0, 0,0);
    $n_items = count($items);
    for ($i = 0; $i < $n_items; $i++) {
        if ($i == $current) ncurses_wattron($window, NCURSES_A_REVERSE);
        ncurses_mvwaddstr($window, 1 + $i, 1, $items[$i]);
        if ($i == $current) ncurses_wattroff($window, NCURSES_A_REVERSE);
    }

    ncurses_wrefresh($window);
    ncurses_keypad($window,TRUE);
    ncurses_noecho();
    ncurses_curs_set(0);

    $do_loop = 1;
    $save_result = 0;
    while($do_loop) {
        $key = ncurses_wgetch($window);
        $move = 0;
        switch ($key) {
            case NCURSES_KEY_UP :
                if ($current > 0) $move = -1;
                break;
            case NCURSES_KEY_DOWN :
                if ($current < $n_menu - 1) $move = 1;
                break;
            case XCURSES_KEY_LF:
            case XCURSES_KEY_CR:
                $do_loop = 0;
                $save_result = 1;
                break;
            case XCURSES_KEY_SPACE:
                if ($checked[$current]) {
                    $checked[$current] = 0;
                    $items[$current] = ' [ ] '.substr($items[$current],5);
                } else {
                    $checked[$current] = 1;
                    $items[$current] = ' [*] '.substr($items[$current],5);
                }
                ncurses_wattron($window, NCURSES_A_REVERSE);
                ncurses_mvwaddstr($window, 1 + $current, 1, $items[$current]);
                ncurses_wattroff($window, NCURSES_A_REVERSE);
                ncurses_wrefresh($window);
                break;
            case XCURSES_KEY_ESC :
                ncurses_flushinp();
                $do_loop = 0;
                break;
        }

        if ($move) {
            ncurses_mvwaddstr($window, 1 + $current, 1, $items[$current]);
            $current += $move;
            ncurses_wattron($window, NCURSES_A_REVERSE);
            ncurses_mvwaddstr($window, 1 + $current, 1, $items[$current]);
            ncurses_wattroff($window, NCURSES_A_REVERSE);
            ncurses_wrefresh($window);
        }
    }

    ncurses_delwin($window);
    $result = NULL;
    if ($save_result) {
        for ($i=0; $i<$n_menu; $i++) {
            $result[$keys[$i]] = $checked[$i];
        }
    }

    return $result;
}


###################################################################################################
# main
###################################################################################################

if (!posix_isatty(STDOUT)) {
    trigger_error('wrong terminal');
    exit;
}

ncurses_init();
error_reporting(E_ALL);

$main_window = ncurses_newwin(0, 0, 0, 0);
ncurses_getmaxyx($main_window, $rows, $cols);

$result = menu_check_list(
    array (
        'items' => array (
            'dude'    => array ('The Dude',        1),
            'walter'  => array ('Walter Sobchak',  0),
            'donny'   => array ('Donny',           1),
            'maude'   => array ('Maude Lebowski',  1),
            'jesus'   => array ('Jesus Quintana',  0),
            'smokey'  => array ('Smokey',          0)
         ),
        'rows' => $rows,
        'cols' => $cols,
        'selected' => 3,
        'centered' => 1
    )
);

ncurses_end();

echo 'Result is:'.var_export($result,TRUE)."\n";

?>

Где посмотреть другие примеры на PHP и прочитать подробнее про ncurses?

На момент написания данного документа существует очень немного статей с примерами на php:

Очень хороший обзор возможностей ncurses содержится в ncurses-HOWTO и Writing Programs with NCURSES.

Исходный код ncurses-расширения php может также послужить неплохим источником информации, а разработчикам, знакомым с perl, может быть полезным изучить исходники некоторых Curses модулей из CPAN, например, Curses::UI, Curses::Application, Curses::Forms, Curses::Widgets.

wbr, fisher

see also:blitz templates, fast php-template engine