First commit

This commit is contained in:
Pierre Hubert
2016-11-19 12:08:12 +01:00
commit 990540b2b9
4706 changed files with 931207 additions and 0 deletions

View File

@ -0,0 +1,64 @@
## 3.2.0 - 2015-08-13
### Added
- Multi-line support for `input` method [https://github.com/thephpleague/climate/pull/67](https://github.com/thephpleague/climate/pull/67)
- `extend` method for _much_ easier extending of CLImate
### Fixed
- Unnecessary progress bar re-drawing when the output hadn't changed [https://github.com/thephpleague/climate/pull/69](https://github.com/thephpleague/climate/pull/69)
- Progress label no longer removed once progress reaches 100%
- Non-prefixed paramaters for `arguments` method now show in usage description [https://github.com/thephpleague/climate/issues/65](https://github.com/thephpleague/climate/issues/65)
## 3.1.1 - 2015-05-01
### Fixed
- Windows support added for `password` thanks to @Seldaek and [seld/cli-prompt](https://packagist.org/packages/seld/cli-prompt)
## 3.1.0 - 2015-04-30
### Added
- `password` prompt
- `checkboxes` prompt
- `radio` prompt
- 'file' as output option
## 3.0.0 - 2015-03-01
### Changed
- Custom output writers are added simply via the `output` property on CLImate now, as opposed to the immense amount of scaffolding required before
### Added
- Argument parsing
- StdErr output
- Buffer output
- `animate` method for running ASCII animations in the terminal. Because it's fun.
- Input now bolds the default response if it exists
## 2.6.1 - 2015-01-18
### Fixed
- Added `forceAnsiOn` and `forceAnsiOff` methods to address systems that were not identified correctly
## 2.6.0 - 2015-01-07
### Added
- Allow for passing an array of arrays into `columns` method
- `tab` method, for indenting text
- `padding` method, for padding strings to an equal width with a character
- `League\CLImate\TerminalObject\Repeatable` for repeatable objects such as `tab` and `br`
- `League\CLImate\Decorator\Parser\Ansi` and `League\CLImate\Decorator\Parser\NonAnsi`
- Factories:
+ `League\CLImate\Decorator\Parser\ParserFactory`
+ `League\CLImate\Util\System\SystemFactory`
- Terminal Objects now are appropriately namespaced as `Basic` or `Dynamic`
- Readers and Writers are appropriately namespaced as such under `League\CLImate\Util`
### Fixed
- Labels for `advance` method
- Non-ansi terminals will now have plaintext output instead of jumbled characters
- `border` method now default to full terminal width

View File

@ -0,0 +1,22 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)

View File

@ -0,0 +1,32 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/climate).
## Pull Requests
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
- **Create feature branches** - Don't ask us to pull from your master branch.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
## Running Tests
``` bash
$ composer test
```
**Happy coding**!

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Joe Tannenbaum
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,46 @@
<p align="center"><img src="http://climate.thephpleague.com/img/CLImate_Blink-100k.gif" width="300" alt="CLImate" /></p>
[![Latest Version](https://img.shields.io/github/tag/thephpleague/climate.svg?style=flat&label=release)](https://github.com/thephpleague/climate/tags)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.md)
[![Build Status](https://img.shields.io/travis/thephpleague/climate/master.svg?style=flat)](https://travis-ci.org/thephpleague/climate)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/climate.svg?style=flat)](https://scrutinizer-ci.com/g/thephpleague/climate/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/climate.svg?style=flat)](https://scrutinizer-ci.com/g/thephpleague/climate)
[![Total Downloads](https://img.shields.io/packagist/dt/league/climate.svg?style=flat)](https://packagist.org/packages/league/climate)
Running PHP from the command line? CLImate is your new best bud.
CLImate allows you to easily output colored text, special formats, and more.
## Table of Contents
+ [Installation](#installation)
+ [Requirements](#requirements)
+ [Documentation](#documentation)
+ [Credits](#credits)
## Installation
Using [composer](https://packagist.org/packages/league/climate):
```bash
$ composer require league/climate
```
## Requirements
The following versions of PHP are supported by this version.
+ PHP 5.4
+ PHP 5.5
+ PHP 5.6
+ HHVM
## Documentation
CLImate has [full documentation](http://climate.thephpleague.com), powered by [Jekyll](http://jekyllrb.com/).
Contribute to this documentation in the [gh-pages branch](https://github.com/thephpleague/climate/tree/gh-pages/).
## Credits
Much love to [Damian Makki](https://dribbble.com/damianmakki) for the logo.

View File

@ -0,0 +1,6 @@
_ _ ___ _ _
| || | / _ \| || |
| || |_| | | | || |_
|__ _| | | |__ _|
| | | |_| | | |
|_| \___/ |_|

View File

@ -0,0 +1,17 @@
( )
H
H
_H_
.-'-.-'-.
/ \
| |
| .-------'._
| / / '.' '. \
| \ \ @ @ / /
| '---------'
| _______|
| .'-+-+-+|
| '.-+-+-+|
| """""" |
'-.__ __.-'
"""

View File

@ -0,0 +1,6 @@
______ _____ _ ______ _____
| ____/\ |_ _| | | ____| __ \
| |__ / \ | | | | | |__ | | | |
| __/ /\ \ | | | | | __| | | | |
| | / ____ \ _| |_| |____| |____| |__| |
|_|/_/ \_\_____|______|______|_____/

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>

View File

@ -0,0 +1,6 @@
_____ _____ _____ ______ _____
| __ \ /\ / ____/ ____| ____| __ \
| |__) / \ | (___| (___ | |__ | | | |
| ___/ /\ \ \___ \\___ \| __| | | | |
| | / ____ \ ____) |___) | |____| |__| |
|_| /_/ \_\_____/_____/|______|_____/

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;
}
}

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() ?: '');
}
}

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();
}
}

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');
}
}

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);
}
}

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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

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);
}
}

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);
}
}

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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

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);
}
}
}

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;
}
}
}

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);
}
}

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));
}
}

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);
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace League\CLImate\Settings;
interface SettingsInterface
{
/**
* @return void
*/
public function add();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

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, '');
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

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);
}
}

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;
}
}

View File

@ -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}";
}
}

View File

@ -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;
}
}

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);
}
}

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;
}
}

View File

@ -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));
}
}

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);
}
}

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));
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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');
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace League\CLImate\TerminalObject\Dynamic;
class Password extends Input
{
public function prompt()
{
$this->writePrompt();
return $this->reader->hidden();
}
}

View File

@ -0,0 +1,264 @@
<?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 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 one line and clear it to the end
$line_count = (strlen($label) > 0) ? 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);
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);
}
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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,132 @@
<?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) {
// Look for anything that has the $art filename
$paths = glob($dir . '/' . $art . $pattern);
// 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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace League\CLImate\TerminalObject\Helper;
interface SleeperInterface
{
/**
* @param int|float $percentage
*/
public function speed($percentage);
public function sleep();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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';
}
}

View File

@ -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;
}
}

View File

@ -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();
}

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";
}
}

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));
}
}

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;
}
}
}

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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace League\CLImate\Util\Reader;
interface ReaderInterface
{
/**
* @return string
*/
public function line();
/**
* @return string
*/
public function multiLine();
}

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');
}
}
}

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));
}
}

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);
}
}

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();
}
}

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');
}
}

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;
}
}

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;
}
}

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 = "";
}
}

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());
}
}
}

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);
}
}

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);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace League\CLImate\Util\Writer;
interface WriterInterface
{
/**
* @param string $content
*
* @return void
*/
public function write($content);
}