ANSI: improve vt100 terminal emulation

store each coordinate's attributes independently and add support
for a few more escape codes
This commit is contained in:
terrafrost 2015-05-23 11:55:03 -05:00
parent e3d251ac57
commit cc0420b36b

View File

@ -117,6 +117,22 @@ class File_ANSI
*/ */
var $old_y; var $old_y;
/**
* An empty attribute cell
*
* @var Object
* @access private
*/
var $base_attr_cell;
/**
* The current attribute cell
*
* @var Object
* @access private
*/
var $attr_cell;
/** /**
* An empty attribute row * An empty attribute row
* *
@ -141,62 +157,6 @@ class File_ANSI
*/ */
var $attrs; var $attrs;
/**
* The current foreground color
*
* @var String
* @access private
*/
var $foreground;
/**
* The current background color
*
* @var String
* @access private
*/
var $background;
/**
* Bold flag
*
* @var Boolean
* @access private
*/
var $bold;
/**
* Underline flag
*
* @var Boolean
* @access private
*/
var $underline;
/**
* Blink flag
*
* @var Boolean
* @access private
*/
var $blink;
/**
* Reverse flag
*
* @var Boolean
* @access private
*/
var $reverse;
/**
* Color flag
*
* @var Boolean
* @access private
*/
var $color;
/** /**
* Current ANSI code * Current ANSI code
* *
@ -205,6 +165,14 @@ class File_ANSI
*/ */
var $ansi; var $ansi;
/**
* Tokenization
*
* @var Array
* @access private
*/
var $tokenization;
/** /**
* Default Constructor. * Default Constructor.
* *
@ -213,6 +181,16 @@ class File_ANSI
*/ */
function File_ANSI() function File_ANSI()
{ {
$attr_cell = new stdClass();
$attr_cell->bold = false;
$attr_cell->underline = false;
$attr_cell->blink = false;
$attr_cell->background = 'black';
$attr_cell->foreground = 'white';
$attr_cell->reverse = false;
$this->base_attr_cell = clone($attr_cell);
$this->attr_cell = clone($attr_cell);
$this->setHistory(200); $this->setHistory(200);
$this->setDimensions(80, 24); $this->setDimensions(80, 24);
} }
@ -232,17 +210,9 @@ class File_ANSI
$this->max_y = $y - 1; $this->max_y = $y - 1;
$this->x = $this->y = 0; $this->x = $this->y = 0;
$this->history = $this->history_attrs = array(); $this->history = $this->history_attrs = array();
$this->attr_row = array_fill(0, $this->max_x + 1, ''); $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
$this->screen = array_fill(0, $this->max_y + 1, ''); $this->screen = array_fill(0, $this->max_y + 1, '');
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row); $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
$this->foreground = 'white';
$this->background = 'black';
$this->bold = false;
$this->underline = false;
$this->blink = false;
$this->reverse = false;
$this->color = false;
$this->ansi = ''; $this->ansi = '';
} }
@ -278,6 +248,7 @@ class File_ANSI
*/ */
function appendString($source) function appendString($source)
{ {
$this->tokenization = array('');
for ($i = 0; $i < strlen($source); $i++) { for ($i = 0; $i < strlen($source); $i++) {
if (strlen($this->ansi)) { if (strlen($this->ansi)) {
$this->ansi.= $source[$i]; $this->ansi.= $source[$i];
@ -294,6 +265,8 @@ class File_ANSI
default: default:
continue 2; continue 2;
} }
$this->tokenization[] = $this->ansi;
$this->tokenization[] = '';
// http://ascii-table.com/ansi-escape-sequences-vt-100.php // http://ascii-table.com/ansi-escape-sequences-vt-100.php
switch ($this->ansi) { switch ($this->ansi) {
case "\x1B[H": // Move cursor to upper left corner case "\x1B[H": // Move cursor to upper left corner
@ -315,7 +288,7 @@ class File_ANSI
case "\x1B[K": // Clear screen from cursor right case "\x1B[K": // Clear screen from cursor right
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x); $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
array_splice($this->attrs[$this->y], $this->x + 1); array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - $this->x - 1, $this->base_attr_cell));
break; break;
case "\x1B[2K": // Clear entire line case "\x1B[2K": // Clear entire line
$this->screen[$this->y] = str_repeat(' ', $this->x); $this->screen[$this->y] = str_repeat(' ', $this->x);
@ -323,6 +296,7 @@ class File_ANSI
break; break;
case "\x1B[?1h": // set cursor key to application case "\x1B[?1h": // set cursor key to application
case "\x1B[?25h": // show the cursor case "\x1B[?25h": // show the cursor
case "\x1B(B": // set united states g0 character set
break; break;
case "\x1BE": // Move to next line case "\x1BE": // Move to next line
$this->_newLine(); $this->_newLine();
@ -330,6 +304,10 @@ class File_ANSI
break; break;
default: default:
switch (true) { switch (true) {
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
$this->old_y = $this->y;
$this->y+= $match[1];
break;
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
$this->old_x = $this->x; $this->old_x = $this->x;
$this->old_y = $this->y; $this->old_y = $this->y;
@ -338,64 +316,42 @@ class File_ANSI
break; break;
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
$this->old_x = $this->x; $this->old_x = $this->x;
$x = $match[1] - 1; $this->x+= $match[1];
break;
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
$this->old_x = $this->x;
$this->x-= $match[1];
break; break;
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
break; break;
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
$attr_cell = &$this->attr_cell;
$mods = explode(';', $match[1]); $mods = explode(';', $match[1]);
foreach ($mods as $mod) { foreach ($mods as $mod) {
switch ($mod) { switch ($mod) {
case 0: // Turn off character attributes case 0: // Turn off character attributes
$this->attrs[$this->y][$this->x] = ''; $attr_cell = clone($this->base_attr_cell);
if ($this->bold) $this->attrs[$this->y][$this->x].= '</b>';
if ($this->underline) $this->attrs[$this->y][$this->x].= '</u>';
if ($this->blink) $this->attrs[$this->y][$this->x].= '</blink>';
if ($this->color) $this->attrs[$this->y][$this->x].= '</span>';
if ($this->reverse) {
$temp = $this->background;
$this->background = $this->foreground;
$this->foreground = $temp;
}
$this->bold = $this->underline = $this->blink = $this->color = $this->reverse = false;
break; break;
case 1: // Turn bold mode on case 1: // Turn bold mode on
if (!$this->bold) { $attr_cell->bold = true;
$this->attrs[$this->y][$this->x] = '<b>';
$this->bold = true;
}
break; break;
case 4: // Turn underline mode on case 4: // Turn underline mode on
if (!$this->underline) { $attr_cell->underline = true;
$this->attrs[$this->y][$this->x] = '<u>';
$this->underline = true;
}
break; break;
case 5: // Turn blinking mode on case 5: // Turn blinking mode on
if (!$this->blink) { $attr_cell->blink = true;
$this->attrs[$this->y][$this->x] = '<blink>';
$this->blink = true;
}
break; break;
case 7: // Turn reverse video on case 7: // Turn reverse video on
$this->reverse = !$this->reverse; $attr_cell->reverse = !$attr_cell->reverse;
$temp = $this->background; $temp = $attr_cell->background;
$this->background = $this->foreground; $attr_cell->background = $attr_cell->foreground;
$this->foreground = $temp; $attr_cell->foreground = $temp;
$this->attrs[$this->y][$this->x] = '<span style="color: ' . $this->foreground . '; background: ' . $this->background . '">';
if ($this->color) {
$this->attrs[$this->y][$this->x] = '</span>' . $this->attrs[$this->y][$this->x];
}
$this->color = true;
break; break;
default: // set colors default: // set colors
//$front = $this->reverse ? &$this->background : &$this->foreground; //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
$front = &$this->{ $this->reverse ? 'background' : 'foreground' }; $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
//$back = $this->reverse ? &$this->foreground : &$this->background; //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
$back = &$this->{ $this->reverse ? 'foreground' : 'background' }; $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
switch ($mod) { switch ($mod) {
case 30: $front = 'black'; break; case 30: $front = 'black'; break;
case 31: $front = 'red'; break; case 31: $front = 'red'; break;
@ -416,28 +372,22 @@ class File_ANSI
case 47: $back = 'white'; break; case 47: $back = 'white'; break;
default: default:
user_error('Unsupported attribute: ' . $mod); //user_error('Unsupported attribute: ' . $mod);
$this->ansi = ''; $this->ansi = '';
break 2; break 2;
} }
unset($temp);
$this->attrs[$this->y][$this->x] = '<span style="color: ' . $this->foreground . '; background: ' . $this->background . '">';
if ($this->color) {
$this->attrs[$this->y][$this->x] = '</span>' . $this->attrs[$this->y][$this->x];
}
$this->color = true;
} }
} }
break; break;
default: default:
user_error("{$this->ansi} unsupported\r\n"); //user_error("{$this->ansi} is unsupported\r\n");
} }
} }
$this->ansi = ''; $this->ansi = '';
continue; continue;
} }
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
switch ($source[$i]) { switch ($source[$i]) {
case "\r": case "\r":
$this->x = 0; $this->x = 0;
@ -445,12 +395,32 @@ class File_ANSI
case "\n": case "\n":
$this->_newLine(); $this->_newLine();
break; break;
case "\x08": // backspace
if ($this->x) {
$this->x--;
$this->attrs[$this->y][$this->x] = clone($this->base_attr_cell);
$this->screen[$this->y] = substr_replace(
$this->screen[$this->y],
$source[$i],
$this->x,
1
);
}
break;
case "\x0F": // shift case "\x0F": // shift
break; break;
case "\x1B": // start ANSI escape code case "\x1B": // start ANSI escape code
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
// array_pop($this->tokenization);
//}
$this->ansi.= "\x1B"; $this->ansi.= "\x1B";
break; break;
default: default:
$this->attrs[$this->y][$this->x] = clone($this->attr_cell);
if ($this->x > strlen($this->screen[$this->y])) {
$this->screen[$this->y] = str_repeat(' ', $this->x);
}
$this->screen[$this->y] = substr_replace( $this->screen[$this->y] = substr_replace(
$this->screen[$this->y], $this->screen[$this->y],
$source[$i], $source[$i],
@ -498,6 +468,63 @@ class File_ANSI
$this->y++; $this->y++;
} }
/**
* Returns the current coordinate without preformating
*
* @access private
* @return String
*/
function _processCoordinate($last_attr, $cur_attr, $char)
{
$output = '';
if ($last_attr != $cur_attr) {
$close = $open = '';
if ($last_attr->foreground != $cur_attr->foreground) {
if ($cur_attr->foreground != 'white') {
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
}
if ($last_attr->foreground != 'white') {
$close = '</span>' . $close;
}
}
if ($last_attr->background != $cur_attr->background) {
if ($cur_attr->background != 'black') {
$open.= '<span style="background: ' . $cur_attr->background . '">';
}
if ($last_attr->backtground != 'black') {
$close = '</span>' . $close;
}
}
if ($last_attr->bold != $cur_attr->bold) {
if ($cur_attr->bold) {
$open.= '<b>';
} else {
$close = '</b>' . $close;
}
}
if ($last_attr->underline != $cur_attr->underline) {
if ($cur_attr->underline) {
$open.= '<u>';
} else {
$close = '</u>' . $close;
}
}
if ($last_attr->blink != $cur_attr->blink) {
if ($cur_attr->blink) {
$open.= '<blink>';
} else {
$close = '</blink>' . $close;
}
}
$output.= $close . $open;
}
$output.= htmlspecialchars($char);
return $output;
}
/** /**
* Returns the current screen without preformating * Returns the current screen without preformating
* *
@ -507,18 +534,16 @@ class File_ANSI
function _getScreen() function _getScreen()
{ {
$output = ''; $output = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i <= $this->max_y; $i++) { for ($i = 0; $i <= $this->max_y; $i++) {
for ($j = 0; $j <= $this->max_x + 1; $j++) { for ($j = 0; $j <= $this->max_x; $j++) {
if (isset($this->attrs[$i][$j])) { $cur_attr = $this->attrs[$i][$j];
$output.= $this->attrs[$i][$j]; $output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
} $last_attr = $this->attrs[$i][$j];
if (isset($this->screen[$i][$j])) {
$output.= htmlspecialchars($this->screen[$i][$j]);
}
} }
$output.= "\r\n"; $output.= "\r\n";
} }
return rtrim($output); return $output; //rtrim($output);
} }
/** /**
@ -529,7 +554,7 @@ class File_ANSI
*/ */
function getScreen() function getScreen()
{ {
return '<pre style="color: white; background: black" width="' . ($this->max_x + 1) . '">' . $this->_getScreen() . '</pre>'; return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
} }
/** /**
@ -541,19 +566,17 @@ class File_ANSI
function getHistory() function getHistory()
{ {
$scrollback = ''; $scrollback = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i < count($this->history); $i++) { for ($i = 0; $i < count($this->history); $i++) {
for ($j = 0; $j <= $this->max_x + 1; $j++) { for ($j = 0; $j <= $this->max_x + 1; $j++) {
if (isset($this->history_attrs[$i][$j])) { $cur_attr = $this->history_attrs[$i][$j];
$scrollback.= $this->history_attrs[$i][$j]; $scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
} $last_attr = $this->history_attrs[$i][$j];
if (isset($this->history[$i][$j])) {
$scrollback.= htmlspecialchars($this->history[$i][$j]);
}
} }
$scrollback.= "\r\n"; $scrollback.= "\r\n";
} }
$scrollback.= $this->_getScreen(); $scrollback.= $this->_getScreen();
return '<pre style="color: white; background: black" width="' . ($this->max_x + 1) . '">' . $scrollback . '</pre>'; return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
} }
} }