scanner/ncurses/ncurses.php
Correl Roush 57215e1191 A few bug fixes and a whole lot of documentation Mantis: 2691
git-svn-id: file:///srv/svn/scanner/trunk@16 a0501263-5b7a-4423-a8ba-1edf086583e7
2008-02-29 15:39:44 +00:00

670 lines
18 KiB
PHP

<?php
/**
* PHP ncurses library
*
* Provides a clean, object-oriented interface to the PHP ncurses functions to ease
* the creation of elaborate, functional CLI applications.
*
* @author Correl J. Roush <correl@gmail.com>
* @copyright Copyright (c) 2008, Correl J. Roush
* @version 0.1
*
* @package Nc
*/
/**
* PHP ncurses application
*
* @package Nc
*/
abstract class NcApp {
var $screen;
/**
* Constructor
*
* Initializes the ncurses environment, as well as the fullscreen window {@link $screen}
*
* @param boolean $echo Echo keyboard input to the screen
* @param boolean $cursor Display the cursor on the screen
*/
public function __construct( $echo = false, $cursor = false ) {
ncurses_init();
$echo ? ncurses_echo() : ncurses_noecho();
ncurses_curs_set( (bool)$cursor );
$this->screen = ncurses_newwin( 0, 0, 0, 0 );
ncurses_refresh();
$this->run();
}
/**
* Destructor
*
* Clears the screen and ends the ncurses session
*/
public function __destruct() {
ncurses_clear();
ncurses_refresh();
ncurses_end();
}
/**
* 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:
* <code>array(
* 'columns' => array( 'key' => 'Column Name', ... ),
* 'records' => array( array( 'key' => 'value', ... ), ... )
* )</code>
*/
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:
* <code>array( array( 'key' => 'value', ... ), ... )</code>
*
* @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;
}
}
?>