Changeset View
Changeset View
Standalone View
Standalone View
src/console/view/PhutilConsoleTable.php
- This file was added.
<?php | |||||
/** | |||||
* Show a table in the console. Usage: | |||||
* | |||||
* $table = id(new PhutilConsoleTable()) | |||||
* ->addColumn('id', array('title' => 'ID', 'align' => 'right')) | |||||
* ->addColumn('name', array('title' => 'Username', 'align' => 'center')) | |||||
* ->addColumn('email', array('title' => 'Email Address')) | |||||
* | |||||
* ->addRow(array( | |||||
* 'id' => 12345, | |||||
* 'name' => 'alicoln', | |||||
* 'email' => 'abraham@lincoln.com', | |||||
* )) | |||||
* ->addRow(array( | |||||
* 'id' => 99999999, | |||||
* 'name' => 'jbloggs', | |||||
* 'email' => 'joe@bloggs.com', | |||||
* )) | |||||
* | |||||
* ->setBorders(true) | |||||
* ->draw(); | |||||
*/ | |||||
final class PhutilConsoleTable extends PhutilConsoleView { | |||||
private $columns = array(); | |||||
private $data = array(); | |||||
private $widths = array(); | |||||
private $borders = false; | |||||
private $padding = 1; | |||||
private $showHeader = true; | |||||
const ALIGN_LEFT = 'left'; | |||||
const ALIGN_CENTER = 'center'; | |||||
const ALIGN_RIGHT = 'right'; | |||||
/* -( Configuration )------------------------------------------------------ */ | |||||
public function setBorders($borders) { | |||||
$this->borders = $borders; | |||||
return $this; | |||||
} | |||||
public function setPadding($padding) { | |||||
$this->padding = $padding; | |||||
return $this; | |||||
} | |||||
public function setShowHeader($show_header) { | |||||
$this->showHeader = $show_header; | |||||
return $this; | |||||
} | |||||
/* -( Data )--------------------------------------------------------------- */ | |||||
public function addColumn($key, array $column) { | |||||
PhutilTypeSpec::checkMap($column, array( | |||||
'title' => 'string', | |||||
'align' => 'optional string', | |||||
)); | |||||
$this->columns[$key] = $column; | |||||
return $this; | |||||
} | |||||
public function addColumns(array $columns) { | |||||
foreach ($columns as $key => $column) { | |||||
$this->addColumn($key, $column); | |||||
} | |||||
return $this; | |||||
} | |||||
public function addRow(array $data) { | |||||
$this->data[] = $data; | |||||
foreach ($data as $key => $value) { | |||||
$this->widths[$key] = max( | |||||
idx($this->widths, $key, 0), | |||||
phutil_utf8_console_strlen($value)); | |||||
} | |||||
return $this; | |||||
} | |||||
/* -( Drawing )------------------------------------------------------------ */ | |||||
protected function drawView() { | |||||
return $this->drawLines( | |||||
array_merge( | |||||
$this->getHeader(), | |||||
$this->getBody(), | |||||
$this->getFooter())); | |||||
} | |||||
private function getHeader() { | |||||
$output = array(); | |||||
if ($this->borders) { | |||||
$output[] = $this->formatSeparator('='); | |||||
} | |||||
if (!$this->showHeader) { | |||||
return $output; | |||||
} | |||||
$columns = array(); | |||||
foreach ($this->columns as $key => $column) { | |||||
$title = tsprintf('**%s**', $column['title']); | |||||
if ($this->shouldAddSpacing($key, $column)) { | |||||
$title = $this->alignString( | |||||
$title, | |||||
$this->getWidth($key), | |||||
idx($column, 'align', self::ALIGN_LEFT)); | |||||
} | |||||
$columns[] = $title; | |||||
} | |||||
$output[] = $this->formatRow($columns); | |||||
if ($this->borders) { | |||||
$output[] = $this->formatSeparator('='); | |||||
} | |||||
return $output; | |||||
} | |||||
private function getBody() { | |||||
$output = array(); | |||||
foreach ($this->data as $data) { | |||||
$columns = array(); | |||||
foreach ($this->columns as $key => $column) { | |||||
if (!$this->shouldAddSpacing($key, $column)) { | |||||
$columns[] = idx($data, $key, ''); | |||||
} else { | |||||
$columns[] = $this->alignString( | |||||
idx($data, $key, ''), | |||||
$this->getWidth($key), | |||||
idx($column, 'align', self::ALIGN_LEFT)); | |||||
} | |||||
} | |||||
$output[] = $this->formatRow($columns); | |||||
} | |||||
return $output; | |||||
} | |||||
private function getFooter() { | |||||
$output = array(); | |||||
if ($this->borders) { | |||||
$columns = array(); | |||||
foreach ($this->getColumns() as $column) { | |||||
$columns[] = str_repeat('=', $this->getWidth($column)); | |||||
} | |||||
$output[] = array( | |||||
'+', | |||||
$this->implode('+', $columns), | |||||
'+', | |||||
); | |||||
} | |||||
return $output; | |||||
} | |||||
/* -( Internals )---------------------------------------------------------- */ | |||||
/** | |||||
* Returns if the specified column should have spacing added. | |||||
* | |||||
* @return bool | |||||
*/ | |||||
private function shouldAddSpacing($key, $column) { | |||||
if (!$this->borders) { | |||||
if (last_key($this->columns) === $key) { | |||||
if (idx($column, 'align', self::ALIGN_LEFT) === self::ALIGN_LEFT) { | |||||
// Don't add extra spaces to this column since it's the last column, | |||||
// left aligned, and we're not showing borders. This prevents | |||||
// unnecessary empty lines from appearing when the extra spaces | |||||
// wrap around the terminal. | |||||
return false; | |||||
} | |||||
} | |||||
} | |||||
return true; | |||||
} | |||||
/** | |||||
* Returns the column IDs. | |||||
* | |||||
* @return list<string> | |||||
*/ | |||||
protected function getColumns() { | |||||
return array_keys($this->columns); | |||||
} | |||||
/** | |||||
* Get the width of a specific column, including padding. | |||||
* | |||||
* @param string | |||||
* @return int | |||||
*/ | |||||
protected function getWidth($key) { | |||||
$width = max( | |||||
idx($this->widths, $key), | |||||
phutil_utf8_console_strlen( | |||||
idx(idx($this->columns, $key, array()), 'title', ''))); | |||||
return $width + 2 * $this->padding; | |||||
} | |||||
protected function alignString($string, $width, $align) { | |||||
$num_padding = $width - | |||||
(2 * $this->padding) - phutil_utf8_console_strlen($string); | |||||
switch ($align) { | |||||
case self::ALIGN_LEFT: | |||||
$num_left_padding = 0; | |||||
$num_right_padding = $num_padding; | |||||
break; | |||||
case self::ALIGN_CENTER: | |||||
$num_left_padding = (int)($num_padding / 2); | |||||
$num_right_padding = $num_padding - $num_left_padding; | |||||
break; | |||||
case self::ALIGN_RIGHT: | |||||
$num_left_padding = $num_padding; | |||||
$num_right_padding = 0; | |||||
break; | |||||
} | |||||
$left_padding = str_repeat(' ', $num_left_padding); | |||||
$right_padding = str_repeat(' ', $num_right_padding); | |||||
return array( | |||||
$left_padding, | |||||
$string, | |||||
$right_padding, | |||||
); | |||||
} | |||||
/** | |||||
* Format cells into an entire row. | |||||
* | |||||
* @param list<string> | |||||
* @return string | |||||
*/ | |||||
protected function formatRow(array $columns) { | |||||
$padding = str_repeat(' ', $this->padding); | |||||
if ($this->borders) { | |||||
$separator = $padding.'|'.$padding; | |||||
return array( | |||||
'|'.$padding, | |||||
$this->implode($separator, $columns), | |||||
$padding.'|', | |||||
); | |||||
} else { | |||||
return $this->implode($padding, $columns); | |||||
} | |||||
} | |||||
protected function formatSeparator($string) { | |||||
$columns = array(); | |||||
if ($this->borders) { | |||||
$separator = '+'; | |||||
} else { | |||||
$separator = ''; | |||||
} | |||||
foreach ($this->getColumns() as $column) { | |||||
$columns[] = str_repeat($string, $this->getWidth($column)); | |||||
} | |||||
return array( | |||||
$separator, | |||||
$this->implode($separator, $columns), | |||||
$separator, | |||||
); | |||||
} | |||||
} |