1cbeaa4a0SAndreas Gohr<?php 2cbeaa4a0SAndreas Gohr 3cbeaa4a0SAndreas Gohrnamespace splitbrain\phpcli; 4cbeaa4a0SAndreas Gohr 5cbeaa4a0SAndreas Gohr/** 6cbeaa4a0SAndreas Gohr * Class TableFormatter 7cbeaa4a0SAndreas Gohr * 8cbeaa4a0SAndreas Gohr * Output text in multiple columns 9cbeaa4a0SAndreas Gohr * 10cbeaa4a0SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 11cbeaa4a0SAndreas Gohr * @license MIT 12cbeaa4a0SAndreas Gohr */ 13cbeaa4a0SAndreas Gohrclass TableFormatter 14cbeaa4a0SAndreas Gohr{ 15cbeaa4a0SAndreas Gohr /** @var string border between columns */ 16cbeaa4a0SAndreas Gohr protected $border = ' '; 17cbeaa4a0SAndreas Gohr 18cbeaa4a0SAndreas Gohr /** @var int the terminal width */ 19cbeaa4a0SAndreas Gohr protected $max = 74; 20cbeaa4a0SAndreas Gohr 21cbeaa4a0SAndreas Gohr /** @var Colors for coloring output */ 22cbeaa4a0SAndreas Gohr protected $colors; 23cbeaa4a0SAndreas Gohr 24cbeaa4a0SAndreas Gohr /** 25cbeaa4a0SAndreas Gohr * TableFormatter constructor. 26cbeaa4a0SAndreas Gohr * 27cbeaa4a0SAndreas Gohr * @param Colors|null $colors 28cbeaa4a0SAndreas Gohr */ 29cbeaa4a0SAndreas Gohr public function __construct(Colors $colors = null) 30cbeaa4a0SAndreas Gohr { 31cbeaa4a0SAndreas Gohr // try to get terminal width 32cbeaa4a0SAndreas Gohr $width = 0; 33cbeaa4a0SAndreas Gohr if (isset($_SERVER['COLUMNS'])) { 34cbeaa4a0SAndreas Gohr // from environment 35cbeaa4a0SAndreas Gohr $width = (int)$_SERVER['COLUMNS']; 36cbeaa4a0SAndreas Gohr } 37cbeaa4a0SAndreas Gohr if (!$width) { 38cbeaa4a0SAndreas Gohr // via tput command 39cbeaa4a0SAndreas Gohr $width = @exec('tput cols'); 40cbeaa4a0SAndreas Gohr } 41cbeaa4a0SAndreas Gohr if ($width) { 42*43d3f077SAndreas Gohr $this->max = $width - 1; 43cbeaa4a0SAndreas Gohr } 44cbeaa4a0SAndreas Gohr 45cbeaa4a0SAndreas Gohr if ($colors) { 46cbeaa4a0SAndreas Gohr $this->colors = $colors; 47cbeaa4a0SAndreas Gohr } else { 48cbeaa4a0SAndreas Gohr $this->colors = new Colors(); 49cbeaa4a0SAndreas Gohr } 50cbeaa4a0SAndreas Gohr } 51cbeaa4a0SAndreas Gohr 52cbeaa4a0SAndreas Gohr /** 53cbeaa4a0SAndreas Gohr * The currently set border (defaults to ' ') 54cbeaa4a0SAndreas Gohr * 55cbeaa4a0SAndreas Gohr * @return string 56cbeaa4a0SAndreas Gohr */ 57cbeaa4a0SAndreas Gohr public function getBorder() 58cbeaa4a0SAndreas Gohr { 59cbeaa4a0SAndreas Gohr return $this->border; 60cbeaa4a0SAndreas Gohr } 61cbeaa4a0SAndreas Gohr 62cbeaa4a0SAndreas Gohr /** 63cbeaa4a0SAndreas Gohr * Set the border. The border is set between each column. Its width is 64cbeaa4a0SAndreas Gohr * added to the column widths. 65cbeaa4a0SAndreas Gohr * 66cbeaa4a0SAndreas Gohr * @param string $border 67cbeaa4a0SAndreas Gohr */ 68cbeaa4a0SAndreas Gohr public function setBorder($border) 69cbeaa4a0SAndreas Gohr { 70cbeaa4a0SAndreas Gohr $this->border = $border; 71cbeaa4a0SAndreas Gohr } 72cbeaa4a0SAndreas Gohr 73cbeaa4a0SAndreas Gohr /** 74cbeaa4a0SAndreas Gohr * Width of the terminal in characters 75cbeaa4a0SAndreas Gohr * 76cbeaa4a0SAndreas Gohr * initially autodetected 77cbeaa4a0SAndreas Gohr * 78cbeaa4a0SAndreas Gohr * @return int 79cbeaa4a0SAndreas Gohr */ 80cbeaa4a0SAndreas Gohr public function getMaxWidth() 81cbeaa4a0SAndreas Gohr { 82cbeaa4a0SAndreas Gohr return $this->max; 83cbeaa4a0SAndreas Gohr } 84cbeaa4a0SAndreas Gohr 85cbeaa4a0SAndreas Gohr /** 86cbeaa4a0SAndreas Gohr * Set the width of the terminal to assume (in characters) 87cbeaa4a0SAndreas Gohr * 88cbeaa4a0SAndreas Gohr * @param int $max 89cbeaa4a0SAndreas Gohr */ 90cbeaa4a0SAndreas Gohr public function setMaxWidth($max) 91cbeaa4a0SAndreas Gohr { 92cbeaa4a0SAndreas Gohr $this->max = $max; 93cbeaa4a0SAndreas Gohr } 94cbeaa4a0SAndreas Gohr 95cbeaa4a0SAndreas Gohr /** 96cbeaa4a0SAndreas Gohr * Takes an array with dynamic column width and calculates the correct width 97cbeaa4a0SAndreas Gohr * 98cbeaa4a0SAndreas Gohr * Column width can be given as fixed char widths, percentages and a single * width can be given 99cbeaa4a0SAndreas Gohr * for taking the remaining available space. When mixing percentages and fixed widths, percentages 100cbeaa4a0SAndreas Gohr * refer to the remaining space after allocating the fixed width 101cbeaa4a0SAndreas Gohr * 102cbeaa4a0SAndreas Gohr * @param array $columns 103cbeaa4a0SAndreas Gohr * @return int[] 104cbeaa4a0SAndreas Gohr * @throws Exception 105cbeaa4a0SAndreas Gohr */ 106cbeaa4a0SAndreas Gohr protected function calculateColLengths($columns) 107cbeaa4a0SAndreas Gohr { 108cbeaa4a0SAndreas Gohr $idx = 0; 109cbeaa4a0SAndreas Gohr $border = $this->strlen($this->border); 110cbeaa4a0SAndreas Gohr $fixed = (count($columns) - 1) * $border; // borders are used already 111cbeaa4a0SAndreas Gohr $fluid = -1; 112cbeaa4a0SAndreas Gohr 113cbeaa4a0SAndreas Gohr // first pass for format check and fixed columns 114cbeaa4a0SAndreas Gohr foreach ($columns as $idx => $col) { 115cbeaa4a0SAndreas Gohr // handle fixed columns 116cbeaa4a0SAndreas Gohr if ((string)intval($col) === (string)$col) { 117cbeaa4a0SAndreas Gohr $fixed += $col; 118cbeaa4a0SAndreas Gohr continue; 119cbeaa4a0SAndreas Gohr } 120cbeaa4a0SAndreas Gohr // check if other colums are using proper units 121cbeaa4a0SAndreas Gohr if (substr($col, -1) == '%') { 122cbeaa4a0SAndreas Gohr continue; 123cbeaa4a0SAndreas Gohr } 124cbeaa4a0SAndreas Gohr if ($col == '*') { 125cbeaa4a0SAndreas Gohr // only one fluid 126cbeaa4a0SAndreas Gohr if ($fluid < 0) { 127cbeaa4a0SAndreas Gohr $fluid = $idx; 128cbeaa4a0SAndreas Gohr continue; 129cbeaa4a0SAndreas Gohr } else { 130cbeaa4a0SAndreas Gohr throw new Exception('Only one fluid column allowed!'); 131cbeaa4a0SAndreas Gohr } 132cbeaa4a0SAndreas Gohr } 133cbeaa4a0SAndreas Gohr throw new Exception("unknown column format $col"); 134cbeaa4a0SAndreas Gohr } 135cbeaa4a0SAndreas Gohr 136cbeaa4a0SAndreas Gohr $alloc = $fixed; 137cbeaa4a0SAndreas Gohr $remain = $this->max - $alloc; 138cbeaa4a0SAndreas Gohr 139cbeaa4a0SAndreas Gohr // second pass to handle percentages 140cbeaa4a0SAndreas Gohr foreach ($columns as $idx => $col) { 141cbeaa4a0SAndreas Gohr if (substr($col, -1) != '%') { 142cbeaa4a0SAndreas Gohr continue; 143cbeaa4a0SAndreas Gohr } 144cbeaa4a0SAndreas Gohr $perc = floatval($col); 145cbeaa4a0SAndreas Gohr 146cbeaa4a0SAndreas Gohr $real = (int)floor(($perc * $remain) / 100); 147cbeaa4a0SAndreas Gohr 148cbeaa4a0SAndreas Gohr $columns[$idx] = $real; 149cbeaa4a0SAndreas Gohr $alloc += $real; 150cbeaa4a0SAndreas Gohr } 151cbeaa4a0SAndreas Gohr 152cbeaa4a0SAndreas Gohr $remain = $this->max - $alloc; 153cbeaa4a0SAndreas Gohr if ($remain < 0) { 154cbeaa4a0SAndreas Gohr throw new Exception("Wanted column widths exceed available space"); 155cbeaa4a0SAndreas Gohr } 156cbeaa4a0SAndreas Gohr 157cbeaa4a0SAndreas Gohr // assign remaining space 158cbeaa4a0SAndreas Gohr if ($fluid < 0) { 159cbeaa4a0SAndreas Gohr $columns[$idx] += ($remain); // add to last column 160cbeaa4a0SAndreas Gohr } else { 161cbeaa4a0SAndreas Gohr $columns[$fluid] = $remain; 162cbeaa4a0SAndreas Gohr } 163cbeaa4a0SAndreas Gohr 164cbeaa4a0SAndreas Gohr return $columns; 165cbeaa4a0SAndreas Gohr } 166cbeaa4a0SAndreas Gohr 167cbeaa4a0SAndreas Gohr /** 168cbeaa4a0SAndreas Gohr * Displays text in multiple word wrapped columns 169cbeaa4a0SAndreas Gohr * 170cbeaa4a0SAndreas Gohr * @param int[] $columns list of column widths (in characters, percent or '*') 171cbeaa4a0SAndreas Gohr * @param string[] $texts list of texts for each column 172cbeaa4a0SAndreas Gohr * @param array $colors A list of color names to use for each column. use empty string for default 173cbeaa4a0SAndreas Gohr * @return string 174cbeaa4a0SAndreas Gohr * @throws Exception 175cbeaa4a0SAndreas Gohr */ 176cbeaa4a0SAndreas Gohr public function format($columns, $texts, $colors = array()) 177cbeaa4a0SAndreas Gohr { 178cbeaa4a0SAndreas Gohr $columns = $this->calculateColLengths($columns); 179cbeaa4a0SAndreas Gohr 180cbeaa4a0SAndreas Gohr $wrapped = array(); 181cbeaa4a0SAndreas Gohr $maxlen = 0; 182cbeaa4a0SAndreas Gohr 183cbeaa4a0SAndreas Gohr foreach ($columns as $col => $width) { 184cbeaa4a0SAndreas Gohr $wrapped[$col] = explode("\n", $this->wordwrap($texts[$col], $width, "\n", true)); 185cbeaa4a0SAndreas Gohr $len = count($wrapped[$col]); 186cbeaa4a0SAndreas Gohr if ($len > $maxlen) { 187cbeaa4a0SAndreas Gohr $maxlen = $len; 188cbeaa4a0SAndreas Gohr } 189cbeaa4a0SAndreas Gohr 190cbeaa4a0SAndreas Gohr } 191cbeaa4a0SAndreas Gohr 192cbeaa4a0SAndreas Gohr $last = count($columns) - 1; 193cbeaa4a0SAndreas Gohr $out = ''; 194cbeaa4a0SAndreas Gohr for ($i = 0; $i < $maxlen; $i++) { 195cbeaa4a0SAndreas Gohr foreach ($columns as $col => $width) { 196cbeaa4a0SAndreas Gohr if (isset($wrapped[$col][$i])) { 197cbeaa4a0SAndreas Gohr $val = $wrapped[$col][$i]; 198cbeaa4a0SAndreas Gohr } else { 199cbeaa4a0SAndreas Gohr $val = ''; 200cbeaa4a0SAndreas Gohr } 201cbeaa4a0SAndreas Gohr $chunk = $this->pad($val, $width); 202cbeaa4a0SAndreas Gohr if (isset($colors[$col]) && $colors[$col]) { 203cbeaa4a0SAndreas Gohr $chunk = $this->colors->wrap($chunk, $colors[$col]); 204cbeaa4a0SAndreas Gohr } 205cbeaa4a0SAndreas Gohr $out .= $chunk; 206cbeaa4a0SAndreas Gohr 207cbeaa4a0SAndreas Gohr // border 208cbeaa4a0SAndreas Gohr if ($col != $last) { 209cbeaa4a0SAndreas Gohr $out .= $this->border; 210cbeaa4a0SAndreas Gohr } 211cbeaa4a0SAndreas Gohr } 212cbeaa4a0SAndreas Gohr $out .= "\n"; 213cbeaa4a0SAndreas Gohr } 214cbeaa4a0SAndreas Gohr return $out; 215cbeaa4a0SAndreas Gohr 216cbeaa4a0SAndreas Gohr } 217cbeaa4a0SAndreas Gohr 218cbeaa4a0SAndreas Gohr /** 219cbeaa4a0SAndreas Gohr * Pad the given string to the correct length 220cbeaa4a0SAndreas Gohr * 221cbeaa4a0SAndreas Gohr * @param string $string 222cbeaa4a0SAndreas Gohr * @param int $len 223cbeaa4a0SAndreas Gohr * @return string 224cbeaa4a0SAndreas Gohr */ 225cbeaa4a0SAndreas Gohr protected function pad($string, $len) 226cbeaa4a0SAndreas Gohr { 227cbeaa4a0SAndreas Gohr $strlen = $this->strlen($string); 228cbeaa4a0SAndreas Gohr if ($strlen > $len) return $string; 229cbeaa4a0SAndreas Gohr 230cbeaa4a0SAndreas Gohr $pad = $len - $strlen; 231cbeaa4a0SAndreas Gohr return $string . str_pad('', $pad, ' '); 232cbeaa4a0SAndreas Gohr } 233cbeaa4a0SAndreas Gohr 234cbeaa4a0SAndreas Gohr /** 235cbeaa4a0SAndreas Gohr * Measures char length in UTF-8 when possible 236cbeaa4a0SAndreas Gohr * 237cbeaa4a0SAndreas Gohr * @param $string 238cbeaa4a0SAndreas Gohr * @return int 239cbeaa4a0SAndreas Gohr */ 240cbeaa4a0SAndreas Gohr protected function strlen($string) 241cbeaa4a0SAndreas Gohr { 242cbeaa4a0SAndreas Gohr // don't count color codes 243cbeaa4a0SAndreas Gohr $string = preg_replace("/\33\\[\\d+(;\\d+)?m/", '', $string); 244cbeaa4a0SAndreas Gohr 245cbeaa4a0SAndreas Gohr if (function_exists('mb_strlen')) { 246cbeaa4a0SAndreas Gohr return mb_strlen($string, 'utf-8'); 247cbeaa4a0SAndreas Gohr } 248cbeaa4a0SAndreas Gohr 249cbeaa4a0SAndreas Gohr return strlen($string); 250cbeaa4a0SAndreas Gohr } 251cbeaa4a0SAndreas Gohr 252cbeaa4a0SAndreas Gohr /** 253cbeaa4a0SAndreas Gohr * @param string $string 254cbeaa4a0SAndreas Gohr * @param int $start 255cbeaa4a0SAndreas Gohr * @param int|null $length 256cbeaa4a0SAndreas Gohr * @return string 257cbeaa4a0SAndreas Gohr */ 258cbeaa4a0SAndreas Gohr protected function substr($string, $start = 0, $length = null) 259cbeaa4a0SAndreas Gohr { 260cbeaa4a0SAndreas Gohr if (function_exists('mb_substr')) { 261cbeaa4a0SAndreas Gohr return mb_substr($string, $start, $length); 262cbeaa4a0SAndreas Gohr } else { 263cbeaa4a0SAndreas Gohr return substr($string, $start, $length); 264cbeaa4a0SAndreas Gohr } 265cbeaa4a0SAndreas Gohr } 266cbeaa4a0SAndreas Gohr 267cbeaa4a0SAndreas Gohr /** 268cbeaa4a0SAndreas Gohr * @param string $str 269cbeaa4a0SAndreas Gohr * @param int $width 270cbeaa4a0SAndreas Gohr * @param string $break 271cbeaa4a0SAndreas Gohr * @param bool $cut 272cbeaa4a0SAndreas Gohr * @return string 273cbeaa4a0SAndreas Gohr * @link http://stackoverflow.com/a/4988494 274cbeaa4a0SAndreas Gohr */ 275cbeaa4a0SAndreas Gohr protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) 276cbeaa4a0SAndreas Gohr { 277cbeaa4a0SAndreas Gohr $lines = explode($break, $str); 278cbeaa4a0SAndreas Gohr foreach ($lines as &$line) { 279cbeaa4a0SAndreas Gohr $line = rtrim($line); 280cbeaa4a0SAndreas Gohr if ($this->strlen($line) <= $width) { 281cbeaa4a0SAndreas Gohr continue; 282cbeaa4a0SAndreas Gohr } 283cbeaa4a0SAndreas Gohr $words = explode(' ', $line); 284cbeaa4a0SAndreas Gohr $line = ''; 285cbeaa4a0SAndreas Gohr $actual = ''; 286cbeaa4a0SAndreas Gohr foreach ($words as $word) { 287cbeaa4a0SAndreas Gohr if ($this->strlen($actual . $word) <= $width) { 288cbeaa4a0SAndreas Gohr $actual .= $word . ' '; 289cbeaa4a0SAndreas Gohr } else { 290cbeaa4a0SAndreas Gohr if ($actual != '') { 291cbeaa4a0SAndreas Gohr $line .= rtrim($actual) . $break; 292cbeaa4a0SAndreas Gohr } 293cbeaa4a0SAndreas Gohr $actual = $word; 294cbeaa4a0SAndreas Gohr if ($cut) { 295cbeaa4a0SAndreas Gohr while ($this->strlen($actual) > $width) { 296cbeaa4a0SAndreas Gohr $line .= $this->substr($actual, 0, $width) . $break; 297cbeaa4a0SAndreas Gohr $actual = $this->substr($actual, $width); 298cbeaa4a0SAndreas Gohr } 299cbeaa4a0SAndreas Gohr } 300cbeaa4a0SAndreas Gohr $actual .= ' '; 301cbeaa4a0SAndreas Gohr } 302cbeaa4a0SAndreas Gohr } 303cbeaa4a0SAndreas Gohr $line .= trim($actual); 304cbeaa4a0SAndreas Gohr } 305cbeaa4a0SAndreas Gohr return implode($break, $lines); 306cbeaa4a0SAndreas Gohr } 307cbeaa4a0SAndreas Gohr}