fisrt commit'

This commit is contained in:
2018-05-24 18:51:55 +02:00
commit 906f96a15e
3463 changed files with 425820 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
_ _ ___ _ _
| || | / _ \| || |
| || |_| | | | || |_
|__ _| | | |__ _|
| | | |_| | | |
|_| \___/ |_|
+17
View File
@@ -0,0 +1,17 @@
( )
H
H
_H_
.-'-.-'-.
/ \
| |
| .-------'._
| / / '.' '. \
| \ \ @ @ / /
| '---------'
| _______|
| .'-+-+-+|
| '.-+-+-+|
| """""" |
'-.__ __.-'
"""
+6
View File
@@ -0,0 +1,6 @@
______ _____ _ ______ _____
| ____/\ |_ _| | | ____| __ \
| |__ / \ | | | | | |__ | | | |
| __/ /\ \ | | | | | __| | | | |
| | / ____ \ _| |_| |____| |____| |__| |
|_|/_/ \_\_____|______|______|_____/
+17
View File
@@ -0,0 +1,17 @@
<blue> ( )</blue>
<blue> H</blue>
<blue> H</blue>
<blue> _H_</blue>
<blue> .-'-.-'-.</blue>
<blue> / \</blue>
<blue>| |</blue>
<blue>| .-------'._</blue>
<blue>| /<white>/ '.' '.</white> \</blue>
<blue>| \<white>\ <black><blink>@ @</blink></black> /</white> /</blue>
<blue>| '---------'</blue>
<blue>| _______|</blue>
<blue>| .'<black>-+-+-+</black>|</blue>
<blue>| '.<black>-+-+-+</black>|</blue>
<blue>| """""" |</blue>
<blue>'-.__ __.-'</blue>
<blue> """</blue>
+6
View File
@@ -0,0 +1,6 @@
_____ _____ _____ ______ _____
| __ \ /\ / ____/ ____| ____| __ \
| |__) / \ | (___| (___ | |__ | | | |
| ___/ /\ \ \___ \\___ \| __| | | | |
| | / ____ \ ____) |___) | |____| |__| |
|_| /_/ \_\_____/_____/|______|_____/
+384
View File
@@ -0,0 +1,384 @@
<?php
namespace League\CLImate\Argument;
class Argument
{
/**
* An argument's name.
*
* Use this name when internally referring to the argument.
*
* @var string
*/
protected $name;
/**
* An argument's short representation.
*
* @var string
*/
protected $prefix;
/**
* An argument's long representation.
*
* @var string
*/
protected $longPrefix;
/**
* An argument's description.
*
* @var string
*/
protected $description;
/**
* Whether or not an argument is required.
*
* @var bool
*/
protected $required = false;
/**
* Whether or not an argument only needs to be defined to have a value.
*
* These arguments have the value true when they are defined on the command
* line.
*
* @var bool
*/
protected $noValue = false;
/**
* Which data type to cast an argument's value to.
*
* Valid data types are "string", "int", "float", and "bool".
*
* @var string
*/
protected $castTo = 'string';
/**
* An argument's default value.
*
* @var string
*/
protected $defaultValue;
/**
* An argument's value, after type casting.
*
* @var string|int|float|bool
*/
protected $value;
/**
* Build a new command argument.
*
* @param string $name
*/
public function __construct($name)
{
$this->setName($name);
}
/**
* Build a new command argument from an array.
*
* @param string $name
* @param array $params
*
* @return Argument
*/
public static function createFromArray($name, array $params)
{
$argument = new Argument($name);
$params = self::getSettableArgumentParams($params);
foreach ($params as $key => $value) {
$method = 'set' . ucwords($key);
$argument->{$method}($value);
}
if ($argument->defaultValue()) {
$argument->setValue($argument->defaultValue());
}
return $argument;
}
/**
* Get argument params based on settable properties
*
* @param array $params
*
* @return array
*/
protected static function getSettableArgumentParams(array $params)
{
$allowed = [
'prefix',
'longPrefix',
'description',
'required',
'noValue',
'castTo',
'defaultValue',
];
return array_intersect_key($params, array_flip($allowed));
}
/**
* Retrieve an argument's name.
*
* Use this name when internally referring to the argument.
*
* @return string
*/
public function name()
{
return $this->name;
}
/**
* Set an argument's name.
*
* Use this name when internally referring to the argument.
*
* @param string $name
*/
protected function setName($name)
{
$this->name = trim($name);
}
/**
* Retrieve an argument's short form.
*
* @return string
*/
public function prefix()
{
return $this->prefix;
}
/**
* Set an argument's short form.
*
* @param string $prefix
*/
protected function setPrefix($prefix)
{
$this->prefix = trim($prefix);
}
/**
* Retrieve an argument's long form.
*
* @return string
*/
public function longPrefix()
{
return $this->longPrefix;
}
/**
* Set an argument's short form.
*
* @param string $longPrefix
*/
protected function setLongPrefix($longPrefix)
{
$this->longPrefix = trim($longPrefix);
}
/**
* Determine if an argument has a prefix.
*
* @return bool
*/
public function hasPrefix()
{
return $this->prefix() || $this->longPrefix();
}
/**
* Retrieve an argument's description.
*
* @return string
*/
public function description()
{
return $this->description;
}
/**
* Set an argument's description.
*
* @param string $description
*/
protected function setDescription($description)
{
$this->description = trim($description);
}
/**
* Determine whether or not an argument is required.
*
* @return bool
*/
public function isRequired()
{
return $this->required;
}
/**
* Set whether an argument is required or not.
*
* @param bool $required
*/
protected function setRequired($required)
{
$this->required = (bool) $required;
}
/**
* Determine whether or not an argument only needs to be defined to have a
* value.
*
* @return bool
*/
public function noValue()
{
return $this->noValue;
}
/**
* Set whether or not an argument only needs to be defined to have a value.
*
* @param bool $noValue
*/
protected function setNoValue($noValue)
{
$this->setCastTo('bool');
$this->noValue = (bool) $noValue;
}
/**
* Retrieve the data type to cast an argument's value to.
*
* @return string
*/
public function castTo()
{
return $this->castTo;
}
/**
* Set the data type to cast an argument's value to.
*
* Valid data types are "string", "int", "float", and "bool".
*
* @throws \Exception if $castTo is not a valid data type.
* @param string $castTo
*/
protected function setCastTo($castTo)
{
if (!in_array($castTo, ['string', 'int', 'float', 'bool'])) {
throw new \Exception(
"An argument may only be cast to the data type "
. "'string', 'int', 'float', or 'bool'."
);
}
$this->castTo = $this->noValue() ? 'bool' : $castTo;
}
/**
* Retrieve an argument's default value.
*
* @return string
*/
public function defaultValue()
{
return $this->defaultValue;
}
/**
* Set an argument's default value.
*
* @param string $defaultValue
*/
public function setDefaultValue($defaultValue)
{
$this->defaultValue = $defaultValue;
}
/**
* Retrieve an argument's value.
*
* Argument values are type cast based on the value of $castTo.
*
* @return string|int|float|bool
*/
public function value()
{
return $this->value;
}
/**
* Set an argument's value based on its command line entry.
*
* Argument values are type cast based on the value of $castTo.
*
* @param string|bool $value
*/
public function setValue($value)
{
$cast_method = 'castTo' . ucwords($this->castTo);
$this->value = $this->{$cast_method}($value);
}
/**
* @param string $value
*
* @return string
*/
protected function castToString($value)
{
return (string) $value;
}
/**
* @param string $value
*
* @return int
*/
protected function castToInt($value)
{
return (int) $value;
}
/**
* @param string $value
*
* @return float
*/
protected function castToFloat($value)
{
return (float) $value;
}
/**
* @param string $value
*
* @return bool
*/
protected function castToBool($value)
{
return (bool) $value;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
namespace League\CLImate\Argument;
class Filter
{
protected $arguments = [];
/**
* Set the available arguments
*
* @param array $arguments
*/
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
}
/**
* Retrieve optional arguments
*
* @return Argument[]
*/
public function optional()
{
return $this->filterArguments(['isOptional']);
}
/**
* Retrieve required arguments
*
* @return Argument[]
*/
public function required()
{
return $this->filterArguments(['isRequired']);
}
/**
* Retrieve arguments with prefix
*
* @return Argument[]
*/
public function withPrefix()
{
return $this->filterArguments(['hasPrefix']);
}
/**
* Retrieve arguments without prefix
*
* @return Argument[]
*/
public function withoutPrefix()
{
return $this->filterArguments(['noPrefix']);
}
/**
* Find all required arguments that don't have values after parsing.
*
* These arguments weren't defined on the command line.
*
* @return Argument[]
*/
public function missing()
{
return $this->filterArguments(['isRequired', 'noValue']);
}
/**
* Filter defined arguments as to whether they are required or not
*
* @param string[] $filters
*
* @return Argument[]
*/
protected function filterArguments($filters = [])
{
$arguments = $this->arguments;
foreach ($filters as $filter) {
$arguments = array_filter($arguments, [$this, $filter]);
}
if (in_array('hasPrefix', $filters)) {
usort($arguments, [$this, 'compareByPrefix']);
}
return array_values($arguments);
}
/**
* Determine whether an argument as a prefix
*
* @param Argument $argument
*
* @return bool
*/
protected function noPrefix($argument)
{
return !$argument->hasPrefix();
}
/**
* Determine whether an argument as a prefix
*
* @param Argument $argument
*
* @return bool
*/
protected function hasPrefix($argument)
{
return $argument->hasPrefix();
}
/**
* Determine whether an argument is required
*
* @param Argument $argument
*
* @return bool
*/
protected function isRequired($argument)
{
return $argument->isRequired();
}
/**
* Determine whether an argument is optional
*
* @param Argument $argument
*
* @return bool
*/
protected function isOptional($argument)
{
return !$argument->isRequired();
}
/**
* Determine whether an argument is optional
*
* @param Argument $argument
*
* @return bool
*/
protected function noValue($argument)
{
return is_null($argument->value());
}
/**
* Compare two arguments by their short and long prefixes.
*
* @see usort()
*
* @param Argument $a
* @param Argument $b
*
* @return int
*/
public function compareByPrefix(Argument $a, Argument $b)
{
if ($this->prefixCompareString($a) < $this->prefixCompareString($b)) {
return -1;
}
return 1;
}
/**
* Prep the prefix string for comparison
*
* @param Argument $argument
*
* @return string
*/
protected function prefixCompareString(Argument $argument)
{
return strtolower($argument->longPrefix() ?: $argument->prefix() ?: '');
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
namespace League\CLImate\Argument;
use League\CLImate\CLImate;
class Manager
{
/**
* An array of arguments passed to the program.
*
* @var Argument[] $arguments
*/
protected $arguments = [];
/**
* A program's description.
*
* @var string $description
*/
protected $description;
/**
* Filter class to find various types of arguments
*
* @var \League\CLImate\Argument\Filter $filter
*/
protected $filter;
/**
* Summary builder class
*
* @var \League\CLImate\Argument\Summary $summary
*/
protected $summary;
/**
* Argument parser class
*
* @var \League\CLImate\Argument\Parser $parser
*/
protected $parser;
public function __construct()
{
$this->filter = new Filter();
$this->summary = new Summary();
$this->parser = new Parser();
}
/**
* Add an argument.
*
* @throws \Exception if $argument isn't an array or Argument object.
* @param Argument|string|array $argument
* @param $options
*/
public function add($argument, array $options = [])
{
if (is_array($argument)) {
$this->addMany($argument);
return;
}
if (is_string($argument)) {
$argument = Argument::createFromArray($argument, $options);
}
if (!($argument instanceof Argument)) {
throw new \Exception('Please provide an argument name or object.');
}
$this->arguments[$argument->name()] = $argument;
}
/**
* Add multiple arguments to a CLImate script.
*
* @param array $arguments
*/
protected function addMany(array $arguments = [])
{
foreach ($arguments as $name => $options) {
$this->add($name, $options);
}
}
/**
* Determine if an argument exists.
*
* @param string $name
* @return bool
*/
public function exists($name)
{
return isset($this->arguments[$name]);
}
/**
* Retrieve an argument's value.
*
* @param string $name
* @return string|int|float|bool|null
*/
public function get($name)
{
return isset($this->arguments[$name]) ? $this->arguments[$name]->value() : null;
}
/**
* Retrieve all arguments.
*
* @return Argument[]
*/
public function all()
{
return $this->arguments;
}
/**
* Determine if an argument has been defined on the command line.
*
* This can be useful for making sure an argument is present on the command
* line before parse()'ing them into argument objects.
*
* @param string $name
* @param array $argv
*
* @return bool
*/
public function defined($name, array $argv = null)
{
// The argument isn't defined if it's not defined by the calling code.
if (!$this->exists($name)) {
return false;
}
$argument = $this->arguments[$name];
$command_arguments = $this->parser->arguments($argv);
foreach ($command_arguments as $command_argument) {
if ($this->isArgument($argument, $command_argument)) {
return true;
}
}
return false;
}
/**
* Check if the defined argument matches the command argument.
*
* @param Argument $argument
* @param string $command_argument
*
* @return bool
*/
protected function isArgument($argument, $command_argument)
{
$possibilities = [
$argument->prefix() => "-{$argument->prefix()}",
$argument->longPrefix() => "--{$argument->longPrefix()}",
];
foreach ($possibilities as $key => $search) {
if ($key && strpos($command_argument, $search) === 0) {
return true;
}
}
return false;
}
/**
* Retrieve all arguments as key/value pairs.
*
* @return array
*/
public function toArray()
{
$return = [];
foreach ($this->all() as $name => $argument) {
$return[$name] = $argument->value();
}
return $return;
}
/**
* Set a program's description.
*
* @param string $description
*/
public function description($description)
{
$this->description = trim($description);
}
/**
* Output a script's usage statement.
*
* @param CLImate $climate
* @param array $argv
*/
public function usage(CLImate $climate, array $argv = null)
{
$this->summary
->setClimate($climate)
->setDescription($this->description)
->setCommand($this->parser->command($argv))
->setFilter($this->filter, $this->all())
->output();
}
/**
* Parse command line arguments into CLImate arguments.
*
* @throws \Exception if required arguments aren't defined.
* @param array $argv
*/
public function parse(array $argv = null)
{
$this->parser->setFilter($this->filter, $this->all());
$this->parser->parse($argv);
}
/**
* Get the trailing arguments
*
* @return string|null
*/
public function trailing()
{
return $this->parser->trailing();
}
}
+278
View File
@@ -0,0 +1,278 @@
<?php
namespace League\CLImate\Argument;
class Parser
{
/**
* Filter class to find various types of arguments
*
* @var \League\CLImate\Argument\Filter $filter
*/
protected $filter;
/**
* Summary builder class
*
* @var \League\CLImate\Argument\Summary $summary
*/
protected $summary;
protected $trailing;
public function __construct()
{
$this->summary = new Summary();
}
/**
* @param Filter $filter
* @param Argument[] $arguments
*
* @return \League\CLImate\Argument\Parser
*/
public function setFilter($filter, $arguments)
{
$this->filter = $filter;
$this->filter->setArguments($arguments);
return $this;
}
/**
* Parse command line arguments into CLImate arguments.
*
* @throws \Exception if required arguments aren't defined.
* @param array $argv
*/
public function parse(array $argv = null)
{
$cliArguments = $this->arguments($argv);
if (in_array('--', $cliArguments)) {
$cliArguments = $this->removeTrailingArguments($cliArguments);
}
$unParsedArguments = $this->prefixedArguments($cliArguments);
$this->nonPrefixedArguments($unParsedArguments);
// After parsing find out which arguments were required but not
// defined on the command line.
$missingArguments = $this->filter->missing();
if (count($missingArguments) > 0) {
throw new \Exception(
'The following arguments are required: '
. $this->summary->short($missingArguments) . '.'
);
}
}
/**
* Get the command name.
*
* @param array $argv
*
* @return string
*/
public function command(array $argv = null)
{
return $this->getCommandAndArguments($argv)['command'];
}
/**
* Get the passed arguments.
*
* @param array $argv
*
* @return array
*/
public function arguments(array $argv = null)
{
return $this->getCommandAndArguments($argv)['arguments'];
}
/**
* Get the trailing arguments
*
* @return string|null
*/
public function trailing()
{
return $this->trailing;
}
/**
* Remove the trailing arguments from the parser and set them aside
*
* @param array $arguments
*
* @return array
*/
protected function removeTrailingArguments(array $arguments)
{
$trailing = array_splice($arguments, array_search('--', $arguments));
array_shift($trailing);
$this->trailing = implode(' ', $trailing);
return $arguments;
}
/**
* Parse command line options into prefixed CLImate arguments.
*
* Prefixed arguments are arguments with a prefix (-) or a long prefix (--)
* on the command line.
*
* Return the arguments passed on the command line that didn't match up with
* prefixed arguments so they can be assigned to non-prefixed arguments.
*
* @param array $argv
* @return array
*/
protected function prefixedArguments(array $argv = [])
{
foreach ($argv as $key => $passed_argument) {
$argv = $this->trySettingArgumentValue($argv, $key, $passed_argument);
}
// Send un-parsed arguments back upstream.
return array_values($argv);
}
/**
* Parse unset command line options into non-prefixed CLImate arguments.
*
* Non-prefixed arguments are parsed after the prefixed arguments on the
* command line, in the order that they're defined in the script.
*
* @param array $unParsedArguments
*/
protected function nonPrefixedArguments(array $unParsedArguments = [])
{
foreach ($this->filter->withoutPrefix() as $key => $argument) {
if (isset($unParsedArguments[$key])) {
$argument->setValue($unParsedArguments[$key]);
}
}
}
/**
* Parse the name and value of the argument passed in
*
* @param string $cliArgument
* @return string[] [$name, $value]
*/
protected function getNameAndValue($cliArgument)
{
// Look for arguments defined in the "key=value" format.
if (strpos($cliArgument, '=') !== false) {
return explode('=', $cliArgument, 2);
}
// If the argument isn't in "key=value" format then assume it's in
// "key value" format and define the value after we've found the
// matching CLImate argument.
return [$cliArgument, null];
}
/**
* Attempt to set the an argument's value and remove applicable
* arguments from array
*
* @param array $argv
* @param int $key
* @param string $passed_argument
*
* @return array The new $argv
*/
protected function trySettingArgumentValue($argv, $key, $passed_argument)
{
list($name, $value) = $this->getNameAndValue($passed_argument);
// Look for the argument in our defined $arguments
// and assign their value.
if (!($argument = $this->findPrefixedArgument($name))) {
return $argv;
}
// We found an argument key, so take it out of the array.
unset($argv[$key]);
return $this->setArgumentValue($argv, $argument, $key, $value);
}
/**
* Set the argument's value
*
* @param array $argv
* @param Argument $argument
* @param int $key
* @param string|null $value
*
* @return array The new $argv
*/
protected function setArgumentValue($argv, $argument, $key, $value)
{
// Arguments are given the value true if they only need to
// be defined on the command line to be set.
if ($argument->noValue()) {
$argument->setValue(true);
return $argv;
}
if (is_null($value)) {
if (count($argv) === 0) {
return $argv;
}
// If the value wasn't previously defined in "key=value"
// format then define it from the next command argument.
$argument->setValue($argv[++$key]);
unset($argv[$key]);
return $argv;
}
$argument->setValue($value);
return $argv;
}
/**
* Search for argument in defined prefix arguments
*
* @param string $name
*
* @return Argument|false
*/
protected function findPrefixedArgument($name)
{
foreach ($this->filter->withPrefix() as $argument) {
if (in_array($name, ["-{$argument->prefix()}", "--{$argument->longPrefix()}"])) {
return $argument;
}
}
return false;
}
/**
* Pull a command name and arguments from $argv.
*
* @param array $argv
* @return array
*/
protected function getCommandAndArguments(array $argv = null)
{
// If no $argv is provided then use the global PHP defined $argv.
if (is_null($argv)) {
global $argv;
}
$arguments = $argv;
$command = array_shift($arguments);
return compact('arguments', 'command');
}
}
+211
View File
@@ -0,0 +1,211 @@
<?php
namespace League\CLImate\Argument;
use League\CLImate\CLImate;
class Summary
{
/**
* @var \League\CLImate\CLImate $climate
*/
protected $climate;
/**
* @var string $description
*/
protected $description;
/**
* @var string $command
*/
protected $command;
/**
* @var Filter $filter
*/
protected $filter;
/**
* @param \League\CLImate\CLImate $climate
*
* @return \League\CLImate\Argument\Summary
*/
public function setClimate(CLImate $climate)
{
$this->climate = $climate;
return $this;
}
/**
* @param string $description
*
* @return \League\CLImate\Argument\Summary
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* @param string $command
*
* @return \League\CLImate\Argument\Summary
*/
public function setCommand($command)
{
$this->command = $command;
return $this;
}
/**
* @param Filter $filter
* @param Argument[] $arguments
*
* @return \League\CLImate\Argument\Summary
*/
public function setFilter($filter, $arguments)
{
$this->filter = $filter;
$this->filter->setArguments($arguments);
return $this;
}
/**
* Output the full summary for the program
*/
public function output()
{
// Print the description if it's defined.
if ($this->description) {
$this->climate->out($this->description)->br();
}
// Print the usage statement with the arguments without a prefix at the end.
$this->climate->out("Usage: {$this->command} "
. $this->short($this->getOrderedArguments()));
// Print argument details.
foreach (['required', 'optional'] as $type) {
$this->outputArguments($this->filter->{$type}(), $type);
}
}
/**
* Build a short summary of a list of arguments.
*
* @param Argument[] $arguments
*
* @return string
*/
public function short($arguments)
{
return implode(' ', array_map([$this, 'argumentBracketed'], $arguments));
}
/**
* Build an argument's summary for use in a usage statement.
*
* For example, "-u username, --user username", "--force", or
* "-c count (default: 7)".
*
* @param Argument $argument
*
* @return string
*/
public function argument(Argument $argument)
{
$summary = $this->prefixedArguments($argument);
$printedName = strstr($summary, ' ' . $argument->name());
// Print the argument name if it's not printed yet.
if (!$printedName && !$argument->noValue()) {
$summary .= $argument->name();
}
if ($argument->defaultValue()) {
$summary .= " (default: {$argument->defaultValue()})";
}
return $summary;
}
/**
* Build argument summary surrounded by brackets
*
* @param Argument $argument
*
* @return string
*/
protected function argumentBracketed(Argument $argument)
{
return '[' . $this->argument($argument) . ']';
}
/**
* Get the arguments ordered by whether or not they have a prefix
*
* @return Argument[]
*/
protected function getOrderedArguments()
{
return array_merge($this->filter->withPrefix(), $this->filter->withoutPrefix());
}
/**
* Print out the argument list
*
* @param array $arguments
* @param string $type
*/
protected function outputArguments($arguments, $type)
{
if (count($arguments) == 0) {
return;
}
$this->climate->br()->out(ucwords($type) . ' Arguments:');
foreach ($arguments as $argument) {
$this->climate->tab()->out($this->argument($argument));
if ($argument->description()) {
$this->climate->tab(2)->out($argument->description());
}
}
}
/**
* Builds the summary for any prefixed arguments
*
* @param Argument $argument
*
* @return string
*/
protected function prefixedArguments(Argument $argument)
{
$prefixes = [$argument->prefix(), $argument->longPrefix()];
$summary = [];
foreach ($prefixes as $key => $prefix) {
if (!$prefix) {
continue;
}
$sub = str_repeat('-', $key + 1) . $prefix;
if (!$argument->noValue()) {
$sub .= " {$argument->name()}";
}
$summary[] = $sub;
}
return implode(', ', $summary);
}
}
+442
View File
@@ -0,0 +1,442 @@
<?php
namespace League\CLImate;
use League\CLImate\Argument\Manager as ArgumentManager;
use League\CLImate\Decorator\Style;
use League\CLImate\Settings\Manager as SettingsManager;
use League\CLImate\TerminalObject\Router\Router;
use League\CLImate\Util\Helper;
use League\CLImate\Util\Output;
use League\CLImate\Util\UtilFactory;
/**
* @method mixed black(string $str = null)
* @method mixed red(string $str = null)
* @method mixed green(string $str = null)
* @method mixed yellow(string $str = null)
* @method mixed blue(string $str = null)
* @method mixed magenta(string $str = null)
* @method mixed cyan(string $str = null)
* @method mixed lightGray(string $str = null)
* @method mixed darkGray(string $str = null)
* @method mixed lightRed(string $str = null)
* @method mixed lightGreen(string $str = null)
* @method mixed lightYellow(string $str = null)
* @method mixed lightBlue(string $str = null)
* @method mixed lightMagenta(string $str = null)
* @method mixed lightCyan(string $str = null)
* @method mixed white(string $str = null)
*
* @method mixed backgroundBlack(string $str = null)
* @method mixed backgroundRed(string $str = null)
* @method mixed backgroundGreen(string $str = null)
* @method mixed backgroundYellow(string $str = null)
* @method mixed backgroundBlue(string $str = null)
* @method mixed backgroundMagenta(string $str = null)
* @method mixed backgroundCyan(string $str = null)
* @method mixed backgroundLightGray(string $str = null)
* @method mixed backgroundDarkGray(string $str = null)
* @method mixed backgroundLightRed(string $str = null)
* @method mixed backgroundLightGreen(string $str = null)
* @method mixed backgroundLightYellow(string $str = null)
* @method mixed backgroundLightBlue(string $str = null)
* @method mixed backgroundLightMagenta(string $str = null)
* @method mixed backgroundLightCyan(string $str = null)
* @method mixed backgroundWhite(string $str = null)
*
* @method mixed bold(string $str = null)
* @method mixed dim(string $str = null)
* @method mixed underline(string $str = null)
* @method mixed blink(string $str = null)
* @method mixed invert(string $str = null)
* @method mixed hidden(string $str = null)
*
* @method mixed info(string $str = null)
* @method mixed comment(string $str = null)
* @method mixed whisper(string $str = null)
* @method mixed shout(string $str = null)
* @method mixed error(string $str = null)
*
* @method mixed out(string $str)
* @method mixed inline(string $str)
* @method mixed table(array $data)
* @method mixed json(mixed $var)
* @method mixed br($count = 1)
* @method mixed tab($count = 1)
* @method mixed draw(string $art)
* @method mixed border(string $char = null, integer $length = null)
* @method mixed dump(mixed $var)
* @method mixed flank(string $output, string $char = null, integer $length = null)
* @method mixed progress(integer $total = null)
* @method mixed padding(integer $length = 0, string $char = '.')
* @method mixed input(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed confirm(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed password(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed checkboxes(string $prompt, array $options, Util\Reader\ReaderInterface $reader = null)
* @method mixed radio(string $prompt, array $options, Util\Reader\ReaderInterface $reader = null)
* @method mixed animation(string $art, TerminalObject\Helper\Sleeper $sleeper = null)
* @method mixed columns(array $data, $column_count = null)
* @method mixed clear()
*
* @method CLImate addArt(string $dir)
*/
class CLImate
{
/**
* An instance of the Style class
*
* @var \League\CLImate\Decorator\Style $style
*/
public $style;
/**
* An instance of the Terminal Object Router class
*
* @var \League\CLImate\TerminalObject\Router\Router $router
*/
protected $router;
/**
* An instance of the Settings Manager class
*
* @var \League\CLImate\Settings\Manager $settings
*/
protected $settings;
/**
* An instance of the Argument Manager class
*
* @var \League\CLImate\Argument\Manager $arguments
*/
public $arguments;
/**
* An instance of the Output class
*
* @var \League\CLImate\Util\Output $output
*/
public $output;
/**
* An instance of the Util Factory
*
* @var \League\CLImate\Util\UtilFactory $util
*/
protected $util;
public function __construct()
{
$this->setStyle(new Style());
$this->setRouter(new Router());
$this->setSettingsManager(new SettingsManager());
$this->setOutput(new Output());
$this->setUtil(new UtilFactory());
$this->setArgumentManager(new ArgumentManager());
}
/**
* Set the style property
*
* @param \League\CLImate\Decorator\Style $style
*/
public function setStyle(Style $style)
{
$this->style = $style;
}
/**
* Set the router property
*
* @param \League\CLImate\TerminalObject\Router\Router $router
*/
public function setRouter(Router $router)
{
$this->router = $router;
}
/**
* Set the settings property
*
* @param \League\CLImate\Settings\Manager $manager
*/
public function setSettingsManager(SettingsManager $manager)
{
$this->settings = $manager;
}
/**
* Set the arguments property
*
* @param \League\CLImate\Argument\Manager $manager
*/
public function setArgumentManager(ArgumentManager $manager)
{
$this->arguments = $manager;
}
/**
* Set the output property
*
* @param \League\CLImate\Util\Output $output
*/
public function setOutput(Output $output)
{
$this->output = $output;
}
/**
* Set the util property
*
* @param \League\CLImate\Util\UtilFactory $util
*/
public function setUtil(UtilFactory $util)
{
$this->util = $util;
}
/**
* Extend CLImate with custom methods
*
* @param string|object|array $class
* @param string $key Optional custom key instead of class name
*
* @return \League\CLImate\CLImate
*/
public function extend($class, $key = null)
{
$this->router->addExtension($key, $class);
return $this;
}
/**
* Force ansi support on
*
* @return \League\CLImate\CLImate
*/
public function forceAnsiOn()
{
$this->util->system->forceAnsi();
return $this;
}
/**
* Force ansi support off
*
* @return \League\CLImate\CLImate
*/
public function forceAnsiOff()
{
$this->util->system->forceAnsi(false);
return $this;
}
/**
* Write line to writer once
*
* @param string|array $writer
*
* @return \League\CLImate\CLImate
*/
public function to($writer)
{
$this->output->once($writer);
return $this;
}
/**
* Output the program's usage statement
*
* @param array $argv
*/
public function usage(array $argv = null)
{
return $this->arguments->usage($this, $argv);
}
/**
* Set the program's description
*
* @param string $description
*
* @return \League\CLImate\CLImate
*/
public function description($description)
{
$this->arguments->description($description);
return $this;
}
/**
* Check if we have valid output
*
* @param mixed $output
*
* @return boolean
*/
protected function hasOutput($output)
{
if (!empty($output)) {
return true;
}
// Check for type first to avoid errors with objects/arrays/etc
return ((is_string($output) || is_numeric($output)) && strlen($output) > 0);
}
/**
* Search for the method within the string
* and route it if we find one.
*
* @param string $method
* @param string $name
*
* @return string The new string without the executed method.
*/
protected function parseStyleMethod($method, $name)
{
// If the name starts with this method string...
if (substr($name, 0, strlen($method)) == $method) {
// ...remove the method name from the beginning of the string...
$name = substr($name, strlen($method));
// ...and trim off any of those underscores hanging around
$name = ltrim($name, '_');
$this->style->set($method);
}
return $name;
}
/**
* Search for any style methods within the name and apply them
*
* @param string $name
* @param array $method_search
*
* @return string Anything left over after applying styles
*/
protected function applyStyleMethods($name, $method_search = null)
{
// Get all of the possible style attributes
$method_search = $method_search ?: array_keys($this->style->all());
$new_name = $this->searchForStyleMethods($name, $method_search);
// While we still have a name left and we keep finding methods,
// loop through the possibilities
if (strlen($new_name) > 0 && $new_name != $name) {
return $this->applyStyleMethods($new_name, $method_search);
}
return $new_name;
}
/**
* Search for style methods in the current name
*
* @param string $name
* @param array $search
* @return string
*/
protected function searchForStyleMethods($name, $search)
{
// Loop through the possible methods
foreach ($search as $method) {
// See if we found a valid method
$name = $this->parseStyleMethod($method, $name);
}
return $name;
}
/**
* Build up the terminal object and return it
*
* @param string $name
* @param array $arguments
*
* @return object|null
*/
protected function buildTerminalObject($name, $arguments)
{
// Retrieve the parser for the current set of styles
$parser = $this->style->parser($this->util->system);
// Reset the styles
$this->style->reset();
// Execute the terminal object
$this->router->settings($this->settings);
$this->router->parser($parser);
$this->router->output($this->output);
$this->router->util($this->util);
return $this->router->execute($name, $arguments);
}
/**
* Route anything leftover after styles were applied
*
* @param string $name
* @param array $arguments
*
* @return object|null
*/
protected function routeRemainingMethod($name, array $arguments)
{
// If we still have something left, let's figure out what it is
if ($this->router->exists($name)) {
$obj = $this->buildTerminalObject($name, $arguments);
// If something was returned, return it
if (is_object($obj)) {
return $obj;
}
} elseif ($this->settings->exists($name)) {
$this->settings->add($name, reset($arguments));
// Handle passthroughs to the arguments manager.
} else {
// If we can't find it at this point, let's fail gracefully
$this->out(reset($arguments));
}
}
/**
* Magic method for anything called that doesn't exist
*
* @param string $requested_method
* @param array $arguments
*
* @return \League\CLImate\CLImate|\League\CLImate\TerminalObject\Dynamic\DynamicTerminalObject
*
* List of many of the possible method being called here
* documented at the top of this class.
*/
public function __call($requested_method, $arguments)
{
// Apply any style methods that we can find first
$name = $this->applyStyleMethods(Helper::snakeCase($requested_method));
// The first argument is the string|array|object we want to echo out
$output = reset($arguments);
if (strlen($name)) {
// If we have something left, let's try and route it to the appropriate place
if ($result = $this->routeRemainingMethod($name, $arguments)) {
return $result;
}
} elseif ($this->hasOutput($output)) {
// If we have fulfilled all of the requested methods and we have output, output it
$this->out($output);
}
return $this;
}
}
@@ -0,0 +1,72 @@
<?php
namespace League\CLImate\Decorator\Component;
class BackgroundColor extends Color
{
/**
* The difference to add to a foreground color code
* to get a background color code
*
* @const integer ADD
*/
const ADD = 10;
/**
* Get the code for the requested color
*
* @param mixed $val
*
* @return mixed
*/
public function get($val)
{
$color = parent::get($this->strip($val));
if ($color) {
$color += self::ADD;
}
return $color;
}
/**
* Set the current background color
*
* @param mixed $val
*
* @return boolean
*/
public function set($val)
{
return parent::set($this->strip($val));
}
/**
* Get all of the available background colors
*
* @return array
*/
public function all()
{
$colors = [];
foreach ($this->colors as $color => $code) {
$colors['background_' . $color] = $code + self::ADD;
}
return $colors;
}
/**
* Strip the color of any prefixes
*
* @param string $val
*
* @return string
*/
protected function strip($val)
{
return str_replace('background_', '', $val);
}
}
@@ -0,0 +1,53 @@
<?php
namespace League\CLImate\Decorator\Component;
abstract class BaseDecorator implements DecoratorInterface
{
/**
* An array of defaults for the decorator
*
* @var array $defaults;
*/
protected $defaults = [];
/**
* An array of currently set codes for the decorator
*
* @var array $current;
*/
protected $current = [];
public function __construct()
{
$this->defaults();
}
/**
* Load up the defaults for this decorator
*/
public function defaults()
{
foreach ($this->defaults as $name => $code) {
$this->add($name, $code);
}
}
/**
* Reset the currently set decorator
*/
public function reset()
{
$this->current = [];
}
/**
* Retrieve the currently set codes for the decorator
*
* @return array
*/
public function current()
{
return $this->current;
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace League\CLImate\Decorator\Component;
class Color extends BaseDecorator
{
/**
* The available colors
*
* @var array
*/
protected $colors = [];
/**
* An array of default colors
*
* @var array $defaults
*/
protected $defaults = [
'default' => 39,
'black' => 30,
'red' => 31,
'green' => 32,
'yellow' => 33,
'blue' => 34,
'magenta' => 35,
'cyan' => 36,
'light_gray' => 37,
'dark_gray' => 90,
'light_red' => 91,
'light_green' => 92,
'light_yellow' => 93,
'light_blue' => 94,
'light_magenta' => 95,
'light_cyan' => 96,
'white' => 97,
];
/**
* Add a color into the mix
*
* @param string $key
* @param integer $value
*/
public function add($key, $value)
{
$this->colors[$key] = (int) $value;
}
/**
* Retrieve all of available colors
*
* @return array
*/
public function all()
{
return $this->colors;
}
/**
* Get the code for the color
*
* @param string $val
*
* @return string
*/
public function get($val)
{
// If we already have the code, just return that
if (is_numeric($val)) {
return $val;
}
if (array_key_exists($val, $this->colors)) {
return $this->colors[$val];
}
return null;
}
/**
* Set the current color
*
* @param string $val
*
* @return boolean
*/
public function set($val)
{
$code = $this->get($val);
if ($code) {
$this->current = [$code];
return true;
}
return false;
}
}
@@ -0,0 +1,77 @@
<?php
namespace League\CLImate\Decorator\Component;
class Command extends BaseDecorator
{
/**
* Commands that correspond to a color in the $colors property
*
* @var array
*/
public $commands = [];
/**
* The default commands available
*
* @var array $defaults
*/
protected $defaults = [
'info' => 'green',
'comment' => 'yellow',
'whisper' => 'light_gray',
'shout' => 'red',
'error' => 'light_red',
];
/**
* Add a command into the mix
*
* @param string $key
* @param mixed $value
*/
public function add($key, $value)
{
$this->commands[$key] = $value;
}
/**
* Retrieve all of the available commands
*
* @return array
*/
public function all()
{
return $this->commands;
}
/**
* Get the style that corresponds to the command
*
* @param string $val
*
* @return string
*/
public function get($val)
{
if (array_key_exists($val, $this->commands)) {
return $this->commands[$val];
}
return null;
}
/**
* Set the currently used command
*
* @param string $val
*
* @return string|false
*/
public function set($val)
{
// Return the code because it is a string corresponding
// to a property in another class
return ($code = $this->get($val)) ? $code : false;
}
}
@@ -0,0 +1,28 @@
<?php
namespace League\CLImate\Decorator\Component;
interface DecoratorInterface
{
public function add($key, $value);
/**
* @return void
*/
public function defaults();
public function get($val);
public function set($val);
public function all();
public function current();
/**
* @return void
*/
public function reset();
}
@@ -0,0 +1,89 @@
<?php
namespace League\CLImate\Decorator\Component;
class Format extends BaseDecorator
{
/**
* The available formatting options
*
* @var array
*/
protected $formats = [];
/**
* An array of default formats
*
* @var array $defaults
*/
protected $defaults = [
'bold' => 1,
'dim' => 2,
'underline' => 4,
'blink' => 5,
'invert' => 7,
'hidden' => 8,
];
/**
* Add a format into the mix
*
* @param string $key
* @param mixed $value
*/
public function add($key, $value)
{
$this->formats[$key] = (int) $value;
}
/**
* Retrieve all of the available formats
*
* @return array
*/
public function all()
{
return $this->formats;
}
/**
* Get the code for the format
*
* @param string $val
*
* @return string
*/
public function get($val)
{
// If we already have the code, just return that
if (is_numeric($val)) {
return $val;
}
if (array_key_exists($val, $this->formats)) {
return $this->formats[$val];
}
return null;
}
/**
* Set the current format
*
* @param string $val
*
* @return boolean
*/
public function set($val)
{
$code = $this->get($val);
if ($code) {
$this->current[] = $code;
return true;
}
return false;
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace League\CLImate\Decorator\Parser;
use League\CLImate\Util\Helper;
class Ansi extends Parser
{
/**
* Wrap the string in the current style
*
* @param string $str
*
* @return string
*/
public function apply($str)
{
return $this->start() . $this->parse($str) . $this->end();
}
/**
* Get the string that begins the style
*
* @param string $codes
* @return string
*/
protected function start($codes = null)
{
$codes = $codes ?: $this->currentCode();
$codes = $this->codeStr($codes);
return $this->wrapCodes($codes);
}
/**
* Get the string that ends the style
*
* @param string|array $codes
* @return string
*/
protected function end($codes = null)
{
if (empty($codes)) {
$codes = [0];
} else {
$codes = Helper::toArray($codes);
// Reset everything back to normal up front
array_unshift($codes, 0);
}
return $this->wrapCodes($this->codeStr($codes));
}
/**
* Wrap the code string in the full escaped sequence
*
* @param string $codes
*
* @return string
*/
protected function wrapCodes($codes)
{
return "\e[{$codes}m";
}
/**
* Parse the string for tags and replace them with their codes
*
* @param string $str
*
* @return string
*/
protected function parse($str)
{
$count = preg_match_all($this->tags->regex(), $str, $matches);
// If we didn't find anything, return the string right back
if (!$count || !is_array($matches)) {
return $str;
}
// All we want is the array of actual strings matched
$matches = reset($matches);
return $this->parseTags($str, $matches);
}
/**
* Parse the given string for the tags and replace them with the appropriate codes
*
* @param string $str
* @param array $tags
*
* @return string
*/
protected function parseTags($str, $tags)
{
// Let's keep a history of styles applied
$history = ($this->currentCode()) ? [$this->currentCode()] : [];
foreach ($tags as $tag) {
$str = $this->replaceTag($str, $tag, $history);
}
return $str;
}
/**
* Replace the tag in the str
*
* @param string $str
* @param string $tag
* @param array $history
*
* @return string
*/
protected function replaceTag($str, $tag, &$history)
{
// We will be replacing tags one at a time, can't pass this by reference
$replace_count = 1;
if (strstr($tag, '/')) {
// We are closing out the tag, pop off the last element and get the codes that are left
array_pop($history);
$replace = $this->end($history);
} else {
// We are starting a new tag, add it onto the history and replace with correct color code
$history[] = $this->tags->value($tag);
$replace = $this->start($this->tags->value($tag));
}
return str_replace($tag, $replace, $str, $replace_count);
}
/**
* Stringify the codes
*
* @param mixed $codes
*
* @return string
*/
protected function codeStr($codes)
{
// If we get something that is already a code string, just pass it back
if (!is_array($codes) && strstr($codes, ';')) {
return $codes;
}
$codes = Helper::toArray($codes);
// Sort for the sake of consistency and testability
sort($codes);
return implode(';', $codes);
}
/**
* Retrieve the current style code
*
* @return string
*/
protected function currentCode()
{
return $this->codeStr($this->current);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace League\CLImate\Decorator\Parser;
class NonAnsi extends Parser
{
/**
* Strip the string of any tags
*
* @param string $str
*
* @return string
*/
public function apply($str)
{
return preg_replace($this->tags->regex(), '', $str);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace League\CLImate\Decorator\Parser;
use League\CLImate\Decorator\Tags;
abstract class Parser
{
/**
* An array of the currently applied codes
*
* @var array $current;
*/
protected $current = [];
/**
* An array of the tags that should be searched for
* and their corresponding replacements
*
* @var \League\CLImate\Decorator\Tags $tags
*/
public $tags;
public function __construct(array $current, Tags $tags)
{
$this->current = $current;
$this->tags = $tags;
}
/**
* Wrap the string in the current style
*
* @param string $str
*
* @return string
*/
abstract public function apply($str);
}
@@ -0,0 +1,26 @@
<?php
namespace League\CLImate\Decorator\Parser;
use League\CLImate\Util\System\System;
use League\CLImate\Decorator\Tags;
class ParserFactory
{
/**
* Get an instance of the appropriate Parser class
*
* @param System $system
* @param array $current
* @param Tags $tags
* @return Parser
*/
public static function getInstance(System $system, array $current, Tags $tags)
{
if ($system->hasAnsiSupport()) {
return new Ansi($current, $tags);
}
return new NonAnsi($current, $tags);
}
}
@@ -0,0 +1,23 @@
<?php
namespace League\CLImate\Decorator\Parser;
trait ParserImporter
{
/**
* An instance of the Parser class
*
* @var \League\CLImate\Decorator\Parser\Parser $parser
*/
protected $parser;
/**
* Import the parser and set the property
*
* @param \League\CLImate\Decorator\Parser\Parser $parser
*/
public function parser(Parser $parser)
{
$this->parser = $parser;
}
}
+295
View File
@@ -0,0 +1,295 @@
<?php
namespace League\CLImate\Decorator;
use League\CLImate\Decorator\Parser\ParserFactory;
use League\CLImate\Util\Helper;
use League\CLImate\Util\System\System;
/**
* @method void addColor(string $color, integer $code)
* @method void addFormat(string $format, integer $code)
* @method void addCommand(string $command, mixed $style)
*/
class Style
{
/**
* An array of Decorator objects
*
* @var Component\DecoratorInterface[] $style
*/
protected $style = [];
/**
* An array of the available Decorators
* and their corresponding class names
*
* @var array $available
*/
protected $available = [
'format' => 'Format',
'color' => 'Color',
'background' => 'BackgroundColor',
'command' => 'Command',
];
protected $parser;
/**
* An array of the current styles applied
*
* @var array $current
*/
protected $current = [];
public function __construct()
{
foreach ($this->available as $key => $class) {
$class = 'League\CLImate\Decorator\Component\\' . $class;
$this->style[$key] = new $class();
}
}
/**
* Get all of the styles available
*
* @return array
*/
public function all()
{
$all = [];
foreach ($this->style as $style) {
$all = array_merge($all, $this->convertToCodes($style->all()));
}
return $all;
}
/**
* Attempt to get the corresponding code for the style
*
* @param mixed $key
*
* @return mixed
*/
public function get($key)
{
foreach ($this->style as $style) {
if ($code = $style->get($key)) {
return $code;
}
}
return false;
}
/**
* Attempt to set some aspect of the styling,
* return true if attempt was successful
*
* @param string $key
*
* @return boolean
*/
public function set($key)
{
foreach ($this->style as $style) {
if ($code = $style->set($key)) {
return $this->validateCode($code);
}
}
return false;
}
/**
* Reset the current styles applied
*
*/
public function reset()
{
foreach ($this->style as $style) {
$style->reset();
}
}
/**
* Get a new instance of the Parser class based on the current settings
*
* @param \League\CLImate\Util\System\System $system
*
* @return \League\CLImate\Decorator\Parser\Parser
*/
public function parser(System $system)
{
return ParserFactory::getInstance($system, $this->current(), new Tags($this->all()));
}
/**
* Compile an array of the current codes
*
* @return array
*/
public function current()
{
$full_current = [];
foreach ($this->style as $style) {
$full_current = array_merge($full_current, Helper::toArray($style->current()));
}
$full_current = array_filter($full_current);
return array_values($full_current);
}
/**
* Make sure that the code is an integer, if not let's try and get it there
*
* @param mixed $code
*
* @return boolean
*/
protected function validateCode($code)
{
if (is_integer($code)) {
return true;
}
// Plug it back in and see what we get
if (is_string($code)) {
return $this->set($code);
}
if (is_array($code)) {
return $this->validateCodeArray($code);
}
return false;
}
/**
* Validate an array of codes
*
* @param array $codes
*
* @return boolean
*/
protected function validateCodeArray(array $codes)
{
// Loop through it and add each of the properties
$adds = [];
foreach ($codes as $code) {
$adds[] = $this->set($code);
}
// If any of them came back true, we're good to go
return in_array(true, $adds);
}
/**
* Convert the array of codes to integers
*
* @param array $codes
* @return array
*/
protected function convertToCodes(array $codes)
{
foreach ($codes as $key => $code) {
if (is_int($code)) {
continue;
}
$codes[$key] = $this->getCode($code);
}
return $codes;
}
/**
* Retrieve the integers from the mixed code input
*
* @param string|array $code
*
* @return integer|array
*/
protected function getCode($code)
{
if (is_array($code)) {
return $this->getCodeArray($code);
}
return $this->get($code);
}
/**
* Retrieve an array of integers from the array of codes
*
* @param array $codes
*
* @return array
*/
protected function getCodeArray(array $codes)
{
foreach ($codes as $key => $code) {
$codes[$key] = $this->get($code);
}
return $codes;
}
/**
* Parse the add method for the style they are trying to add
*
* @param string $method
*
* @return string
*/
protected function parseAddMethod($method)
{
return strtolower(substr($method, 3, strlen($method)));
}
/**
* Add a custom style
*
* @param string $style
* @param string $key
* @param string $value
*/
protected function add($style, $key, $value)
{
$this->style[$style]->add($key, $value);
// If we are adding a color, make sure it gets added
// as a background color too
if ($style == 'color') {
$this->style['background']->add($key, $value);
}
}
/**
* Magic Methods
*
* List of possible magic methods are at the top of this class
*
* @param string $requested_method
* @param array $arguments
*/
public function __call($requested_method, $arguments)
{
// The only methods we are concerned about are 'add' methods
if (substr($requested_method, 0, 3) != 'add') {
return false;
}
$style = $this->parseAddMethod($requested_method);
if (array_key_exists($style, $this->style)) {
list($key, $value) = $arguments;
$this->add($style, $key, $value);
}
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace League\CLImate\Decorator;
class Tags
{
/**
* Original keys passed in to build tags
*
* @var array $tags
*/
protected $keys = [];
/**
* Available tags and their values
*
* @var array $tags
*/
protected $tags = [];
public function __construct(array $keys)
{
$this->keys = $keys;
$this->build();
}
/**
* Get all available tags
*
* @return array
*/
public function all()
{
return $this->tags;
}
/**
* Get the value of the requested tag
*
* @param string $key
*
* @return string|null
*/
public function value($key)
{
return (array_key_exists($key, $this->tags)) ? $this->tags[$key] : null;
}
/**
* Get the regular expression that can be used to parse the string for tags
*
* @return string
*/
public function regex()
{
return '(<(?:(?:(?:\\\)*\/)*(?:' . implode('|', array_keys($this->keys)) . '))>)';
}
/**
* Build the search and replace for all of the various style tags
*/
protected function build()
{
foreach ($this->keys as $tag => $code) {
$this->tags["<{$tag}>"] = $code;
$this->tags["</{$tag}>"] = $code;
$this->tags["<\\/{$tag}>"] = $code;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace League\CLImate\Settings;
class Art implements SettingsInterface
{
/**
* An array of valid art directories
* @var array[] $dirs
*/
public $dirs = [];
/**
* Add directories of art
*/
public function add()
{
$this->dirs = array_merge($this->dirs, func_get_args());
$this->dirs = array_filter($this->dirs);
$this->dirs = array_values($this->dirs);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace League\CLImate\Settings;
class Manager
{
/**
* An array of settings that have been... set
*
* @var array $settings
*/
protected $settings = [];
/**
* Check and see if the requested setting is a valid, registered setting
*
* @param string $name
*
* @return boolean
*/
public function exists($name)
{
return class_exists($this->getPath($name));
}
/**
* Add a setting
*
* @param string $name
* @param mixed $value
*/
public function add($name, $value)
{
$setting = $this->getPath($name);
$key = $this->getClassName($name);
// If the current key doesn't exist in the settings array, set it up
if (!array_key_exists($name, $this->settings)) {
$this->settings[$key] = new $setting();
}
$this->settings[$key]->add($value);
}
/**
* Get the value of the requested setting if it exists
*
* @param string $key
*
* @return mixed
*/
public function get($key)
{
if (array_key_exists($key, $this->settings)) {
return $this->settings[$key];
}
return false;
}
/**
* Get the short name for the requested settings class
*
* @param string $name
*
* @return string
*/
protected function getPath($name)
{
return 'League\CLImate\Settings\\' . $this->getClassName($name);
}
/**
* Get the short class name for the setting
*
* @param string $name
*
* @return string
*/
protected function getClassName($name)
{
return ucwords(str_replace('add_', '', $name));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace League\CLImate\Settings;
trait SettingsImporter
{
/**
* Dictates any settings that a class may need access to
*
* @return array
*/
public function settings()
{
return [];
}
/**
* Import the setting into the class
*
* @param \League\CLImate\Settings\SettingsInterface $setting
*/
public function importSetting($setting)
{
$short_name = basename(str_replace('\\', '/', get_class($setting)));
$method = 'importSetting' . $short_name;
if (method_exists($this, $method)) {
$this->$method($setting);
}
}
}
@@ -0,0 +1,11 @@
<?php
namespace League\CLImate\Settings;
interface SettingsInterface
{
/**
* @return void
*/
public function add();
}
@@ -0,0 +1,45 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\Settings\SettingsImporter;
use League\CLImate\Util\UtilImporter;
abstract class BasicTerminalObject implements BasicTerminalObjectInterface
{
use SettingsImporter, ParserImporter, UtilImporter;
/**
* Set the property if there is a valid value
*
* @param string $key
* @param string $value
*/
protected function set($key, $value)
{
if (strlen($value)) {
$this->$key = $value;
}
}
/**
* Get the parser for the current object
*
* @return \League\CLImate\Decorator\Parser\Parser
*/
public function getParser()
{
return $this->parser;
}
/**
* Check if this object requires a new line to be added after the output
*
* @return boolean
*/
public function sameLine()
{
return false;
}
}
@@ -0,0 +1,34 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
use League\CLImate\Decorator\Parser\Parser;
use League\CLImate\Util\UtilFactory;
interface BasicTerminalObjectInterface
{
public function result();
public function settings();
/**
* @param $setting
* @return void
*/
public function importSetting($setting);
/**
* @return boolean
*/
public function sameLine();
/**
* @param \League\CLImate\Decorator\Parser\Parser $parser
*/
public function parser(Parser $parser);
/**
* @param UtilFactory $util
*/
public function util(UtilFactory $util);
}
@@ -0,0 +1,67 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Border extends BasicTerminalObject
{
/**
* The character to repeat for the border
*
* @var string $char
*/
protected $char = '-';
/**
* The length of the border
*
* @var integer $length
*/
protected $length;
public function __construct($char = null, $length = null)
{
$this->char($char)->length($length);
}
/**
* Set the character to repeat for the border
*
* @param string $char
*
* @return Border
*/
public function char($char)
{
$this->set('char', $char);
return $this;
}
/**
* Set the length of the border
*
* @param integer $length
*
* @return Border
*/
public function length($length)
{
$this->set('length', $length);
return $this;
}
/**
* Return the border
*
* @return string
*/
public function result()
{
$length = $this->length ?: $this->util->width() ?: 100;
$str = str_repeat($this->char, $length);
$str = substr($str, 0, $length);
return $str;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Br extends Repeatable
{
/**
* Return an empty string
*
* @return string
*/
public function result()
{
return array_fill(0, $this->count, '');
}
}
@@ -0,0 +1,21 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Clear extends BasicTerminalObject
{
/**
* Clear the terminal
*
* @return string
*/
public function result()
{
return "\e[H\e[2J";
}
public function sameLine()
{
return true;
}
}
@@ -0,0 +1,207 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
use League\CLImate\TerminalObject\Helper\StringLength;
class Columns extends BasicTerminalObject
{
use StringLength;
/**
* Number of columns
*
* @var integer $column_count
*/
protected $column_count;
/**
* Data to columnize
*
* @var array $data
*/
protected $data;
public function __construct($data, $column_count = null)
{
$this->data = $data;
$this->column_count = $column_count;
}
/**
* Calculate the number of columns organize data
*
* @return array
*/
public function result()
{
$keys = array_keys($this->data);
$first_key = reset($keys);
return (!is_int($first_key)) ? $this->associativeColumns() : $this->columns();
}
/**
* Get columns for a regular array
*
* @return array
*/
protected function columns()
{
$this->data = $this->setData();
$column_widths = $this->getColumnWidths();
$output = [];
$count = count(reset($this->data));
for ($i = 0; $i < $count; $i++) {
$output[] = $this->getRow($i, $column_widths);
}
return $output;
}
/**
* Re-configure the data into it's final form
*/
protected function setData()
{
// If it's already an array of arrays, we're good to go
if (is_array(reset($this->data))) {
return $this->setArrayOfArraysData();
}
$column_width = $this->getColumnWidth($this->data);
$row_count = $this->getMaxRows($column_width);
return array_chunk($this->data, $row_count);
}
/**
* Re-configure an array of arrays into column arrays
*/
protected function setArrayOfArraysData()
{
$this->setColumnCountViaArray($this->data);
$new_data = array_fill(0, $this->column_count, []);
foreach ($this->data as $items) {
for ($i = 0; $i < $this->column_count; $i++) {
$new_data[$i][] = (array_key_exists($i, $items)) ? $items[$i] : null;
}
}
return $new_data;
}
/**
* Get columns for an associative array
*
* @return array
*/
protected function associativeColumns()
{
$column_width = $this->getColumnWidth(array_keys($this->data));
$output = [];
foreach ($this->data as $key => $value) {
$output[] = $this->pad($key, $column_width) . $value;
}
return $output;
}
/**
* Get the row of data
*
* @param integer $key
* @param array $column_widths
*
* @return string
*/
protected function getRow($key, $column_widths)
{
$row = [];
for ($j = 0; $j < $this->column_count; $j++) {
if (isset($this->data[$j]) && array_key_exists($key, $this->data[$j])) {
$row[] = $this->pad($this->data[$j][$key], $column_widths[$j]);
}
}
return trim(implode('', $row));
}
/**
* Get the standard column width
*
* @param array $data
*
* @return integer
*/
protected function getColumnWidth($data)
{
// Return the maximum width plus a buffer
return $this->maxStrLen($data) + 5;
}
/**
* Get an array of each column's width
*
* @return array
*/
protected function getColumnWidths()
{
$column_widths = [];
for ($i = 0; $i < $this->column_count; $i++) {
if (!isset($this->data[$i])) {
$column_widths[] = 0;
continue;
}
$column_widths[] = $this->getColumnWidth($this->data[$i]);
}
return $column_widths;
}
/**
* Set the count property
*
* @param integer $column_width
*/
protected function setColumnCount($column_width)
{
$this->column_count = (int) floor($this->util->width() / $column_width);
}
/**
* Set the count property via an array
*
* @param array $items
*/
protected function setColumnCountViaArray($items)
{
$counts = array_map(function ($arr) {
return count($arr);
}, $items);
$this->column_count = max($counts);
}
/**
* Get the number of rows per column
*
* @param integer $column_width
*
* @return integer
*/
protected function getMaxRows($column_width)
{
if (!$this->column_count) {
$this->setColumnCount($column_width);
}
return ceil(count($this->data) / $this->column_count);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
use League\CLImate\TerminalObject\Helper\Art;
class Draw extends BasicTerminalObject
{
use Art;
public function __construct($art)
{
// Add the default art directory
$this->addDir(__DIR__ . '/../../ASCII');
$this->art = $art;
}
/**
* Return the art
*
* @return array
*/
public function result()
{
$file = $this->artFile($this->art) ?: $this->artFile($this->default_art);
return $this->parse($file);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Dump extends BasicTerminalObject
{
/**
* The data to convert to JSON
*
* @var mixed $data
*/
protected $data;
public function __construct($data)
{
$this->data = $data;
}
/**
* Return the data as JSON
*
* @return string
*/
public function result()
{
ob_start();
var_dump($this->data);
$result = ob_get_contents();
ob_end_clean();
return $result;
}
}
@@ -0,0 +1,74 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Flank extends BasicTerminalObject
{
/**
* The string that will be flanked
*
* @var string $str
*/
protected $str;
/**
* The character(s) to repeat on either side of the string
*
* @var string $char
*/
protected $char = '#';
/**
* How many times the character(s) should be repeated on either side
*
* @var integer $repeat
*/
protected $repeat = 3;
public function __construct($str, $char = null, $repeat = null)
{
$this->str = $str;
$this->char($char)->repeat($repeat);
}
/**
* Set the character(s) to repeat on either side
*
* @param string $char
*
* @return Flank
*/
public function char($char)
{
$this->set('char', $char);
return $this;
}
/**
* Set the repeat of the flank character(s)
*
* @param integer $repeat
*
* @return Flank
*/
public function repeat($repeat)
{
$this->set('repeat', $repeat);
return $this;
}
/**
* Return the flanked string
*
* @return string
*/
public function result()
{
$flank = str_repeat($this->char, $this->repeat);
return "{$flank} {$this->str} {$flank}";
}
}
@@ -0,0 +1,16 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Inline extends Out
{
/**
* Check if this object requires a new line should be added after the output
*
* @return boolean
*/
public function sameLine()
{
return true;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Json extends BasicTerminalObject
{
/**
* The data to convert to JSON
*
* @var mixed $data
*/
protected $data;
public function __construct($data)
{
$this->data = $data;
}
/**
* Return the data as JSON
*
* @return string
*/
public function result()
{
return json_encode($this->data, JSON_PRETTY_PRINT);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
class Out extends BasicTerminalObject
{
/**
* The content to output
*
* @var string $content
*/
protected $content;
public function __construct($content)
{
$this->content = $content;
}
/**
* Return the content to output
*
* @return string
*/
public function result()
{
return $this->content;
}
}
@@ -0,0 +1,18 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
abstract class Repeatable extends BasicTerminalObject
{
/**
* How many times the element should be repeated
*
* @var integer
*/
protected $count;
public function __construct($count = 1)
{
$this->count = (int) round(max((int) $count, 1));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
/**
* Tab class to enable tabs to be output without using the escape character.
*/
class Tab extends Repeatable
{
/**
* Check if this object requires a new line should be added after the output.
*
* @return boolean
*/
public function sameLine()
{
return true;
}
/**
* Return the relevant number of tab characters.
*
* @return string
*/
public function result()
{
return str_repeat("\t", $this->count);
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
namespace League\CLImate\TerminalObject\Basic;
use League\CLImate\TerminalObject\Helper\StringLength;
class Table extends BasicTerminalObject
{
use StringLength;
/**
* The data for the table, an array of (arrays|objects)
*
* @var array $data
*/
protected $data = [];
/**
* An array of the widths of each column in the table
*
* @var array $column_widths
*/
protected $column_widths = [];
/**
* The width of the table
*
* @var integer $table_width
*/
protected $table_width = 0;
/**
* The divider between table cells
*
* @var string $column_divider
*/
protected $column_divider = ' | ';
/**
* The border to divide each row of the table
*
* @var string $border
*/
protected $border;
/**
* The array of rows that will ultimately be returned
*
* @var array $rows
*/
protected $rows = [];
public function __construct(array $data)
{
$this->data = $data;
}
/**
* Return the built rows
*
* @return array
*/
public function result()
{
$this->column_widths = $this->getColumnWidths();
$this->table_width = $this->getWidth();
$this->border = $this->getBorder();
$this->buildHeaderRow();
foreach ($this->data as $key => $columns) {
$this->rows[] = $this->buildRow($columns);
$this->rows[] = $this->border;
}
return $this->rows;
}
/**
* Determine the width of the table
*
* @return integer
*/
protected function getWidth()
{
$first_row = reset($this->data);
$first_row = $this->buildRow($first_row);
return $this->lengthWithoutTags($first_row);
}
/**
* Get the border for each row based on the table width
*/
protected function getBorder()
{
return (new Border())->length($this->table_width)->result();
}
/**
* Check for a header row (if it's an array of associative arrays or objects),
* if there is one, tack it onto the front of the rows array
*/
protected function buildHeaderRow()
{
$header_row = $this->getHeaderRow();
if ($header_row) {
$this->rows[] = $this->border;
$this->rows[] = $this->buildRow($header_row);
$this->rows[] = (new Border())->char('=')->length($this->table_width)->result();
} else {
$this->rows[] = $this->border;
}
}
/**
* Get table row
*
* @param mixed $columns
*
* @return string
*/
protected function buildRow($columns)
{
$row = [];
foreach ($columns as $key => $column) {
$row[] = $this->buildCell($key, $column);
}
$row = implode($this->column_divider, $row);
return trim($this->column_divider . $row . $this->column_divider);
}
/**
* Build the string for this particular table cell
*
* @param mixed $key
* @param string $column
*
* @return string
*/
protected function buildCell($key, $column)
{
return $this->pad($column, $this->column_widths[$key]);
}
/**
* Get the header row for the table if it's an associative array or object
*
* @return mixed
*/
protected function getHeaderRow()
{
$first_item = reset($this->data);
if (is_object($first_item)) {
$first_item = get_object_vars($first_item);
}
$keys = array_keys($first_item);
$first_key = reset($keys);
// We have an associative array (probably), let's have a header row
if (!is_int($first_key)) {
return array_combine($keys, $keys);
}
return false;
}
/**
* Determine the width of each column
*
* @return array
*/
protected function getColumnWidths()
{
$first_row = reset($this->data);
if (is_object($first_row)) {
$first_row = get_object_vars($first_row);
}
// Create an array with the columns as keys and values of zero
$column_widths = $this->getDefaultColumnWidths($first_row);
foreach ($this->data as $columns) {
foreach ($columns as $key => $column) {
$column_widths[$key] = $this->getCellWidth($column_widths[$key], $column);
}
}
return $column_widths;
}
/**
* Set up an array of default column widths
*
* @param array $columns
*
* @return array
*/
protected function getDefaultColumnWidths(array $columns)
{
$widths = $this->arrayOfStrLens(array_keys($columns));
return array_combine(array_keys($columns), $widths);
}
/**
* Determine the width of the columns without tags
*
* @param array $current_width
* @param string $str
*
* @return integer
*/
protected function getCellWidth($current_width, $str)
{
return max($current_width, $this->lengthWithoutTags($str));
}
}
@@ -0,0 +1,213 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\TerminalObject\Dynamic\Animation\Keyframe;
use League\CLImate\TerminalObject\Helper\Art;
use League\CLImate\TerminalObject\Helper\Sleeper;
class Animation extends DynamicTerminalObject
{
use Art;
/**
* @var \League\CLImate\TerminalObject\Helper\Sleeper $sleeper
*/
protected $sleeper;
/**
* @var \League\CLImate\TerminalObject\Dynamic\Animation\Keyframe $keyframes
*/
protected $keyframes;
public function __construct($art, Sleeper $sleeper = null, Keyframe $keyframes = null)
{
// Add the default art directory
$this->addDir(__DIR__ . '/../../ASCII');
$this->setSleeper($sleeper);
$this->setKeyFrames($keyframes);
$this->art = $art;
}
/**
* Run a basic animation
*/
public function run()
{
$files = $this->artDir($this->art);
$animation = [];
foreach ($files as $file) {
$animation[] = $this->parse($file);
}
$this->animate($animation);
}
/**
* Set the speed of the animation based on a percentage
* (50% slower, 200% faster, etc)
*
* @param int|float $percentage
*
* @return \League\CLImate\TerminalObject\Dynamic\Animation
*/
public function speed($percentage)
{
$this->sleeper->speed($percentage);
return $this;
}
/**
* Scroll the art
*
* @param string $direction
* @return bool
*/
public function scroll($direction = 'right')
{
$this->setupKeyframes();
$mapping = $this->getScrollDirectionMapping();
if (!array_key_exists($direction, $mapping)) {
return false;
}
$lines = $this->getLines();
$enter_from = $mapping[$direction];
$exit_to = $mapping[$enter_from];
$this->animate($this->keyframes->scroll($lines, $enter_from, $exit_to));
}
/**
* Animate the art exiting the screen
*
* @param string $direction top|bottom|right|left
*/
public function exitTo($direction)
{
$this->setupKeyframes();
$this->animate($this->keyframes->exitTo($this->getLines(), $direction));
}
/**
* Animate the art entering the screen
*
* @param string $direction top|bottom|right|left
*/
public function enterFrom($direction)
{
$this->setupKeyframes();
$this->animate($this->keyframes->enterFrom($this->getLines(), $direction));
}
protected function getScrollDirectionMapping()
{
return [
'left' => 'right',
'right' => 'left',
'top' => 'bottom',
'bottom' => 'top',
'up' => 'bottom',
'down' => 'top',
];
}
protected function getLines()
{
return $this->parse($this->artFile($this->art));
}
/**
* @param \League\CLImate\TerminalObject\Helper\Sleeper $sleeper
*/
protected function setSleeper($sleeper = null)
{
$this->sleeper = $sleeper ?: new Sleeper();
}
/**
* @param League\CLImate\TerminalObject\Dynamic\Animation\Keyframe $keyframes
*/
protected function setKeyFrames($keyframes)
{
$this->keyframes = $keyframes ?: new Keyframe;
}
/**
* Set up the necessary properties on the Keyframe class
*/
protected function setupKeyframes()
{
$this->keyframes->parser($this->parser);
$this->keyframes->util($this->util);
}
/**
* Animate the given keyframes
*
* @param array $keyframes Array of arrays
*/
protected function animate(array $keyframes)
{
$count = 0;
foreach ($keyframes as $lines) {
$this->writeKeyFrame($lines, $count);
$this->sleeper->sleep();
$count = count($lines);
}
}
/**
* Write the current keyframe to the terminal, line by line
*
* @param array $lines
* @param integer $count
*/
protected function writeKeyFrame(array $lines, $count)
{
foreach ($lines as $key => $line) {
$content = $this->getLineFormatted($line, $key, $count);
$this->output->write($this->parser->apply($content));
}
}
/**
* Format the line to re-write previous lines, if necessary
*
* @param string $line
* @param integer $key
* @param integer $last_frame_count
*
* @return string
*/
protected function getLineFormatted($line, $key, $last_frame_count)
{
// If this is the first thing we're writing, just return the line
if ($last_frame_count == 0) {
return $line;
}
$content = '';
// If this is the first line of the frame,
// move the cursor up the total number of previous lines from the previous frame
if ($key == 0) {
$content .= $this->util->cursor->up($last_frame_count);
}
$content .= $this->util->cursor->startOfCurrentLine();
$content .= $this->util->cursor->deleteCurrentLine();
$content .= $line;
return $content;
}
}
@@ -0,0 +1,265 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic\Animation;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\TerminalObject\Helper\StringLength;
use League\CLImate\Util\UtilImporter;
class Keyframe
{
use StringLength, ParserImporter, UtilImporter;
/**
* Get the enter keyframes for the desired direction
*
* @param array $lines
* @param string $direction
*
* @return array
*/
public function enterFrom($lines, $direction)
{
return array_reverse($this->exitTo($lines, $direction));
}
/**
* Get the exit keyframes for the desired direction
*
* @param array $lines
* @param string $direction
*
* @return array
*/
public function exitTo($lines, $direction)
{
$lines = $this->adjustLines($lines, $direction);
$line_method = $this->getLineMethod($direction);
$direction_keyframes = $this->getDirectionFrames($direction, $lines, $line_method);
$keyframes = array_fill(0, 4, $lines);
$keyframes = array_merge($keyframes, $direction_keyframes);
$keyframes[] = array_fill(0, count($lines), '');
return $keyframes;
}
/**
* Get scroll keyframes
*
* @param array $lines
* @param string $enter_from
* @param string $exit_to
*
* @return array
*/
public function scroll($lines, $enter_from, $exit_to)
{
$keyframes = $this->enterFrom($lines, $enter_from);
$keyframes = array_merge($keyframes, $this->exitTo($lines, $exit_to));
$keyframes = array_unique($keyframes, SORT_REGULAR);
$keyframes[] = reset($keyframes);
return $keyframes;
}
/**
* Get the line parser for the direction
*
* @param string $direction
* @return string
*/
protected function getLineMethod($direction)
{
return 'current' . ucwords(strtolower($direction)) . 'Line';
}
/**
* Adjust the array of lines if necessary
*
* @param array $lines
* @param string $direction
*
* @return array
*/
protected function adjustLines(array $lines, $direction)
{
$adjust_method = 'adjust' . ucwords(strtolower($direction)) . 'Lines';
if (method_exists($this, $adjust_method)) {
return $this->$adjust_method($lines);
}
return $lines;
}
/**
* Pad the array of lines for "right" animation
*
* @param array $lines
* @return array
*/
protected function adjustRightLines(array $lines)
{
return $this->padArray($lines, $this->util->width());
}
/**
* Pad the array of lines for "left" animation
*
* @param array $lines
* @return array
*/
protected function adjustLeftLines(array $lines)
{
return $this->padArray($lines, $this->maxStrLen($lines));
}
/**
* Get the keyframes appropriate for the animation direction
*
* @param string $direction
* @param array $lines
* @param string $line_method
*
* @return array
*/
protected function getDirectionFrames($direction, array $lines, $line_method)
{
$mapping = [
'exitHorizontalFrames' => ['left', 'right'],
'exitVerticalFrames' => ['top', 'bottom'],
];
foreach ($mapping as $method => $directions) {
if (in_array($direction, $directions)) {
return $this->$method($lines, $line_method);
}
}
// Fail gracefully, simply return an array
return [];
}
/**
* Create horizontal exit animation keyframes for the art
*
* @param array $lines
* @param string $line_method
*
* @return array
*/
protected function exitHorizontalFrames(array $lines, $line_method)
{
$keyframes = [];
$length = strlen($lines[0]);
for ($i = $length; $i > 0; $i--) {
$keyframes[] = $this->getHorizontalKeyframe($lines, $i, $line_method, $length);
}
return $keyframes;
}
/**
* Get the keyframe for a horizontal animation
*
* @param array $lines
* @param int $frame_number
* @param string $line_method
* @param int $length
*
* @return array
*/
protected function getHorizontalKeyframe(array $lines, $frame_number, $line_method, $length)
{
$keyframe = [];
foreach ($lines as $line) {
$keyframe[] = $this->$line_method($line, $frame_number, $length);
}
return $keyframe;
}
/**
* Create vertical exit animation keyframes for the art
*
* @param array $lines
* @param string $line_method
*
* @return array
*/
protected function exitVerticalFrames(array $lines, $line_method)
{
$keyframes = [];
$line_count = count($lines);
for ($i = $line_count - 1; $i >= 0; $i--) {
$keyframes[] = $this->$line_method($lines, $line_count, $i);
}
return $keyframes;
}
/**
* Get the current line as it is exiting left
*
* @param string $line
* @param int $frame_number
*
* @return string
*/
protected function currentLeftLine($line, $frame_number)
{
return substr($line, -$frame_number);
}
/**
* Get the current line as it is exiting right
*
* @param string $line
* @param int $frame_number
* @param int $length
*
* @return string
*/
protected function currentRightLine($line, $frame_number, $length)
{
return str_repeat(' ', $length - $frame_number) . substr($line, 0, $frame_number);
}
/**
* Slice off X number of lines from the bottom and fill the rest with empty strings
*
* @param array $lines
* @param integer $total_lines
* @param integer $current
*
* @return array
*/
protected function currentTopLine($lines, $total_lines, $current)
{
$keyframe = array_slice($lines, -$current, $current);
return array_merge($keyframe, array_fill(0, $total_lines - $current, ''));
}
/**
* Slice off X number of lines from the top and fill the rest with empty strings
*
* @param array $lines
* @param integer $total_lines
* @param integer $current
*
* @return array
*/
protected function currentBottomLine($lines, $total_lines, $current)
{
$keyframe = array_fill(0, $total_lines - $current, '');
return array_merge($keyframe, array_slice($lines, 0, $current));
}
}
@@ -0,0 +1,219 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic\Checkbox;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\TerminalObject\Helper\StringLength;
use League\CLImate\Util\UtilImporter;
class Checkbox
{
use StringLength, ParserImporter, UtilImporter;
/**
* The value of the checkbox
*
* @var string|int|bool $value
*/
protected $value;
/**
* The label for the checkbox
*
* @var string|int $label
*/
protected $label;
/**
* Whether the checkbox is checked
*
* @var bool $checked
*/
protected $checked = false;
/**
* Whether pointer is currently pointing at the checkbox
*
* @var bool $current
*/
protected $current = false;
/**
* Whether the checkbox is the first in the group
*
* @var bool $first
*/
protected $first = false;
/**
* Whether the checkbox is the last in the group
*
* @var bool $last
*/
protected $last = false;
public function __construct($label, $value)
{
$this->value = (!is_int($value)) ? $value : $label;
$this->label = $label;
}
/**
* @return bool
*/
public function isCurrent()
{
return $this->current;
}
/**
* @return bool
*/
public function isChecked()
{
return $this->checked;
}
/**
* @return bool
*/
public function isFirst()
{
return $this->first;
}
/**
* @return bool
*/
public function isLast()
{
return $this->last;
}
/**
* Set whether the pointer is currently pointing at this checkbox
*
* @param bool $current
*
* @return Checkbox
*/
public function setCurrent($current = true)
{
$this->current = $current;
return $this;
}
/**
* Set whether the checkbox is currently checked
*
* @param bool $checked
*
* @return Checkbox
*/
public function setChecked($checked = true)
{
$this->checked = $checked;
return $this;
}
/**
* @return Checkbox
*/
public function setFirst()
{
$this->first = true;
return $this;
}
/**
* @return Checkbox
*/
public function setLast()
{
$this->last = true;
return $this;
}
/**
* @return string|int|bool
*/
public function getValue()
{
return $this->value;
}
/**
* Build out basic checkbox string based on current options
*
* @return string
*/
protected function buildCheckboxString()
{
$parts = [
($this->isCurrent()) ? $this->pointer() : ' ',
$this->checkbox($this->isChecked()),
$this->label,
];
$line = implode(' ', $parts);
return $line . $this->getPaddingString($line);
}
/**
* Get the padding string based on the length of the terminal/line
*
* @param string $line
*
* @return string
*/
protected function getPaddingString($line)
{
$length = $this->util->system->width() - $this->lengthWithoutTags($line);
return str_repeat(' ', $length);
}
/**
* Get the checkbox symbol
*
* @param bool $checked
*
* @return string
*/
protected function checkbox($checked)
{
if ($checked) {
return html_entity_decode("&#x25CF;");
}
return html_entity_decode("&#x25CB;");
}
/**
* Get the pointer symbol
*
* @return string
*/
protected function pointer()
{
return html_entity_decode("&#x276F;");
}
public function __toString()
{
if ($this->isFirst()) {
return $this->buildCheckboxString();
}
if ($this->isLast()) {
return $this->buildCheckboxString() . $this->util->cursor->left(10) . '<hidden>';
}
return $this->buildCheckboxString();
}
}
@@ -0,0 +1,191 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic\Checkbox;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\Util\OutputImporter;
use League\CLImate\Util\UtilImporter;
class CheckboxGroup
{
use OutputImporter, ParserImporter, UtilImporter;
protected $checkboxes = [];
protected $count;
public function __construct(array $options)
{
foreach ($options as $key => $option) {
$this->checkboxes[] = new Checkbox($option, $key);
}
$this->count = count($this->checkboxes);
$this->checkboxes[0]->setFirst()->setCurrent();
$this->checkboxes[$this->count - 1]->setLast();
}
public function write()
{
array_map([$this, 'writeCheckbox'], $this->checkboxes);
}
/**
* Retrieve the checked option values
*
* @return array
*/
public function getCheckedValues()
{
return array_values(array_map([$this, 'getValue'], $this->getChecked()));
}
/**
* Set the newly selected option based on the direction
*
* @param string $direction 'previous' or 'next'
*/
public function setCurrent($direction)
{
list($option, $key) = $this->getCurrent();
$option->setCurrent(false);
$new_key = $this->getCurrentKey($direction, $option, $key);
$this->checkboxes[$new_key]->setCurrent();
}
/**
* Toggle the current option's checked status
*/
public function toggleCurrent()
{
list($option, $key) = $this->getCurrent();
$option->setChecked(!$option->isChecked());
}
/**
* Get the number of checkboxes
*
* @return int
*/
public function count()
{
return $this->count;
}
/**
* Retrieve the checked options
*
* @return array
*/
protected function getChecked()
{
return array_filter($this->checkboxes, [$this, 'isChecked']);
}
/**
* Determine whether the option is checked
*
* @param Checkbox $option
*
* @return bool
*/
protected function isChecked($option)
{
return $option->isChecked();
}
/**
* Retrieve the option's value
*
* @param Checkbox $option
*
* @return mixed
*/
protected function getValue($option)
{
return $option->getValue();
}
/**
* Get the currently selected option
*
* @return array
*/
protected function getCurrent()
{
foreach ($this->checkboxes as $key => $option) {
if ($option->isCurrent()) {
return [$option, $key];
}
}
}
/**
* Retrieve the correct current key
*
* @param string $direction 'previous' or 'next'
* @param Checkbox $option
* @param int $key
*
* @return int
*/
protected function getCurrentKey($direction, $option, $key)
{
$method = 'get' . ucwords($direction). 'Key';
return $this->{$method}($option, $key);
}
/**
* @param Checkbox $option
* @param int $key
*
* @return int
*/
protected function getPreviousKey($option, $key)
{
if ($option->isFirst()) {
return count($this->checkboxes) - 1;
}
return --$key;
}
/**
* @param Checkbox $option
* @param int $key
*
* @return int
*/
protected function getNextKey($option, $key)
{
if ($option->isLast()) {
return 0;
}
return ++$key;
}
/**
* @param Checkbox $checkbox
*/
protected function writeCheckbox($checkbox)
{
$checkbox->util($this->util);
$checkbox->parser($this->parser);
$parsed = $this->parser->apply((string) $checkbox);
if ($checkbox->isLast()) {
$this->output->sameLine()->write($parsed);
return;
}
$this->output->write($parsed);
}
}
@@ -0,0 +1,38 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic\Checkbox;
class RadioGroup extends CheckboxGroup
{
/**
* Toggle the currently selected option, uncheck all of the others
*/
public function toggleCurrent()
{
list($checkbox, $checkbox_key) = $this->getCurrent();
$checkbox->setChecked(!$checkbox->isChecked());
foreach ($this->checkboxes as $key => $checkbox) {
if ($key == $checkbox_key) {
continue;
}
$checkbox->setChecked(false);
}
}
/**
* Get the checked option
*
* @return string|bool|int
*/
public function getCheckedValues()
{
if ($checked = $this->getChecked()) {
return reset($checked)->getValue();
}
return null;
}
}
@@ -0,0 +1,159 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\Util\Reader\ReaderInterface;
use League\CLImate\Util\Reader\Stdin;
class Checkboxes extends InputAbstract
{
/**
* The options to choose from
*
* @var Checkbox\CheckboxGroup $checkboxes
*/
protected $checkboxes;
public function __construct($prompt, array $options, ReaderInterface $reader = null)
{
$this->prompt = $prompt;
$this->reader = $reader ?: new Stdin();
$this->checkboxes = $this->buildCheckboxes($options);
}
/**
* Do it! Prompt the user for information!
*
* @return string
*/
public function prompt()
{
$this->output->write($this->parser->apply($this->promptFormatted()));
$this->writeCheckboxes();
$this->util->system->exec('stty sane');
return $this->checkboxes->getCheckedValues();
}
/**
* Build out the checkboxes
*
* @param array $options
*
* @return Checkbox\CheckboxGroup
*/
protected function buildCheckboxes(array $options)
{
return new Checkbox\CheckboxGroup($options);
}
/**
* Format the prompt string
*
* @return string
*/
protected function promptFormatted()
{
return $this->prompt . ' (use <space> to select)';
}
/**
* Output the checkboxes and listen for any keystrokes
*/
protected function writeCheckboxes()
{
$this->updateCheckboxView();
$this->util->system->exec('stty -icanon');
$this->output->sameLine()->write($this->util->cursor->hide());
$this->listenForInput();
}
/**
* Listen for input and act on it
*/
protected function listenForInput()
{
while ($char = $this->reader->char(1)) {
if ($this->handleCharacter($char)) {
break;
}
$this->moveCursorToTop();
$this->updateCheckboxView();
}
}
/**
* Take the appropriate action based on the input character,
* returns whether to stop listening or not
*
* @param string $char
*
* @return bool
*/
protected function handleCharacter($char)
{
switch ($char) {
case "\n":
$this->output->sameLine()->write($this->util->cursor->defaultStyle());
$this->output->sameLine()->write("\e[0m");
return true; // Break the while loop as well
case "\e":
$this->handleAnsi();
break;
case ' ':
$this->checkboxes->toggleCurrent();
break;
}
return false;
}
/**
* Move the cursor to the top of the option list
*/
protected function moveCursorToTop()
{
$output = $this->util->cursor->up($this->checkboxes->count() - 1);
$output .= $this->util->cursor->startOfCurrentLine();
$this->output->sameLine()->write($output);
}
/**
* Handle any ANSI characters
*/
protected function handleAnsi()
{
switch ($this->reader->char(2)) {
// Up arrow
case '[A':
$this->checkboxes->setCurrent('previous');
break;
// Down arrow
case '[B':
$this->checkboxes->setCurrent('next');
break;
}
}
/**
* Re-write the checkboxes based on the current objects
*/
protected function updateCheckboxView()
{
$this->checkboxes->util($this->util);
$this->checkboxes->output($this->output);
$this->checkboxes->parser($this->parser);
$this->checkboxes->write();
}
}
@@ -0,0 +1,19 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Confirm extends Input
{
/**
* Let us know if the user confirmed
*
* @return boolean
*/
public function confirmed()
{
$this->accept(['y', 'n'], true);
$this->strict();
return ($this->prompt() == 'y');
}
}
@@ -0,0 +1,18 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\Settings\SettingsImporter;
use League\CLImate\Util\OutputImporter;
use League\CLImate\Util\UtilImporter;
/**
* The dynamic terminal object doesn't adhere to the basic terminal object
* contract, which is why it gets its own base class
*/
abstract class DynamicTerminalObject implements DynamicTerminalObjectInterface
{
use SettingsImporter, ParserImporter, OutputImporter, UtilImporter;
}
@@ -0,0 +1,27 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\Decorator\Parser\Parser;
use League\CLImate\Util\UtilFactory;
interface DynamicTerminalObjectInterface
{
public function settings();
/**
* @param $setting
* @return void
*/
public function importSetting($setting);
/**
* @param \League\CLImate\Decorator\Parser\Parser $parser
*/
public function parser(Parser $parser);
/**
* @param UtilFactory $util
*/
public function util(UtilFactory $util);
}
@@ -0,0 +1,283 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\Util\Reader\ReaderInterface;
use League\CLImate\Util\Reader\Stdin;
class Input extends InputAbstract
{
/**
* An array of acceptable responses
*
* @var array|object $acceptable
*/
protected $acceptable;
/**
* Whether we should be strict about the response given
*
* @var boolean $strict
*/
protected $strict = false;
/**
* Whether to accept multiple lines of input
*
* Terminated by ^D
*
* @var boolean $multiLine
*/
protected $multiLine = false;
/**
* Whether we should display the acceptable responses to the user
*
* @var boolean $show_acceptable
*/
protected $show_acceptable = false;
/**
* A default answer in the case of no user response,
* prevents re-prompting
*
* @var string
*/
protected $default = '';
public function __construct($prompt, ReaderInterface $reader = null)
{
$this->prompt = $prompt;
$this->reader = $reader ?: new Stdin();
}
/**
* Do it! Prompt the user for information!
*
* @return string
*/
public function prompt()
{
$this->writePrompt();
$response = $this->valueOrDefault($this->getUserInput());
if ($this->isValidResponse($response)) {
return $response;
}
return $this->prompt();
}
/**
* Define the acceptable responses and whether or not to
* display them to the user
*
* @param array|object $acceptable
* @param boolean $show
*
* @return \League\CLImate\TerminalObject\Dynamic\Input
*/
public function accept($acceptable, $show = false)
{
$this->acceptable = $acceptable;
$this->show_acceptable = $show;
return $this;
}
/**
* Define whether we should be strict about exact responses
*
* @return \League\CLImate\TerminalObject\Dynamic\Input
*/
public function strict()
{
$this->strict = true;
return $this;
}
/**
* Set a default response
*
* @param string $default
*
* @return \League\CLImate\TerminalObject\Dynamic\Input
*/
public function defaultTo($default)
{
$this->default = $default;
return $this;
}
/**
* Set multiline input to true
*
* @return \League\CLImate\TerminalObject\Dynamic\Input
*/
public function multiLine()
{
$this->multiLine = true;
return $this;
}
/**
* @return string
*/
protected function getUserInput()
{
if ($this->multiLine) {
return $this->reader->multiLine();
}
return $this->reader->line();
}
/**
* Write out the formatted prompt
*/
protected function writePrompt()
{
$prompt = $this->parser->apply($this->promptFormatted());
$this->output->sameLine()->write($prompt);
}
/**
* If no response was given and there is a default, return it,
* otherwise return response
*
* @param string $response
*
* @return string
*/
protected function valueOrDefault($response)
{
if (strlen($response) == 0 && strlen($this->default)) {
return $this->default;
}
return $response;
}
/**
* Format the acceptable responses as options
*
* @return string
*/
protected function acceptableFormatted()
{
if (!is_array($this->acceptable)) {
return '';
}
$this->acceptable = array_map([$this, 'acceptableItemFormatted'], $this->acceptable);
return '[' . implode('/', $this->acceptable) . ']';
}
/**
* Format the acceptable item depending on whether it is the default or not
*
* @param string $item
*
* @return string
*/
protected function acceptableItemFormatted($item)
{
if ($item == $this->default) {
return '<bold>' . $item . '</bold>';
}
return $item;
}
/**
* Format the prompt incorporating spacing and any acceptable options
*
* @return string
*/
protected function promptFormatted()
{
$prompt = $this->prompt . ' ';
if ($this->show_acceptable) {
$prompt .= $this->acceptableFormatted() . ' ';
}
return $prompt;
}
/**
* Apply some string manipulation functions for normalization
*
* @param string|array $var
* @return array
*/
protected function levelPlayingField($var)
{
$levelers = ['trim', 'strtolower'];
foreach ($levelers as $leveler) {
if (is_array($var)) {
$var = array_map($leveler, $var);
} else {
$var = $leveler($var);
}
}
return $var;
}
/**
* Determine whether or not the acceptable property is of type closure
*
* @return boolean
*/
protected function acceptableIsClosure()
{
return (is_object($this->acceptable) && $this->acceptable instanceof \Closure);
}
/**
* Determine if the user's response is in the acceptable responses array
*
* @param string $response
*
* @return boolean $response
*/
protected function isAcceptableResponse($response)
{
if ($this->strict) {
return in_array($response, $this->acceptable);
}
$acceptable = $this->levelPlayingField($this->acceptable);
$response = $this->levelPlayingField($response);
return in_array($response, $acceptable);
}
/**
* Determine if the user's response is valid based on the current settings
*
* @param string $response
*
* @return boolean $response
*/
protected function isValidResponse($response)
{
if (empty($this->acceptable)) {
return true;
}
if ($this->acceptableIsClosure()) {
return call_user_func($this->acceptable, $response);
}
return $this->isAcceptableResponse($response);
}
}
@@ -0,0 +1,37 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
use League\CLImate\Util\Reader\ReaderInterface;
use League\CLImate\Util\Reader\Stdin;
abstract class InputAbstract extends DynamicTerminalObject
{
/**
* The prompt text
*
* @var string $prompt
*/
protected $prompt;
/**
* An instance of ReaderInterface
*
* @var \League\CLImate\Util\Reader\ReaderInterface $reader
*/
protected $reader;
/**
* Do it! Prompt the user for information!
*
* @return string
*/
abstract public function prompt();
/**
* Format the prompt incorporating spacing and any acceptable options
*
* @return string
*/
abstract protected function promptFormatted();
}
@@ -0,0 +1,136 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Padding extends DynamicTerminalObject
{
/**
* The length that lines should be padded to
*
* @var integer $length
*/
protected $length = 0;
/**
* The character(s) that should be used to pad
*
* @var string $char
*/
protected $char = '.';
/**
* If they pass in a padding character, set the char
*
* @param int $length
* @param string $char
*/
public function __construct($length = null, $char = null)
{
if ($length !== null) {
$this->length($length);
}
if (is_string($char)) {
$this->char($char);
}
}
/**
* Set the character(s) that should be used to pad
*
* @param string $char
*
* @return \League\CLImate\TerminalObject\Dynamic\Padding
*/
public function char($char)
{
$this->char = $char;
return $this;
}
/**
* Set the length of the line that should be generated
*
* @param integer $length
*
* @return \League\CLImate\TerminalObject\Dynamic\Padding
*/
public function length($length)
{
$this->length = $length;
return $this;
}
/**
* Get the length of the line based on the width of the terminal window
*
* @return integer
*/
protected function getLength()
{
if (!$this->length) {
$this->length = $this->util->width();
}
return $this->length;
}
/**
* Pad the content with the characters
*
* @param string $content
*
* @return string
*/
protected function padContent($content)
{
if (strlen($this->char) > 0) {
$length = $this->getLength();
$padding_length = ceil($length / strlen($this->char));
$padding = str_repeat($this->char, $padding_length);
$content .= substr($padding, 0, $length - strlen($content));
}
return $content;
}
/**
* Output the content and pad to the previously defined length
*
* @param string $content
*
* @return \League\CLImate\TerminalObject\Dynamic\Padding
*/
public function label($content)
{
// Handle long labels by splitting them across several lines
$lines = str_split($content, $this->util->width());
$content = array_pop($lines);
foreach ($lines as $line) {
$this->output->write($this->parser->apply($line));
}
$content = $this->padContent($content);
$content = $this->parser->apply($content);
$this->output->sameLine();
$this->output->write($content);
return $this;
}
/**
* Output result
*
* @param string $content
*/
public function result($content)
{
$this->output->write($this->parser->apply(' ' . $content));
}
}
@@ -0,0 +1,13 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Password extends Input
{
public function prompt()
{
$this->writePrompt();
return $this->reader->hidden();
}
}
@@ -0,0 +1,280 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Progress extends DynamicTerminalObject
{
/**
* The total number of items involved
*
* @var integer $total
*/
protected $total = 0;
/**
* The current item that the progress bar represents
*
* @var integer $current
*/
protected $current = 0;
/**
* The current percentage displayed
*
* @var string $current_percentage
*/
protected $current_percentage = '';
/**
* The string length of the bar when at 100%
*
* @var integer $bar_str_len
*/
protected $bar_str_len;
/**
* Flag indicating whether we are writing the bar for the first time
*
* @var boolean $first_line
*/
protected $first_line = true;
/**
* Current status bar label
*
* @var string $label
*/
protected $label;
/**
* Force a redraw every time
*
* @var boolean $force_redraw
*/
protected $force_redraw = false;
/**
* If this progress bar ever displayed a label.
*
* @var boolean $has_label_line
*/
protected $has_label_line = false;
/**
* If they pass in a total, set the total
*
* @param integer $total
*/
public function __construct($total = null)
{
if ($total !== null) {
$this->total($total);
}
}
/**
* Set the total property
*
* @param integer $total
*
* @return Progress
*/
public function total($total)
{
$this->total = $total;
return $this;
}
/**
* Determines the current percentage we are at and re-writes the progress bar
*
* @param integer $current
* @param mixed $label
* @throws \Exception
*/
public function current($current, $label = null)
{
if ($this->total == 0) {
// Avoid dividing by 0
throw new \Exception('The progress total must be greater than zero.');
}
if ($current > $this->total) {
throw new \Exception('The current is greater than the total.');
}
$this->drawProgressBar($current, $label);
$this->current = $current;
$this->label = $label;
}
/**
* Increments the current position we are at and re-writes the progress bar
*
* @param integer $increment The number of items to increment by
* @param string $label
*/
public function advance($increment = 1, $label = null)
{
$this->current($this->current + $increment, $label);
}
/**
* Force the progress bar to redraw every time regardless of whether it has changed or not
*
* @param boolean $force
* @return Progress
*/
public function forceRedraw($force = true)
{
$this->force_redraw = !!$force;
return $this;
}
/**
* Draw the progress bar, if necessary
*
* @param string $current
* @param string $label
*/
protected function drawProgressBar($current, $label)
{
$percentage = $this->percentageFormatted($current / $this->total);
if ($this->shouldRedraw($percentage, $label)) {
$progress_bar = $this->getProgressBar($current, $label);
$this->output->write($this->parser->apply($progress_bar));
}
$this->current_percentage = $percentage;
}
/**
* Build the progress bar str and return it
*
* @param integer $current
* @param string $label
*
* @return string
*/
protected function getProgressBar($current, $label)
{
if ($this->first_line) {
// Drop down a line, we are about to
// re-write this line for the progress bar
$this->output->write('');
$this->first_line = false;
}
// Move the cursor up and clear it to the end
$line_count = $this->has_label_line ? 2 : 1;
$progress_bar = $this->util->cursor->up($line_count);
$progress_bar .= $this->util->cursor->startOfCurrentLine();
$progress_bar .= $this->util->cursor->deleteCurrentLine();
$progress_bar .= $this->getProgressBarStr($current, $label);
// If this line has a label then set that this progress bar has a label line
if (strlen($label) > 0) {
$this->has_label_line = true;
}
return $progress_bar;
}
/**
* Get the progress bar string, basically:
* =============> 50% label
*
* @param integer $current
* @param string $label
*
* @return string
*/
protected function getProgressBarStr($current, $label)
{
$percentage = $current / $this->total;
$bar_length = round($this->getBarStrLen() * $percentage);
$bar = $this->getBar($bar_length);
$number = $this->percentageFormatted($percentage);
if ($label) {
$label = $this->labelFormatted($label);
// If this line doesn't have a label, but we've had one before,
// then ensure the label line is cleared
} elseif ($this->has_label_line) {
$label = $this->labelFormatted('');
}
return trim("{$bar} {$number}{$label}");
}
/**
* Get the string for the actual bar based on the current length
*
* @param integer $length
*
* @return string
*/
protected function getBar($length)
{
$bar = str_repeat('=', $length);
$padding = str_repeat(' ', $this->getBarStrLen() - $length);
return "{$bar}>{$padding}";
}
/**
* Get the length of the bar string based on the width of the terminal window
*
* @return integer
*/
protected function getBarStrLen()
{
if (!$this->bar_str_len) {
// Subtract 10 because of the '> 100%' plus some padding, max 100
$this->bar_str_len = min($this->util->width() - 10, 100);
}
return $this->bar_str_len;
}
/**
* Format the percentage so it looks pretty
*
* @param integer $percentage
* @return float
*/
protected function percentageFormatted($percentage)
{
return round($percentage * 100) . '%';
}
/**
* Format the label so it is positioned correctly
*
* @param string $label
* @return string
*/
protected function labelFormatted($label)
{
return "\n" . $this->util->cursor->startOfCurrentLine() . $this->util->cursor->deleteCurrentLine() . $label;
}
/**
* Determine whether the progress bar has changed and we need to redrew
*
* @param string $percentage
* @param string $label
*
* @return boolean
*/
protected function shouldRedraw($percentage, $label)
{
return ($this->force_redraw || $percentage != $this->current_percentage || $label != $this->label);
}
}
@@ -0,0 +1,18 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Radio extends Checkboxes
{
/**
* Build out the checkboxes
*
* @param array $options
*
* @return Checkbox\RadioGroup
*/
protected function buildCheckboxes(array $options)
{
return new Checkbox\RadioGroup($options);
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace League\CLImate\TerminalObject\Helper;
trait Art
{
/**
* The directories we should be looking for art in
*
* @var array $art_dirs
*/
protected $art_dirs = [];
/**
* The default art if we can't find what the user requested
*
* @var string $default_art
*/
protected $default_art = '404';
/**
* The art requested by the user
*
* @var string $art
*/
protected $art = '';
/**
* Specify which settings Draw needs to import
*
* @return array
*/
public function settings()
{
return ['Art'];
}
/**
* Import the Art setting (any directories the user added)
*
* @param \League\CLImate\Settings\Art $setting
*/
public function importSettingArt($setting)
{
foreach ($setting->dirs as $dir) {
$this->addDir($dir);
}
}
/**
* Add a directory to search for art in
*
* @param string $dir
*/
protected function addDir($dir)
{
// Add any additional directories to the top of the array
// so that the user can override art
array_unshift($this->art_dirs, rtrim($dir, '/'));
// Keep the array clean
$this->art_dirs = array_unique($this->art_dirs);
$this->art_dirs = array_filter($this->art_dirs);
$this->art_dirs = array_values($this->art_dirs);
}
/**
* Find a valid art path
*
* @param string $art
*
* @return array
*/
protected function artDir($art)
{
return $this->fileSearch($art, '/*.*');
}
/**
* Find a valid art path
*
* @param string $art
*
* @return string
*/
protected function artFile($art)
{
$files = $this->fileSearch($art, '.*');
return reset($files);
}
/**
* Find a set of files in the current art directories
* based on a pattern
*
* @param string $art
* @param string $pattern
*
* @return array
*/
protected function fileSearch($art, $pattern)
{
foreach ($this->art_dirs as $dir) {
$directory_iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
$paths = [];
$regex = '~' . preg_quote($art) . $pattern . '~';
foreach ($directory_iterator as $file) {
if ($file->isDir()) {
continue;
}
// Look for anything that has the $art filename
if (preg_match($regex, $file)) {
$paths[] = $file->getPathname();
}
}
asort($paths);
// If we've got one, no need to look any further
if (!empty($paths)) {
return $paths;
}
}
return [];
}
/**
* Parse the contents of the file and return each line
*
* @param string $path
*
* @return array
*/
protected function parse($path)
{
$output = file_get_contents($path);
$output = explode("\n", $output);
$output = array_map('rtrim', $output);
return $output;
}
}
@@ -0,0 +1,37 @@
<?php
namespace League\CLImate\TerminalObject\Helper;
class Sleeper implements SleeperInterface
{
/**
* The default length of the sleep
*
* @var int|float $speed
*/
protected $speed = 50000;
/**
* Set the speed based on a percentage (50% slower, 200% faster, etc)
*
* @param int|float $percentage
*
* @return float
*/
public function speed($percentage)
{
if (is_numeric($percentage) && $percentage > 0) {
$this->speed *= (100 / $percentage);
}
return $this->speed;
}
/**
* Sleep for the specified amount of time
*/
public function sleep()
{
usleep($this->speed);
}
}
@@ -0,0 +1,13 @@
<?php
namespace League\CLImate\TerminalObject\Helper;
interface SleeperInterface
{
/**
* @param int|float $percentage
*/
public function speed($percentage);
public function sleep();
}
@@ -0,0 +1,112 @@
<?php
namespace League\CLImate\TerminalObject\Helper;
trait StringLength
{
/**
* Tags the should not be ultimately considered
* when calculating any string lengths
*
* @var array $ignore_tags
*/
protected $ignore_tags = [];
/**
* Set the ignore tags property
*/
protected function setIgnoreTags()
{
if (!count($this->ignore_tags)) {
$this->ignore_tags = array_keys($this->parser->tags->all());
}
}
/**
* Determine the length of the string without any tags
*
* @param string $str
*
* @return integer
*/
protected function lengthWithoutTags($str)
{
$this->setIgnoreTags();
return mb_strwidth($this->withoutTags($str), 'UTF-8');
}
/**
* Get the string without the tags that are to be ignored
*
* @param string $str
*
* @return string
*/
protected function withoutTags($str)
{
$this->setIgnoreTags();
return str_replace($this->ignore_tags, '', $str);
}
/**
* Apply padding to a string
*
* @param string $str
* @param string $final_length
* @param string $padding_side
*
* @return string
*/
protected function pad($str, $final_length, $padding_side = 'right')
{
$padding = $final_length - $this->lengthWithoutTags($str);
if ($padding_side == 'left') {
return str_repeat(' ', $padding) . $str;
}
return $str . str_repeat(' ', $padding);
}
/**
* Apply padding to an array of strings
*
* @param array $arr
* @param integer $final_length
* @param string $padding_side
*
* @return array
*/
protected function padArray($arr, $final_length, $padding_side = 'right')
{
foreach ($arr as $key => $value) {
$arr[$key] = $this->pad($value, $final_length, $padding_side);
}
return $arr;
}
/**
* Find the max string length in an array
*
* @param array $arr
* @return int
*/
protected function maxStrLen(array $arr)
{
return max($this->arrayOfStrLens($arr));
}
/**
* Get an array of the string lengths from an array of strings
*
* @param array $arr
* @return array
*/
protected function arrayOfStrLens(array $arr)
{
return array_map([$this, 'lengthWithoutTags'], $arr);
}
}
@@ -0,0 +1,89 @@
<?php
namespace League\CLImate\TerminalObject\Router;
abstract class BaseRouter implements RouterInterface
{
protected $extensions = [];
/**
* Add a custom extension to CLImate
*
* @param string $key
* @param string $class
*/
public function addExtension($key, $class)
{
$this->extensions[$key] = $class;
}
/**
* Get the full path for the class based on the key
*
* @param string $class
*
* @return string
*/
public function path($class)
{
return $this->getExtension($class) ?: $this->getPath($this->shortName($class));
}
/**
* Determines if the requested class is a
* valid terminal object class
*
* @param string $class
*
* @return boolean
*/
public function exists($class)
{
$class = $this->path($class);
return (is_object($class) || class_exists($class));
}
/**
* Get the full path for the terminal object class
*
* @param string $class
*
* @return string
*/
protected function getPath($class)
{
return 'League\CLImate\TerminalObject\\' . $this->pathPrefix() . '\\' . $class;
}
/**
* Get an extension by its key
*
* @param string $key
*
* @return string|false Full class path to extension
*/
protected function getExtension($key)
{
if (array_key_exists($key, $this->extensions)) {
return $this->extensions[$key];
}
return false;
}
/**
* Get the class short name
*
* @param string $name
*
* @return string
*/
protected function shortName($name)
{
$name = str_replace('_', ' ', $name);
$name = ucwords($name);
return str_replace(' ', '', $name);
}
}
@@ -0,0 +1,42 @@
<?php
namespace League\CLImate\TerminalObject\Router;
use League\CLImate\Util\Helper;
use League\CLImate\Util\OutputImporter;
class BasicRouter extends BaseRouter
{
use OutputImporter;
/**
* @return string
*/
public function pathPrefix()
{
return 'Basic';
}
/**
* Execute a basic terminal object
*
* @param \League\CLImate\TerminalObject\Basic\BasicTerminalObject $obj
* @return void
*/
public function execute($obj)
{
$results = Helper::toArray($obj->result());
$this->output->persist();
foreach ($results as $result) {
if ($obj->sameLine()) {
$this->output->sameLine();
}
$this->output->write($obj->getParser()->apply($result));
}
$this->output->persist(false);
}
}
@@ -0,0 +1,32 @@
<?php
namespace League\CLImate\TerminalObject\Router;
use League\CLImate\Util\OutputImporter;
class DynamicRouter extends BaseRouter
{
use OutputImporter;
/**
* @return string
*/
public function pathPrefix()
{
return 'Dynamic';
}
/**
* Execute a dynamic terminal object using given arguments
*
* @param \League\CLImate\TerminalObject\Dynamic\DynamicTerminalObject $obj
*
* @return \League\CLImate\TerminalObject\Dynamic\DynamicTerminalObject
*/
public function execute($obj)
{
$obj->output($this->output);
return $obj;
}
}
@@ -0,0 +1,145 @@
<?php
namespace League\CLImate\TerminalObject\Router;
use League\CLImate\Util\Helper;
class ExtensionCollection
{
/**
* @var array collection
*/
protected $collection = ['basic' => [], 'dynamic' => []];
/**
* @var string $basic_interface
*/
protected $basic_interface = 'League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface';
/**
* @var string $dynamic_interface
*/
protected $dynamic_interface = 'League\CLImate\TerminalObject\Dynamic\DynamicTerminalObjectInterface';
public function __construct($key, $class)
{
$this->createCollection($key, $class);
}
public function collection()
{
return $this->collection;
}
/**
* Create the collection from the key/class
*
* @param string $original_key
* @param string|object|array $original_class
*
* @return type
*/
protected function createCollection($original_key, $original_class)
{
$collection = $this->convertToArray($original_key, $original_class);
foreach ($collection as $key => $class) {
$this->validateExtension($class);
$this->collection[$this->getType($class)][$this->getKey($key, $class)] = $class;
}
}
/**
* Convert the given class and key to an array of classes
*
* @param string|object|array $class
* @param string $key Optional custom key instead of class name
*
* @return array
*/
protected function convertToArray($key, $class)
{
if (is_array($class)) {
return $class;
}
return [$this->getKey($key, $class) => $class];
}
/**
* Ensure that the extension is valid
*
* @param string|object|array $class
*/
protected function validateExtension($class)
{
$this->validateClassExists($class);
$this->validateClassImplementation($class);
}
/**
* @param string|object $class
*
* @throws \Exception if extension class does not exist
*/
protected function validateClassExists($class)
{
if (is_string($class) && !class_exists($class)) {
throw new \Exception('Class does not exist: ' . $class);
}
}
/**
* @param string|object $class
*
* @throws \Exception if extension class does not implement either Dynamic or Basic interface
*/
protected function validateClassImplementation($class)
{
$str_class = is_string($class);
$valid_implementation = (is_a($class, $this->basic_interface, $str_class)
|| is_a($class, $this->dynamic_interface, $str_class));
if (!$valid_implementation) {
throw new \Exception('Class must implement either '
. $this->basic_interface . ' or ' . $this->dynamic_interface);
}
}
/**
* Determine the extension key based on the class
*
* @param string|null $key
* @param string|object $class
*
* @return string
*/
protected function getKey($key, $class)
{
if ($key === null || !is_string($key)) {
$class_path = (is_string($class)) ? $class : get_class($class);
$key = explode('\\', $class_path);
$key = end($key);
}
return Helper::snakeCase($key);
}
/**
* Get the type of class the extension implements
*
* @param string|object $class
*
* @return string 'basic' or 'dynamic'
*/
protected function getType($class)
{
if (is_a($class, $this->basic_interface, is_string($class))) {
return 'basic';
}
return 'dynamic';
}
}
@@ -0,0 +1,157 @@
<?php
namespace League\CLImate\TerminalObject\Router;
use League\CLImate\Decorator\Parser\ParserImporter;
use League\CLImate\Settings\Manager;
use League\CLImate\Settings\SettingsImporter;
use League\CLImate\Util\OutputImporter;
use League\CLImate\Util\UtilImporter;
class Router
{
use ParserImporter, SettingsImporter, OutputImporter, UtilImporter;
/**
* An instance of the Settings Manager class
*
* @var \League\CLImate\Settings\Manager $settings;
*/
protected $settings;
/**
* An instance of the Dynamic Router class
*
* @var \League\CLImate\TerminalObject\Router\DynamicRouter $dynamic;
*/
protected $dynamic;
/**
* An instance of the Basic Router class
*
* @var \League\CLImate\TerminalObject\Router\BasicRouter $basic;
*/
protected $basic;
public function __construct(DynamicRouter $dynamic = null, BasicRouter $basic = null)
{
$this->dynamic = $dynamic ?: new DynamicRouter();
$this->basic = $basic ?: new BasicRouter();
}
/**
* Register a custom class with the router
*
* @param string $key
* @param string $class
*/
public function addExtension($key, $class)
{
$extension = new ExtensionCollection($key, $class);
foreach ($extension->collection() as $obj_type => $collection) {
foreach ($collection as $obj_key => $obj_class) {
$this->{$obj_type}->addExtension($obj_key, $obj_class);
}
}
}
/**
* Check if the name matches an existing terminal object
*
* @param string $name
*
* @return boolean
*/
public function exists($name)
{
return ($this->basic->exists($name) || $this->dynamic->exists($name));
}
/**
* Execute a terminal object using given arguments
*
* @param string $name
* @param mixed $arguments
*
* @return null|\League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface
*/
public function execute($name, $arguments)
{
$router = $this->getRouter($name);
$router->output($this->output);
$obj = $this->getObject($router, $name, $arguments);
$obj->parser($this->parser);
$obj->util($this->util);
// If the object needs any settings, import them
foreach ($obj->settings() as $obj_setting) {
$setting = $this->settings->get($obj_setting);
if ($setting) {
$obj->importSetting($setting);
}
}
return $router->execute($obj);
}
/**
* Get the object whether it's a string or already instantiated
*
* @param \League\CLImate\TerminalObject\Router\RouterInterface $router
* @param string $name
* @param array $arguments
*
* @return \League\CLImate\TerminalObject\Dynamic\DynamicTerminalObjectInterface|\League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface
*/
protected function getObject($router, $name, $arguments)
{
$obj = $router->path($name);
if (is_string($obj)) {
$obj = (new \ReflectionClass($obj))->newInstanceArgs($arguments);
}
if (method_exists($obj, 'arguments')) {
call_user_func_array([$obj, 'arguments'], $arguments);
}
return $obj;
}
/**
* Determine which type of router we are using and return it
*
* @param string $name
*
* @return \League\CLImate\TerminalObject\Router\RouterInterface|null
*/
protected function getRouter($name)
{
if ($this->basic->exists($name)) {
return $this->basic;
}
if ($this->dynamic->exists($name)) {
return $this->dynamic;
}
}
/**
* Set the settings property
*
* @param \League\CLImate\Settings\Manager $settings
*
* @return Router
*/
public function settings(Manager $settings)
{
$this->settings = $settings;
return $this;
}
}
@@ -0,0 +1,29 @@
<?php
namespace League\CLImate\TerminalObject\Router;
interface RouterInterface
{
/**
* @param $class
* @return string
*/
public function path($class);
/**
* @param $class
* @return boolean
*/
public function exists($class);
/**
* @param $obj
* @return null|\League\CLImate\TerminalObject\Dynamic\DynamicTerminalObject
*/
public function execute($obj);
/**
* @return string
*/
public function pathPrefix();
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace League\CLImate\Util;
class Cursor
{
/**
* Move the cursor up in the terminal x number of lines.
*
* @param int $lines
*
* @return string
*/
public function up($lines = 1)
{
return "\e[{$lines}A";
}
/**
* Move the cursor left in the terminal x number of columns.
*
* @param int $cols
*
* @return string
*/
public function left($cols = 1)
{
return "\e[{$cols}D";
}
/**
* Move cursor to the beginning of the current line.
*
* @return string
*/
public function startOfCurrentLine()
{
return "\r";
}
/**
* Delete the current line to the end.
*
* @return string
*/
public function deleteCurrentLine()
{
return "\e[K";
}
/**
* Get the style for hiding the cursor
*
* @return string
*/
public function hide()
{
return "\e[?25l";
}
/**
* Get the style for returning the cursor to its default
*
* @return string
*/
public function defaultStyle()
{
return "\e[?25h";
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace League\CLImate\Util;
class Helper
{
/**
* @param string|array $var
*
* @return array
*/
public static function toArray($var)
{
if (!is_array($var)) {
return [$var];
}
return $var;
}
/**
* Flatten a multi-dimensional array
*
* @param array $arr
*
* @return array
*/
public static function flatten(array $arr)
{
$flattened = [];
array_walk_recursive($arr, function ($a) use (&$flattened) {
$flattened[] = $a;
});
return $flattened;
}
/**
* Convert a string to snake case
*
* @param string $str
*
* @return string
*/
public static function snakeCase($str)
{
return strtolower(preg_replace('/(.)([A-Z])/', '$1_$2', $str));
}
}
+313
View File
@@ -0,0 +1,313 @@
<?php
namespace League\CLImate\Util;
use League\CLImate\Util\Writer\WriterInterface;
class Output
{
/**
* The content to be output
*
* @var string $content
*/
protected $content;
/**
* Whether or not to add a new line after the output
*
* @var boolean $new_line
*/
protected $new_line = true;
/**
* The array of available writers
*
* @var array[] $writers
*/
protected $writers = [];
/**
* Default writers when one isn't specifed
*
* @var WriterInterface[] $default
*/
protected $default = [];
/**
* Writers to be used just once
*
* @var null|array $once
*/
protected $once;
protected $persist = false;
public function __construct()
{
$this->add('out', new Writer\StdOut);
$this->add('error', new Writer\StdErr);
$this->add('buffer', new Writer\Buffer);
$this->defaultTo('out');
}
/**
* Dictate that a new line should not be added after the output
*/
public function sameLine()
{
$this->new_line = false;
return $this;
}
/**
* Add a writer to the available writers
*
* @param string $key
* @param WriterInterface|array $writer
*
* @return \League\CLImate\Util\Output
*/
public function add($key, $writer)
{
$this->writers[$key] = $this->resolve(Helper::toArray($writer));
return $this;
}
/**
* Set the default writer
*
* @param string|array $keys
*/
public function defaultTo($keys)
{
$this->default = $this->getWriters($keys);
}
/**
* Add a default writer
*
* @param string|array $keys
*/
public function addDefault($keys)
{
$this->default = array_merge($this->default, $this->getWriters($keys));
}
/**
* Register a writer to be used just once
*
* @param string|array $keys
*
* @return \League\CLImate\Util\Output
*/
public function once($keys)
{
$this->once = $this->getWriters($keys);
return $this;
}
/**
* Persist or un-persist one time writers (for multi-line output)
*
* @param bool $persist
*
* @return \League\CLImate\Util\Output
*/
public function persist($persist = true)
{
$this->persist = (bool) $persist;
if (!$this->persist) {
$this->resetOneTimers();
}
return $this;
}
/**
* Get a specific writer
*
* @throws \Exception if writer key doesn't exist
* @param string $writer
*
* @return WriterInterface|array
*/
public function get($writer)
{
if (!array_key_exists($writer, $this->writers)) {
throw new \Exception('Unknown writer [' . $writer . ']');
}
if (count($this->writers[$writer]) == 1) {
return reset($this->writers[$writer]);
}
return $this->writers[$writer];
}
/**
* Get the currently available writers
*
* @return array
*/
public function getAvailable()
{
$writers = [];
foreach ($this->writers as $key => $writer) {
$writers[$key] = $this->getReadable($writer);
}
return $writers;
}
/**
* Write the content using the provided writer
*
* @param string $content
*/
public function write($content)
{
if ($this->new_line) {
$content .= PHP_EOL;
}
foreach ($this->getCurrentWriters() as $writer) {
$writer->write($content);
}
$this->resetOneTimers();
}
/**
* Resolve the writer(s) down to an array of WriterInterface classes
*
* @param WriterInterface|array|string $writer
*
* @return array
*/
protected function resolve($writer)
{
$resolver = 'resolve' . ucwords(gettype($writer)) . 'Writer';
if (method_exists($this, $resolver) && $resolved = $this->{$resolver}($writer)) {
return $resolved;
}
$this->handleUnknownWriter($writer);
}
/**
* @param array $writer
*
* @return array
*/
protected function resolveArrayWriter($writer)
{
return Helper::flatten(array_map([$this, 'resolve'], $writer));
}
/**
* @param object $writer
*
* @return WriterInterface|false
*/
protected function resolveObjectWriter($writer)
{
if ($writer instanceof WriterInterface) {
return $writer;
}
return false;
}
/**
* @param string $writer
*
* @return array|false
*/
protected function resolveStringWriter($writer)
{
if (is_string($writer) && array_key_exists($writer, $this->writers)) {
return $this->writers[$writer];
}
return false;
}
/**
* @param mixed $writer
* @throws \Exception For non-valid writer
*/
protected function handleUnknownWriter($writer)
{
// If we've gotten this far and don't know what it is,
// let's at least try and give a helpful error message
if (is_object($writer)) {
throw new \Exception('Class [' . get_class($writer) . '] must implement '
. 'League\CLImate\Util\Writer\WriterInterface.');
}
// No idea, just tell them we can't resolve it
throw new \Exception('Unable to resolve writer [' . $writer . ']');
}
/**
* Get the readable version of the writer(s)
*
* @param array $writer
*
* @return string|array
*/
protected function getReadable(array $writer)
{
$classes = array_map('get_class', $writer);
if (count($classes) == 1) {
return reset($classes);
}
return $classes;
}
/**
* Get the writers based on their keys
*
* @param string|array $keys
*
* @return array
*/
protected function getWriters($keys)
{
$writers = array_flip(Helper::toArray($keys));
return Helper::flatten(array_intersect_key($this->writers, $writers));
}
/**
* @return WriterInterface[]
*/
protected function getCurrentWriters()
{
return $this->once ?: $this->default;
}
/**
* Reset anything only used for the current content being written
*/
protected function resetOneTimers()
{
// Reset new line flag for next time
$this->new_line = true;
if (!$this->persist) {
// Reset once since we only want to use it... once.
$this->once = null;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace League\CLImate\Util;
trait OutputImporter
{
/**
* An instance of the OutputFactory
*
* @var \League\CLImate\Util\Output $output
*/
protected $output;
/**
* Sets the $output property
*
* @param Output $output
*/
public function output(Output $output)
{
$this->output = $output;
}
}
@@ -0,0 +1,16 @@
<?php
namespace League\CLImate\Util\Reader;
interface ReaderInterface
{
/**
* @return string
*/
public function line();
/**
* @return string
*/
public function multiLine();
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace League\CLImate\Util\Reader;
use Seld\CliPrompt\CliPrompt;
class Stdin implements ReaderInterface
{
protected $stdIn = false;
/**
* Read the line typed in by the user
*
* @return string
*/
public function line()
{
return trim(fgets($this->getStdIn(), 1024));
}
/**
* Read from STDIN until EOF (^D) is reached
*
* @return string
*/
public function multiLine()
{
return trim(stream_get_contents($this->getStdIn()));
}
/**
* Read one character
*
* @param int $count
*
* @return string
*/
public function char($count = 1)
{
return fread($this->getStdIn(), $count);
}
/**
* Read the line, but hide what the user is typing
*
* @return string
*/
public function hidden()
{
return CliPrompt::hiddenPrompt();
}
/**
* Return a valid STDIN, even if it previously EOF'ed
*
* Lazily re-opens STDIN after hitting an EOF
*
* @return resource
* @throws \Exception
*/
protected function getStdIn()
{
if ($this->stdIn && !feof($this->stdIn)) {
return $this->stdIn;
}
try {
$this->setStdIn();
} catch (\Error $e) {
throw new \Exception('Unable to read from STDIN', 0, $e);
}
return $this->stdIn;
}
/**
* Attempt to set the stdin property
*
* @throws \Exception
*/
protected function setStdIn()
{
if ($this->stdIn !== false) {
fclose($this->stdIn);
}
$this->stdIn = fopen('php://stdin', 'r');
if (!$this->stdIn) {
throw new \Exception('Unable to read from STDIN');
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace League\CLImate\Util\System;
class Linux extends System
{
/**
* Get the width of the terminal
*
* @return integer|null
*/
public function width()
{
return $this->getDimension($this->exec('tput cols'));
}
/**
* Get the height of the terminal
*
* @return integer|null
*/
public function height()
{
return $this->getDimension($this->exec('tput lines'));
}
/**
* Determine if system has access to bash commands
*
* @return bool
*/
public function canAccessBash()
{
return (rtrim($this->exec("/usr/bin/env bash -c 'echo OK'")) === 'OK');
}
/**
* Display a hidden response prompt and return the response
*
* @param string $prompt
*
* @return string
*/
public function hiddenResponsePrompt($prompt)
{
$bash_command = 'read -s -p "' . $prompt . '" response && echo $response';
return rtrim($this->exec("/usr/bin/env bash -c '{$bash_command}'"));
}
/**
* Determine if dimension is numeric and return it
*
* @param integer|string|null $dimension
*
* @return integer|null
*/
protected function getDimension($dimension)
{
return (is_numeric($dimension)) ? $dimension : null;
}
/**
* Check if the stream supports ansi escape characters.
*
* Based on https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Output/StreamOutput.php
*
* @return bool
*/
protected function systemHasAnsiSupport()
{
return (function_exists('posix_isatty') && @posix_isatty(STDOUT));
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace League\CLImate\Util\System;
abstract class System
{
protected $force_ansi;
/**
* Force ansi on or off
*
* @param bool $force
*/
public function forceAnsi($force = true)
{
$this->force_ansi = $force;
}
/**
* @return integer|null
*/
abstract public function width();
/**
* @return integer|null
*/
abstract public function height();
/**
* Check if the stream supports ansi escape characters.
*
* @return bool
*/
abstract protected function systemHasAnsiSupport();
/**
* Check if we are forcing ansi, fallback to system support
*
* @return bool
*/
public function hasAnsiSupport()
{
if (is_bool($this->force_ansi)) {
return $this->force_ansi;
}
return $this->systemHasAnsiSupport();
}
/**
* Wraps exec function, allowing the dimension methods to decouple
*
* @param string $command
* @param boolean $full
*
* @return string|array
*/
public function exec($command, $full = false)
{
if ($full) {
exec($command, $output);
return $output;
}
return exec($command);
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace League\CLImate\Util\System;
class SystemFactory
{
/**
* @var \League\CLImate\Util\System\System $instance
*/
protected static $instance;
/**
* Get an instance of the appropriate System class
*
* @return \League\CLImate\Util\System\System
*/
public static function getInstance()
{
if (static::$instance) {
return static::$instance;
}
static::$instance = self::getSystem();
return static::$instance;
}
/**
* Set the $instance property to the appropriate system
*
* @return \League\CLImate\Util\System\System
*/
protected static function getSystem()
{
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
return new Windows();
}
return new Linux();
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace League\CLImate\Util\System;
class Windows extends System
{
/**
* Get the width of the terminal
*
* @return integer|null
*/
public function width()
{
return $this->getDimension('width');
}
/**
* Get the height of the terminal
*
* @return integer|null
*/
public function height()
{
return $this->getDimension('height');
}
/**
* Get specified terminal dimension
*
* @param string $key
*
* @return integer|null
*/
protected function getDimension($key)
{
$index = array_search($key, ['height', 'width']);
$dimensions = $this->getDimensions();
return (!empty($dimensions[$index])) ? $dimensions[$index] : null;
}
/**
* Get information about the dimensions of the terminal
*
* @return array
*/
protected function getDimensions()
{
$output = $this->exec('mode', true);
if (!is_array($output)) {
return [];
}
$output = implode("\n", $output);
preg_match_all('/.*:\s*(\d+)/', $output, $matches);
return (!empty($matches[1])) ? $matches[1] : [];
}
/**
* Check if the stream supports ansi escape characters.
*
* Based on https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Output/StreamOutput.php
*
* @return bool
*/
protected function systemHasAnsiSupport()
{
return (getenv('ANSICON') === true || getenv('ConEmuANSI') === 'ON');
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace League\CLImate\Util;
use League\CLImate\Util\System\SystemFactory;
use League\CLImate\Util\System\System;
class UtilFactory
{
/**
* A instance of the appropriate System class
*
* @var \League\CLImate\Util\System\System
*/
public $system;
/**
* A instance of the Cursor class
*
* @var \League\CLImate\Util\Cursor
*/
public $cursor;
public function __construct(System $system = null, Cursor $cursor = null)
{
$this->system = $system ?: SystemFactory::getInstance();
$this->cursor = $cursor ?: new Cursor();
}
/**
* Get the width of the terminal
*
* @return integer
*/
public function width()
{
return (int) $this->getDimension($this->system->width(), 80);
}
/**
* Get the height of the terminal
*
* @return integer
*/
public function height()
{
return (int) $this->getDimension($this->system->height(), 25);
}
/**
* Determine if the value is numeric, fallback to a default if not
*
* @param integer|null $dimension
* @param integer $default
*
* @return integer
*/
protected function getDimension($dimension, $default)
{
return (is_numeric($dimension)) ? $dimension : $default;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace League\CLImate\Util;
trait UtilImporter
{
/**
* An instance of the UtilFactory
*
* @var \League\CLImate\Util\UtilFactory $util
*/
protected $util;
/**
* Sets the $util property
*
* @param UtilFactory $util
*/
public function util(UtilFactory $util)
{
$this->util = $util;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace League\CLImate\Util\Writer;
class Buffer implements WriterInterface
{
/**
* @var string $contents The buffered data.
*/
protected $contents = "";
/**
* Write the content to the buffer.
*
* @param string $content
*
* @return void
*/
public function write($content)
{
$this->contents .= $content;
}
/**
* Get the buffered data.
*
* @return string
*/
public function get()
{
return $this->contents;
}
/**
* Clean the buffer and throw away any data.
*
* @return void
*/
public function clean()
{
$this->contents = "";
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace League\CLImate\Util\Writer;
class File implements WriterInterface
{
/** @var resource|string */
protected $resource;
/** @var boolean $close_locally */
protected $close_locally = false;
/** @var boolean $use_locking */
protected $use_locking = false;
/** @var boolean $gzip_file */
protected $gzip_file = false;
/**
* @param string|resource $resource
* @param bool $use_locking
* @param bool $gzip_file
*/
public function __construct($resource, $use_locking = false, $gzip_file = false)
{
$this->resource = $resource;
$this->use_locking = $use_locking;
$this->gzip_file = $gzip_file;
}
public function lock()
{
$this->use_locking = true;
return $this;
}
public function gzipped()
{
$this->gzip_file = true;
return $this;
}
/**
* Write the content to the stream
*
* @param string $content
*/
public function write($content)
{
$resource = $this->getResource();
if ($this->use_locking) {
flock($resource, LOCK_EX);
}
gzwrite($resource, $content);
if ($this->use_locking) {
flock($resource, LOCK_UN);
}
}
protected function getResource()
{
if (is_resource($this->resource)) {
return $this->resource;
}
$this->close_locally = true;
if (!is_writable($this->resource)) {
throw new \Exception("The resource [{$this->resource}] is not writable");
}
if (!($this->resource = $this->openResource())) {
throw new \Exception("The resource could not be opened");
}
return $this->resource;
}
protected function openResource()
{
if ($this->gzip_file) {
return gzopen($this->resource, 'a');
}
return fopen($this->resource, 'a');
}
public function _destruct()
{
if ($this->close_locally) {
gzclose($this->getResource());
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace League\CLImate\Util\Writer;
class StdErr implements WriterInterface
{
/**
* Write the content to the stream
*
* @param string $content
*/
public function write($content)
{
fwrite(\STDERR, $content);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace League\CLImate\Util\Writer;
class StdOut implements WriterInterface
{
/**
* Write the content to the stream
*
* @param string $content
*/
public function write($content)
{
fwrite(\STDOUT, $content);
}
}
@@ -0,0 +1,13 @@
<?php
namespace League\CLImate\Util\Writer;
interface WriterInterface
{
/**
* @param string $content
*
* @return void
*/
public function write($content);
}