diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -228,6 +228,10 @@ 'ArcanistGoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistGoLintLinterTestCase.php', 'ArcanistGoTestResultParser' => 'unit/parser/ArcanistGoTestResultParser.php', 'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php', + 'ArcanistGridCell' => 'console/grid/ArcanistGridCell.php', + 'ArcanistGridColumn' => 'console/grid/ArcanistGridColumn.php', + 'ArcanistGridRow' => 'console/grid/ArcanistGridRow.php', + 'ArcanistGridView' => 'console/grid/ArcanistGridView.php', 'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php', 'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php', 'ArcanistHardpoint' => 'hardpoint/ArcanistHardpoint.php', @@ -1246,6 +1250,10 @@ 'ArcanistGoLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistGoTestResultParser' => 'ArcanistTestResultParser', 'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase', + 'ArcanistGridCell' => 'Phobject', + 'ArcanistGridColumn' => 'Phobject', + 'ArcanistGridRow' => 'Phobject', + 'ArcanistGridView' => 'Phobject', 'ArcanistHLintLinter' => 'ArcanistExternalLinter', 'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistHardpoint' => 'Phobject', diff --git a/src/console/grid/ArcanistGridCell.php b/src/console/grid/ArcanistGridCell.php new file mode 100644 --- /dev/null +++ b/src/console/grid/ArcanistGridCell.php @@ -0,0 +1,56 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setContent($content) { + $this->content = $content; + return $this; + } + + public function getContent() { + return $this->content; + } + + public function getContentDisplayWidth() { + $lines = $this->getContentDisplayLines(); + + $width = 0; + foreach ($lines as $line) { + $width = max($width, phutil_utf8_console_strlen($line)); + } + + return $width; + } + + public function getContentDisplayLines() { + $content = $this->getContent(); + $content = tsprintf('%B', $content); + $content = phutil_string_cast($content); + + $lines = phutil_split_lines($content, false); + + $result = array(); + foreach ($lines as $line) { + $result[] = tsprintf('%R', $line); + } + + return $result; + } + + +} diff --git a/src/console/grid/ArcanistGridColumn.php b/src/console/grid/ArcanistGridColumn.php new file mode 100644 --- /dev/null +++ b/src/console/grid/ArcanistGridColumn.php @@ -0,0 +1,41 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setAlignment($alignment) { + $this->alignment = $alignment; + return $this; + } + + public function getAlignment() { + return $this->alignment; + } + + public function setDisplayWidth($display_width) { + $this->displayWidth = $display_width; + return $this; + } + + public function getDisplayWidth() { + return $this->displayWidth; + } + +} diff --git a/src/console/grid/ArcanistGridRow.php b/src/console/grid/ArcanistGridRow.php new file mode 100644 --- /dev/null +++ b/src/console/grid/ArcanistGridRow.php @@ -0,0 +1,40 @@ +setInstancesOf('ArcanistGridCell') + ->setUniqueMethod('getKey') + ->setContext($this, 'setCells') + ->checkValue($cells); + + $this->cells = $cells; + + return $this; + } + + public function getCells() { + return $this->cells; + } + + public function hasCell($key) { + return isset($this->cells[$key]); + } + + public function getCell($key) { + if (!isset($this->cells[$key])) { + throw new Exception( + pht( + 'Row has no cell "%s".\n', + $key)); + } + + return $this->cells[$key]; + } + + +} diff --git a/src/console/grid/ArcanistGridView.php b/src/console/grid/ArcanistGridView.php new file mode 100644 --- /dev/null +++ b/src/console/grid/ArcanistGridView.php @@ -0,0 +1,247 @@ +columns = $columns; + return $this; + } + + public function getColumns() { + return $this->columns; + } + + public function newColumn($key) { + $column = id(new ArcanistGridColumn()) + ->setKey($key); + + $this->columns[$key] = $column; + + return $column; + } + + public function newRow(array $cells) { + assert_instances_of($cells, 'ArcanistGridCell'); + + $row = id(new ArcanistGridRow()) + ->setCells($cells); + + $this->rows[] = $row; + + return $row; + } + + public function drawGrid() { + $columns = $this->getColumns(); + if (!$columns) { + throw new Exception( + pht( + 'Can not draw a grid with no columns!')); + } + + $rows = array(); + foreach ($this->rows as $row) { + $rows[] = $this->drawRow($row); + } + + $rows = phutil_glue($rows, tsprintf("\n")); + + return tsprintf("%s\n", $rows); + } + + private function getDisplayWidth($key) { + if (!isset($this->displayWidths[$key])) { + $column = $this->getColumn($key); + + $width = $column->getDisplayWidth(); + if ($width === null) { + $width = 1; + foreach ($this->getRows() as $row) { + if (!$row->hasCell($key)) { + continue; + } + + $cell = $row->getCell($key); + $width = max($width, $cell->getContentDisplayWidth()); + } + } + + $this->displayWidths[$key] = $width; + } + + return $this->displayWidths[$key]; + } + + public function getColumn($key) { + if (!isset($this->columns[$key])) { + throw new Exception( + pht( + 'Grid has no column "%s".', + $key)); + } + + return $this->columns[$key]; + } + + public function getRows() { + return $this->rows; + } + + private function drawRow(ArcanistGridRow $row) { + $columns = $this->getColumns(); + + $cells = $row->getCells(); + + $out = array(); + $widths = array(); + foreach ($columns as $column_key => $column) { + $display_width = $this->getDisplayWidth($column_key); + + $cell = idx($cells, $column_key); + if ($cell) { + $content = $cell->getContentDisplayLines(); + } else { + $content = array(''); + } + + foreach ($content as $line_key => $line) { + $line_width = phutil_utf8_console_strlen($line); + + if ($line_width === $display_width) { + continue; + } + + if ($line_width < $display_width) { + $line = $this->padContentLineToWidth( + $line, + $line_width, + $display_width, + $column->getAlignment()); + } else if ($line_width > $display_width) { + $line = $this->truncateContentLineToWidth( + $line, + $line_width, + $display_width, + $column->getAlignment()); + } + + $content[$line_key] = $line; + } + + $out[] = $content; + $widths[] = $display_width; + } + + return $this->drawRowLayout($out, $widths); + } + + private function drawRowLayout(array $raw_cells, array $display_widths) { + $line_count = 0; + foreach ($raw_cells as $key => $cells) { + $raw_cells[$key] = array_values($cells); + $line_count = max($line_count, count($cells)); + } + + $line_head = ''; + $cell_separator = ' '; + $line_tail = ''; + + $out = array(); + $cell_count = count($raw_cells); + for ($ii = 0; $ii < $line_count; $ii++) { + $line = array(); + for ($jj = 0; $jj < $cell_count; $jj++) { + if (isset($raw_cells[$jj][$ii])) { + $raw_line = $raw_cells[$jj][$ii]; + } else { + $display_width = $display_widths[$jj]; + $raw_line = str_repeat(' ', $display_width); + } + $line[] = $raw_line; + } + + $line = array( + $line_head, + phutil_glue($line, $cell_separator), + $line_tail, + ); + + $out[] = $line; + } + + $out = phutil_glue($out, tsprintf("\n")); + + return $out; + } + + private function padContentLineToWidth( + $line, + $src_width, + $dst_width, + $alignment) { + + $delta = ($dst_width - $src_width); + + switch ($alignment) { + case ArcanistGridColumn::ALIGNMENT_LEFT: + $head = null; + $tail = str_repeat(' ', $delta); + break; + case ArcanistGridColumn::ALIGNMENT_CENTER: + $head_delta = (int)floor($delta / 2); + $tail_delta = (int)ceil($delta / 2); + + if ($head_delta) { + $head = str_repeat(' ', $head_delta); + } else { + $head = null; + } + + if ($tail_delta) { + $tail = str_repeat(' ', $tail_delta); + } else { + $tail = null; + } + break; + case ArcanistGridColumn::ALIGNMENT_RIGHT: + $head = str_repeat(' ', $delta); + $tail = null; + break; + default: + throw new Exception( + pht( + 'Unknown column alignment "%s".', + $alignment)); + } + + $result = array(); + + if ($head !== null) { + $result[] = $head; + } + + $result[] = $line; + + if ($tail !== null) { + $result[] = $tail; + } + + return $result; + } + + private function truncateContentLineToWidth( + $line, + $src_width, + $dst_width, + $alignment) { + return $line; + } + +}