307 lines
8.0 KiB
PHP
307 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace splitbrain\phpcli;
|
|
|
|
/**
|
|
* Class TableFormatter
|
|
*
|
|
* Output text in multiple columns
|
|
*
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
* @license MIT
|
|
*/
|
|
class TableFormatter
|
|
{
|
|
/** @var string border between columns */
|
|
protected $border = ' ';
|
|
|
|
/** @var int the terminal width */
|
|
protected $max = 74;
|
|
|
|
/** @var Colors for coloring output */
|
|
protected $colors;
|
|
|
|
/**
|
|
* TableFormatter constructor.
|
|
*
|
|
* @param Colors|null $colors
|
|
*/
|
|
public function __construct(Colors $colors = null)
|
|
{
|
|
// try to get terminal width
|
|
$width = 0;
|
|
if (isset($_SERVER['COLUMNS'])) {
|
|
// from environment
|
|
$width = (int)$_SERVER['COLUMNS'];
|
|
}
|
|
if (!$width) {
|
|
// via tput command
|
|
$width = @exec('tput cols');
|
|
}
|
|
if ($width) {
|
|
$this->max = $width;
|
|
}
|
|
|
|
if ($colors) {
|
|
$this->colors = $colors;
|
|
} else {
|
|
$this->colors = new Colors();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The currently set border (defaults to ' ')
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBorder()
|
|
{
|
|
return $this->border;
|
|
}
|
|
|
|
/**
|
|
* Set the border. The border is set between each column. Its width is
|
|
* added to the column widths.
|
|
*
|
|
* @param string $border
|
|
*/
|
|
public function setBorder($border)
|
|
{
|
|
$this->border = $border;
|
|
}
|
|
|
|
/**
|
|
* Width of the terminal in characters
|
|
*
|
|
* initially autodetected
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getMaxWidth()
|
|
{
|
|
return $this->max;
|
|
}
|
|
|
|
/**
|
|
* Set the width of the terminal to assume (in characters)
|
|
*
|
|
* @param int $max
|
|
*/
|
|
public function setMaxWidth($max)
|
|
{
|
|
$this->max = $max;
|
|
}
|
|
|
|
/**
|
|
* Takes an array with dynamic column width and calculates the correct width
|
|
*
|
|
* Column width can be given as fixed char widths, percentages and a single * width can be given
|
|
* for taking the remaining available space. When mixing percentages and fixed widths, percentages
|
|
* refer to the remaining space after allocating the fixed width
|
|
*
|
|
* @param array $columns
|
|
* @return int[]
|
|
* @throws Exception
|
|
*/
|
|
protected function calculateColLengths($columns)
|
|
{
|
|
$idx = 0;
|
|
$border = $this->strlen($this->border);
|
|
$fixed = (count($columns) - 1) * $border; // borders are used already
|
|
$fluid = -1;
|
|
|
|
// first pass for format check and fixed columns
|
|
foreach ($columns as $idx => $col) {
|
|
// handle fixed columns
|
|
if ((string)intval($col) === (string)$col) {
|
|
$fixed += $col;
|
|
continue;
|
|
}
|
|
// check if other colums are using proper units
|
|
if (substr($col, -1) == '%') {
|
|
continue;
|
|
}
|
|
if ($col == '*') {
|
|
// only one fluid
|
|
if ($fluid < 0) {
|
|
$fluid = $idx;
|
|
continue;
|
|
} else {
|
|
throw new Exception('Only one fluid column allowed!');
|
|
}
|
|
}
|
|
throw new Exception("unknown column format $col");
|
|
}
|
|
|
|
$alloc = $fixed;
|
|
$remain = $this->max - $alloc;
|
|
|
|
// second pass to handle percentages
|
|
foreach ($columns as $idx => $col) {
|
|
if (substr($col, -1) != '%') {
|
|
continue;
|
|
}
|
|
$perc = floatval($col);
|
|
|
|
$real = (int)floor(($perc * $remain) / 100);
|
|
|
|
$columns[$idx] = $real;
|
|
$alloc += $real;
|
|
}
|
|
|
|
$remain = $this->max - $alloc;
|
|
if ($remain < 0) {
|
|
throw new Exception("Wanted column widths exceed available space");
|
|
}
|
|
|
|
// assign remaining space
|
|
if ($fluid < 0) {
|
|
$columns[$idx] += ($remain); // add to last column
|
|
} else {
|
|
$columns[$fluid] = $remain;
|
|
}
|
|
|
|
return $columns;
|
|
}
|
|
|
|
/**
|
|
* Displays text in multiple word wrapped columns
|
|
*
|
|
* @param int[] $columns list of column widths (in characters, percent or '*')
|
|
* @param string[] $texts list of texts for each column
|
|
* @param array $colors A list of color names to use for each column. use empty string for default
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function format($columns, $texts, $colors = array())
|
|
{
|
|
$columns = $this->calculateColLengths($columns);
|
|
|
|
$wrapped = array();
|
|
$maxlen = 0;
|
|
|
|
foreach ($columns as $col => $width) {
|
|
$wrapped[$col] = explode("\n", $this->wordwrap($texts[$col], $width, "\n", true));
|
|
$len = count($wrapped[$col]);
|
|
if ($len > $maxlen) {
|
|
$maxlen = $len;
|
|
}
|
|
|
|
}
|
|
|
|
$last = count($columns) - 1;
|
|
$out = '';
|
|
for ($i = 0; $i < $maxlen; $i++) {
|
|
foreach ($columns as $col => $width) {
|
|
if (isset($wrapped[$col][$i])) {
|
|
$val = $wrapped[$col][$i];
|
|
} else {
|
|
$val = '';
|
|
}
|
|
$chunk = $this->pad($val, $width);
|
|
if (isset($colors[$col]) && $colors[$col]) {
|
|
$chunk = $this->colors->wrap($chunk, $colors[$col]);
|
|
}
|
|
$out .= $chunk;
|
|
|
|
// border
|
|
if ($col != $last) {
|
|
$out .= $this->border;
|
|
}
|
|
}
|
|
$out .= "\n";
|
|
}
|
|
return $out;
|
|
|
|
}
|
|
|
|
/**
|
|
* Pad the given string to the correct length
|
|
*
|
|
* @param string $string
|
|
* @param int $len
|
|
* @return string
|
|
*/
|
|
protected function pad($string, $len)
|
|
{
|
|
$strlen = $this->strlen($string);
|
|
if ($strlen > $len) return $string;
|
|
|
|
$pad = $len - $strlen;
|
|
return $string . str_pad('', $pad, ' ');
|
|
}
|
|
|
|
/**
|
|
* Measures char length in UTF-8 when possible
|
|
*
|
|
* @param $string
|
|
* @return int
|
|
*/
|
|
protected function strlen($string)
|
|
{
|
|
// don't count color codes
|
|
$string = preg_replace("/\33\\[\\d+(;\\d+)?m/", '', $string);
|
|
|
|
if (function_exists('mb_strlen')) {
|
|
return mb_strlen($string, 'utf-8');
|
|
}
|
|
|
|
return strlen($string);
|
|
}
|
|
|
|
/**
|
|
* @param string $string
|
|
* @param int $start
|
|
* @param int|null $length
|
|
* @return string
|
|
*/
|
|
protected function substr($string, $start = 0, $length = null)
|
|
{
|
|
if (function_exists('mb_substr')) {
|
|
return mb_substr($string, $start, $length);
|
|
} else {
|
|
return substr($string, $start, $length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $str
|
|
* @param int $width
|
|
* @param string $break
|
|
* @param bool $cut
|
|
* @return string
|
|
* @link http://stackoverflow.com/a/4988494
|
|
*/
|
|
protected function wordwrap($str, $width = 75, $break = "\n", $cut = false)
|
|
{
|
|
$lines = explode($break, $str);
|
|
foreach ($lines as &$line) {
|
|
$line = rtrim($line);
|
|
if ($this->strlen($line) <= $width) {
|
|
continue;
|
|
}
|
|
$words = explode(' ', $line);
|
|
$line = '';
|
|
$actual = '';
|
|
foreach ($words as $word) {
|
|
if ($this->strlen($actual . $word) <= $width) {
|
|
$actual .= $word . ' ';
|
|
} else {
|
|
if ($actual != '') {
|
|
$line .= rtrim($actual) . $break;
|
|
}
|
|
$actual = $word;
|
|
if ($cut) {
|
|
while ($this->strlen($actual) > $width) {
|
|
$line .= $this->substr($actual, 0, $width) . $break;
|
|
$actual = $this->substr($actual, $width);
|
|
}
|
|
}
|
|
$actual .= ' ';
|
|
}
|
|
}
|
|
$line .= trim($actual);
|
|
}
|
|
return implode($break, $lines);
|
|
}
|
|
} |