* @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: * 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; } } ?>