diff --git a/ncurses/example.php b/ncurses/example.php index c1b789e..0b75255 100644 --- a/ncurses/example.php +++ b/ncurses/example.php @@ -17,27 +17,44 @@ require_once( 'ncurses.php' ); * @package Nc * @subpackage Example */ -class MyApp extends NcApp { - function run() { - $nc_main = new NcWindow( $this, 0, 0, 0, 0 ); - $nc_main->title( 'Hello There' ); - $nc_main->write( 0, 0, file_get_contents( __FILE__ ) ); - $nc_main->get_char(); - $nc_small = new NcWindow( $nc_main, 10, 60, 5, 20 ); - $nc_small->title( 'Progress Bars' ); - ncurses_attron( NCURSES_A_REVERSE ); - $bar_1 = new NcProgressBar( $nc_small, 0, 0, 0, 100, true ); - $bar_1->pos( 57 ); - $bar_2 = new NcProgressBar( $nc_small, 0, 1, 0, 100, false ); - $bar_2->pos( 11 ); - $nc_small->write( 3, 0, 'Text Input:' ); - $input = new NcTextInput( $nc_small, -12, 3, 12 ); - $foo = $input->get(); - $nc_small->write_centered( 4, 'Press any key to destroy this window' ); - $nc_small->get_char(); - $nc_small->hide(); - $nc_main->get_char(); + +define( 'DEBUG', 1 ); +class LogView extends NcTableView { + public function __construct($parent, $height, $width, $y, $x, $params = array()) { + parent::__construct($parent, $height, $width, $y, $x, $params); + // If we don't prevent the event logging table from logging its own signals, + // it'll find itself in a recursive spiral into memory usage hell + $this->emit_log_filter[] = 'change'; } } -$app = new MyApp(); + +class MyEventApp extends NcApp { + function init() { + $this->screen->title('Ncurses Example Application'); + $input = array(); + for($i = 0; $i < 3; $i++) { + $input[] = new NcTextInput($this->screen, 0, 2*$i, 0); + } + $log = new LogView($this->screen, $this->screen->height() - 11, 0, 7, 0, array( + 'columns' => array( + 'class' => 'Class', + 'message' => 'Message' + ) + )); + $log->title('Application Log'); + $progress = new NcProgressBar($this->screen, 0, $this->screen->height() - 4, 0); + $button = new NcButton($this->screen, 3, 10, $this->screen->height() - 3, 0, 'Quit'); + Signaler::connect($this, 'log', $log, 'load'); + Signaler::connect($log, 'change', $progress, 'pos'); + Signaler::connect($this, 'log_items', $progress, 'max'); + Signaler::connect($button, 'clicked', $this, 'quit'); + $log->load($this->log); + } + function log(&$object, $message) { + parent::log($object, $message); + $this->emit('log_items', count($this->log) - 1); + } +} +$app = new MyEventApp(); +$app->exec(); ?> diff --git a/ncurses/ncurses.button.php b/ncurses/ncurses.button.php new file mode 100644 index 0000000..96ecb21 --- /dev/null +++ b/ncurses/ncurses.button.php @@ -0,0 +1,63 @@ +label($label); + } + public function title($value = false) { + parent::title(''); + } + public function label($value = false) { + if( $value === false ) { + return $this->label; + } else { + if( $this->active() ) { + $this->attribute_on( NCURSES_A_REVERSE ); + } + $this->write_centered(floor($this->height() / 2), $value); + $this->attribute_off(NCURSES_A_REVERSE); + $this->label = $value; + } + } + public function click() { + $this->emit('clicked'); + } + public function focus() { + parent::focus(); + $this->label($this->label); + } + public function blur() { + parent::blur(); + $this->label($this->label); + } + public function on_key_press($key) { + switch( $key ) { + case 13: + // RETURN + $this->click(); + break; + default: + parent::on_key_press($key); + } + } +} +?> diff --git a/ncurses/ncurses.php b/ncurses/ncurses.php index e48af33..233e0d2 100644 --- a/ncurses/ncurses.php +++ b/ncurses/ncurses.php @@ -12,13 +12,23 @@ * @package Nc */ +require_once('signals.php'); +include_once('ncurses.window.php'); +include_once('ncurses.button.php'); +include_once('ncurses.progressbar.php'); +include_once('ncurses.textinput.php'); +include_once('ncurses.tableview.php'); + /** * PHP ncurses application * * @package Nc */ -abstract class NcApp { - var $screen; +abstract class NcApp extends Signaler { + protected $nc_screen; + protected $screen; + protected $log = array(); + protected $done = false; /** * Constructor @@ -32,9 +42,11 @@ abstract class NcApp { ncurses_init(); $echo ? ncurses_echo() : ncurses_noecho(); ncurses_curs_set( (bool)$cursor ); - $this->screen = ncurses_newwin( 0, 0, 0, 0 ); + $this->nc_screen = ncurses_newwin( 0, 0, 0, 0 ); ncurses_refresh(); - $this->run(); + $this->log($this, 'nCurses Initialized'); + $this->screen = new NcWindow($this, 0, 0, 0, 0); + $this->init(); } /** * Destructor @@ -46,625 +58,29 @@ abstract class NcApp { ncurses_refresh(); ncurses_end(); } + public function log(&$object, $message) { + if( DEBUG ) { + $this->log[] = array( + 'class' => get_class($object), + 'message' => $message + ); + $this->emit('log', $this->log); + } + } + public function exec() { + if( $this->screen instanceof NcWindow ) { + while(!$this->done) { + $key = ncurses_getch(); + $this->screen->on_key_press($key); + } + } + } + public function quit() { + $this->done = true; + } /** * This is a placeholder function for application code */ - abstract function run(); -} - -/** -* Ncurses window -* -* Provides a basic window object. A border can be optionally enabled if the window -* is large enough to accomodate it, and is enabled by default. If a title is set, -* it will be visible at the top of the border if one exists. -* -* If a parent window is provided, the window will be registered as a child of the -* parent, and will be relatively positioned to the parent. -* -* @package Nc -*/ -class NcWindow { - /** - * @var resource nc_window resource for the border window, if one exists - */ - var $frame; - /** - * @var resource nc_window resource for the container window - */ - var $window; - /** - * @var array nc_panel resources for the window and its container - */ - var $panels = array(); - /** - * @var array children array of child NcWindow references - */ - var $children = array(); - /** - * @var NcWindow reference to this object's direct parent, if one exists - */ - var $parent = false; - /** - * @var integer Absolute column position of the window - */ - var $x; - /** - * @var integer Absolute row position of the window - */ - var $y; - /** - * @var integer Width of contained window - */ - var $width; - /** - * @var integer Height of contained window - */ - var $height; - /** - * @var string Window title - */ - var $title; - - /** - * Constructor - * - * Builds the requested ncurses window. - * - * @param NcWindow $parent Parent window - * @param integer $height Height - * @param integer $width Width - * @param integer $y Row - * @param integer $x Column - * @param boolean $framed Include a frame (border). Required for window titles. - */ - public function __construct( $parent, $height, $width, $y, $x, $framed = true ) { - if( $parent instanceof NcWindow ) { - $this->parent = $parent; - $this->parent->add_child( $this ); - $x += $parent->x; - $y += $parent->y; - $width += $width <= 0 ? $parent->width : 0; - $height += $height <= 0 ? $parent->height : 0; - } - if( $framed ) { - $this->frame = ncurses_newwin( $height, $width, $y, $x ); - ncurses_getmaxyx( $this->frame, $max_y, $max_x ); - if( $max_y >= 2 && $max_x >= 2 ) { - $this->window = ncurses_newwin( $max_y - 2, $max_x - 2, $y + 1, $x + 1 ); - ncurses_wborder( $this->frame, 0,0, 0,0, 0,0, 0,0 ); - ncurses_wrefresh( $this->frame ); - $this->panels[] = ncurses_new_panel( $this->frame ); - } else { - $this->window = $this->frame; - $this->frame = false; - } - } else { - $this->frame = false; - $this->window = ncurses_newwin( $height, $width, $y, $x ); - ncurses_getmaxyx( $this->window, $max_y, $max_x ); - } - $this->width = $this->frame ? $max_x - 2 : $max_x; - $this->height = $this->frame ? $max_y - 2 : $max_y; - $this->x = $this->frame ? $x + 1 : $x; - $this->y = $this->frame ? $y + 1 : $y; - ncurses_wrefresh( $this->window ); - $this->panels[] = ncurses_new_panel( $this->window ); - ncurses_update_panels(); - } - /** - * Destructor - */ - public function __destruct() { - foreach( $this->panels as $id => $panel ) { - ncurses_del_panel( $this->panels[$id] ); - } - ncurses_delwin( $this->window ); - if( $this->frame !== false ) { - ncurses_delwin( $this->frame ); - } - } - /** - * Set or retrieve a window title - * - * Sets window title to the value provided, returns the current window title - * if one is not. - * - * @param string $value New window title - * @return string Window title - */ - public function title( $value = false ) { - if( $value === false ) { - return $this->title; - } elseif( $this->frame !== false ) { - $this->title = $value; - ncurses_wborder( $this->frame, 0,0, 0,0, 0,0, 0,0 ); - ncurses_mvwaddstr( $this->frame, 0, 1, $this->title ); - ncurses_update_panels(); - ncurses_doupdate(); - } - } - /** - * Activate an ncurses output attribute - * - * @param integer NCURSES_A_* constant - */ - public function attribute_on( $attribute ) { - ncurses_wattron( $this->window, $attribute ); - } - /** - * Deactivate an ncurses output attribute - * - * @param integer NCURSES_A_* constant - */ - public function attribute_off( $attribute ) { - ncurses_wattroff( $this->window, $attribute ); - } - /** - * Write text to the window - * - * Writes text to the window at the specified coordinates. - * Text is truncated to prevent it overflowing off the side of the window. If - * the text contains newlines, it is split and each new line will continue from - * the same x position as the first, rather than wrapping back to the far left - * side of the window as ncurses normally would. - * - * @param integer $y Row - * @param integer $x Column - * @param string $text Text to write - * @param boolean $update Update the window upon completion - */ - public function write( $y, $x, $text, $update = true ) { - $y = $y < 0 ? $this->height + $y : $y; - $x = $x < 0 ? $this->width + $x : $x; - $lines = preg_split( '/\r?(\n|\r)/', $text ); - foreach( $lines as $id => $line ) { - $line = substr( $line, 0, $this->width - $x ); - ncurses_mvwaddstr( $this->window, $y + $id, $x, $line ); - } - if( $update ) { - ncurses_update_panels(); - ncurses_doupdate(); - } - } - /** - * Write centered text - * - * Writes text centered horizontally within the window on the specified line. - * - * @param integer $y Row - * @param string $text Text to write - * @param boolean $update Update the window upon completion - */ - public function write_centered( $y, $text, $update = true ) { - $this->write( - $y, - floor( $this->width / 2 ) - floor( strlen( $text ) / 2 ), - $text, - $update - ); - } - /** - * Write fixed-width text - * - * Writes a string truncated or padded with spaces to fill the requested width. - * If the width is unspecified or zero, the width is defined as the length from - * the starting coordinate to the right edge of the window. - * - * @param integer $y Row - * @param integer $x Column - * @param string $text Text to write - * @param integer $width Field length - * @param boolean $update Update the window upon completion - */ - public function write_fixed( $y, $x, $text, $width = 0, $update = true ) { - $width = $width > 0 ? $width : $this->width - $x; - $this->write( $y, $x, $this->_string_fixed( $text, $width ), $update ); - } - /** - * Build a fixed-width string - * - * Truncates a string if it exceeds the specified width, pads it with spaces - * if it is too short. - * - * @param string $text Text to format - * @param integer $width Field length - * @return string Fixed-width string - */ - protected function _string_fixed( $text, $width = 0 ) { - $width = $width < 0 ? 0 : $width; - $text = substr( $text, 0, $width ); - $text .= str_repeat( ' ', $width - strlen( $text ) ); - return $text; - } - /** - * Assign a window as a child of this window - * - * Children of a window are hidden and shown along with their parent widget. - * - * @param NcWindow $child Child window - */ - public function add_child( $child ) { - $this->children[] = $child; - } - /** - * Hide the window - * - * @param boolean $update Update the window upon completion - */ - public function hide( $update = true ) { - foreach( $this->panels as $panel ) { - ncurses_hide_panel( $panel ); - } - foreach( $this->children as $child ) { - $child->hide( false ); - } - if( $update ) { - ncurses_update_panels(); - ncurses_doupdate(); - } - } - /** - * Show the window - * - * @param boolean $update Update the window upon completion - */ - public function show( $update = true ) { - foreach( $this->panels as $panel ) { - ncurses_show_panel( $panel ); - } - foreach( $this->children as $child ) { - $child->show( false ); - } - if( $update ) { - ncurses_update_panels(); - ncurses_doupdate(); - } - } - /** - * Clear the window's contents - * - * @param boolean $update Update the window upon completion - */ - public function erase( $update = true ) { - ncurses_werase( $this->window ); - if( $update ) { - ncurses_wrefresh( $this->window ); - ncurses_doupdate(); - } - } - /** - * Read a character from the keyboard and return it - * - * @param boolean $flush Flush the input buffer before requesting a key - * @return integer Keycode - */ - public function get_char( $flush = true ) { - if( $flush ) { - ncurses_flushinp(); - } - return ncurses_getch(); - //return ncurses_wgetch( $this->window ); - } -} - -/** -* Progress Bar -* -* @package Nc -*/ -class NcProgressBar extends NcWindow { - /** - * @var float Maximum value - */ - var $max; - /** - * @var float Current value - */ - var $value; - /** - * @var boolean Show percentage - */ - var $show_pct; - - /** - * Constructor - * - * Creates a new progress bar widget - * - * @param NcWindow $parent Parent window - * @param integer $width Width - * @param integer $y Row - * @param integer $x Column - * @param float $max Maximum value - * @param boolean $show_pct Display the percentage alongside the progress bar - */ - public function __construct( &$parent, $width, $y, $x, $max = 100, $show_pct = true ) { - parent::__construct( $parent, 1, $width, $y, $x, false ); - $this->max = $max; - $this->show_pct = $show_pct; - $this->pos( 0 ); - } - /** - * Set or get the current value - * - * @param float $value New value - * @return float Current value - */ - public function pos( $value = false ) { - if( $value === false ) { - return $this->value; - } else { - $value = $value > $this->max ? $this->max : $value; - $this->value = $value; - $this->write( 0, 0, '[', false ); - $width = $this->width - ( $this->show_pct ? 7 : 2 ); - $width = $width >= 0 ? $width : 0; - $complete = floor( $width * $value / $this->max ); - $this->write( 0, 1, str_repeat( '#', $complete ), false ); - $this->write( 0, 1 + $complete, str_repeat( ' ', $width - $complete ), false ); - $this->write( 0, $width + 1, ']', false ); - if( $this->show_pct ) { - $this->write( 0, $width + 3, sprintf( '%3d%%', floor( $value / $this->max * 100 ) ), false ); - } - ncurses_update_panels(); - ncurses_doupdate(); - } - } - /** - * Get or set maximum value - * - * @param float $value New maximum - * @return float Current maximum - */ - public function max( $value = false ) { - if( $value === false ) { - return $this->max; - } else { - $this->max = $value > 0 ? $value : $this->max; - $this->pos( $this->value ); - } - } -} - -/** -* Text Input -* -* @package Nc -*/ -class NcTextInput extends NcWindow { - /** - * @var string Current value - */ - var $value = ''; - /** - * @var boolean Active - */ - var $active = false; - /** - * @var string Perl regular expression for filtering input - */ - var $filter = false; - - /** - * Constructor - * - * Creates a new text input widget - * - * @param NcWindow $parent Parent window - * @param integer $width Width - * @param integer $y Row - * @param integer $x Column - * @param string $value Initial value - * @param string $filter Perl regular expression for filtering input - */ - public function __construct( &$parent, $width, $y, $x, $value = '', $filter = false ) { - parent::__construct( $parent, 1, $width, $y, $x, false ); - $this->value = $value; - $this->filter = $filter !== false && @preg_match( $filter, '' ) !== false ? $filter : false; - $this->update(); - } - /** - * Update the input control on the screen - * - * Widget is highlighted when active - */ - public function update() { - $this->attribute_on( NCURSES_A_UNDERLINE ); - if( $this->active ) { - $this->attribute_on( NCURSES_A_REVERSE ); - } - $this->write_fixed( 0, 0, substr( $this->value, - $this->width ) ); - $this->attribute_off( NCURSES_A_UNDERLINE ); - $this->attribute_off( NCURSES_A_REVERSE ); - } - /** - * Get or set value - */ - public function value( $value = false ) { - if( $value === false ) { - return $this->value; - } else { - $this->value = $value; - } - } - /** - * Activate input widget and accept input - * - * Input ends when the user presses the enter key. - * If a filter has been set and the input does not pass the filter, the value - * reverts to the previous value, and this function returns false. - * - * @return string|boolean Text entered, or false - */ - public function get() { - $this->active = true; - $this->update(); - while( $this->active ) { - $key = $this->get_char(); - switch( $key ) { - case NCURSES_KEY_BACKSPACE: - $this->value = substr( $this->value, 0, strlen( $this->value ) - 1 ); - break; - case 13: - $this->active = false; - break; - default: - if( in_array( $key, range( 32, 126 ) ) ) { - $this->value .= chr( $key ); - } - } - $this->update(); - } - $this->update(); - return $this->filter !== false && @preg_match( $this->filter, $this->value ) < 1 ? false : $this->value; - } -} - -/** -* Table View -* -* @package Nc -*/ -class NcTableView extends NcWindow { - /** - * @var array Column headings - */ - var $columns; - /** - * @var array Recordset - */ - var $records; - /** - * @var integer Currently selected record index - */ - var $selected; - - /** - * Constructor - * - * Creates a new table view - * - * @param NcWindow $parent Parent window - * @param integer $height Height - * @param integer $width Width - * @param integer $y Row - * @param integer $x Column - * @param array Array of optional parameters, as follows: - * array( - * 'columns' => array( 'key' => 'Column Name', ... ), - * 'records' => array( array( 'key' => 'value', ... ), ... ) - * ) - */ - public function __construct( $parent, $height, $width, $y, $x, $params = array() ) { - parent::__construct( $parent, $height, $width, $y, $x ); - $params = !is_array( $params ) ? array() : $params; - $this->columns = isset( $params['columns'] ) && is_array( $params['columns'] ) ? $params['columns'] : array(); - $this->records = isset( $params['records'] ) && is_array( $params['records'] ) ? array_values( $params['records'] ) : array(); - } - /** - * Redraw the table and select a record - * - * Redraws the table with the specified record selected and the view scrolled - * accordingly. - * - * @param integer $selected Selected row - */ - public function update( $selected = 0 ) { - $selected = $selected >= 0 ? $selected : 0; - $selected = $selected < count( $this->records ) ? $selected : count( $this->records ) - 1; - $this->selected = $selected; - $cols = isset( $this->columns ) && is_array( $this->columns ) ? $this->columns : array(); - if( count( $cols ) == 0 ) { - // If there's no columns defined, try and grab 'em from the keys from the first record - $record = $this->records[0]; - $record = is_array( $record ) ? $record : array( $record ); - foreach( array_keys( $record ) as $col ) { - $cols[$col] = $col; - } - } - $column_ids = array_keys( $cols ); - $column_widths = $rows = array(); - foreach( $cols as $id => $column ) { - $column_widths[$id] = strlen( $column ); - } - foreach( $this->records as $id => $record ) { - $record = is_array( $record ) ? $record : array( $record ); - $fields = array(); - foreach( $column_ids as $col ) { - $field = isset( $record[$col] ) ? $record[$col] : ''; - $field = is_scalar( $field ) ? $field : gettype( $field ); - $fields[$col] = $field; - $column_widths[$col] = $column_widths[$col] < strlen( $field ) ? strlen( $field ) : $column_widths[$col]; - } - $rows[] = $fields; - } - foreach( $cols as $id => $col ) { - $cols[$id] = $col . str_repeat( ' ', $column_widths[$id] - strlen( $col ) ); - } - $this->attribute_on( NCURSES_A_UNDERLINE ); - $this->attribute_on( NCURSES_A_BOLD ); - $this->write_fixed( 0, 0, implode( ' ', $cols ), 0, false ); - $this->attribute_off( NCURSES_A_UNDERLINE ); - $this->attribute_off( NCURSES_A_BOLD ); - - $max_items = ( $this->height - 2 ); - $offset = ( $selected > $max_items ) ? $selected - $max_items : 0; - foreach( $rows as $id => $row ) { - if( $id < $offset ) { - continue; - } elseif( $id - $offset > $max_items ) { - // We've reached the maximum row height for this window - break; - } - foreach( $row as $col => $field ) { - $row[$col] = $field . str_repeat( ' ', $column_widths[$col] - strlen( $field ) ); - } - if( $selected == $id ) { - $this->attribute_on( NCURSES_A_REVERSE ); - } - $this->write_fixed( $id - $offset + 1, 0, implode( ' | ', $row ), 0, false ); - $this->attribute_off( NCURSES_A_REVERSE ); - } - $x = 0; - foreach( array_values( $column_widths ) as $id => $width ) { - $x += $width + ( $id == 0 ? 1 : 3 ); - // Every Column - ncurses_wmove( $this->window, 1, $x ); - ncurses_wvline( $this->window, NCURSES_ACS_VLINE, $this->height - 1 ); - // Header Row - ncurses_wmove( $this->window, 0, $x ); - $this->attribute_on( NCURSES_A_UNDERLINE ); - ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 ); - $this->attribute_off( NCURSES_A_UNDERLINE ); - // Highlighted Row - if( count( $this->records ) > 0 ) { - ncurses_wmove( $this->window, $selected - $offset + 1, $x ); - $this->attribute_on( NCURSES_A_REVERSE ); - ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 ); - $this->attribute_off( NCURSES_A_REVERSE ); - } - } - // Finally, update - ncurses_wrefresh( $this->window ); - ncurses_doupdate(); - } - /** - * Load a recordset - * - * Recordset should be an array in the following format: - * array( array( 'key' => 'value', ... ), ... ) - * - * @param array $records Recordset - * @return boolean True if recordset loaded successfully, false otherwise - */ - public function load( $records ) { - if( !is_array( $records ) ) { - return false; - } - $this->records = array_values( $records ); - $this->update(); - return true; - } + abstract function init(); } ?> diff --git a/ncurses/ncurses.progressbar.php b/ncurses/ncurses.progressbar.php new file mode 100644 index 0000000..1d7300f --- /dev/null +++ b/ncurses/ncurses.progressbar.php @@ -0,0 +1,81 @@ +enabled = false; // Cannot accept focus + $this->max = $max; + $this->show_pct = $show_pct; + $this->pos( 0 ); + } + /** + * Set or get the current value + * + * @param float $value New value + * @return float Current value + */ + public function pos( $value = false ) { + if( $value === false ) { + return $this->value; + } else { + $value = $value > $this->max ? $this->max : $value; + $this->value = $value; + $this->write( 0, 0, '[', false ); + $width = $this->width - ( $this->show_pct ? 7 : 2 ); + $width = $width >= 0 ? $width : 0; + $complete = floor( $width * $value / $this->max ); + $this->write( 0, 1, str_repeat( '#', $complete ), false ); + $this->write( 0, 1 + $complete, str_repeat( ' ', $width - $complete ), false ); + $this->write( 0, $width + 1, ']', false ); + if( $this->show_pct ) { + $this->write( 0, $width + 3, sprintf( '%3d%%', floor( $value / $this->max * 100 ) ), false ); + } + ncurses_update_panels(); + ncurses_doupdate(); + } + } + /** + * Get or set maximum value + * + * @param float $value New maximum + * @return float Current maximum + */ + public function max( $value = false ) { + if( $value === false ) { + return $this->max; + } else { + $this->max = $value > 0 ? $value : $this->max; + $this->pos( $this->value ); + } + } +} +?> diff --git a/ncurses/ncurses.tableview.php b/ncurses/ncurses.tableview.php new file mode 100644 index 0000000..f91f7ab --- /dev/null +++ b/ncurses/ncurses.tableview.php @@ -0,0 +1,169 @@ +array( + * 'columns' => array( 'key' => 'Column Name', ... ), + * 'records' => array( array( 'key' => 'value', ... ), ... ) + * ) + */ + public function __construct( $parent, $height, $width, $y, $x, $params = array() ) { + parent::__construct( $parent, $height, $width, $y, $x ); + $params = !is_array( $params ) ? array() : $params; + $this->columns = isset( $params['columns'] ) && is_array( $params['columns'] ) ? $params['columns'] : array(); + $this->records = isset( $params['records'] ) && is_array( $params['records'] ) ? array_values( $params['records'] ) : array(); + } + /** + * Redraw the table and select a record + * + * Redraws the table with the specified record selected and the view scrolled + * accordingly. + * + * @param integer $selected Selected row + */ + public function update( $selected = null ) { + $selected = $selected === null ? $this->selected : $selected; + $selected = $selected >= 0 ? $selected : 0; + $selected = $selected < count( $this->records ) ? $selected : count( $this->records ) - 1; + $this->selected = $selected; + $cols = isset( $this->columns ) && is_array( $this->columns ) ? $this->columns : array(); + if( count( $cols ) == 0 ) { + // If there's no columns defined, try and grab 'em from the keys from the first record + $record = $this->records[0]; + $record = is_array( $record ) ? $record : array( $record ); + foreach( array_keys( $record ) as $col ) { + $cols[$col] = $col; + } + } + $column_ids = array_keys( $cols ); + $column_widths = $rows = array(); + foreach( $cols as $id => $column ) { + $column_widths[$id] = strlen( $column ); + } + foreach( $this->records as $id => $record ) { + $record = is_array( $record ) ? $record : array( $record ); + $fields = array(); + foreach( $column_ids as $col ) { + $field = isset( $record[$col] ) ? $record[$col] : ''; + $field = is_scalar( $field ) ? $field : gettype( $field ); + $fields[$col] = $field; + $column_widths[$col] = $column_widths[$col] < strlen( $field ) ? strlen( $field ) : $column_widths[$col]; + } + $rows[] = $fields; + } + foreach( $cols as $id => $col ) { + $cols[$id] = $col . str_repeat( ' ', $column_widths[$id] - strlen( $col ) ); + } + $this->attribute_on( NCURSES_A_UNDERLINE ); + $this->attribute_on( NCURSES_A_BOLD ); + $this->write_fixed( 0, 0, implode( ' ', $cols ), 0, false ); + $this->attribute_off( NCURSES_A_UNDERLINE ); + $this->attribute_off( NCURSES_A_BOLD ); + + $max_items = ( $this->height - 2 ); + $offset = ( $selected > $max_items ) ? $selected - $max_items : 0; + foreach( $rows as $id => $row ) { + if( $id < $offset ) { + continue; + } elseif( $id - $offset > $max_items ) { + // We've reached the maximum row height for this window + break; + } + foreach( $row as $col => $field ) { + $row[$col] = $field . str_repeat( ' ', $column_widths[$col] - strlen( $field ) ); + } + if( $selected == $id ) { + $this->attribute_on( NCURSES_A_REVERSE ); + } + $this->write_fixed( $id - $offset + 1, 0, implode( ' | ', $row ), 0, false ); + $this->attribute_off( NCURSES_A_REVERSE ); + } + $x = 0; + foreach( array_values( $column_widths ) as $id => $width ) { + $x += $width + ( $id == 0 ? 1 : 3 ); + // Every Column + ncurses_wmove( $this->window, 1, $x ); + ncurses_wvline( $this->window, NCURSES_ACS_VLINE, $this->height - 1 ); + // Header Row + ncurses_wmove( $this->window, 0, $x ); + $this->attribute_on( NCURSES_A_UNDERLINE ); + ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 ); + $this->attribute_off( NCURSES_A_UNDERLINE ); + // Highlighted Row + if( count( $this->records ) > 0 ) { + ncurses_wmove( $this->window, $selected - $offset + 1, $x ); + $this->attribute_on( NCURSES_A_REVERSE ); + ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 ); + $this->attribute_off( NCURSES_A_REVERSE ); + } + } + // Finally, update + ncurses_wrefresh( $this->window ); + ncurses_doupdate(); + $this->emit('change', $this->selected); + } + /** + * Load a recordset + * + * Recordset should be an array in the following format: + * array( array( 'key' => 'value', ... ), ... ) + * + * @param array $records Recordset + * @return boolean True if recordset loaded successfully, false otherwise + */ + public function load( $records ) { + if( !is_array( $records ) ) { + return false; + } + $this->records = array_values( $records ); + $this->update(); + return true; + } + + public function on_key_press($key) { + switch( $key ) { + case NCURSES_KEY_UP: + $this->update($this->selected - 1); + break; + case NCURSES_KEY_DOWN: + $this->update($this->selected + 1); + break; + case NCURSES_KEY_PPAGE: + $this->update($this->selected - $this->height - 2 ); + break; + case NCURSES_KEY_NPAGE: + $this->update($this->selected + $this->height - 2 ); + break; + default: + parent::on_key_press($key); + } + } +} +?> diff --git a/ncurses/ncurses.textinput.php b/ncurses/ncurses.textinput.php new file mode 100644 index 0000000..b6299eb --- /dev/null +++ b/ncurses/ncurses.textinput.php @@ -0,0 +1,135 @@ +last_value = $this->value = $value; + $this->filter = $filter !== false && @preg_match( $filter, '' ) !== false ? $filter : false; + $this->update(); + } + /** + * Update the input control on the screen + * + * Widget is highlighted when active + */ + public function update() { + $this->attribute_on( NCURSES_A_UNDERLINE ); + if( $this->active ) { + $this->attribute_on( NCURSES_A_REVERSE ); + } + $this->write_fixed( 0, 0, substr( $this->value, - $this->width ) ); + $this->attribute_off( NCURSES_A_UNDERLINE ); + $this->attribute_off( NCURSES_A_REVERSE ); + } + /** + * Get or set value + */ + public function value( $value = false ) { + if( $value === false ) { + return $this->value; + } else { + $this->value = $value; + } + } + /** + * Activate input widget and accept input + * + * Input ends when the user presses the enter key. + * If a filter has been set and the input does not pass the filter, the value + * reverts to the previous value, and this function returns false. + * + * @return string|boolean Text entered, or false + */ + public function get() { + $this->active = true; + $this->update(); + while( $this->active ) { + $key = $this->get_char(); + switch( $key ) { + case NCURSES_KEY_BACKSPACE: + $this->value = substr( $this->value, 0, strlen( $this->value ) - 1 ); + break; + case 13: + $this->active = false; + break; + default: + if( in_array( $key, range( 32, 126 ) ) ) { + $this->value .= chr( $key ); + } + } + $this->update(); + } + $this->update(); + return $this->filter !== false && @preg_match( $this->filter, $this->value ) < 1 ? false : $this->value; + } + public function focus() { + parent::focus(); + $this->active = true; + $this->update(); + } + public function blur() { + $this->active = false; + $this->update(); + parent::blur(); + } + public function on_key_press($key) { + switch( $key ) { + case NCURSES_KEY_BACKSPACE: + $this->value = substr( $this->value, 0, strlen( $this->value ) - 1 ); + break; + case 27: + // ESC + $this->value = $this->last_value; + break; + case 9: + // TAB + case 13: + // RETURN + if( $this->value !== $this->last_value ) { + $this->emit('change', $this->value); + } + $this->blur(); + break; + default: + if( in_array( $key, range( 32, 126 ) ) ) { + $this->value .= chr( $key ); + } + parent::on_key_press($key); + } + $this->update(); + } +} +?> diff --git a/ncurses/ncurses.window.php b/ncurses/ncurses.window.php new file mode 100644 index 0000000..9b80756 --- /dev/null +++ b/ncurses/ncurses.window.php @@ -0,0 +1,425 @@ + +* @copyright Copyright (c) 2008, Correl J. Roush +* @version 0.1 +* +* @package Nc +*/ + +require_once('signals.php'); + +/** +* Ncurses window +* +* Provides a basic window object. A border can be optionally enabled if the window +* is large enough to accomodate it, and is enabled by default. If a title is set, +* it will be visible at the top of the border if one exists. +* +* If a parent window is provided, the window will be registered as a child of the +* parent, and will be relatively positioned to the parent. +* +* @package Nc +*/ +class NcWindow extends Signaler { + protected static $max_id = 0; + protected $id = 0; + /** + * @var resource nc_window resource for the border window, if one exists + */ + protected $frame; + /** + * @var resource nc_window resource for the container window + */ + protected $window; + /** + * @var array nc_panel resources for the window and its container + */ + protected $panels = array(); + /** + * @var array children array of child NcWindow references + */ + protected $children = array(); + /** + * @var NcWindow reference to this object's direct parent, if one exists + */ + protected $parent = false; + protected $app = false; + /** + * @var int ID of the current active object + */ + public static $active_window = false; + public static $windows = array(); + /** + * @var integer Absolute column position of the window + */ + protected $x; + /** + * @var integer Absolute row position of the window + */ + protected $y; + /** + * @var integer Width of contained window + */ + protected $width; + /** + * @var integer Height of contained window + */ + protected $height; + /** + * @var string Window title + */ + protected $title; + protected $enabled = true; + protected $visible = true; + protected $shortcuts = array(); + protected $emit_log_filter = array('keypress'); + + /** + * Constructor + * + * Builds the requested ncurses window. + * + * @param NcWindow $parent Parent window + * @param integer $height Height + * @param integer $width Width + * @param integer $y Row + * @param integer $x Column + * @param boolean $framed Include a frame (border). Required for window titles. + */ + public function __construct( &$parent, $height, $width, $y, $x, $framed = true ) { + $this->id = count(self::$windows); + self::$windows[] = &$this; + if( self::$active_window === false ) { + self::$active_window = $this->id; + } + if( $parent instanceof NcWindow ) { + $this->parent = &$parent; + $this->app = &$parent->app; + $x += $parent->x; + $y += $parent->y; + $width += $width <= 0 ? $parent->width : 0; + $height += $height <= 0 ? $parent->height : 0; + } elseif( $parent instanceof NcApp ) { + $this->app = &$parent; + } + if( $framed ) { + $this->frame = ncurses_newwin( $height, $width, $y, $x ); + ncurses_getmaxyx( $this->frame, $max_y, $max_x ); + if( $max_y >= 2 && $max_x >= 2 ) { + $this->window = ncurses_newwin( $max_y - 2, $max_x - 2, $y + 1, $x + 1 ); + ncurses_wborder( $this->frame, 0,0, 0,0, 0,0, 0,0 ); + ncurses_wrefresh( $this->frame ); + $this->panels[] = ncurses_new_panel( $this->frame ); + } else { + $this->window = $this->frame; + $this->frame = false; + } + } else { + $this->frame = false; + $this->window = ncurses_newwin( $height, $width, $y, $x ); + ncurses_getmaxyx( $this->window, $max_y, $max_x ); + } + $this->width = $this->frame ? $max_x - 2 : $max_x; + $this->height = $this->frame ? $max_y - 2 : $max_y; + $this->x = $this->frame ? $x + 1 : $x; + $this->y = $this->frame ? $y + 1 : $y; + ncurses_wrefresh( $this->window ); + $this->panels[] = ncurses_new_panel( $this->window ); + ncurses_update_panels(); + if( $this->parent instanceof NcWindow ) { + $this->parent->add_child( $this ); + } + + self::connect($this, '__emit', $this, '__emit'); + } + /** + * Destructor + */ + public function __destruct() { + foreach( $this->panels as $id => $panel ) { + ncurses_del_panel( $this->panels[$id] ); + } + ncurses_delwin( $this->window ); + if( $this->frame !== false ) { + ncurses_delwin( $this->frame ); + } + } + + static public function get_window($id) { + return self::$windows[$id]; + } + /** + * Set or retrieve a window title + * + * Sets window title to the value provided, returns the current window title + * if one is not. + * + * @param string $value New window title + * @return string Window title + */ + public function title( $value = false ) { + if( $value === false ) { + return $this->title; + } elseif( $this->frame !== false ) { + $this->title = $value; + ncurses_wborder( $this->frame, 0,0, 0,0, 0,0, 0,0 ); + ncurses_mvwaddstr( $this->frame, 0, 1, $this->title ); + ncurses_update_panels(); + ncurses_doupdate(); + } + } + public function height() { return $this->height; } + public function width() { return $this->width; } + public function enabled() { return (bool)$this->enabled; } + public function active() { return (bool)($this->id === self::$active_window); } + /** + * Activate an ncurses output attribute + * + * @param integer NCURSES_A_* constant + */ + public function attribute_on( $attribute ) { + ncurses_wattron( $this->window, $attribute ); + } + /** + * Deactivate an ncurses output attribute + * + * @param integer NCURSES_A_* constant + */ + public function attribute_off( $attribute ) { + ncurses_wattroff( $this->window, $attribute ); + } + /** + * Write text to the window + * + * Writes text to the window at the specified coordinates. + * Text is truncated to prevent it overflowing off the side of the window. If + * the text contains newlines, it is split and each new line will continue from + * the same x position as the first, rather than wrapping back to the far left + * side of the window as ncurses normally would. + * + * @param integer $y Row + * @param integer $x Column + * @param string $text Text to write + * @param boolean $update Update the window upon completion + */ + public function write( $y, $x, $text, $update = true ) { + $y = $y < 0 ? $this->height + $y : $y; + $x = $x < 0 ? $this->width + $x : $x; + $lines = preg_split( '/\r?(\n|\r)/', $text ); + foreach( $lines as $id => $line ) { + $line = substr( $line, 0, $this->width - $x ); + ncurses_mvwaddstr( $this->window, $y + $id, $x, $line ); + } + if( $update ) { + ncurses_update_panels(); + ncurses_doupdate(); + } + } + /** + * Write centered text + * + * Writes text centered horizontally within the window on the specified line. + * + * @param integer $y Row + * @param string $text Text to write + * @param boolean $update Update the window upon completion + */ + public function write_centered( $y, $text, $update = true ) { + $this->write( + $y, + floor( $this->width / 2 ) - floor( strlen( $text ) / 2 ), + $text, + $update + ); + } + /** + * Write fixed-width text + * + * Writes a string truncated or padded with spaces to fill the requested width. + * If the width is unspecified or zero, the width is defined as the length from + * the starting coordinate to the right edge of the window. + * + * @param integer $y Row + * @param integer $x Column + * @param string $text Text to write + * @param integer $width Field length + * @param boolean $update Update the window upon completion + */ + public function write_fixed( $y, $x, $text, $width = 0, $update = true ) { + $width = $width > 0 ? $width : $this->width - $x; + $this->write( $y, $x, $this->_string_fixed( $text, $width ), $update ); + } + /** + * Build a fixed-width string + * + * Truncates a string if it exceeds the specified width, pads it with spaces + * if it is too short. + * + * @param string $text Text to format + * @param integer $width Field length + * @return string Fixed-width string + */ + protected function _string_fixed( $text, $width = 0 ) { + $width = $width < 0 ? 0 : $width; + $text = substr( $text, 0, $width ); + $text .= str_repeat( ' ', $width - strlen( $text ) ); + return $text; + } + /** + * Assign a window as a child of this window + * + * Children of a window are hidden and shown along with their parent widget. + * + * @param NcWindow $child Child window + */ + public function add_child( $child ) { + $this->children[$child->id] = $child; + if( count($this->children) == 1 ) { + // Make the first child active + self::$active_window = $child->id; + $child->focus(); + } + } + /** + * Hide the window + * + * @param boolean $update Update the window upon completion + */ + public function hide( $update = true ) { + foreach( $this->panels as $panel ) { + ncurses_hide_panel( $panel ); + } + foreach( $this->children as $child ) { + $child->hide( false ); + } + if( $update ) { + ncurses_update_panels(); + ncurses_doupdate(); + } + $this->visible = false; + } + /** + * Show the window + * + * @param boolean $update Update the window upon completion + */ + public function show( $update = true ) { + foreach( $this->panels as $panel ) { + ncurses_show_panel( $panel ); + } + foreach( $this->children as $child ) { + $child->show( false ); + } + if( $update ) { + ncurses_update_panels(); + ncurses_doupdate(); + } + $this->visible = true; + } + /** + * Clear the window's contents + * + * @param boolean $update Update the window upon completion + */ + public function erase( $update = true ) { + ncurses_werase( $this->window ); + if( $update ) { + ncurses_wrefresh( $this->window ); + ncurses_doupdate(); + } + } + /** + * Read a character from the keyboard and return it + * + * @param boolean $flush Flush the input buffer before requesting a key + * @return integer Keycode + */ + public function get_char( $flush = true ) { + if( $flush ) { + ncurses_flushinp(); + } + return ncurses_getch(); + //return ncurses_wgetch( $this->window ); + } + + public function shortcut($key, $object = null) { + $key = ord(strtolower(chr($key))); + if( $object !== null ) { + $this->shortcuts[$key] = $object; + } elseif( in_array($key, array_keys($this->shortcuts)) ) { + unset($this->shortcuts[$key]); + } + } + /* Event slots */ + public function log($message) { + if( $this->app ) { + $this->app->log($this, "[{$this->id}] $message"); + } + } + public function __emit($signal, $args) { + if( !in_array($signal, $this->emit_log_filter) ) { + foreach($args as $key => $value) { + $args[$key] = (string)$value; + } + $args = implode(',', $args); + $this->log("Emitted {$signal}({$args})"); + } + } + public function focus() { + self::$active_window = $this->id; + $this->emit('focus'); + } + public function blur() { + $this->emit('blur'); + if( $this->parent instanceof NcWindow ) { + $this->parent->focus_next(); + } + } + public function disable() { + $this->enabled = false; + $this->blur(); + } + public function focus_next() { + $next = false; + $first = false; + foreach( $this->children as $child ) { + if( $child->visible && $child->enabled ) { + if( $first === false ) { + $first = $child; + } + $next = &$child; + if( self::$active_window && $next->id > self::$active_window ) { + break; + } + } + } + if( $next === false ) { + $next = &$this; + } elseif( $next->id == self::$active_window ) { + $next = &$first; + } + $next->focus(); + } + public function on_key_press($key) { + if( self::$active_window === $this->id ) { + // This is the active window, handle input + switch($key) { + case 9: + // TAB + $this->blur(); + break; + } + } else { + // Pass it along to the active child widget + self::get_window(self::$active_window)->on_key_press($key); + } + $this->emit('keypress', $key); + } +} +?> diff --git a/ncurses/signals.php b/ncurses/signals.php new file mode 100644 index 0000000..cc39ebc --- /dev/null +++ b/ncurses/signals.php @@ -0,0 +1,53 @@ +_listeners) ) { + $emitter->_listeners[$signal] = array(); + } + $emitter->_listeners[$signal][] = array( + 'object' => &$responder, + 'method' => $slot + ); + } + protected function emit() { + self::$current = &$this; + $args = func_get_args(); + if( count($args) == 0 ) { + // Called without any arguments, raise an error + return false; + } + $signal = array_shift($args); + $sent = 0; + if( array_key_exists($signal, $this->_listeners) ) { + foreach( $this->_listeners[$signal] as $key => $listener ) { + if( + !is_object($listener['object']) + || !method_exists($listener['object'], $listener['method']) + ) { + // Something happened to the object since it was connected, + // and it is no longer usable. Drop it. + unset($this->_listeners[$key]); + } else { + // Everything's good, set the callback loose. + $result = call_user_func_array(array($listener['object'], $listener['method']), $args); + if( $result !== false ) { + $sent++; + } + } + } + } + // Special signal for debugging purposes + if( $signal !== '__emit' ) { $this->emit('__emit', $signal, $args); } + return $sent; + } +} +?> diff --git a/scanner.php b/scanner.php index c6e6def..b723c89 100644 --- a/scanner.php +++ b/scanner.php @@ -361,10 +361,10 @@ if( $curses ) { $nc_faults->update( $nc_faults->selected + 1 ); break; case NCURSES_KEY_PPAGE: - $nc_faults->update( $nc_faults->selected - $nc_faults->height - 1 ); + $nc_faults->update( $nc_faults->selected - $nc_faults->height() - 1 ); break; case NCURSES_KEY_NPAGE: - $nc_faults->update( $nc_faults->selected + $nc_faults->height - 1 ); + $nc_faults->update( $nc_faults->selected + $nc_faults->height() - 1 ); break; case 10: case 13: @@ -395,8 +395,8 @@ if( $curses ) { $filename = $nc_save_file_input->get(); $nc_save_confirm = new NcWindow( $nc_save, -10, -10, 5, 5 ); $nc_save_confirm->title( get_class( $modules['output'] ) ); - $nc_save_confirm->write_centered( floor( $nc_save_confirm->height / 2 ) - 2, "Save results to '{$filename}'?" ); - $nc_save_confirm->write_centered( floor( $nc_save_confirm->height / 2 ), '[Y]es [N]o [C]ancel' ); + $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ) - 2, "Save results to '{$filename}'?" ); + $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ), '[Y]es [N]o [C]ancel' ); $valid_key = false; while( !$valid_key ) { switch( $nc_save_confirm->get_char() ) { @@ -417,8 +417,8 @@ if( $curses ) { $result = $modules['output']->write( $filename ); $save_message = ( $result === false ) ? 'Save Failed' : 'Saved successfully'; $nc_save_confirm->erase(); - $nc_save_confirm->write_centered( floor( $nc_save_confirm->height / 2 ) - 2, $save_message ); - $nc_save_confirm->write_centered( floor( $nc_save_confirm->height / 2 ), 'Press any key to continue' ); + $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ) - 2, $save_message ); + $nc_save_confirm->write_centered( floor( $nc_save_confirm->height() / 2 ), 'Press any key to continue' ); $nc_save_confirm->show(); $nc_save_confirm->get_char(); }