├── src ├── ASCII │ ├── 404.txt │ ├── failed.txt │ ├── passed.txt │ ├── bender.txt │ ├── fancy-bender.txt │ └── the-league.txt ├── Settings │ ├── SettingsInterface.php │ ├── Art.php │ ├── SettingsImporter.php │ └── Manager.php ├── Util │ ├── Writer │ │ ├── WriterInterface.php │ │ ├── StdErr.php │ │ ├── StdOut.php │ │ ├── Buffer.php │ │ └── File.php │ ├── Reader │ │ ├── ReaderInterface.php │ │ └── Stdin.php │ ├── UtilImporter.php │ ├── OutputImporter.php │ ├── System │ │ ├── SystemFactory.php │ │ ├── System.php │ │ ├── Windows.php │ │ └── Linux.php │ ├── Helper.php │ ├── Cursor.php │ ├── UtilFactory.php │ └── Output.php ├── TerminalObject │ ├── Dynamic │ │ ├── Password.php │ │ ├── Confirm.php │ │ ├── Radio.php │ │ ├── DynamicTerminalObject.php │ │ ├── DynamicTerminalObjectInterface.php │ │ ├── InputAbstract.php │ │ ├── Checkbox │ │ │ ├── RadioGroup.php │ │ │ ├── CheckboxGroup.php │ │ │ └── Checkbox.php │ │ ├── Padding.php │ │ ├── Spinner.php │ │ ├── Checkboxes.php │ │ ├── Animation.php │ │ ├── Input.php │ │ ├── Animation │ │ │ └── Keyframe.php │ │ └── Progress.php │ ├── Helper │ │ ├── SleeperInterface.php │ │ ├── Sleeper.php │ │ ├── StringLength.php │ │ └── Art.php │ ├── Basic │ │ ├── Br.php │ │ ├── Inline.php │ │ ├── Clear.php │ │ ├── Repeatable.php │ │ ├── Out.php │ │ ├── Json.php │ │ ├── Draw.php │ │ ├── Tab.php │ │ ├── Dump.php │ │ ├── BasicTerminalObjectInterface.php │ │ ├── BasicTerminalObject.php │ │ ├── Border.php │ │ ├── Flank.php │ │ ├── Columns.php │ │ └── Table.php │ └── Router │ │ ├── RouterInterface.php │ │ ├── DynamicRouter.php │ │ ├── BasicRouter.php │ │ ├── BaseRouter.php │ │ ├── ExtensionCollection.php │ │ └── Router.php ├── Decorator │ ├── Parser │ │ ├── NonAnsi.php │ │ ├── ParserImporter.php │ │ ├── ParserFactory.php │ │ ├── Parser.php │ │ └── Ansi.php │ ├── Component │ │ ├── DecoratorInterface.php │ │ ├── BaseDecorator.php │ │ ├── BackgroundColor.php │ │ ├── Command.php │ │ ├── Format.php │ │ └── Color.php │ ├── Tags.php │ └── Style.php ├── Argument │ ├── Filter.php │ ├── Summary.php │ ├── Manager.php │ └── Parser.php └── Logger.php ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md └── CHANGELOG.md /src/ASCII/404.txt: -------------------------------------------------------------------------------- 1 | _ _ ___ _ _ 2 | | || | / _ \| || | 3 | | || |_| | | | || |_ 4 | |__ _| | | |__ _| 5 | | | | |_| | | | 6 | |_| \___/ |_| -------------------------------------------------------------------------------- /src/Settings/SettingsInterface.php: -------------------------------------------------------------------------------- 1 | writePrompt(); 10 | 11 | return $this->reader->hidden(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ASCII/bender.txt: -------------------------------------------------------------------------------- 1 | ( ) 2 | H 3 | H 4 | _H_ 5 | .-'-.-'-. 6 | / \ 7 | | | 8 | | .-------'._ 9 | | / / '.' '. \ 10 | | \ \ @ @ / / 11 | | '---------' 12 | | _______| 13 | | .'-+-+-+| 14 | | '.-+-+-+| 15 | | """""" | 16 | '-.__ __.-' 17 | """ -------------------------------------------------------------------------------- /src/TerminalObject/Helper/SleeperInterface.php: -------------------------------------------------------------------------------- 1 | count, ''); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Util/Writer/StdErr.php: -------------------------------------------------------------------------------- 1 | tags->regex(), '', $str); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Clear.php: -------------------------------------------------------------------------------- 1 | accept(['y', 'n'], true); 15 | $this->strict(); 16 | 17 | return ($this->prompt() == 'y'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Radio.php: -------------------------------------------------------------------------------- 1 | count = (int) round(max((int) $count, 1)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Util/UtilImporter.php: -------------------------------------------------------------------------------- 1 | util = $util; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Util/OutputImporter.php: -------------------------------------------------------------------------------- 1 | output = $output; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Settings/Art.php: -------------------------------------------------------------------------------- 1 | dirs = array_merge($this->dirs, func_get_args()); 19 | $this->dirs = array_filter($this->dirs); 20 | $this->dirs = array_values($this->dirs); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Decorator/Component/DecoratorInterface.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Out.php: -------------------------------------------------------------------------------- 1 | content = $content; 17 | } 18 | 19 | /** 20 | * Return the content to output 21 | * 22 | * @return string 23 | */ 24 | public function result() 25 | { 26 | return $this->content; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ASCII/fancy-bender.txt: -------------------------------------------------------------------------------- 1 | ( ) 2 | H 3 | H 4 | _H_ 5 | .-'-.-'-. 6 | / \ 7 | | | 8 | | .-------'._ 9 | | // '.' '. \ 10 | | \\ @ @ / / 11 | | '---------' 12 | | _______| 13 | | .'-+-+-+| 14 | | '.-+-+-+| 15 | | """""" | 16 | '-.__ __.-' 17 | """ -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Json.php: -------------------------------------------------------------------------------- 1 | data = $data; 17 | } 18 | 19 | /** 20 | * Return the data as JSON 21 | * 22 | * @return string 23 | */ 24 | public function result() 25 | { 26 | return json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/DynamicTerminalObject.php: -------------------------------------------------------------------------------- 1 | addDir(__DIR__ . '/../../ASCII'); 15 | 16 | $this->art = $art; 17 | } 18 | 19 | /** 20 | * Return the art 21 | * 22 | * @return array 23 | */ 24 | public function result() 25 | { 26 | $file = $this->artFile($this->art); 27 | 28 | return $this->parse($file); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Tab.php: -------------------------------------------------------------------------------- 1 | count); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/DynamicTerminalObjectInterface.php: -------------------------------------------------------------------------------- 1 | hasAnsiSupport()) { 21 | return new Ansi($current, $tags); 22 | } 23 | 24 | return new NonAnsi($current, $tags); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Dump.php: -------------------------------------------------------------------------------- 1 | data = $data; 17 | } 18 | 19 | /** 20 | * Return the data as JSON 21 | * 22 | * @return string 23 | */ 24 | public function result() 25 | { 26 | ob_start(); 27 | 28 | var_dump($this->data); 29 | 30 | $result = ob_get_contents(); 31 | 32 | ob_end_clean(); 33 | 34 | return $result; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/TerminalObject/Router/DynamicRouter.php: -------------------------------------------------------------------------------- 1 | output($this->output); 29 | 30 | return $obj; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Settings/SettingsImporter.php: -------------------------------------------------------------------------------- 1 | $method($setting); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/BasicTerminalObjectInterface.php: -------------------------------------------------------------------------------- 1 | 0) { 24 | $this->speed *= (100 / $percentage); 25 | } 26 | 27 | return $this->speed; 28 | } 29 | 30 | /** 31 | * Sleep for the specified amount of time 32 | */ 33 | public function sleep() 34 | { 35 | usleep($this->speed); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Util/Writer/Buffer.php: -------------------------------------------------------------------------------- 1 | contents .= $content; 22 | } 23 | 24 | 25 | /** 26 | * Get the buffered data. 27 | * 28 | * @return string 29 | */ 30 | public function get() 31 | { 32 | return $this->contents; 33 | } 34 | 35 | /** 36 | * Clean the buffer and throw away any data. 37 | * 38 | * @return void 39 | */ 40 | public function clean() 41 | { 42 | $this->contents = ""; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Decorator/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | current = $current; 27 | $this->tags = $tags; 28 | } 29 | 30 | /** 31 | * Wrap the string in the current style 32 | * 33 | * @param string $str 34 | * 35 | * @return string 36 | */ 37 | abstract public function apply($str); 38 | } 39 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/InputAbstract.php: -------------------------------------------------------------------------------- 1 | getCurrent(); 13 | 14 | $checkbox->setChecked(!$checkbox->isChecked()); 15 | 16 | foreach ($this->checkboxes as $key => $checkbox) { 17 | if ($key == $checkbox_key) { 18 | continue; 19 | } 20 | 21 | $checkbox->setChecked(false); 22 | } 23 | } 24 | 25 | /** 26 | * Get the checked option 27 | * 28 | * @return string|bool|int 29 | */ 30 | public function getCheckedValues() 31 | { 32 | if ($checked = $this->getChecked()) { 33 | return reset($checked)->getValue(); 34 | } 35 | 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Util/System/SystemFactory.php: -------------------------------------------------------------------------------- 1 | result()); 29 | 30 | $this->output->persist(); 31 | 32 | foreach ($results as $result) { 33 | if ($obj->sameLine()) { 34 | $this->output->sameLine(); 35 | } 36 | 37 | $this->output->write($obj->getParser()->apply($result)); 38 | } 39 | 40 | $this->output->persist(false); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Util/Helper.php: -------------------------------------------------------------------------------- 1 | $key = $value; 23 | } 24 | } 25 | 26 | /** 27 | * Get the parser for the current object 28 | * 29 | * @return \League\CLImate\Decorator\Parser\Parser 30 | */ 31 | public function getParser() 32 | { 33 | return $this->parser; 34 | } 35 | 36 | /** 37 | * Check if this object requires a new line to be added after the output 38 | * 39 | * @return boolean 40 | */ 41 | public function sameLine() 42 | { 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Decorator/Component/BaseDecorator.php: -------------------------------------------------------------------------------- 1 | defaults(); 24 | } 25 | 26 | /** 27 | * Load up the defaults for this decorator 28 | */ 29 | public function defaults() 30 | { 31 | foreach ($this->defaults as $name => $code) { 32 | $this->add($name, $code); 33 | } 34 | } 35 | 36 | /** 37 | * Reset the currently set decorator 38 | */ 39 | public function reset() 40 | { 41 | $this->current = []; 42 | } 43 | 44 | /** 45 | * Retrieve the currently set codes for the decorator 46 | * 47 | * @return array 48 | */ 49 | public function current() 50 | { 51 | return $this->current; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/climate", 3 | "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", 4 | "keywords": ["cli","php", "terminal", "command", "colors"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Joe Tannenbaum", 9 | "email": "hey@joe.codes", 10 | "homepage": "http://joe.codes/", 11 | "role": "Developer" 12 | }, { 13 | "name": "Craig Duncan", 14 | "email": "git@duncanc.co.uk", 15 | "homepage": "https://github.com/duncan3dc", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "psr/log": "^1.0", 21 | "php": "^5.6|^7.0", 22 | "seld/cli-prompt": "^1.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5.7.16", 26 | "mockery/mockery": "^1.0", 27 | "mikey179/vfsStream": "^1.4" 28 | }, 29 | "suggest": { 30 | "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "League\\CLImate\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "League\\CLImate\\Tests\\": "tests/" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "phpunit" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/climate). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[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). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **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. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /src/Util/Cursor.php: -------------------------------------------------------------------------------- 1 | char($char)->length($length); 24 | } 25 | 26 | /** 27 | * Set the character to repeat for the border 28 | * 29 | * @param string $char 30 | * 31 | * @return Border 32 | */ 33 | public function char($char) 34 | { 35 | $this->set('char', $char); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Set the length of the border 42 | * 43 | * @param integer $length 44 | * 45 | * @return Border 46 | */ 47 | public function length($length) 48 | { 49 | $this->set('length', $length); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Return the border 56 | * 57 | * @return string 58 | */ 59 | public function result() 60 | { 61 | $length = $this->length ?: $this->util->width() ?: 100; 62 | $str = str_repeat($this->char, $length); 63 | $str = substr($str, 0, $length); 64 | 65 | return $str; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Util/System/System.php: -------------------------------------------------------------------------------- 1 | force_ansi = $force; 17 | } 18 | 19 | /** 20 | * @return integer|null 21 | */ 22 | abstract public function width(); 23 | 24 | /** 25 | * @return integer|null 26 | */ 27 | abstract public function height(); 28 | 29 | /** 30 | * Check if the stream supports ansi escape characters. 31 | * 32 | * @return bool 33 | */ 34 | abstract protected function systemHasAnsiSupport(); 35 | 36 | /** 37 | * Check if we are forcing ansi, fallback to system support 38 | * 39 | * @return bool 40 | */ 41 | public function hasAnsiSupport() 42 | { 43 | if (is_bool($this->force_ansi)) { 44 | return $this->force_ansi; 45 | } 46 | 47 | return $this->systemHasAnsiSupport(); 48 | } 49 | 50 | /** 51 | * Wraps exec function, allowing the dimension methods to decouple 52 | * 53 | * @param string $command 54 | * @param boolean $full 55 | * 56 | * @return string|array 57 | */ 58 | public function exec($command, $full = false) 59 | { 60 | if ($full) { 61 | exec($command, $output); 62 | 63 | return $output; 64 | } 65 | 66 | return exec($command); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Util/UtilFactory.php: -------------------------------------------------------------------------------- 1 | system = $system ?: SystemFactory::getInstance(); 28 | $this->cursor = $cursor ?: new Cursor(); 29 | } 30 | 31 | /** 32 | * Get the width of the terminal 33 | * 34 | * @return integer 35 | */ 36 | 37 | public function width() 38 | { 39 | return (int) $this->getDimension($this->system->width(), 80); 40 | } 41 | 42 | /** 43 | * Get the height of the terminal 44 | * 45 | * @return integer 46 | */ 47 | 48 | public function height() 49 | { 50 | return (int) $this->getDimension($this->system->height(), 25); 51 | } 52 | 53 | /** 54 | * Determine if the value is numeric, fallback to a default if not 55 | * 56 | * @param integer|null $dimension 57 | * @param integer $default 58 | * 59 | * @return integer 60 | */ 61 | 62 | protected function getDimension($dimension, $default) 63 | { 64 | return (is_numeric($dimension)) ? $dimension : $default; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Decorator/Component/BackgroundColor.php: -------------------------------------------------------------------------------- 1 | strip($val)); 25 | 26 | if ($color) { 27 | $color += self::ADD; 28 | } 29 | 30 | return $color; 31 | } 32 | 33 | /** 34 | * Set the current background color 35 | * 36 | * @param mixed $val 37 | * 38 | * @return boolean 39 | */ 40 | public function set($val) 41 | { 42 | return parent::set($this->strip($val)); 43 | } 44 | 45 | /** 46 | * Get all of the available background colors 47 | * 48 | * @return array 49 | */ 50 | public function all() 51 | { 52 | $colors = []; 53 | 54 | foreach ($this->colors as $color => $code) { 55 | $colors['background_' . $color] = $code + self::ADD; 56 | } 57 | 58 | return $colors; 59 | } 60 | 61 | /** 62 | * Strip the color of any prefixes 63 | * 64 | * @param string $val 65 | * 66 | * @return string 67 | */ 68 | protected function strip($val) 69 | { 70 | return str_replace('background_', '', $val); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Flank.php: -------------------------------------------------------------------------------- 1 | str = $str; 31 | 32 | $this->char($char)->repeat($repeat); 33 | } 34 | 35 | /** 36 | * Set the character(s) to repeat on either side 37 | * 38 | * @param string $char 39 | * 40 | * @return Flank 41 | */ 42 | public function char($char) 43 | { 44 | $this->set('char', $char); 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Set the repeat of the flank character(s) 51 | * 52 | * @param integer $repeat 53 | * 54 | * @return Flank 55 | */ 56 | public function repeat($repeat) 57 | { 58 | $this->set('repeat', $repeat); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Return the flanked string 65 | * 66 | * @return string 67 | */ 68 | public function result() 69 | { 70 | $flank = str_repeat($this->char, $this->repeat); 71 | 72 | return "{$flank} {$this->str} {$flank}"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Decorator/Tags.php: -------------------------------------------------------------------------------- 1 | keys = $keys; 26 | $this->build(); 27 | } 28 | 29 | /** 30 | * Get all available tags 31 | * 32 | * @return array 33 | */ 34 | 35 | public function all() 36 | { 37 | return $this->tags; 38 | } 39 | 40 | /** 41 | * Get the value of the requested tag 42 | * 43 | * @param string $key 44 | * 45 | * @return string|null 46 | */ 47 | 48 | public function value($key) 49 | { 50 | return (array_key_exists($key, $this->tags)) ? $this->tags[$key] : null; 51 | } 52 | 53 | /** 54 | * Get the regular expression that can be used to parse the string for tags 55 | * 56 | * @return string 57 | */ 58 | 59 | public function regex() 60 | { 61 | return '(<(?:(?:(?:\\\)*\/)*(?:' . implode('|', array_keys($this->keys)) . '))>)'; 62 | } 63 | 64 | /** 65 | * Build the search and replace for all of the various style tags 66 | */ 67 | 68 | protected function build() 69 | { 70 | foreach ($this->keys as $tag => $code) { 71 | $this->tags["<{$tag}>"] = $code; 72 | $this->tags[""] = $code; 73 | $this->tags["<\\/{$tag}>"] = $code; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Decorator/Component/Command.php: -------------------------------------------------------------------------------- 1 | 'green', 21 | 'comment' => 'yellow', 22 | 'whisper' => 'light_gray', 23 | 'shout' => 'red', 24 | 'error' => 'light_red', 25 | ]; 26 | 27 | /** 28 | * Add a command into the mix 29 | * 30 | * @param string $key 31 | * @param mixed $value 32 | */ 33 | public function add($key, $value) 34 | { 35 | $this->commands[$key] = $value; 36 | } 37 | 38 | /** 39 | * Retrieve all of the available commands 40 | * 41 | * @return array 42 | */ 43 | public function all() 44 | { 45 | return $this->commands; 46 | } 47 | 48 | /** 49 | * Get the style that corresponds to the command 50 | * 51 | * @param string $val 52 | * 53 | * @return string 54 | */ 55 | public function get($val) 56 | { 57 | if (array_key_exists($val, $this->commands)) { 58 | return $this->commands[$val]; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | /** 65 | * Set the currently used command 66 | * 67 | * @param string $val 68 | * 69 | * @return string|false 70 | */ 71 | public function set($val) 72 | { 73 | // Return the code because it is a string corresponding 74 | // to a property in another class 75 | return ($code = $this->get($val)) ? $code : false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Util/System/Windows.php: -------------------------------------------------------------------------------- 1 | getDimension('width'); 15 | } 16 | 17 | /** 18 | * Get the height of the terminal 19 | * 20 | * @return integer|null 21 | */ 22 | public function height() 23 | { 24 | return $this->getDimension('height'); 25 | } 26 | 27 | /** 28 | * Get specified terminal dimension 29 | * 30 | * @param string $key 31 | * 32 | * @return integer|null 33 | */ 34 | 35 | protected function getDimension($key) 36 | { 37 | $index = array_search($key, ['height', 'width']); 38 | $dimensions = $this->getDimensions(); 39 | 40 | return (!empty($dimensions[$index])) ? $dimensions[$index] : null; 41 | } 42 | 43 | /** 44 | * Get information about the dimensions of the terminal 45 | * 46 | * @return array 47 | */ 48 | protected function getDimensions() 49 | { 50 | $output = $this->exec('mode CON', true); 51 | 52 | if (!is_array($output)) { 53 | return []; 54 | } 55 | 56 | $output = implode("\n", $output); 57 | 58 | preg_match_all('/.*:\s*(\d+)/', $output, $matches); 59 | 60 | return (!empty($matches[1])) ? $matches[1] : []; 61 | } 62 | 63 | /** 64 | * Check if the stream supports ansi escape characters. 65 | * 66 | * Based on https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Output/StreamOutput.php 67 | * 68 | * @return bool 69 | */ 70 | protected function systemHasAnsiSupport() 71 | { 72 | return (getenv('ANSICON') === true || getenv('ConEmuANSI') === 'ON'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

CLImate

2 | 3 | [![Latest Version](https://img.shields.io/github/tag/thephpleague/climate.svg?style=flat&label=release)](https://github.com/thephpleague/climate/tags) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/thephpleague/climate/master.svg?style=flat)](https://travis-ci.org/thephpleague/climate) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/climate.svg?style=flat)](https://scrutinizer-ci.com/g/thephpleague/climate/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/climate.svg?style=flat)](https://scrutinizer-ci.com/g/thephpleague/climate) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/league/climate.svg?style=flat)](https://packagist.org/packages/league/climate) 9 | 10 | Running PHP from the command line? CLImate is your new best bud. 11 | 12 | CLImate allows you to easily output colored text, special formats, and more. 13 | 14 | 15 | ## Installation 16 | 17 | The recommended method of installing this library is via [Composer](https://getcomposer.org/). 18 | 19 | Run the following command from your project root: 20 | 21 | ```bash 22 | $ composer require league/climate 23 | ``` 24 | 25 | 26 | ## Usage 27 | 28 | ```php 29 | require_once __DIR__ . "/vendor/autoload.php"; 30 | 31 | $climate = new \League\CLImate\CLImate; 32 | 33 | $climate->red('Whoa now this text is red.'); 34 | $climate->blue('Blue? Wow!'); 35 | ``` 36 | 37 | _Read more at https://climate.thephpleague.com/_ 38 | 39 | 40 | ## Credits 41 | 42 | This library was created by [Joe Tannenbaum](https://joe.codes/). 43 | It is currently maintained and developed by [Craig Duncan](https://twitter.com/duncan3dc). 44 | Much love to [Damian Makki](https://dribbble.com/damianmakki) for the logo. 45 | -------------------------------------------------------------------------------- /src/Decorator/Component/Format.php: -------------------------------------------------------------------------------- 1 | 1, 21 | 'dim' => 2, 22 | 'underline' => 4, 23 | 'blink' => 5, 24 | 'invert' => 7, 25 | 'hidden' => 8, 26 | ]; 27 | 28 | /** 29 | * Add a format into the mix 30 | * 31 | * @param string $key 32 | * @param mixed $value 33 | */ 34 | public function add($key, $value) 35 | { 36 | $this->formats[$key] = (int) $value; 37 | } 38 | 39 | /** 40 | * Retrieve all of the available formats 41 | * 42 | * @return array 43 | */ 44 | public function all() 45 | { 46 | return $this->formats; 47 | } 48 | 49 | /** 50 | * Get the code for the format 51 | * 52 | * @param string $val 53 | * 54 | * @return string 55 | */ 56 | public function get($val) 57 | { 58 | // If we already have the code, just return that 59 | if (is_numeric($val)) { 60 | return $val; 61 | } 62 | 63 | if (array_key_exists($val, $this->formats)) { 64 | return $this->formats[$val]; 65 | } 66 | 67 | return null; 68 | } 69 | 70 | /** 71 | * Set the current format 72 | * 73 | * @param string $val 74 | * 75 | * @return boolean 76 | */ 77 | public function set($val) 78 | { 79 | $code = $this->get($val); 80 | 81 | if ($code) { 82 | $this->current[] = $code; 83 | 84 | return true; 85 | } 86 | 87 | return false; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Util/System/Linux.php: -------------------------------------------------------------------------------- 1 | getDimension($this->exec('tput cols')); 15 | } 16 | 17 | /** 18 | * Get the height of the terminal 19 | * 20 | * @return integer|null 21 | */ 22 | public function height() 23 | { 24 | return $this->getDimension($this->exec('tput lines')); 25 | } 26 | 27 | /** 28 | * Determine if system has access to bash commands 29 | * 30 | * @return bool 31 | */ 32 | public function canAccessBash() 33 | { 34 | return (rtrim($this->exec("/usr/bin/env bash -c 'echo OK'")) === 'OK'); 35 | } 36 | 37 | /** 38 | * Display a hidden response prompt and return the response 39 | * 40 | * @param string $prompt 41 | * 42 | * @return string 43 | */ 44 | public function hiddenResponsePrompt($prompt) 45 | { 46 | $bash_command = 'read -s -p "' . $prompt . '" response && echo $response'; 47 | 48 | return rtrim($this->exec("/usr/bin/env bash -c '{$bash_command}'")); 49 | } 50 | 51 | /** 52 | * Determine if dimension is numeric and return it 53 | * 54 | * @param integer|string|null $dimension 55 | * 56 | * @return integer|null 57 | */ 58 | protected function getDimension($dimension) 59 | { 60 | return (is_numeric($dimension)) ? $dimension : null; 61 | } 62 | 63 | /** 64 | * Check if the stream supports ansi escape characters. 65 | * 66 | * Based on https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Output/StreamOutput.php 67 | * 68 | * @return bool 69 | */ 70 | protected function systemHasAnsiSupport() 71 | { 72 | return (function_exists('posix_isatty') && @posix_isatty(STDOUT)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | 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. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | 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/) 23 | -------------------------------------------------------------------------------- /src/Settings/Manager.php: -------------------------------------------------------------------------------- 1 | getPath($name)); 24 | } 25 | 26 | /** 27 | * Add a setting 28 | * 29 | * @param string $name 30 | * @param mixed $value 31 | */ 32 | public function add($name, $value) 33 | { 34 | $setting = $this->getPath($name); 35 | $key = $this->getClassName($name); 36 | 37 | // If the current key doesn't exist in the settings array, set it up 38 | if (!array_key_exists($name, $this->settings)) { 39 | $this->settings[$key] = new $setting(); 40 | } 41 | 42 | $this->settings[$key]->add($value); 43 | } 44 | 45 | /** 46 | * Get the value of the requested setting if it exists 47 | * 48 | * @param string $key 49 | * 50 | * @return mixed 51 | */ 52 | public function get($key) 53 | { 54 | if (array_key_exists($key, $this->settings)) { 55 | return $this->settings[$key]; 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * Get the short name for the requested settings class 63 | * 64 | * @param string $name 65 | * 66 | * @return string 67 | */ 68 | protected function getPath($name) 69 | { 70 | return 'League\CLImate\Settings\\' . $this->getClassName($name); 71 | } 72 | 73 | /** 74 | * Get the short class name for the setting 75 | * 76 | * @param string $name 77 | * 78 | * @return string 79 | */ 80 | protected function getClassName($name) 81 | { 82 | return ucwords(str_replace('add_', '', $name)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Util/Reader/Stdin.php: -------------------------------------------------------------------------------- 1 | getStdIn(), 1024)); 19 | } 20 | 21 | /** 22 | * Read from STDIN until EOF (^D) is reached 23 | * 24 | * @return string 25 | */ 26 | public function multiLine() 27 | { 28 | return trim(stream_get_contents($this->getStdIn())); 29 | } 30 | 31 | /** 32 | * Read one character 33 | * 34 | * @param int $count 35 | * 36 | * @return string 37 | */ 38 | public function char($count = 1) 39 | { 40 | return fread($this->getStdIn(), $count); 41 | } 42 | 43 | /** 44 | * Read the line, but hide what the user is typing 45 | * 46 | * @return string 47 | */ 48 | public function hidden() 49 | { 50 | return CliPrompt::hiddenPrompt(); 51 | } 52 | 53 | /** 54 | * Return a valid STDIN, even if it previously EOF'ed 55 | * 56 | * Lazily re-opens STDIN after hitting an EOF 57 | * 58 | * @return resource 59 | * @throws \Exception 60 | */ 61 | protected function getStdIn() 62 | { 63 | if ($this->stdIn && !feof($this->stdIn)) { 64 | return $this->stdIn; 65 | } 66 | 67 | try { 68 | $this->setStdIn(); 69 | } catch (\Error $e) { 70 | throw new \Exception('Unable to read from STDIN', 0, $e); 71 | } 72 | 73 | return $this->stdIn; 74 | } 75 | 76 | /** 77 | * Attempt to set the stdin property 78 | * 79 | * @throws \Exception 80 | */ 81 | protected function setStdIn() 82 | { 83 | if ($this->stdIn !== false) { 84 | fclose($this->stdIn); 85 | } 86 | 87 | $this->stdIn = fopen('php://stdin', 'r'); 88 | 89 | if (!$this->stdIn) { 90 | throw new \Exception('Unable to read from STDIN'); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/TerminalObject/Router/BaseRouter.php: -------------------------------------------------------------------------------- 1 | extensions[$key] = $class; 18 | } 19 | 20 | /** 21 | * Get the full path for the class based on the key 22 | * 23 | * @param string $class 24 | * 25 | * @return string 26 | */ 27 | public function path($class) 28 | { 29 | return $this->getExtension($class) ?: $this->getPath($this->shortName($class)); 30 | } 31 | 32 | /** 33 | * Determines if the requested class is a 34 | * valid terminal object class 35 | * 36 | * @param string $class 37 | * 38 | * @return boolean 39 | */ 40 | public function exists($class) 41 | { 42 | $class = $this->path($class); 43 | 44 | return (is_object($class) || class_exists($class)); 45 | } 46 | 47 | /** 48 | * Get the full path for the terminal object class 49 | * 50 | * @param string $class 51 | * 52 | * @return string 53 | */ 54 | protected function getPath($class) 55 | { 56 | return 'League\CLImate\TerminalObject\\' . $this->pathPrefix() . '\\' . $class; 57 | } 58 | 59 | /** 60 | * Get an extension by its key 61 | * 62 | * @param string $key 63 | * 64 | * @return string|false Full class path to extension 65 | */ 66 | protected function getExtension($key) 67 | { 68 | if (array_key_exists($key, $this->extensions)) { 69 | return $this->extensions[$key]; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /** 76 | * Get the class short name 77 | * 78 | * @param string $name 79 | * 80 | * @return string 81 | */ 82 | protected function shortName($name) 83 | { 84 | $name = str_replace('_', ' ', $name); 85 | $name = ucwords($name); 86 | 87 | return str_replace(' ', '', $name); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Decorator/Component/Color.php: -------------------------------------------------------------------------------- 1 | 39, 21 | 'black' => 30, 22 | 'red' => 31, 23 | 'green' => 32, 24 | 'yellow' => 33, 25 | 'blue' => 34, 26 | 'magenta' => 35, 27 | 'cyan' => 36, 28 | 'light_gray' => 37, 29 | 'dark_gray' => 90, 30 | 'light_red' => 91, 31 | 'light_green' => 92, 32 | 'light_yellow' => 93, 33 | 'light_blue' => 94, 34 | 'light_magenta' => 95, 35 | 'light_cyan' => 96, 36 | 'white' => 97, 37 | ]; 38 | 39 | /** 40 | * Add a color into the mix 41 | * 42 | * @param string $key 43 | * @param integer $value 44 | */ 45 | public function add($key, $value) 46 | { 47 | $this->colors[$key] = (int) $value; 48 | } 49 | 50 | /** 51 | * Retrieve all of available colors 52 | * 53 | * @return array 54 | */ 55 | public function all() 56 | { 57 | return $this->colors; 58 | } 59 | 60 | /** 61 | * Get the code for the color 62 | * 63 | * @param string $val 64 | * 65 | * @return string 66 | */ 67 | public function get($val) 68 | { 69 | // If we already have the code, just return that 70 | if (is_numeric($val)) { 71 | return $val; 72 | } 73 | 74 | if (array_key_exists($val, $this->colors)) { 75 | return $this->colors[$val]; 76 | } 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * Set the current color 83 | * 84 | * @param string $val 85 | * 86 | * @return boolean 87 | */ 88 | public function set($val) 89 | { 90 | $code = $this->get($val); 91 | 92 | if ($code) { 93 | $this->current = [$code]; 94 | 95 | return true; 96 | } 97 | 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Util/Writer/File.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 27 | $this->use_locking = $use_locking; 28 | $this->gzip_file = $gzip_file; 29 | } 30 | 31 | public function lock() 32 | { 33 | $this->use_locking = true; 34 | 35 | return $this; 36 | } 37 | 38 | public function gzipped() 39 | { 40 | $this->gzip_file = true; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Write the content to the stream 47 | * 48 | * @param string $content 49 | */ 50 | public function write($content) 51 | { 52 | $resource = $this->getResource(); 53 | 54 | if ($this->use_locking) { 55 | flock($resource, LOCK_EX); 56 | } 57 | 58 | gzwrite($resource, $content); 59 | 60 | if ($this->use_locking) { 61 | flock($resource, LOCK_UN); 62 | } 63 | } 64 | 65 | protected function getResource() 66 | { 67 | if (is_resource($this->resource)) { 68 | return $this->resource; 69 | } 70 | 71 | $this->close_locally = true; 72 | 73 | if (!is_writable($this->resource)) { 74 | throw new \Exception("The resource [{$this->resource}] is not writable"); 75 | } 76 | 77 | if (!($this->resource = $this->openResource())) { 78 | throw new \Exception("The resource could not be opened"); 79 | } 80 | 81 | return $this->resource; 82 | } 83 | 84 | protected function openResource() 85 | { 86 | if ($this->gzip_file) { 87 | return gzopen($this->resource, 'a'); 88 | } 89 | 90 | return fopen($this->resource, 'a'); 91 | } 92 | 93 | public function _destruct() 94 | { 95 | if ($this->close_locally) { 96 | gzclose($this->getResource()); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/TerminalObject/Helper/StringLength.php: -------------------------------------------------------------------------------- 1 | ignore_tags)) { 21 | $this->ignore_tags = array_keys($this->parser->tags->all()); 22 | } 23 | } 24 | 25 | /** 26 | * Determine the length of the string without any tags 27 | * 28 | * @param string $str 29 | * 30 | * @return integer 31 | */ 32 | protected function lengthWithoutTags($str) 33 | { 34 | $this->setIgnoreTags(); 35 | 36 | return mb_strwidth($this->withoutTags($str), 'UTF-8'); 37 | } 38 | 39 | /** 40 | * Get the string without the tags that are to be ignored 41 | * 42 | * @param string $str 43 | * 44 | * @return string 45 | */ 46 | protected function withoutTags($str) 47 | { 48 | $this->setIgnoreTags(); 49 | 50 | return str_replace($this->ignore_tags, '', $str); 51 | } 52 | 53 | /** 54 | * Apply padding to a string 55 | * 56 | * @param string $str 57 | * @param string $final_length 58 | * @param string $padding_side 59 | * 60 | * @return string 61 | */ 62 | protected function pad($str, $final_length, $padding_side = 'right') 63 | { 64 | $padding = $final_length - $this->lengthWithoutTags($str); 65 | 66 | if ($padding_side == 'left') { 67 | return str_repeat(' ', $padding) . $str; 68 | } 69 | 70 | return $str . str_repeat(' ', $padding); 71 | } 72 | 73 | /** 74 | * Apply padding to an array of strings 75 | * 76 | * @param array $arr 77 | * @param integer $final_length 78 | * @param string $padding_side 79 | * 80 | * @return array 81 | */ 82 | protected function padArray($arr, $final_length, $padding_side = 'right') 83 | { 84 | foreach ($arr as $key => $value) { 85 | $arr[$key] = $this->pad($value, $final_length, $padding_side); 86 | } 87 | 88 | return $arr; 89 | } 90 | 91 | /** 92 | * Find the max string length in an array 93 | * 94 | * @param array $arr 95 | * @return int 96 | */ 97 | protected function maxStrLen(array $arr) 98 | { 99 | return max($this->arrayOfStrLens($arr)); 100 | } 101 | 102 | /** 103 | * Get an array of the string lengths from an array of strings 104 | * 105 | * @param array $arr 106 | * @return array 107 | */ 108 | protected function arrayOfStrLens(array $arr) 109 | { 110 | return array_map([$this, 'lengthWithoutTags'], $arr); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Padding.php: -------------------------------------------------------------------------------- 1 | length($length); 32 | } 33 | 34 | if (is_string($char)) { 35 | $this->char($char); 36 | } 37 | } 38 | 39 | /** 40 | * Set the character(s) that should be used to pad 41 | * 42 | * @param string $char 43 | * 44 | * @return \League\CLImate\TerminalObject\Dynamic\Padding 45 | */ 46 | public function char($char) 47 | { 48 | $this->char = $char; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Set the length of the line that should be generated 55 | * 56 | * @param integer $length 57 | * 58 | * @return \League\CLImate\TerminalObject\Dynamic\Padding 59 | */ 60 | public function length($length) 61 | { 62 | $this->length = $length; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Get the length of the line based on the width of the terminal window 69 | * 70 | * @return integer 71 | */ 72 | protected function getLength() 73 | { 74 | if (!$this->length) { 75 | $this->length = $this->util->width(); 76 | } 77 | 78 | return $this->length; 79 | } 80 | 81 | /** 82 | * Pad the content with the characters 83 | * 84 | * @param string $content 85 | * 86 | * @return string 87 | */ 88 | protected function padContent($content) 89 | { 90 | if (strlen($this->char) > 0) { 91 | $length = $this->getLength(); 92 | $padding_length = ceil($length / mb_strlen($this->char)); 93 | 94 | $padding = str_repeat($this->char, $padding_length); 95 | $content .= mb_substr($padding, 0, $length - mb_strlen($content)); 96 | } 97 | 98 | return $content; 99 | } 100 | 101 | /** 102 | * Output the content and pad to the previously defined length 103 | * 104 | * @param string $content 105 | * 106 | * @return \League\CLImate\TerminalObject\Dynamic\Padding 107 | */ 108 | public function label($content) 109 | { 110 | // Handle long labels by splitting them across several lines 111 | $lines = []; 112 | $stop = mb_strlen($content); 113 | $width = $this->util->width(); 114 | for ($i = 0; $i < $stop; $i += $width) { 115 | $lines[] = mb_substr($content, $i, $width); 116 | } 117 | $content = array_pop($lines); 118 | 119 | foreach ($lines as $line) { 120 | $this->output->write($this->parser->apply($line)); 121 | } 122 | 123 | $content = $this->padContent($content); 124 | $content = $this->parser->apply($content); 125 | 126 | $this->output->sameLine(); 127 | $this->output->write($content); 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Output result 134 | * 135 | * @param string $content 136 | */ 137 | public function result($content) 138 | { 139 | $this->output->write($this->parser->apply(' ' . $content)); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/TerminalObject/Helper/Art.php: -------------------------------------------------------------------------------- 1 | dirs as $dir) { 46 | $this->addDir($dir); 47 | } 48 | } 49 | 50 | /** 51 | * Add a directory to search for art in 52 | * 53 | * @param string $dir 54 | */ 55 | protected function addDir($dir) 56 | { 57 | // Add any additional directories to the top of the array 58 | // so that the user can override art 59 | array_unshift($this->art_dirs, rtrim($dir, '/')); 60 | 61 | // Keep the array clean 62 | $this->art_dirs = array_unique($this->art_dirs); 63 | $this->art_dirs = array_filter($this->art_dirs); 64 | $this->art_dirs = array_values($this->art_dirs); 65 | } 66 | 67 | /** 68 | * Find a valid art path 69 | * 70 | * @param string $art 71 | * 72 | * @return array 73 | */ 74 | protected function artDir($art) 75 | { 76 | return $this->fileSearch($art, '/*.*'); 77 | } 78 | 79 | /** 80 | * Find a valid art path 81 | * 82 | * @param string $art 83 | * 84 | * @return string 85 | */ 86 | protected function artFile($art) 87 | { 88 | $files = $this->fileSearch($art, '.*'); 89 | 90 | if (count($files) === 0) { 91 | $this->addDir(__DIR__ . '/../../ASCII'); 92 | $files = $this->fileSearch($this->default_art, '.*'); 93 | } 94 | 95 | if (count($files) === 0) { 96 | throw new \UnexpectedValueException("Unable to find an art file with the name '{$art}'"); 97 | } 98 | 99 | return reset($files); 100 | } 101 | 102 | /** 103 | * Find a set of files in the current art directories 104 | * based on a pattern 105 | * 106 | * @param string $art 107 | * @param string $pattern 108 | * 109 | * @return array 110 | */ 111 | protected function fileSearch($art, $pattern) 112 | { 113 | foreach ($this->art_dirs as $dir) { 114 | $directory_iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); 115 | 116 | $paths = []; 117 | $regex = '~' . preg_quote($art) . $pattern . '~'; 118 | 119 | foreach ($directory_iterator as $file) { 120 | if ($file->isDir()) { 121 | continue; 122 | } 123 | 124 | // Look for anything that has the $art filename 125 | if (preg_match($regex, $file)) { 126 | $paths[] = $file->getPathname(); 127 | } 128 | } 129 | 130 | asort($paths); 131 | 132 | // If we've got one, no need to look any further 133 | if (!empty($paths)) { 134 | return $paths; 135 | } 136 | } 137 | 138 | return []; 139 | } 140 | 141 | /** 142 | * Parse the contents of the file and return each line 143 | * 144 | * @param string $path 145 | * 146 | * @return array 147 | */ 148 | protected function parse($path) 149 | { 150 | $output = file_get_contents($path); 151 | $output = explode("\n", $output); 152 | $output = array_map('rtrim', $output); 153 | 154 | return $output; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Spinner.php: -------------------------------------------------------------------------------- 1 | label = $label; 56 | } 57 | 58 | if (count($characters) < 1) { 59 | $characters = []; 60 | $size = 5; 61 | $positions = array_merge(range(0, $size - 1), range($size - 2, 1, -1)); 62 | foreach ($positions as $pos) { 63 | $line = str_repeat("-", $size); 64 | $characters[] = "[" . substr($line, 0, $pos) . "=" . substr($line, $pos + 1) . "]"; 65 | } 66 | } 67 | $this->characters(...$characters); 68 | } 69 | 70 | 71 | /** 72 | * Set the length of time to wait between drawing each stage. 73 | * 74 | * @param float $timeLimit 75 | * 76 | * @return Spinner 77 | */ 78 | public function timeLimit($timeLimit) 79 | { 80 | $this->timeLimit = (float) $timeLimit; 81 | 82 | return $this; 83 | } 84 | 85 | 86 | /** 87 | * Set the character to loop around. 88 | * 89 | * @param string $characters 90 | * 91 | * @return Spinner 92 | */ 93 | public function characters(...$characters) 94 | { 95 | if (count($characters) < 1) { 96 | throw new \UnexpectedValueException("You must specify the characters to use"); 97 | } 98 | 99 | $this->characters = $characters; 100 | 101 | return $this; 102 | } 103 | 104 | 105 | /** 106 | * Re-writes the spinner 107 | * 108 | * @param string $label 109 | * 110 | * @return void 111 | */ 112 | public function advance($label = null) 113 | { 114 | if ($label === null) { 115 | $label = $this->label; 116 | } 117 | 118 | if ($this->lastDrawn) { 119 | $time = microtime(true) - $this->lastDrawn; 120 | if ($time < $this->timeLimit) { 121 | return; 122 | } 123 | } 124 | 125 | ++$this->current; 126 | if ($this->current >= count($this->characters)) { 127 | $this->current = 0; 128 | } 129 | 130 | $characters = $this->characters[$this->current]; 131 | $this->drawSpinner($characters, $label); 132 | $this->lastDrawn = microtime(true); 133 | } 134 | 135 | 136 | /** 137 | * Draw the spinner 138 | * 139 | * @param string $characters 140 | * @param string $label 141 | */ 142 | private function drawSpinner($characters, $label) 143 | { 144 | $spinner = ""; 145 | 146 | if ($this->firstLine) { 147 | $this->firstLine = false; 148 | } else { 149 | $spinner .= $this->util->cursor->up(1); 150 | $spinner .= $this->util->cursor->startOfCurrentLine(); 151 | $spinner .= $this->util->cursor->deleteCurrentLine(); 152 | } 153 | 154 | $spinner .= trim("{$characters} {$label}"); 155 | 156 | $this->output->write($this->parser->apply($spinner)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/TerminalObject/Router/ExtensionCollection.php: -------------------------------------------------------------------------------- 1 | [], 'dynamic' => []]; 13 | 14 | /** 15 | * @var string $basic_interface 16 | */ 17 | protected $basic_interface = 'League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface'; 18 | 19 | /** 20 | * @var string $dynamic_interface 21 | */ 22 | protected $dynamic_interface = 'League\CLImate\TerminalObject\Dynamic\DynamicTerminalObjectInterface'; 23 | 24 | public function __construct($key, $class) 25 | { 26 | $this->createCollection($key, $class); 27 | } 28 | 29 | public function collection() 30 | { 31 | return $this->collection; 32 | } 33 | 34 | /** 35 | * Create the collection from the key/class 36 | * 37 | * @param string $original_key 38 | * @param string|object|array $original_class 39 | * 40 | * @return type 41 | */ 42 | protected function createCollection($original_key, $original_class) 43 | { 44 | $collection = $this->convertToArray($original_key, $original_class); 45 | 46 | foreach ($collection as $key => $class) { 47 | $this->validateExtension($class); 48 | $this->collection[$this->getType($class)][$this->getKey($key, $class)] = $class; 49 | } 50 | } 51 | 52 | /** 53 | * Convert the given class and key to an array of classes 54 | * 55 | * @param string|object|array $class 56 | * @param string $key Optional custom key instead of class name 57 | * 58 | * @return array 59 | */ 60 | protected function convertToArray($key, $class) 61 | { 62 | if (is_array($class)) { 63 | return $class; 64 | } 65 | 66 | return [$this->getKey($key, $class) => $class]; 67 | } 68 | 69 | /** 70 | * Ensure that the extension is valid 71 | * 72 | * @param string|object|array $class 73 | */ 74 | protected function validateExtension($class) 75 | { 76 | $this->validateClassExists($class); 77 | $this->validateClassImplementation($class); 78 | } 79 | 80 | /** 81 | * @param string|object $class 82 | * 83 | * @throws \Exception if extension class does not exist 84 | */ 85 | protected function validateClassExists($class) 86 | { 87 | if (is_string($class) && !class_exists($class)) { 88 | throw new \Exception('Class does not exist: ' . $class); 89 | } 90 | } 91 | 92 | /** 93 | * @param string|object $class 94 | * 95 | * @throws \Exception if extension class does not implement either Dynamic or Basic interface 96 | */ 97 | protected function validateClassImplementation($class) 98 | { 99 | $str_class = is_string($class); 100 | 101 | $valid_implementation = (is_a($class, $this->basic_interface, $str_class) 102 | || is_a($class, $this->dynamic_interface, $str_class)); 103 | 104 | if (!$valid_implementation) { 105 | throw new \Exception('Class must implement either ' 106 | . $this->basic_interface . ' or ' . $this->dynamic_interface); 107 | } 108 | } 109 | 110 | /** 111 | * Determine the extension key based on the class 112 | * 113 | * @param string|null $key 114 | * @param string|object $class 115 | * 116 | * @return string 117 | */ 118 | protected function getKey($key, $class) 119 | { 120 | if ($key === null || !is_string($key)) { 121 | $class_path = (is_string($class)) ? $class : get_class($class); 122 | 123 | $key = explode('\\', $class_path); 124 | $key = end($key); 125 | } 126 | 127 | return Helper::snakeCase($key); 128 | } 129 | 130 | /** 131 | * Get the type of class the extension implements 132 | * 133 | * @param string|object $class 134 | * 135 | * @return string 'basic' or 'dynamic' 136 | */ 137 | protected function getType($class) 138 | { 139 | if (is_a($class, $this->basic_interface, is_string($class))) { 140 | return 'basic'; 141 | } 142 | 143 | return 'dynamic'; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Checkboxes.php: -------------------------------------------------------------------------------- 1 | prompt = $prompt; 20 | $this->reader = $reader ?: new Stdin(); 21 | 22 | $this->checkboxes = $this->buildCheckboxes($options); 23 | } 24 | 25 | /** 26 | * Do it! Prompt the user for information! 27 | * 28 | * @return string 29 | */ 30 | public function prompt() 31 | { 32 | $this->output->write($this->parser->apply($this->promptFormatted())); 33 | 34 | $this->writeCheckboxes(); 35 | 36 | $this->util->system->exec('stty sane'); 37 | 38 | return $this->checkboxes->getCheckedValues(); 39 | } 40 | 41 | /** 42 | * Build out the checkboxes 43 | * 44 | * @param array $options 45 | * 46 | * @return Checkbox\CheckboxGroup 47 | */ 48 | protected function buildCheckboxes(array $options) 49 | { 50 | return new Checkbox\CheckboxGroup($options); 51 | } 52 | 53 | /** 54 | * Format the prompt string 55 | * 56 | * @return string 57 | */ 58 | protected function promptFormatted() 59 | { 60 | return $this->prompt . ' (use to select)'; 61 | } 62 | 63 | /** 64 | * Output the checkboxes and listen for any keystrokes 65 | */ 66 | protected function writeCheckboxes() 67 | { 68 | $this->updateCheckboxView(); 69 | 70 | $this->util->system->exec('stty -icanon'); 71 | $this->output->sameLine()->write($this->util->cursor->hide()); 72 | 73 | $this->listenForInput(); 74 | } 75 | 76 | /** 77 | * Listen for input and act on it 78 | */ 79 | protected function listenForInput() 80 | { 81 | while ($char = $this->reader->char(1)) { 82 | if ($this->handleCharacter($char)) { 83 | break; 84 | } 85 | 86 | $this->moveCursorToTop(); 87 | $this->updateCheckboxView(); 88 | } 89 | } 90 | 91 | /** 92 | * Take the appropriate action based on the input character, 93 | * returns whether to stop listening or not 94 | * 95 | * @param string $char 96 | * 97 | * @return bool 98 | */ 99 | protected function handleCharacter($char) 100 | { 101 | switch ($char) { 102 | case "\n": 103 | $this->output->sameLine()->write($this->util->cursor->defaultStyle()); 104 | $this->output->sameLine()->write("\e[0m"); 105 | return true; // Break the while loop as well 106 | 107 | case "\e": 108 | $this->handleAnsi(); 109 | break; 110 | 111 | case ' ': 112 | $this->checkboxes->toggleCurrent(); 113 | break; 114 | } 115 | 116 | return false; 117 | } 118 | 119 | /** 120 | * Move the cursor to the top of the option list 121 | */ 122 | protected function moveCursorToTop() 123 | { 124 | $output = $this->util->cursor->up($this->checkboxes->count() - 1); 125 | $output .= $this->util->cursor->startOfCurrentLine(); 126 | 127 | $this->output->sameLine()->write($output); 128 | } 129 | 130 | /** 131 | * Handle any ANSI characters 132 | */ 133 | protected function handleAnsi() 134 | { 135 | switch ($this->reader->char(2)) { 136 | // Up arrow 137 | case '[A': 138 | $this->checkboxes->setCurrent('previous'); 139 | break; 140 | 141 | // Down arrow 142 | case '[B': 143 | $this->checkboxes->setCurrent('next'); 144 | break; 145 | } 146 | } 147 | 148 | /** 149 | * Re-write the checkboxes based on the current objects 150 | */ 151 | protected function updateCheckboxView() 152 | { 153 | $this->checkboxes->util($this->util); 154 | $this->checkboxes->output($this->output); 155 | $this->checkboxes->parser($this->parser); 156 | 157 | $this->checkboxes->write(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Argument/Filter.php: -------------------------------------------------------------------------------- 1 | arguments = $arguments; 17 | } 18 | 19 | /** 20 | * Retrieve optional arguments 21 | * 22 | * @return Argument[] 23 | */ 24 | public function optional() 25 | { 26 | return $this->filterArguments(['isOptional']); 27 | } 28 | 29 | /** 30 | * Retrieve required arguments 31 | * 32 | * @return Argument[] 33 | */ 34 | public function required() 35 | { 36 | return $this->filterArguments(['isRequired']); 37 | } 38 | 39 | /** 40 | * Retrieve arguments with prefix 41 | * 42 | * @return Argument[] 43 | */ 44 | public function withPrefix() 45 | { 46 | return $this->filterArguments(['hasPrefix']); 47 | } 48 | 49 | /** 50 | * Retrieve arguments without prefix 51 | * 52 | * @return Argument[] 53 | */ 54 | public function withoutPrefix() 55 | { 56 | return $this->filterArguments(['noPrefix']); 57 | } 58 | 59 | /** 60 | * Find all required arguments that don't have values after parsing. 61 | * 62 | * These arguments weren't defined on the command line. 63 | * 64 | * @return Argument[] 65 | */ 66 | public function missing() 67 | { 68 | return $this->filterArguments(['isRequired', 'noValue']); 69 | } 70 | 71 | /** 72 | * Filter defined arguments as to whether they are required or not 73 | * 74 | * @param string[] $filters 75 | * 76 | * @return Argument[] 77 | */ 78 | protected function filterArguments($filters = []) 79 | { 80 | $arguments = $this->arguments; 81 | 82 | foreach ($filters as $filter) { 83 | $arguments = array_filter($arguments, [$this, $filter]); 84 | } 85 | 86 | if (in_array('hasPrefix', $filters)) { 87 | usort($arguments, [$this, 'compareByPrefix']); 88 | } 89 | 90 | return array_values($arguments); 91 | } 92 | 93 | /** 94 | * Determine whether an argument as a prefix 95 | * 96 | * @param Argument $argument 97 | * 98 | * @return bool 99 | */ 100 | protected function noPrefix($argument) 101 | { 102 | return !$argument->hasPrefix(); 103 | } 104 | 105 | /** 106 | * Determine whether an argument as a prefix 107 | * 108 | * @param Argument $argument 109 | * 110 | * @return bool 111 | */ 112 | protected function hasPrefix($argument) 113 | { 114 | return $argument->hasPrefix(); 115 | } 116 | 117 | /** 118 | * Determine whether an argument is required 119 | * 120 | * @param Argument $argument 121 | * 122 | * @return bool 123 | */ 124 | protected function isRequired($argument) 125 | { 126 | return $argument->isRequired(); 127 | } 128 | 129 | /** 130 | * Determine whether an argument is optional 131 | * 132 | * @param Argument $argument 133 | * 134 | * @return bool 135 | */ 136 | protected function isOptional($argument) 137 | { 138 | return !$argument->isRequired(); 139 | } 140 | 141 | /** 142 | * Determine whether an argument is optional 143 | * 144 | * @param Argument $argument 145 | * 146 | * @return bool 147 | */ 148 | protected function noValue($argument) 149 | { 150 | return $argument->values() == []; 151 | } 152 | 153 | /** 154 | * Compare two arguments by their short and long prefixes. 155 | * 156 | * @see usort() 157 | * 158 | * @param Argument $a 159 | * @param Argument $b 160 | * 161 | * @return int 162 | */ 163 | public function compareByPrefix(Argument $a, Argument $b) 164 | { 165 | if ($this->prefixCompareString($a) < $this->prefixCompareString($b)) { 166 | return -1; 167 | } 168 | 169 | return 1; 170 | } 171 | 172 | /** 173 | * Prep the prefix string for comparison 174 | * 175 | * @param Argument $argument 176 | * 177 | * @return string 178 | */ 179 | protected function prefixCompareString(Argument $argument) 180 | { 181 | return mb_strtolower($argument->longPrefix() ?: $argument->prefix() ?: ''); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Decorator/Parser/Ansi.php: -------------------------------------------------------------------------------- 1 | start() . $this->parse($str) . $this->end(); 20 | } 21 | 22 | /** 23 | * Get the string that begins the style 24 | * 25 | * @param string $codes 26 | * @return string 27 | */ 28 | protected function start($codes = null) 29 | { 30 | $codes = $codes ?: $this->currentCode(); 31 | $codes = $this->codeStr($codes); 32 | 33 | return $this->wrapCodes($codes); 34 | } 35 | 36 | /** 37 | * Get the string that ends the style 38 | * 39 | * @param string|array $codes 40 | * @return string 41 | */ 42 | protected function end($codes = null) 43 | { 44 | if (empty($codes)) { 45 | $codes = [0]; 46 | } else { 47 | $codes = Helper::toArray($codes); 48 | 49 | // Reset everything back to normal up front 50 | array_unshift($codes, 0); 51 | } 52 | 53 | return $this->wrapCodes($this->codeStr($codes)); 54 | } 55 | 56 | /** 57 | * Wrap the code string in the full escaped sequence 58 | * 59 | * @param string $codes 60 | * 61 | * @return string 62 | */ 63 | 64 | protected function wrapCodes($codes) 65 | { 66 | return "\e[{$codes}m"; 67 | } 68 | 69 | /** 70 | * Parse the string for tags and replace them with their codes 71 | * 72 | * @param string $str 73 | * 74 | * @return string 75 | */ 76 | 77 | protected function parse($str) 78 | { 79 | $count = preg_match_all($this->tags->regex(), $str, $matches); 80 | 81 | // If we didn't find anything, return the string right back 82 | if (!$count || !is_array($matches)) { 83 | return $str; 84 | } 85 | 86 | // All we want is the array of actual strings matched 87 | $matches = reset($matches); 88 | 89 | return $this->parseTags($str, $matches); 90 | } 91 | 92 | /** 93 | * Parse the given string for the tags and replace them with the appropriate codes 94 | * 95 | * @param string $str 96 | * @param array $tags 97 | * 98 | * @return string 99 | */ 100 | 101 | protected function parseTags($str, $tags) 102 | { 103 | // Let's keep a history of styles applied 104 | $history = ($this->currentCode()) ? [$this->currentCode()] : []; 105 | 106 | foreach ($tags as $tag) { 107 | $str = $this->replaceTag($str, $tag, $history); 108 | } 109 | 110 | return $str; 111 | } 112 | 113 | /** 114 | * Replace the tag in the str 115 | * 116 | * @param string $str 117 | * @param string $tag 118 | * @param array $history 119 | * 120 | * @return string 121 | */ 122 | 123 | protected function replaceTag($str, $tag, &$history) 124 | { 125 | // We will be replacing tags one at a time, can't pass this by reference 126 | $replace_count = 1; 127 | 128 | if (strpos($tag, '/')) { 129 | // We are closing out the tag, pop off the last element and get the codes that are left 130 | array_pop($history); 131 | $replace = $this->end($history); 132 | } else { 133 | // We are starting a new tag, add it onto the history and replace with correct color code 134 | $history[] = $this->tags->value($tag); 135 | $replace = $this->start($this->tags->value($tag)); 136 | } 137 | 138 | return str_replace($tag, $replace, $str, $replace_count); 139 | } 140 | 141 | /** 142 | * Stringify the codes 143 | * 144 | * @param mixed $codes 145 | * 146 | * @return string 147 | */ 148 | 149 | protected function codeStr($codes) 150 | { 151 | // If we get something that is already a code string, just pass it back 152 | if (!is_array($codes) && strpos($codes, ';')) { 153 | return $codes; 154 | } 155 | 156 | $codes = Helper::toArray($codes); 157 | 158 | // Sort for the sake of consistency and testability 159 | sort($codes); 160 | 161 | return implode(';', $codes); 162 | } 163 | 164 | /** 165 | * Retrieve the current style code 166 | * 167 | * @return string 168 | */ 169 | 170 | protected function currentCode() 171 | { 172 | return $this->codeStr($this->current); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/TerminalObject/Router/Router.php: -------------------------------------------------------------------------------- 1 | dynamic = $dynamic ?: new DynamicRouter(); 39 | $this->basic = $basic ?: new BasicRouter(); 40 | } 41 | 42 | /** 43 | * Register a custom class with the router 44 | * 45 | * @param string $key 46 | * @param string $class 47 | */ 48 | public function addExtension($key, $class) 49 | { 50 | $extension = new ExtensionCollection($key, $class); 51 | 52 | foreach ($extension->collection() as $obj_type => $collection) { 53 | foreach ($collection as $obj_key => $obj_class) { 54 | $this->{$obj_type}->addExtension($obj_key, $obj_class); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Check if the name matches an existing terminal object 61 | * 62 | * @param string $name 63 | * 64 | * @return boolean 65 | */ 66 | public function exists($name) 67 | { 68 | return ($this->basic->exists($name) || $this->dynamic->exists($name)); 69 | } 70 | 71 | /** 72 | * Execute a terminal object using given arguments 73 | * 74 | * @param string $name 75 | * @param mixed $arguments 76 | * 77 | * @return null|\League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface 78 | */ 79 | public function execute($name, $arguments) 80 | { 81 | $router = $this->getRouter($name); 82 | 83 | $router->output($this->output); 84 | 85 | $obj = $this->getObject($router, $name, $arguments); 86 | 87 | $obj->parser($this->parser); 88 | $obj->util($this->util); 89 | 90 | // If the object needs any settings, import them 91 | foreach ($obj->settings() as $obj_setting) { 92 | $setting = $this->settings->get($obj_setting); 93 | 94 | if ($setting) { 95 | $obj->importSetting($setting); 96 | } 97 | } 98 | 99 | return $router->execute($obj); 100 | } 101 | 102 | /** 103 | * Get the object whether it's a string or already instantiated 104 | * 105 | * @param \League\CLImate\TerminalObject\Router\RouterInterface $router 106 | * @param string $name 107 | * @param array $arguments 108 | * 109 | * @return \League\CLImate\TerminalObject\Dynamic\DynamicTerminalObjectInterface|\League\CLImate\TerminalObject\Basic\BasicTerminalObjectInterface 110 | */ 111 | protected function getObject($router, $name, $arguments) 112 | { 113 | $obj = $router->path($name); 114 | 115 | if (is_string($obj)) { 116 | $obj = (new \ReflectionClass($obj))->newInstanceArgs($arguments); 117 | } 118 | 119 | if (method_exists($obj, 'arguments')) { 120 | call_user_func_array([$obj, 'arguments'], $arguments); 121 | } 122 | 123 | return $obj; 124 | } 125 | 126 | /** 127 | * Determine which type of router we are using and return it 128 | * 129 | * @param string $name 130 | * 131 | * @return \League\CLImate\TerminalObject\Router\RouterInterface|null 132 | */ 133 | protected function getRouter($name) 134 | { 135 | if ($this->basic->exists($name)) { 136 | return $this->basic; 137 | } 138 | 139 | if ($this->dynamic->exists($name)) { 140 | return $this->dynamic; 141 | } 142 | } 143 | 144 | /** 145 | * Set the settings property 146 | * 147 | * @param \League\CLImate\Settings\Manager $settings 148 | * 149 | * @return Router 150 | */ 151 | public function settings(Manager $settings) 152 | { 153 | $this->settings = $settings; 154 | 155 | return $this; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Checkbox/CheckboxGroup.php: -------------------------------------------------------------------------------- 1 | $option) { 20 | $this->checkboxes[] = new Checkbox($option, $key); 21 | } 22 | 23 | $this->count = count($this->checkboxes); 24 | 25 | $this->checkboxes[0]->setFirst()->setCurrent(); 26 | $this->checkboxes[$this->count - 1]->setLast(); 27 | } 28 | 29 | public function write() 30 | { 31 | array_map([$this, 'writeCheckbox'], $this->checkboxes); 32 | } 33 | 34 | /** 35 | * Retrieve the checked option values 36 | * 37 | * @return array 38 | */ 39 | public function getCheckedValues() 40 | { 41 | return array_values(array_map([$this, 'getValue'], $this->getChecked())); 42 | } 43 | 44 | /** 45 | * Set the newly selected option based on the direction 46 | * 47 | * @param string $direction 'previous' or 'next' 48 | */ 49 | public function setCurrent($direction) 50 | { 51 | list($option, $key) = $this->getCurrent(); 52 | 53 | $option->setCurrent(false); 54 | 55 | $new_key = $this->getCurrentKey($direction, $option, $key); 56 | 57 | $this->checkboxes[$new_key]->setCurrent(); 58 | } 59 | 60 | /** 61 | * Toggle the current option's checked status 62 | */ 63 | public function toggleCurrent() 64 | { 65 | list($option) = $this->getCurrent(); 66 | 67 | $option->setChecked(!$option->isChecked()); 68 | } 69 | 70 | /** 71 | * Get the number of checkboxes 72 | * 73 | * @return int 74 | */ 75 | public function count() 76 | { 77 | return $this->count; 78 | } 79 | 80 | /** 81 | * Retrieve the checked options 82 | * 83 | * @return array 84 | */ 85 | protected function getChecked() 86 | { 87 | return array_filter($this->checkboxes, [$this, 'isChecked']); 88 | } 89 | 90 | /** 91 | * Determine whether the option is checked 92 | * 93 | * @param Checkbox $option 94 | * 95 | * @return bool 96 | */ 97 | protected function isChecked($option) 98 | { 99 | return $option->isChecked(); 100 | } 101 | 102 | /** 103 | * Retrieve the option's value 104 | * 105 | * @param Checkbox $option 106 | * 107 | * @return mixed 108 | */ 109 | protected function getValue($option) 110 | { 111 | return $option->getValue(); 112 | } 113 | 114 | /** 115 | * Get the currently selected option 116 | * 117 | * @return array 118 | */ 119 | protected function getCurrent() 120 | { 121 | foreach ($this->checkboxes as $key => $option) { 122 | if ($option->isCurrent()) { 123 | return [$option, $key]; 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Retrieve the correct current key 130 | * 131 | * @param string $direction 'previous' or 'next' 132 | * @param Checkbox $option 133 | * @param int $key 134 | * 135 | * @return int 136 | */ 137 | protected function getCurrentKey($direction, $option, $key) 138 | { 139 | $method = 'get' . ucwords($direction). 'Key'; 140 | 141 | return $this->{$method}($option, $key); 142 | } 143 | 144 | /** 145 | * @param Checkbox $option 146 | * @param int $key 147 | * 148 | * @return int 149 | */ 150 | protected function getPreviousKey($option, $key) 151 | { 152 | if ($option->isFirst()) { 153 | return count($this->checkboxes) - 1; 154 | } 155 | 156 | return --$key; 157 | } 158 | 159 | /** 160 | * @param Checkbox $option 161 | * @param int $key 162 | * 163 | * @return int 164 | */ 165 | protected function getNextKey($option, $key) 166 | { 167 | if ($option->isLast()) { 168 | return 0; 169 | } 170 | 171 | return ++$key; 172 | } 173 | 174 | /** 175 | * @param Checkbox $checkbox 176 | */ 177 | protected function writeCheckbox($checkbox) 178 | { 179 | $checkbox->util($this->util); 180 | $checkbox->parser($this->parser); 181 | 182 | $parsed = $this->parser->apply((string) $checkbox); 183 | 184 | if ($checkbox->isLast()) { 185 | $this->output->sameLine()->write($parsed); 186 | return; 187 | } 188 | 189 | $this->output->write($parsed); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/ASCII/the-league.txt: -------------------------------------------------------------------------------- 1 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 2 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 3 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 4 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhyysoo++//////++oosyyhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 5 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhyo+/-.`` ``.-:+oyhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 6 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhs+:.` `.:+shhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 7 | hhhhhhhhhhhhhhhhhhhhhhhhhhyo:.` `` `.` `.` `.:oyhhhhhhhhhhhhhhhhhhhhhhhhhh 8 | hhhhhhhhhhhhhhhhhhhhhhhyo:` . :/-. -/./`./.-. /` . `:oyhhhhhhhhhhhhhhhhhhhhhhh 9 | hhhhhhhhhhhhhhhhhhhhhy+. `.` /- `o-. +:-+-/-`/:-/ `+ ::-. ./yhhhhhhhhhhhhhhhhhhhhh 10 | hhhhhhhhhhhhhhhhhhhs/` ` .+-- `+.-./--`.` ```---`::./.:/-.` ` `/shhhhhhhhhhhhhhhhhhh 11 | hhhhhhhhhhhhhhhhhy/` . .+.-+--. ``` ```.....`` ````/:-` `/-:- `/yhhhhhhhhhhhhhhhhh 12 | hhhhhhhhhhhhhhhy+. `- :::-:--.` .-:/++++//////++++/:-.` `` `+. -+`.` `+yhhhhhhhhhhhhhhh 13 | hhhhhhhhhhhhhhy- -//` ./` ` .:+++:-.`` ` ` ` ``.-:+++:. -:::-/::- -shhhhhhhhhhhhhh 14 | hhhhhhhhhhhhho` ` `:: ` `:++:.````.`...........`.`````.:++:. -.`- ` `ohhhhhhhhhhhhh 15 | hhhhhhhhhhhh+` ` .+o:.```.`..`..`................` .:o+. +hhhhhhhhhhhh 16 | hhhhhhhhhhh+ .+o-``.`..`..`..`..`..`..`...........```-o+. /hhhhhhhhhhh 17 | hhhhhhhhhh+ `/s- `..`..`..`..`...--:::::--.``........```-s/` +hhhhhhhhhh 18 | hhhhhhhhhs `o+```..`..`..`.-/++oooooooooooo+/-..`..`.`````+s. ohhhhhhhhh 19 | hhhhhhhhy. .y: .`..`..`..`:ooooooooooooooooo+/:-::-...-`..` :y. `yhhhhhhhh 20 | hhhhhhhh/ . .y: .`.`.:---. /oooooooooooooooo/::/:-:++:..`..... -y. . /hhhhhhhh 21 | hhhhhhhy` `.:s:`` s/ .```+ooooo/-ooooooooooooooo+::/+./ooooo+...`..`. /y` `.:s:`` yhhhhhhh 22 | hhhhhhh+ `/ooooo/` /s ..` :ooooo+`ooooooooooooooo//:+/.+oooooooo.`..`..` s/ `/ooooo/` +hhhhhhh 23 | hhhhhhh. oooo/ h-`.`` :ooooo.:-ooooooooooooo:--/+`ooooooooooo`.`..`.`-h` oooo/ .hhhhhhh 24 | hhhhhhh `. `- -h ``.. `+oos.///-/oooooooooo-----./oooooooooos.`..`..` y: `. `- yhhhhhh 25 | hhhhhhy +s `..`` :oo:-///:-:+oooooo-/-/o-.sooooooooooo ..`..`. o+ yhhhhhh 26 | hhhhhhy ++ ..`.. `:o/-/////:-:/oo-/:/o+ oooooooooooo-`.`..`.. ++ shhhhhh 27 | hhhhhhy +o .`..`.. `-:.-.-:///:.-/::oo.-ooooooosooo+`.`..`..` o+ yhhhhhh 28 | hhhhhhh -y `..`..`..:+ooo+../-:/--//:oo/.:::///::-...`` ` ..`. y: yhhhhhh 29 | hhhhhhh. `h-`.`..`-+ooo+/--:/-/:`://-ooo-`.`.-:::/++ooooo+:-``.`.h` .hhhhhhh 30 | hhhhhhh+ ` /s `..`.---.-:://///::/:::ooo:-+oooooooooooooooooo:`. s+ /hhhhhhh 31 | hhhhhhhy .:/:/` `y/ .`.`-/-/-////////:/-/ooo/:`://++oooooooooooo/-`. :y` `/`::` yhhhhhhh 32 | hhhhhhhh/ `/`.`: .y- ..`.:/-/-////////.ooooo:-.`..`....:::///:-..`. -y. .::`/` :hhhhhhhh 33 | hhhhhhhhy` `--/:` .y: `..-:/-//://///:/oooo:/.`..`..`..`..`..`..`. :y- /.:`-` `yhhhhhhhh 34 | hhhhhhhhhs `::.-- .s+````.:/::++/::/+ooo+::.`..`..`..`..`..`..```/s. ..:/-: ohhhhhhhhh 35 | hhhhhhhhhh+ ``.-/: `/s-````-:::/ooooo++/:-.`..`..`..`..`..`..` -s+` /-`:/` +hhhhhhhhhh 36 | hhhhhhhhhhh/ ..``::. .oo-```.-:::::::--...`..`..`..`..`..`.``-oo. ..-:--: /hhhhhhhhhhh 37 | hhhhhhhhhhhh+ -:/.+` .+o:` `.``..`.......`..`..`..`.````:o+- `+.::-` /hhhhhhhhhhhh 38 | hhhhhhhhhhhhho` `:.---: ./++:.`````.`...........`.``` .:+o/. ` :-.:/ ` `ohhhhhhhhhhhhh 39 | hhhhhhhhhhhhhhs- -::-:`.. .:++/:..`` `` ` ` ``..:/++:. ``-::::.` -shhhhhhhhhhhhhh 40 | hhhhhhhhhhhhhhhy+` :.-:.-: ` `.-//+++////////+++//-.` ` ::` .:- `+yhhhhhhhhhhhhhhh 41 | hhhhhhhhhhhhhhhhhy:` /-.:./-:. ` ``......`` `` /-:-:--: `:yhhhhhhhhhhhhhhhhh 42 | hhhhhhhhhhhhhhhhhhhs:` ```/-/:.:--- .` ` `/././::/``` `:shhhhhhhhhhhhhhhhhhh 43 | hhhhhhhhhhhhhhhhhhhhhs/. : -:`-:`+`:: -` --` :-:`:.-: /:-` - ./shhhhhhhhhhhhhhhhhhhhh 44 | hhhhhhhhhhhhhhhhhhhhhhhyo:` `.--`/- +.:+ /-.+ +-/- -+ - `-oyhhhhhhhhhhhhhhhhhhhhhhh 45 | hhhhhhhhhhhhhhhhhhhhhhhhhhyo:`` `` . .- /../ :`.- . ``:oyhhhhhhhhhhhhhhhhhhhhhhhhhh 46 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhys+:`` ``-+syhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 47 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhso+:.`` ``.:/oshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 48 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhysso++////////++oosyhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 49 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 50 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 51 | hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh 52 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Checkbox/Checkbox.php: -------------------------------------------------------------------------------- 1 | value = (!is_int($value)) ? $value : $label; 58 | $this->label = $label; 59 | } 60 | 61 | /** 62 | * @return bool 63 | */ 64 | public function isCurrent() 65 | { 66 | return $this->current; 67 | } 68 | 69 | /** 70 | * @return bool 71 | */ 72 | public function isChecked() 73 | { 74 | return $this->checked; 75 | } 76 | 77 | /** 78 | * @return bool 79 | */ 80 | public function isFirst() 81 | { 82 | return $this->first; 83 | } 84 | 85 | /** 86 | * @return bool 87 | */ 88 | public function isLast() 89 | { 90 | return $this->last; 91 | } 92 | 93 | /** 94 | * Set whether the pointer is currently pointing at this checkbox 95 | * 96 | * @param bool $current 97 | * 98 | * @return Checkbox 99 | */ 100 | public function setCurrent($current = true) 101 | { 102 | $this->current = $current; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set whether the checkbox is currently checked 109 | * 110 | * @param bool $checked 111 | * 112 | * @return Checkbox 113 | */ 114 | public function setChecked($checked = true) 115 | { 116 | $this->checked = $checked; 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * @return Checkbox 123 | */ 124 | public function setFirst() 125 | { 126 | $this->first = true; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @return Checkbox 133 | */ 134 | public function setLast() 135 | { 136 | $this->last = true; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @return string|int|bool 143 | */ 144 | public function getValue() 145 | { 146 | return $this->value; 147 | } 148 | 149 | /** 150 | * Build out basic checkbox string based on current options 151 | * 152 | * @return string 153 | */ 154 | protected function buildCheckboxString() 155 | { 156 | $parts = [ 157 | ($this->isCurrent()) ? $this->pointer() : ' ', 158 | $this->checkbox($this->isChecked()), 159 | $this->label, 160 | ]; 161 | 162 | $line = implode(' ', $parts); 163 | 164 | return $line . $this->getPaddingString($line); 165 | } 166 | 167 | /** 168 | * Get the padding string based on the length of the terminal/line 169 | * 170 | * @param string $line 171 | * 172 | * @return string 173 | */ 174 | protected function getPaddingString($line) 175 | { 176 | $length = $this->util->system->width() - $this->lengthWithoutTags($line); 177 | 178 | return str_repeat(' ', $length); 179 | } 180 | 181 | /** 182 | * Get the checkbox symbol 183 | * 184 | * @param bool $checked 185 | * 186 | * @return string 187 | */ 188 | protected function checkbox($checked) 189 | { 190 | if ($checked) { 191 | return html_entity_decode("●"); 192 | } 193 | 194 | return html_entity_decode("○"); 195 | } 196 | 197 | /** 198 | * Get the pointer symbol 199 | * 200 | * @return string 201 | */ 202 | protected function pointer() 203 | { 204 | return html_entity_decode("❯"); 205 | } 206 | 207 | public function __toString() 208 | { 209 | if ($this->isFirst()) { 210 | return $this->buildCheckboxString(); 211 | } 212 | 213 | if ($this->isLast()) { 214 | return $this->buildCheckboxString() . $this->util->cursor->left(10) . ''; 215 | } 216 | 217 | return $this->buildCheckboxString(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## 3.4.1 - 2018-04-29 5 | 6 | ### Fixed 7 | 8 | * [Json] Don't escape slashes when outputting JSON. [#121](https://github.com/thephpleague/climate/pull/121) 9 | 10 | -------- 11 | 12 | ## 3.4.0 - 2018-04-28 13 | 14 | ### Added 15 | 16 | * [Logger] Added a Logger class to use CLImate as a PSR-3 logger. 17 | 18 | -------- 19 | 20 | ## 3.3.0 - 2018-04-20 21 | 22 | ### Fixed 23 | 24 | * Ensure multibyte strings are supported everywhere. 25 | * Improved support for IDE assistance when using method chaining. [#102](https://github.com/thephpleague/climate/pull/102) 26 | * [Art] Improve handling of missing files. [#114](https://github.com/thephpleague/climate/issues/114) 27 | * [Input] Correct the usage of `defaultTo()` with `accept()`. [#104](https://github.com/thephpleague/climate/pull/104) 28 | * [Windows] Fixed the terminal width detection. [#64](https://github.com/thephpleague/climate/pull/64) 29 | 30 | ### Added 31 | 32 | * [Table] Add support for a prefix argument for each row. [#51](https://github.com/thephpleague/climate/issues/51) 33 | * [Progress] Added an `each()` method. [#112](https://github.com/thephpleague/climate/pull/112) 34 | 35 | ### Changed 36 | 37 | * [Support] Add support for PHP 7.2 38 | * [Support] Drop support for PHP 5.4 39 | * [Support] Drop support for PHP 5.5 40 | * [Support] Drop support for HHVM. 41 | * Suggest the symfony polyfill library is `ext-mbstring` is not available. [#110](https://github.com/thephpleague/climate/pull/110) 42 | 43 | -------- 44 | 45 | ## 3.2.4 - 2016-10-30 46 | 47 | ### Fixed 48 | 49 | * [Progres] Allow labels to be shown/hidden on each iteration. [#98](https://github.com/thephpleague/climate/pull/98) 50 | 51 | -------- 52 | 53 | ## 3.2.3 - 2016-10-17 54 | 55 | ### Added 56 | 57 | * [Support] Added support for PHP 7.1 58 | 59 | -------- 60 | 61 | ## 3.2.2 - 2016-07-18 62 | 63 | ### Fixed 64 | 65 | * [Art] Allow code to be used in a phar. [#86](https://github.com/thephpleague/climate/pull/86) 66 | 67 | -------- 68 | 69 | ## 3.2.1 - 2016-04-05 70 | 71 | ### Added 72 | 73 | * [Arguments] Add a `trailing()` method to get any trailing arguments. 74 | * [Progress] Added a `forceRedraw()` method. [#72](https://github.com/thephpleague/climate/issues/72) 75 | 76 | ### Fixed 77 | 78 | * [Checkbox] Don't cancel out the formatting for the first checkbox. [#77](https://github.com/thephpleague/climate/issues/77) 79 | * [Padding] Ensure formatting is handled. [#78](https://github.com/thephpleague/climate/issues/78) 80 | * [Columns] Prevent error when less items than columns are passed. [#75](https://github.com/thephpleague/climate/pull/75) 81 | 82 | -------- 83 | 84 | ## 3.2.0 - 2015-08-13 85 | 86 | ### Added 87 | - Multi-line support for `input` method [https://github.com/thephpleague/climate/pull/67](https://github.com/thephpleague/climate/pull/67) 88 | - `extend` method for _much_ easier extending of CLImate 89 | 90 | ### Fixed 91 | - 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) 92 | - Progress label no longer removed once progress reaches 100% 93 | - 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) 94 | 95 | ## 3.1.1 - 2015-05-01 96 | 97 | ### Fixed 98 | - Windows support added for `password` thanks to @Seldaek and [seld/cli-prompt](https://packagist.org/packages/seld/cli-prompt) 99 | 100 | ## 3.1.0 - 2015-04-30 101 | 102 | ### Added 103 | - `password` prompt 104 | - `checkboxes` prompt 105 | - `radio` prompt 106 | - 'file' as output option 107 | 108 | ## 3.0.0 - 2015-03-01 109 | 110 | ### Changed 111 | 112 | - Custom output writers are added simply via the `output` property on CLImate now, as opposed to the immense amount of scaffolding required before 113 | 114 | ### Added 115 | 116 | - Argument parsing 117 | - StdErr output 118 | - Buffer output 119 | - `animate` method for running ASCII animations in the terminal. Because it's fun. 120 | - Input now bolds the default response if it exists 121 | 122 | ## 2.6.1 - 2015-01-18 123 | 124 | ### Fixed 125 | 126 | - Added `forceAnsiOn` and `forceAnsiOff` methods to address systems that were not identified correctly 127 | 128 | ## 2.6.0 - 2015-01-07 129 | 130 | ### Added 131 | 132 | - Allow for passing an array of arrays into `columns` method 133 | - `tab` method, for indenting text 134 | - `padding` method, for padding strings to an equal width with a character 135 | - `League\CLImate\TerminalObject\Repeatable` for repeatable objects such as `tab` and `br` 136 | - `League\CLImate\Decorator\Parser\Ansi` and `League\CLImate\Decorator\Parser\NonAnsi` 137 | - Factories: 138 | + `League\CLImate\Decorator\Parser\ParserFactory` 139 | + `League\CLImate\Util\System\SystemFactory` 140 | - Terminal Objects now are appropriately namespaced as `Basic` or `Dynamic` 141 | - Readers and Writers are appropriately namespaced as such under `League\CLImate\Util` 142 | 143 | ### Fixed 144 | 145 | - Labels for `advance` method 146 | - Non-ansi terminals will now have plaintext output instead of jumbled characters 147 | - `border` method now default to full terminal width 148 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Columns.php: -------------------------------------------------------------------------------- 1 | data = $data; 28 | $this->column_count = $column_count; 29 | } 30 | 31 | /** 32 | * Calculate the number of columns organize data 33 | * 34 | * @return array 35 | */ 36 | public function result() 37 | { 38 | $keys = array_keys($this->data); 39 | $first_key = reset($keys); 40 | 41 | return (!is_int($first_key)) ? $this->associativeColumns() : $this->columns(); 42 | } 43 | 44 | /** 45 | * Get columns for a regular array 46 | * 47 | * @return array 48 | */ 49 | protected function columns() 50 | { 51 | $this->data = $this->setData(); 52 | $column_widths = $this->getColumnWidths(); 53 | $output = []; 54 | $count = count(reset($this->data)); 55 | 56 | for ($i = 0; $i < $count; $i++) { 57 | $output[] = $this->getRow($i, $column_widths); 58 | } 59 | 60 | return $output; 61 | } 62 | 63 | /** 64 | * Re-configure the data into it's final form 65 | */ 66 | protected function setData() 67 | { 68 | // If it's already an array of arrays, we're good to go 69 | if (is_array(reset($this->data))) { 70 | return $this->setArrayOfArraysData(); 71 | } 72 | 73 | $column_width = $this->getColumnWidth($this->data); 74 | $row_count = $this->getMaxRows($column_width); 75 | 76 | return array_chunk($this->data, $row_count); 77 | } 78 | 79 | /** 80 | * Re-configure an array of arrays into column arrays 81 | */ 82 | protected function setArrayOfArraysData() 83 | { 84 | $this->setColumnCountViaArray($this->data); 85 | 86 | $new_data = array_fill(0, $this->column_count, []); 87 | 88 | foreach ($this->data as $items) { 89 | for ($i = 0; $i < $this->column_count; $i++) { 90 | $new_data[$i][] = (array_key_exists($i, $items)) ? $items[$i] : null; 91 | } 92 | } 93 | 94 | return $new_data; 95 | } 96 | 97 | /** 98 | * Get columns for an associative array 99 | * 100 | * @return array 101 | */ 102 | protected function associativeColumns() 103 | { 104 | $column_width = $this->getColumnWidth(array_keys($this->data)); 105 | $output = []; 106 | 107 | foreach ($this->data as $key => $value) { 108 | $output[] = $this->pad($key, $column_width) . $value; 109 | } 110 | 111 | return $output; 112 | } 113 | 114 | /** 115 | * Get the row of data 116 | * 117 | * @param integer $key 118 | * @param array $column_widths 119 | * 120 | * @return string 121 | */ 122 | protected function getRow($key, $column_widths) 123 | { 124 | $row = []; 125 | 126 | for ($j = 0; $j < $this->column_count; $j++) { 127 | if (isset($this->data[$j]) && array_key_exists($key, $this->data[$j])) { 128 | $row[] = $this->pad($this->data[$j][$key], $column_widths[$j]); 129 | } 130 | } 131 | 132 | return trim(implode('', $row)); 133 | } 134 | 135 | /** 136 | * Get the standard column width 137 | * 138 | * @param array $data 139 | * 140 | * @return integer 141 | */ 142 | protected function getColumnWidth($data) 143 | { 144 | // Return the maximum width plus a buffer 145 | return $this->maxStrLen($data) + 5; 146 | } 147 | 148 | /** 149 | * Get an array of each column's width 150 | * 151 | * @return array 152 | */ 153 | protected function getColumnWidths() 154 | { 155 | $column_widths = []; 156 | 157 | for ($i = 0; $i < $this->column_count; $i++) { 158 | if (!isset($this->data[$i])) { 159 | $column_widths[] = 0; 160 | continue; 161 | } 162 | $column_widths[] = $this->getColumnWidth($this->data[$i]); 163 | } 164 | 165 | return $column_widths; 166 | } 167 | 168 | /** 169 | * Set the count property 170 | * 171 | * @param integer $column_width 172 | */ 173 | protected function setColumnCount($column_width) 174 | { 175 | $this->column_count = (int) floor($this->util->width() / $column_width); 176 | } 177 | 178 | /** 179 | * Set the count property via an array 180 | * 181 | * @param array $items 182 | */ 183 | protected function setColumnCountViaArray($items) 184 | { 185 | $counts = array_map(function ($arr) { 186 | return count($arr); 187 | }, $items); 188 | 189 | $this->column_count = max($counts); 190 | } 191 | 192 | /** 193 | * Get the number of rows per column 194 | * 195 | * @param integer $column_width 196 | * 197 | * @return integer 198 | */ 199 | protected function getMaxRows($column_width) 200 | { 201 | if (!$this->column_count) { 202 | $this->setColumnCount($column_width); 203 | } 204 | 205 | return ceil(count($this->data) / $this->column_count); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | 1, 23 | LogLevel::ALERT => 2, 24 | LogLevel::CRITICAL => 3, 25 | LogLevel::ERROR => 4, 26 | LogLevel::WARNING => 5, 27 | LogLevel::NOTICE => 6, 28 | LogLevel::INFO => 7, 29 | LogLevel::DEBUG => 8, 30 | ]; 31 | 32 | /** 33 | * @var int $level Ignore logging attempts at a level less than this. 34 | */ 35 | private $level; 36 | 37 | /** 38 | * @var CLImate $climate The underlying climate instance we are using for output. 39 | */ 40 | private $climate; 41 | 42 | /** 43 | * Create a new Logger instance. 44 | * 45 | * @param string $level One of the LogLevel constants 46 | * @param CLImate $climate An existing CLImate instance to use for output 47 | */ 48 | public function __construct($level = LogLevel::INFO, CLImate $climate = null) 49 | { 50 | $this->level = $this->convertLevel($level); 51 | 52 | if ($climate === null) { 53 | $climate = new CLImate; 54 | } 55 | $this->climate = $climate; 56 | 57 | # Define some default styles to use for the output 58 | $commands = [ 59 | "emergency" => ["white", "bold", "background_red"], 60 | "alert" => ["white", "background_yellow"], 61 | "critical" => ["red", "bold"], 62 | "error" => ["red"], 63 | "warning" => "yellow", 64 | "notice" => "light_cyan", 65 | "info" => "green", 66 | "debug" => "dark_gray", 67 | ]; 68 | 69 | # If any of the required styles are not defined then define them now 70 | foreach ($commands as $command => $style) { 71 | if (!$this->climate->style->get($command)) { 72 | $this->climate->style->addCommand($command, $style); 73 | } 74 | } 75 | } 76 | 77 | 78 | /** 79 | * Get a numeric log level for the passed parameter. 80 | * 81 | * @param string $level One of the LogLevel constants 82 | * 83 | * @return int 84 | */ 85 | private function convertLevel($level) 86 | { 87 | # If this is one of the defined string log levels then return it's numeric value 88 | if (!array_key_exists($level, $this->levels)) { 89 | throw new InvalidArgumentException("Unknown log level: {$level}"); 90 | } 91 | 92 | return $this->levels[$level]; 93 | } 94 | 95 | 96 | /** 97 | * Get a new instance logging at a different level 98 | * 99 | * @param string $level One of the LogLevel constants 100 | * 101 | * @return Logger 102 | */ 103 | public function withLogLevel($level) 104 | { 105 | $logger = clone $this; 106 | $logger->level = $this->convertLevel($level); 107 | return $logger; 108 | } 109 | 110 | 111 | /** 112 | * Log messages to a CLImate instance. 113 | * 114 | * @param string $level One of the LogLevel constants 115 | * @param string|object $message If an object is passed it must implement __toString() 116 | * @param array $context Placeholders to be substituted in the message 117 | * 118 | * @return void 119 | */ 120 | public function log($level, $message, array $context = []) 121 | { 122 | if ($this->convertLevel($level) > $this->level) { 123 | return; 124 | } 125 | 126 | # Handle objects implementing __toString 127 | $message = (string)$message; 128 | 129 | # Handle any placeholders in the $context array 130 | foreach ($context as $key => $val) { 131 | $placeholder = "{" . $key . "}"; 132 | 133 | # If this context key is used as a placeholder, then replace it, and remove it from the $context array 134 | if (strpos($message, $placeholder) !== false) { 135 | $val = (string)$val; 136 | $message = str_replace($placeholder, $val, $message); 137 | unset($context[$key]); 138 | } 139 | } 140 | 141 | # Send the message to the climate instance 142 | $this->climate->{$level}($message); 143 | 144 | # Append any context information not used as placeholders 145 | $this->outputRecursiveContext($level, $context, 1); 146 | } 147 | 148 | 149 | /** 150 | * Handle recursive arrays in the logging context. 151 | * 152 | * @param string $level One of the LogLevel constants 153 | * @param array $context The array of context to output 154 | * @param int $indent The current level of indentation to be used 155 | * 156 | * @return void 157 | */ 158 | private function outputRecursiveContext($level, array $context, $indent) 159 | { 160 | foreach ($context as $key => $val) { 161 | $this->climate->tab($indent); 162 | 163 | $this->climate->{$level}()->inline("{$key}: "); 164 | 165 | if (is_array($val)) { 166 | $this->climate->{$level}("["); 167 | $this->outputRecursiveContext($level, $val, $indent + 1); 168 | $this->climate->tab($indent)->{$level}("]"); 169 | } else { 170 | $this->climate->{$level}((string)$val); 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Argument/Summary.php: -------------------------------------------------------------------------------- 1 | climate = $climate; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param string $description 43 | * 44 | * @return \League\CLImate\Argument\Summary 45 | */ 46 | public function setDescription($description) 47 | { 48 | $this->description = $description; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param string $command 55 | * 56 | * @return \League\CLImate\Argument\Summary 57 | */ 58 | public function setCommand($command) 59 | { 60 | $this->command = $command; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param Filter $filter 67 | * @param Argument[] $arguments 68 | * 69 | * @return \League\CLImate\Argument\Summary 70 | */ 71 | public function setFilter($filter, $arguments) 72 | { 73 | $this->filter = $filter; 74 | $this->filter->setArguments($arguments); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Output the full summary for the program 81 | */ 82 | public function output() 83 | { 84 | // Print the description if it's defined. 85 | if ($this->description) { 86 | $this->climate->out($this->description)->br(); 87 | } 88 | 89 | // Print the usage statement with the arguments without a prefix at the end. 90 | $this->climate->out("Usage: {$this->command} " 91 | . $this->short($this->getOrderedArguments())); 92 | 93 | // Print argument details. 94 | foreach (['required', 'optional'] as $type) { 95 | $this->outputArguments($this->filter->{$type}(), $type); 96 | } 97 | } 98 | 99 | /** 100 | * Build a short summary of a list of arguments. 101 | * 102 | * @param Argument[] $arguments 103 | * 104 | * @return string 105 | */ 106 | public function short($arguments) 107 | { 108 | return implode(' ', array_map([$this, 'argumentBracketed'], $arguments)); 109 | } 110 | 111 | /** 112 | * Build an argument's summary for use in a usage statement. 113 | * 114 | * For example, "-u username, --user username", "--force", or 115 | * "-c count (default: 7)". 116 | * 117 | * @param Argument $argument 118 | * 119 | * @return string 120 | */ 121 | public function argument(Argument $argument) 122 | { 123 | $summary = $this->prefixedArguments($argument); 124 | $printedName = mb_strstr($summary, ' ' . $argument->name()); 125 | 126 | // Print the argument name if it's not printed yet. 127 | if (!$printedName && !$argument->noValue()) { 128 | $summary .= $argument->name(); 129 | } 130 | 131 | if ($defaults = $argument->defaultValue()) { 132 | if (count($defaults) == 1) { 133 | $summary .= " (default: {$defaults[0]})"; 134 | } else { 135 | $summary .= ' (defaults: ' . implode(', ', $defaults) . ')'; 136 | } 137 | } 138 | 139 | return $summary; 140 | } 141 | 142 | /** 143 | * Build argument summary surrounded by brackets 144 | * 145 | * @param Argument $argument 146 | * 147 | * @return string 148 | */ 149 | protected function argumentBracketed(Argument $argument) 150 | { 151 | return '[' . $this->argument($argument) . ']'; 152 | } 153 | 154 | /** 155 | * Get the arguments ordered by whether or not they have a prefix 156 | * 157 | * @return Argument[] 158 | */ 159 | protected function getOrderedArguments() 160 | { 161 | return array_merge($this->filter->withPrefix(), $this->filter->withoutPrefix()); 162 | } 163 | 164 | /** 165 | * Print out the argument list 166 | * 167 | * @param array $arguments 168 | * @param string $type 169 | */ 170 | protected function outputArguments($arguments, $type) 171 | { 172 | if (count($arguments) == 0) { 173 | return; 174 | } 175 | 176 | $this->climate->br()->out(mb_convert_case($type, MB_CASE_TITLE) . ' Arguments:'); 177 | 178 | foreach ($arguments as $argument) { 179 | $this->climate->tab()->out($this->argument($argument)); 180 | 181 | if ($argument->description()) { 182 | $this->climate->tab(2)->out($argument->description()); 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Builds the summary for any prefixed arguments 189 | * 190 | * @param Argument $argument 191 | * 192 | * @return string 193 | */ 194 | protected function prefixedArguments(Argument $argument) 195 | { 196 | $prefixes = [$argument->prefix(), $argument->longPrefix()]; 197 | $summary = []; 198 | 199 | foreach ($prefixes as $key => $prefix) { 200 | if (!$prefix) { 201 | continue; 202 | } 203 | 204 | $sub = str_repeat('-', $key + 1) . $prefix; 205 | 206 | if (!$argument->noValue()) { 207 | $sub .= " {$argument->name()}"; 208 | } 209 | 210 | $summary[] = $sub; 211 | } 212 | 213 | return implode(', ', $summary); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Animation.php: -------------------------------------------------------------------------------- 1 | addDir(__DIR__ . '/../../ASCII'); 27 | 28 | $this->setSleeper($sleeper); 29 | $this->setKeyFrames($keyframes); 30 | 31 | $this->art = $art; 32 | } 33 | 34 | /** 35 | * Run a basic animation 36 | */ 37 | public function run() 38 | { 39 | $files = $this->artDir($this->art); 40 | $animation = []; 41 | 42 | foreach ($files as $file) { 43 | $animation[] = $this->parse($file); 44 | } 45 | 46 | $this->animate($animation); 47 | } 48 | 49 | /** 50 | * Set the speed of the animation based on a percentage 51 | * (50% slower, 200% faster, etc) 52 | * 53 | * @param int|float $percentage 54 | * 55 | * @return \League\CLImate\TerminalObject\Dynamic\Animation 56 | */ 57 | public function speed($percentage) 58 | { 59 | $this->sleeper->speed($percentage); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Scroll the art 66 | * 67 | * @param string $direction 68 | * @return bool 69 | */ 70 | public function scroll($direction = 'right') 71 | { 72 | $this->setupKeyframes(); 73 | 74 | $mapping = $this->getScrollDirectionMapping(); 75 | 76 | if (!array_key_exists($direction, $mapping)) { 77 | return false; 78 | } 79 | 80 | $lines = $this->getLines(); 81 | $enter_from = $mapping[$direction]; 82 | $exit_to = $mapping[$enter_from]; 83 | 84 | $this->animate($this->keyframes->scroll($lines, $enter_from, $exit_to)); 85 | } 86 | 87 | /** 88 | * Animate the art exiting the screen 89 | * 90 | * @param string $direction top|bottom|right|left 91 | */ 92 | public function exitTo($direction) 93 | { 94 | $this->setupKeyframes(); 95 | 96 | $this->animate($this->keyframes->exitTo($this->getLines(), $direction)); 97 | } 98 | 99 | /** 100 | * Animate the art entering the screen 101 | * 102 | * @param string $direction top|bottom|right|left 103 | */ 104 | public function enterFrom($direction) 105 | { 106 | $this->setupKeyframes(); 107 | 108 | $this->animate($this->keyframes->enterFrom($this->getLines(), $direction)); 109 | } 110 | 111 | protected function getScrollDirectionMapping() 112 | { 113 | return [ 114 | 'left' => 'right', 115 | 'right' => 'left', 116 | 'top' => 'bottom', 117 | 'bottom' => 'top', 118 | 'up' => 'bottom', 119 | 'down' => 'top', 120 | ]; 121 | } 122 | 123 | protected function getLines() 124 | { 125 | return $this->parse($this->artFile($this->art)); 126 | } 127 | 128 | /** 129 | * @param \League\CLImate\TerminalObject\Helper\Sleeper $sleeper 130 | */ 131 | protected function setSleeper($sleeper = null) 132 | { 133 | $this->sleeper = $sleeper ?: new Sleeper(); 134 | } 135 | 136 | /** 137 | * @param League\CLImate\TerminalObject\Dynamic\Animation\Keyframe $keyframes 138 | */ 139 | protected function setKeyFrames($keyframes) 140 | { 141 | $this->keyframes = $keyframes ?: new Keyframe; 142 | } 143 | 144 | /** 145 | * Set up the necessary properties on the Keyframe class 146 | */ 147 | protected function setupKeyframes() 148 | { 149 | $this->keyframes->parser($this->parser); 150 | $this->keyframes->util($this->util); 151 | } 152 | 153 | /** 154 | * Animate the given keyframes 155 | * 156 | * @param array $keyframes Array of arrays 157 | */ 158 | protected function animate(array $keyframes) 159 | { 160 | $count = 0; 161 | 162 | foreach ($keyframes as $lines) { 163 | $this->writeKeyFrame($lines, $count); 164 | $this->sleeper->sleep(); 165 | $count = count($lines); 166 | } 167 | } 168 | 169 | /** 170 | * Write the current keyframe to the terminal, line by line 171 | * 172 | * @param array $lines 173 | * @param integer $count 174 | */ 175 | protected function writeKeyFrame(array $lines, $count) 176 | { 177 | foreach ($lines as $key => $line) { 178 | $content = $this->getLineFormatted($line, $key, $count); 179 | $this->output->write($this->parser->apply($content)); 180 | } 181 | } 182 | 183 | /** 184 | * Format the line to re-write previous lines, if necessary 185 | * 186 | * @param string $line 187 | * @param integer $key 188 | * @param integer $last_frame_count 189 | * 190 | * @return string 191 | */ 192 | protected function getLineFormatted($line, $key, $last_frame_count) 193 | { 194 | // If this is the first thing we're writing, just return the line 195 | if ($last_frame_count == 0) { 196 | return $line; 197 | } 198 | 199 | $content = ''; 200 | 201 | // If this is the first line of the frame, 202 | // move the cursor up the total number of previous lines from the previous frame 203 | if ($key == 0) { 204 | $content .= $this->util->cursor->up($last_frame_count); 205 | } 206 | 207 | $content .= $this->util->cursor->startOfCurrentLine(); 208 | $content .= $this->util->cursor->deleteCurrentLine(); 209 | $content .= $line; 210 | 211 | return $content; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/TerminalObject/Basic/Table.php: -------------------------------------------------------------------------------- 1 | data = $data; 62 | $this->prefix = $prefix; 63 | } 64 | 65 | /** 66 | * Return the built rows 67 | * 68 | * @return array 69 | */ 70 | public function result() 71 | { 72 | $this->column_widths = $this->getColumnWidths(); 73 | $this->table_width = $this->getWidth(); 74 | $this->border = $this->getBorder(); 75 | 76 | $this->buildHeaderRow(); 77 | 78 | foreach ($this->data as $key => $columns) { 79 | $this->addLine($this->buildRow($columns)); 80 | $this->addLine($this->border); 81 | } 82 | 83 | return $this->rows; 84 | } 85 | 86 | /** 87 | * Append a line to the output. 88 | * 89 | * @param string $line The line to output 90 | * 91 | * @return void 92 | */ 93 | private function addLine($line) 94 | { 95 | $this->rows[] = $this->prefix . $line; 96 | } 97 | 98 | 99 | /** 100 | * Determine the width of the table 101 | * 102 | * @return integer 103 | */ 104 | protected function getWidth() 105 | { 106 | $first_row = reset($this->data); 107 | $first_row = $this->buildRow($first_row); 108 | 109 | return $this->lengthWithoutTags($first_row); 110 | } 111 | 112 | /** 113 | * Get the border for each row based on the table width 114 | */ 115 | protected function getBorder() 116 | { 117 | return (new Border())->length($this->table_width)->result(); 118 | } 119 | 120 | /** 121 | * Check for a header row (if it's an array of associative arrays or objects), 122 | * if there is one, tack it onto the front of the rows array 123 | */ 124 | protected function buildHeaderRow() 125 | { 126 | $this->addLine($this->border); 127 | 128 | $header_row = $this->getHeaderRow(); 129 | if ($header_row) { 130 | $this->addLine($this->buildRow($header_row)); 131 | $this->addLine((new Border)->char('=')->length($this->table_width)->result()); 132 | } 133 | } 134 | 135 | /** 136 | * Get table row 137 | * 138 | * @param mixed $columns 139 | * 140 | * @return string 141 | */ 142 | protected function buildRow($columns) 143 | { 144 | $row = []; 145 | 146 | foreach ($columns as $key => $column) { 147 | $row[] = $this->buildCell($key, $column); 148 | } 149 | 150 | $row = implode($this->column_divider, $row); 151 | 152 | return trim($this->column_divider . $row . $this->column_divider); 153 | } 154 | 155 | /** 156 | * Build the string for this particular table cell 157 | * 158 | * @param mixed $key 159 | * @param string $column 160 | * 161 | * @return string 162 | */ 163 | protected function buildCell($key, $column) 164 | { 165 | return $this->pad($column, $this->column_widths[$key]); 166 | } 167 | 168 | /** 169 | * Get the header row for the table if it's an associative array or object 170 | * 171 | * @return mixed 172 | */ 173 | protected function getHeaderRow() 174 | { 175 | $first_item = reset($this->data); 176 | 177 | if (is_object($first_item)) { 178 | $first_item = get_object_vars($first_item); 179 | } 180 | 181 | $keys = array_keys($first_item); 182 | $first_key = reset($keys); 183 | 184 | // We have an associative array (probably), let's have a header row 185 | if (!is_int($first_key)) { 186 | return array_combine($keys, $keys); 187 | } 188 | 189 | return false; 190 | } 191 | 192 | /** 193 | * Determine the width of each column 194 | * 195 | * @return array 196 | */ 197 | protected function getColumnWidths() 198 | { 199 | $first_row = reset($this->data); 200 | 201 | if (is_object($first_row)) { 202 | $first_row = get_object_vars($first_row); 203 | } 204 | 205 | // Create an array with the columns as keys and values of zero 206 | $column_widths = $this->getDefaultColumnWidths($first_row); 207 | 208 | foreach ($this->data as $columns) { 209 | foreach ($columns as $key => $column) { 210 | $column_widths[$key] = $this->getCellWidth($column_widths[$key], $column); 211 | } 212 | } 213 | 214 | return $column_widths; 215 | } 216 | 217 | /** 218 | * Set up an array of default column widths 219 | * 220 | * @param array $columns 221 | * 222 | * @return array 223 | */ 224 | protected function getDefaultColumnWidths(array $columns) 225 | { 226 | $widths = $this->arrayOfStrLens(array_keys($columns)); 227 | 228 | return array_combine(array_keys($columns), $widths); 229 | } 230 | 231 | /** 232 | * Determine the width of the columns without tags 233 | * 234 | * @param array $current_width 235 | * @param string $str 236 | * 237 | * @return integer 238 | */ 239 | protected function getCellWidth($current_width, $str) 240 | { 241 | return max($current_width, $this->lengthWithoutTags($str)); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Argument/Manager.php: -------------------------------------------------------------------------------- 1 | filter = new Filter(); 47 | $this->summary = new Summary(); 48 | $this->parser = new Parser(); 49 | } 50 | 51 | /** 52 | * Add an argument. 53 | * 54 | * @throws \Exception if $argument isn't an array or Argument object. 55 | * @param Argument|string|array $argument 56 | * @param $options 57 | */ 58 | public function add($argument, array $options = []) 59 | { 60 | if (is_array($argument)) { 61 | $this->addMany($argument); 62 | return; 63 | } 64 | 65 | if (is_string($argument)) { 66 | $argument = Argument::createFromArray($argument, $options); 67 | } 68 | 69 | if (!($argument instanceof Argument)) { 70 | throw new \Exception('Please provide an argument name or object.'); 71 | } 72 | 73 | $this->arguments[$argument->name()] = $argument; 74 | } 75 | 76 | /** 77 | * Add multiple arguments to a CLImate script. 78 | * 79 | * @param array $arguments 80 | */ 81 | protected function addMany(array $arguments = []) 82 | { 83 | foreach ($arguments as $name => $options) { 84 | $this->add($name, $options); 85 | } 86 | } 87 | 88 | /** 89 | * Determine if an argument exists. 90 | * 91 | * @param string $name 92 | * @return bool 93 | */ 94 | public function exists($name) 95 | { 96 | return isset($this->arguments[$name]); 97 | } 98 | 99 | /** 100 | * Retrieve an argument's value. 101 | * 102 | * @param string $name 103 | * @return string|int|float|bool|null 104 | */ 105 | public function get($name) 106 | { 107 | return isset($this->arguments[$name]) ? $this->arguments[$name]->value() : null; 108 | } 109 | 110 | /** 111 | * Retrieve an argument's all values as an array. 112 | * 113 | * @param string $name 114 | * @return string[]|int[]|float[]|bool[] 115 | */ 116 | public function getArray($name) 117 | { 118 | return isset($this->arguments[$name]) ? $this->arguments[$name]->values() : []; 119 | } 120 | 121 | /** 122 | * Retrieve all arguments. 123 | * 124 | * @return Argument[] 125 | */ 126 | public function all() 127 | { 128 | return $this->arguments; 129 | } 130 | 131 | /** 132 | * Determine if an argument has been defined on the command line. 133 | * 134 | * This can be useful for making sure an argument is present on the command 135 | * line before parse()'ing them into argument objects. 136 | * 137 | * @param string $name 138 | * @param array $argv 139 | * 140 | * @return bool 141 | */ 142 | public function defined($name, array $argv = null) 143 | { 144 | // The argument isn't defined if it's not defined by the calling code. 145 | if (!$this->exists($name)) { 146 | return false; 147 | } 148 | 149 | $argument = $this->arguments[$name]; 150 | $command_arguments = $this->parser->arguments($argv); 151 | 152 | foreach ($command_arguments as $command_argument) { 153 | if ($this->isArgument($argument, $command_argument)) { 154 | return true; 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | 161 | /** 162 | * Check if the defined argument matches the command argument. 163 | * 164 | * @param Argument $argument 165 | * @param string $command_argument 166 | * 167 | * @return bool 168 | */ 169 | protected function isArgument($argument, $command_argument) 170 | { 171 | $possibilities = [ 172 | $argument->prefix() => "-{$argument->prefix()}", 173 | $argument->longPrefix() => "--{$argument->longPrefix()}", 174 | ]; 175 | 176 | foreach ($possibilities as $key => $search) { 177 | if ($key && strpos($command_argument, $search) === 0) { 178 | return true; 179 | } 180 | } 181 | 182 | return false; 183 | } 184 | 185 | /** 186 | * Retrieve all arguments as key/value pairs. 187 | * 188 | * @return array 189 | */ 190 | public function toArray() 191 | { 192 | $return = []; 193 | 194 | foreach ($this->all() as $name => $argument) { 195 | $return[$name] = $argument->value(); 196 | } 197 | 198 | return $return; 199 | } 200 | 201 | /** 202 | * Set a program's description. 203 | * 204 | * @param string $description 205 | */ 206 | public function description($description) 207 | { 208 | $this->description = trim($description); 209 | } 210 | 211 | /** 212 | * Output a script's usage statement. 213 | * 214 | * @param CLImate $climate 215 | * @param array $argv 216 | */ 217 | public function usage(CLImate $climate, array $argv = null) 218 | { 219 | $this->summary 220 | ->setClimate($climate) 221 | ->setDescription($this->description) 222 | ->setCommand($this->parser->command($argv)) 223 | ->setFilter($this->filter, $this->all()) 224 | ->output(); 225 | } 226 | 227 | /** 228 | * Parse command line arguments into CLImate arguments. 229 | * 230 | * @throws \Exception if required arguments aren't defined. 231 | * @param array $argv 232 | */ 233 | public function parse(array $argv = null) 234 | { 235 | $this->parser->setFilter($this->filter, $this->all()); 236 | 237 | $this->parser->parse($argv); 238 | } 239 | 240 | /** 241 | * Get the trailing arguments 242 | * 243 | * @return string|null 244 | */ 245 | public function trailing() 246 | { 247 | return $this->parser->trailing(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Input.php: -------------------------------------------------------------------------------- 1 | prompt = $prompt; 51 | $this->reader = $reader ?: new Stdin(); 52 | } 53 | 54 | /** 55 | * Do it! Prompt the user for information! 56 | * 57 | * @return string 58 | */ 59 | public function prompt() 60 | { 61 | $this->writePrompt(); 62 | 63 | $response = $this->valueOrDefault($this->getUserInput()); 64 | 65 | if ($this->isValidResponse($response)) { 66 | return $response; 67 | } 68 | 69 | return $this->prompt(); 70 | } 71 | 72 | /** 73 | * Define the acceptable responses and whether or not to 74 | * display them to the user 75 | * 76 | * @param array|object $acceptable 77 | * @param boolean $show 78 | * 79 | * @return \League\CLImate\TerminalObject\Dynamic\Input 80 | */ 81 | public function accept($acceptable, $show = false) 82 | { 83 | $this->acceptable = $acceptable; 84 | $this->show_acceptable = $show; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Define whether we should be strict about exact responses 91 | * 92 | * @return \League\CLImate\TerminalObject\Dynamic\Input 93 | */ 94 | public function strict() 95 | { 96 | $this->strict = true; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Set a default response 103 | * 104 | * @param string $default 105 | * 106 | * @return \League\CLImate\TerminalObject\Dynamic\Input 107 | */ 108 | public function defaultTo($default) 109 | { 110 | $this->default = $default; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Set multiline input to true 117 | * 118 | * @return \League\CLImate\TerminalObject\Dynamic\Input 119 | */ 120 | public function multiLine() 121 | { 122 | $this->multiLine = true; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | protected function getUserInput() 131 | { 132 | if ($this->multiLine) { 133 | return $this->reader->multiLine(); 134 | } 135 | 136 | return $this->reader->line(); 137 | } 138 | 139 | /** 140 | * Write out the formatted prompt 141 | */ 142 | protected function writePrompt() 143 | { 144 | $prompt = $this->parser->apply($this->promptFormatted()); 145 | 146 | $this->output->sameLine()->write($prompt); 147 | } 148 | 149 | /** 150 | * If no response was given and there is a default, return it, 151 | * otherwise return response 152 | * 153 | * @param string $response 154 | * 155 | * @return string 156 | */ 157 | protected function valueOrDefault($response) 158 | { 159 | if (strlen($response) == 0 && strlen($this->default)) { 160 | return $this->default; 161 | } 162 | 163 | return $response; 164 | } 165 | 166 | /** 167 | * Format the acceptable responses as options 168 | * 169 | * @return string 170 | */ 171 | protected function acceptableFormatted() 172 | { 173 | if (!is_array($this->acceptable)) { 174 | return ''; 175 | } 176 | 177 | $acceptable = array_map([$this, 'acceptableItemFormatted'], $this->acceptable); 178 | 179 | return '[' . implode('/', $acceptable) . ']'; 180 | } 181 | 182 | /** 183 | * Format the acceptable item depending on whether it is the default or not 184 | * 185 | * @param string $item 186 | * 187 | * @return string 188 | */ 189 | protected function acceptableItemFormatted($item) 190 | { 191 | if ($item == $this->default) { 192 | return '' . $item . ''; 193 | } 194 | 195 | return $item; 196 | } 197 | 198 | /** 199 | * Format the prompt incorporating spacing and any acceptable options 200 | * 201 | * @return string 202 | */ 203 | protected function promptFormatted() 204 | { 205 | $prompt = $this->prompt . ' '; 206 | 207 | if ($this->show_acceptable) { 208 | $prompt .= $this->acceptableFormatted() . ' '; 209 | } 210 | 211 | return $prompt; 212 | } 213 | 214 | /** 215 | * Apply some string manipulation functions for normalization 216 | * 217 | * @param string|array $var 218 | * @return array 219 | */ 220 | protected function levelPlayingField($var) 221 | { 222 | $levelers = ['trim', 'mb_strtolower']; 223 | 224 | foreach ($levelers as $leveler) { 225 | if (is_array($var)) { 226 | $var = array_map($leveler, $var); 227 | } else { 228 | $var = $leveler($var); 229 | } 230 | } 231 | 232 | return $var; 233 | } 234 | 235 | /** 236 | * Determine whether or not the acceptable property is of type closure 237 | * 238 | * @return boolean 239 | */ 240 | protected function acceptableIsClosure() 241 | { 242 | return (is_object($this->acceptable) && $this->acceptable instanceof \Closure); 243 | } 244 | 245 | /** 246 | * Determine if the user's response is in the acceptable responses array 247 | * 248 | * @param string $response 249 | * 250 | * @return boolean $response 251 | */ 252 | protected function isAcceptableResponse($response) 253 | { 254 | if ($this->strict) { 255 | return in_array($response, $this->acceptable); 256 | } 257 | 258 | $acceptable = $this->levelPlayingField($this->acceptable); 259 | $response = $this->levelPlayingField($response); 260 | 261 | return in_array($response, $acceptable); 262 | } 263 | 264 | /** 265 | * Determine if the user's response is valid based on the current settings 266 | * 267 | * @param string $response 268 | * 269 | * @return boolean $response 270 | */ 271 | protected function isValidResponse($response) 272 | { 273 | if (empty($this->acceptable)) { 274 | return true; 275 | } 276 | 277 | if ($this->acceptableIsClosure()) { 278 | return call_user_func($this->acceptable, $response); 279 | } 280 | 281 | return $this->isAcceptableResponse($response); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Decorator/Style.php: -------------------------------------------------------------------------------- 1 | 'Format', 31 | 'color' => 'Color', 32 | 'background' => 'BackgroundColor', 33 | 'command' => 'Command', 34 | ]; 35 | 36 | protected $parser; 37 | 38 | /** 39 | * An array of the current styles applied 40 | * 41 | * @var array $current 42 | */ 43 | protected $current = []; 44 | 45 | public function __construct() 46 | { 47 | foreach ($this->available as $key => $class) { 48 | $class = 'League\CLImate\Decorator\Component\\' . $class; 49 | $this->style[$key] = new $class(); 50 | } 51 | } 52 | 53 | /** 54 | * Get all of the styles available 55 | * 56 | * @return array 57 | */ 58 | public function all() 59 | { 60 | $all = []; 61 | 62 | foreach ($this->style as $style) { 63 | $all = array_merge($all, $this->convertToCodes($style->all())); 64 | } 65 | 66 | return $all; 67 | } 68 | 69 | /** 70 | * Attempt to get the corresponding code for the style 71 | * 72 | * @param mixed $key 73 | * 74 | * @return mixed 75 | */ 76 | public function get($key) 77 | { 78 | foreach ($this->style as $style) { 79 | if ($code = $style->get($key)) { 80 | return $code; 81 | } 82 | } 83 | 84 | return false; 85 | } 86 | 87 | /** 88 | * Attempt to set some aspect of the styling, 89 | * return true if attempt was successful 90 | * 91 | * @param string $key 92 | * 93 | * @return boolean 94 | */ 95 | public function set($key) 96 | { 97 | foreach ($this->style as $style) { 98 | if ($code = $style->set($key)) { 99 | return $this->validateCode($code); 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | /** 107 | * Reset the current styles applied 108 | * 109 | */ 110 | public function reset() 111 | { 112 | foreach ($this->style as $style) { 113 | $style->reset(); 114 | } 115 | } 116 | 117 | /** 118 | * Get a new instance of the Parser class based on the current settings 119 | * 120 | * @param \League\CLImate\Util\System\System $system 121 | * 122 | * @return \League\CLImate\Decorator\Parser\Parser 123 | */ 124 | public function parser(System $system) 125 | { 126 | return ParserFactory::getInstance($system, $this->current(), new Tags($this->all())); 127 | } 128 | 129 | /** 130 | * Compile an array of the current codes 131 | * 132 | * @return array 133 | */ 134 | public function current() 135 | { 136 | $full_current = []; 137 | 138 | foreach ($this->style as $style) { 139 | $full_current = array_merge($full_current, Helper::toArray($style->current())); 140 | } 141 | 142 | $full_current = array_filter($full_current); 143 | 144 | return array_values($full_current); 145 | } 146 | 147 | /** 148 | * Make sure that the code is an integer, if not let's try and get it there 149 | * 150 | * @param mixed $code 151 | * 152 | * @return boolean 153 | */ 154 | protected function validateCode($code) 155 | { 156 | if (is_integer($code)) { 157 | return true; 158 | } 159 | 160 | // Plug it back in and see what we get 161 | if (is_string($code)) { 162 | return $this->set($code); 163 | } 164 | 165 | if (is_array($code)) { 166 | return $this->validateCodeArray($code); 167 | } 168 | 169 | return false; 170 | } 171 | 172 | /** 173 | * Validate an array of codes 174 | * 175 | * @param array $codes 176 | * 177 | * @return boolean 178 | */ 179 | protected function validateCodeArray(array $codes) 180 | { 181 | // Loop through it and add each of the properties 182 | $adds = []; 183 | 184 | foreach ($codes as $code) { 185 | $adds[] = $this->set($code); 186 | } 187 | 188 | // If any of them came back true, we're good to go 189 | return in_array(true, $adds); 190 | } 191 | 192 | /** 193 | * Convert the array of codes to integers 194 | * 195 | * @param array $codes 196 | * @return array 197 | */ 198 | protected function convertToCodes(array $codes) 199 | { 200 | foreach ($codes as $key => $code) { 201 | if (is_int($code)) { 202 | continue; 203 | } 204 | 205 | $codes[$key] = $this->getCode($code); 206 | } 207 | 208 | return $codes; 209 | } 210 | 211 | /** 212 | * Retrieve the integers from the mixed code input 213 | * 214 | * @param string|array $code 215 | * 216 | * @return integer|array 217 | */ 218 | protected function getCode($code) 219 | { 220 | if (is_array($code)) { 221 | return $this->getCodeArray($code); 222 | } 223 | 224 | return $this->get($code); 225 | } 226 | 227 | /** 228 | * Retrieve an array of integers from the array of codes 229 | * 230 | * @param array $codes 231 | * 232 | * @return array 233 | */ 234 | protected function getCodeArray(array $codes) 235 | { 236 | foreach ($codes as $key => $code) { 237 | $codes[$key] = $this->get($code); 238 | } 239 | 240 | return $codes; 241 | } 242 | 243 | /** 244 | * Parse the add method for the style they are trying to add 245 | * 246 | * @param string $method 247 | * 248 | * @return string 249 | */ 250 | protected function parseAddMethod($method) 251 | { 252 | return strtolower(substr($method, 3, strlen($method))); 253 | } 254 | 255 | /** 256 | * Add a custom style 257 | * 258 | * @param string $style 259 | * @param string $key 260 | * @param string $value 261 | */ 262 | protected function add($style, $key, $value) 263 | { 264 | $this->style[$style]->add($key, $value); 265 | 266 | // If we are adding a color, make sure it gets added 267 | // as a background color too 268 | if ($style == 'color') { 269 | $this->style['background']->add($key, $value); 270 | } 271 | } 272 | 273 | /** 274 | * Magic Methods 275 | * 276 | * List of possible magic methods are at the top of this class 277 | * 278 | * @param string $requested_method 279 | * @param array $arguments 280 | */ 281 | public function __call($requested_method, $arguments) 282 | { 283 | // The only methods we are concerned about are 'add' methods 284 | if (substr($requested_method, 0, 3) != 'add') { 285 | return false; 286 | } 287 | 288 | $style = $this->parseAddMethod($requested_method); 289 | 290 | if (array_key_exists($style, $this->style)) { 291 | list($key, $value) = $arguments; 292 | $this->add($style, $key, $value); 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Animation/Keyframe.php: -------------------------------------------------------------------------------- 1 | exitTo($lines, $direction)); 24 | } 25 | 26 | /** 27 | * Get the exit keyframes for the desired direction 28 | * 29 | * @param array $lines 30 | * @param string $direction 31 | * 32 | * @return array 33 | */ 34 | public function exitTo($lines, $direction) 35 | { 36 | $lines = $this->adjustLines($lines, $direction); 37 | $line_method = $this->getLineMethod($direction); 38 | 39 | $direction_keyframes = $this->getDirectionFrames($direction, $lines, $line_method); 40 | 41 | $keyframes = array_fill(0, 4, $lines); 42 | $keyframes = array_merge($keyframes, $direction_keyframes); 43 | $keyframes[] = array_fill(0, count($lines), ''); 44 | 45 | return $keyframes; 46 | } 47 | 48 | /** 49 | * Get scroll keyframes 50 | * 51 | * @param array $lines 52 | * @param string $enter_from 53 | * @param string $exit_to 54 | * 55 | * @return array 56 | */ 57 | public function scroll($lines, $enter_from, $exit_to) 58 | { 59 | $keyframes = $this->enterFrom($lines, $enter_from); 60 | $keyframes = array_merge($keyframes, $this->exitTo($lines, $exit_to)); 61 | $keyframes = array_unique($keyframes, SORT_REGULAR); 62 | $keyframes[] = reset($keyframes); 63 | 64 | return $keyframes; 65 | } 66 | 67 | /** 68 | * Get the line parser for the direction 69 | * 70 | * @param string $direction 71 | * @return string 72 | */ 73 | protected function getLineMethod($direction) 74 | { 75 | return 'current' . ucwords(strtolower($direction)) . 'Line'; 76 | } 77 | 78 | /** 79 | * Adjust the array of lines if necessary 80 | * 81 | * @param array $lines 82 | * @param string $direction 83 | * 84 | * @return array 85 | */ 86 | protected function adjustLines(array $lines, $direction) 87 | { 88 | $adjust_method = 'adjust' . ucwords(strtolower($direction)) . 'Lines'; 89 | 90 | if (method_exists($this, $adjust_method)) { 91 | return $this->$adjust_method($lines); 92 | } 93 | 94 | return $lines; 95 | } 96 | 97 | /** 98 | * Pad the array of lines for "right" animation 99 | * 100 | * @param array $lines 101 | * @return array 102 | */ 103 | protected function adjustRightLines(array $lines) 104 | { 105 | return $this->padArray($lines, $this->util->width()); 106 | } 107 | 108 | /** 109 | * Pad the array of lines for "left" animation 110 | * 111 | * @param array $lines 112 | * @return array 113 | */ 114 | protected function adjustLeftLines(array $lines) 115 | { 116 | return $this->padArray($lines, $this->maxStrLen($lines)); 117 | } 118 | 119 | /** 120 | * Get the keyframes appropriate for the animation direction 121 | * 122 | * @param string $direction 123 | * @param array $lines 124 | * @param string $line_method 125 | * 126 | * @return array 127 | */ 128 | protected function getDirectionFrames($direction, array $lines, $line_method) 129 | { 130 | $mapping = [ 131 | 'exitHorizontalFrames' => ['left', 'right'], 132 | 'exitVerticalFrames' => ['top', 'bottom'], 133 | ]; 134 | 135 | foreach ($mapping as $method => $directions) { 136 | if (in_array($direction, $directions)) { 137 | return $this->$method($lines, $line_method); 138 | } 139 | } 140 | 141 | // Fail gracefully, simply return an array 142 | return []; 143 | } 144 | 145 | /** 146 | * Create horizontal exit animation keyframes for the art 147 | * 148 | * @param array $lines 149 | * @param string $line_method 150 | * 151 | * @return array 152 | */ 153 | protected function exitHorizontalFrames(array $lines, $line_method) 154 | { 155 | $keyframes = []; 156 | $length = mb_strlen($lines[0]); 157 | 158 | for ($i = $length; $i > 0; $i--) { 159 | $keyframes[] = $this->getHorizontalKeyframe($lines, $i, $line_method, $length); 160 | } 161 | 162 | return $keyframes; 163 | } 164 | 165 | /** 166 | * Get the keyframe for a horizontal animation 167 | * 168 | * @param array $lines 169 | * @param int $frame_number 170 | * @param string $line_method 171 | * @param int $length 172 | * 173 | * @return array 174 | */ 175 | protected function getHorizontalKeyframe(array $lines, $frame_number, $line_method, $length) 176 | { 177 | $keyframe = []; 178 | 179 | foreach ($lines as $line) { 180 | $keyframe[] = $this->$line_method($line, $frame_number, $length); 181 | } 182 | 183 | return $keyframe; 184 | } 185 | 186 | /** 187 | * Create vertical exit animation keyframes for the art 188 | * 189 | * @param array $lines 190 | * @param string $line_method 191 | * 192 | * @return array 193 | */ 194 | protected function exitVerticalFrames(array $lines, $line_method) 195 | { 196 | $keyframes = []; 197 | $line_count = count($lines); 198 | 199 | for ($i = $line_count - 1; $i >= 0; $i--) { 200 | $keyframes[] = $this->$line_method($lines, $line_count, $i); 201 | } 202 | 203 | return $keyframes; 204 | } 205 | 206 | /** 207 | * Get the current line as it is exiting left 208 | * 209 | * @param string $line 210 | * @param int $frame_number 211 | * 212 | * @return string 213 | */ 214 | protected function currentLeftLine($line, $frame_number) 215 | { 216 | return mb_substr($line, -$frame_number); 217 | } 218 | 219 | 220 | /** 221 | * Get the current line as it is exiting right 222 | * 223 | * @param string $line 224 | * @param int $frame_number 225 | * @param int $length 226 | * 227 | * @return string 228 | */ 229 | protected function currentRightLine($line, $frame_number, $length) 230 | { 231 | return str_repeat(' ', $length - $frame_number) . mb_substr($line, 0, $frame_number); 232 | } 233 | 234 | /** 235 | * Slice off X number of lines from the bottom and fill the rest with empty strings 236 | * 237 | * @param array $lines 238 | * @param integer $total_lines 239 | * @param integer $current 240 | * 241 | * @return array 242 | */ 243 | protected function currentTopLine($lines, $total_lines, $current) 244 | { 245 | $keyframe = array_slice($lines, -$current, $current); 246 | 247 | return array_merge($keyframe, array_fill(0, $total_lines - $current, '')); 248 | } 249 | 250 | /** 251 | * Slice off X number of lines from the top and fill the rest with empty strings 252 | * 253 | * @param array $lines 254 | * @param integer $total_lines 255 | * @param integer $current 256 | * 257 | * @return array 258 | */ 259 | protected function currentBottomLine($lines, $total_lines, $current) 260 | { 261 | $keyframe = array_fill(0, $total_lines - $current, ''); 262 | 263 | return array_merge($keyframe, array_slice($lines, 0, $current)); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Util/Output.php: -------------------------------------------------------------------------------- 1 | add('out', new Writer\StdOut); 49 | $this->add('error', new Writer\StdErr); 50 | $this->add('buffer', new Writer\Buffer); 51 | 52 | $this->defaultTo('out'); 53 | } 54 | 55 | /** 56 | * Dictate that a new line should not be added after the output 57 | */ 58 | public function sameLine() 59 | { 60 | $this->new_line = false; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Add a writer to the available writers 67 | * 68 | * @param string $key 69 | * @param WriterInterface|array $writer 70 | * 71 | * @return \League\CLImate\Util\Output 72 | */ 73 | public function add($key, $writer) 74 | { 75 | $this->writers[$key] = $this->resolve(Helper::toArray($writer)); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Set the default writer 82 | * 83 | * @param string|array $keys 84 | */ 85 | public function defaultTo($keys) 86 | { 87 | $this->default = $this->getWriters($keys); 88 | } 89 | 90 | /** 91 | * Add a default writer 92 | * 93 | * @param string|array $keys 94 | */ 95 | public function addDefault($keys) 96 | { 97 | $this->default = array_merge($this->default, $this->getWriters($keys)); 98 | } 99 | 100 | /** 101 | * Register a writer to be used just once 102 | * 103 | * @param string|array $keys 104 | * 105 | * @return \League\CLImate\Util\Output 106 | */ 107 | public function once($keys) 108 | { 109 | $this->once = $this->getWriters($keys); 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Persist or un-persist one time writers (for multi-line output) 116 | * 117 | * @param bool $persist 118 | * 119 | * @return \League\CLImate\Util\Output 120 | */ 121 | public function persist($persist = true) 122 | { 123 | $this->persist = (bool) $persist; 124 | 125 | if (!$this->persist) { 126 | $this->resetOneTimers(); 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Get a specific writer 134 | * 135 | * @throws \Exception if writer key doesn't exist 136 | * @param string $writer 137 | * 138 | * @return WriterInterface|array 139 | */ 140 | public function get($writer) 141 | { 142 | if (!array_key_exists($writer, $this->writers)) { 143 | throw new \Exception('Unknown writer [' . $writer . ']'); 144 | } 145 | 146 | if (count($this->writers[$writer]) == 1) { 147 | return reset($this->writers[$writer]); 148 | } 149 | 150 | return $this->writers[$writer]; 151 | } 152 | 153 | /** 154 | * Get the currently available writers 155 | * 156 | * @return array 157 | */ 158 | public function getAvailable() 159 | { 160 | $writers = []; 161 | 162 | foreach ($this->writers as $key => $writer) { 163 | $writers[$key] = $this->getReadable($writer); 164 | } 165 | 166 | return $writers; 167 | } 168 | 169 | /** 170 | * Write the content using the provided writer 171 | * 172 | * @param string $content 173 | */ 174 | public function write($content) 175 | { 176 | if ($this->new_line) { 177 | $content .= PHP_EOL; 178 | } 179 | 180 | foreach ($this->getCurrentWriters() as $writer) { 181 | $writer->write($content); 182 | } 183 | 184 | $this->resetOneTimers(); 185 | } 186 | 187 | /** 188 | * Resolve the writer(s) down to an array of WriterInterface classes 189 | * 190 | * @param WriterInterface|array|string $writer 191 | * 192 | * @return array 193 | */ 194 | protected function resolve($writer) 195 | { 196 | $resolver = 'resolve' . ucwords(gettype($writer)) . 'Writer'; 197 | 198 | if (method_exists($this, $resolver) && $resolved = $this->{$resolver}($writer)) { 199 | return $resolved; 200 | } 201 | 202 | $this->handleUnknownWriter($writer); 203 | } 204 | 205 | /** 206 | * @param array $writer 207 | * 208 | * @return array 209 | */ 210 | protected function resolveArrayWriter($writer) 211 | { 212 | return Helper::flatten(array_map([$this, 'resolve'], $writer)); 213 | } 214 | 215 | /** 216 | * @param object $writer 217 | * 218 | * @return WriterInterface|false 219 | */ 220 | protected function resolveObjectWriter($writer) 221 | { 222 | if ($writer instanceof WriterInterface) { 223 | return $writer; 224 | } 225 | 226 | return false; 227 | } 228 | 229 | /** 230 | * @param string $writer 231 | * 232 | * @return array|false 233 | */ 234 | protected function resolveStringWriter($writer) 235 | { 236 | if (is_string($writer) && array_key_exists($writer, $this->writers)) { 237 | return $this->writers[$writer]; 238 | } 239 | 240 | return false; 241 | } 242 | 243 | /** 244 | * @param mixed $writer 245 | * @throws \Exception For non-valid writer 246 | */ 247 | protected function handleUnknownWriter($writer) 248 | { 249 | // If we've gotten this far and don't know what it is, 250 | // let's at least try and give a helpful error message 251 | if (is_object($writer)) { 252 | throw new \Exception('Class [' . get_class($writer) . '] must implement ' 253 | . 'League\CLImate\Util\Writer\WriterInterface.'); 254 | } 255 | 256 | // No idea, just tell them we can't resolve it 257 | throw new \Exception('Unable to resolve writer [' . $writer . ']'); 258 | } 259 | 260 | /** 261 | * Get the readable version of the writer(s) 262 | * 263 | * @param array $writer 264 | * 265 | * @return string|array 266 | */ 267 | protected function getReadable(array $writer) 268 | { 269 | $classes = array_map('get_class', $writer); 270 | 271 | if (count($classes) == 1) { 272 | return reset($classes); 273 | } 274 | 275 | return $classes; 276 | } 277 | 278 | /** 279 | * Get the writers based on their keys 280 | * 281 | * @param string|array $keys 282 | * 283 | * @return array 284 | */ 285 | protected function getWriters($keys) 286 | { 287 | $writers = array_flip(Helper::toArray($keys)); 288 | 289 | return Helper::flatten(array_intersect_key($this->writers, $writers)); 290 | } 291 | 292 | /** 293 | * @return WriterInterface[] 294 | */ 295 | protected function getCurrentWriters() 296 | { 297 | return $this->once ?: $this->default; 298 | } 299 | 300 | /** 301 | * Reset anything only used for the current content being written 302 | */ 303 | protected function resetOneTimers() 304 | { 305 | // Reset new line flag for next time 306 | $this->new_line = true; 307 | 308 | if (!$this->persist) { 309 | // Reset once since we only want to use it... once. 310 | $this->once = null; 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/Argument/Parser.php: -------------------------------------------------------------------------------- 1 | summary = new Summary(); 26 | } 27 | 28 | /** 29 | * @param Filter $filter 30 | * @param Argument[] $arguments 31 | * 32 | * @return \League\CLImate\Argument\Parser 33 | */ 34 | public function setFilter($filter, $arguments) 35 | { 36 | $this->filter = $filter; 37 | $this->filter->setArguments($arguments); 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * Parse command line arguments into CLImate arguments. 44 | * 45 | * @throws \Exception if required arguments aren't defined. 46 | * @param array $argv 47 | */ 48 | public function parse(array $argv = null) 49 | { 50 | $cliArguments = $this->arguments($argv); 51 | 52 | if (in_array('--', $cliArguments)) { 53 | $cliArguments = $this->removeTrailingArguments($cliArguments); 54 | } 55 | 56 | $unParsedArguments = $this->prefixedArguments($cliArguments); 57 | 58 | $this->nonPrefixedArguments($unParsedArguments); 59 | 60 | // After parsing find out which arguments were required but not 61 | // defined on the command line. 62 | $missingArguments = $this->filter->missing(); 63 | 64 | if (count($missingArguments) > 0) { 65 | throw new \Exception( 66 | 'The following arguments are required: ' 67 | . $this->summary->short($missingArguments) . '.' 68 | ); 69 | } 70 | } 71 | 72 | /** 73 | * Get the command name. 74 | * 75 | * @param array $argv 76 | * 77 | * @return string 78 | */ 79 | public function command(array $argv = null) 80 | { 81 | return $this->getCommandAndArguments($argv)['command']; 82 | } 83 | 84 | /** 85 | * Get the passed arguments. 86 | * 87 | * @param array $argv 88 | * 89 | * @return array 90 | */ 91 | public function arguments(array $argv = null) 92 | { 93 | return $this->getCommandAndArguments($argv)['arguments']; 94 | } 95 | 96 | /** 97 | * Get the trailing arguments 98 | * 99 | * @return string|null 100 | */ 101 | public function trailing() 102 | { 103 | return $this->trailing; 104 | } 105 | 106 | /** 107 | * Remove the trailing arguments from the parser and set them aside 108 | * 109 | * @param array $arguments 110 | * 111 | * @return array 112 | */ 113 | protected function removeTrailingArguments(array $arguments) 114 | { 115 | $trailing = array_splice($arguments, array_search('--', $arguments)); 116 | array_shift($trailing); 117 | $this->trailing = implode(' ', $trailing); 118 | 119 | return $arguments; 120 | } 121 | 122 | /** 123 | * Parse command line options into prefixed CLImate arguments. 124 | * 125 | * Prefixed arguments are arguments with a prefix (-) or a long prefix (--) 126 | * on the command line. 127 | * 128 | * Return the arguments passed on the command line that didn't match up with 129 | * prefixed arguments so they can be assigned to non-prefixed arguments. 130 | * 131 | * @param array $argv 132 | * @return array 133 | */ 134 | protected function prefixedArguments(array $argv = []) 135 | { 136 | foreach ($argv as $key => $passed_argument) { 137 | $argv = $this->trySettingArgumentValue($argv, $key, $passed_argument); 138 | } 139 | 140 | // Send un-parsed arguments back upstream. 141 | return array_values($argv); 142 | } 143 | 144 | /** 145 | * Parse unset command line options into non-prefixed CLImate arguments. 146 | * 147 | * Non-prefixed arguments are parsed after the prefixed arguments on the 148 | * command line, in the order that they're defined in the script. 149 | * 150 | * @param array $unParsedArguments 151 | */ 152 | protected function nonPrefixedArguments(array $unParsedArguments = []) 153 | { 154 | foreach ($this->filter->withoutPrefix() as $key => $argument) { 155 | if (isset($unParsedArguments[$key])) { 156 | $argument->setValue($unParsedArguments[$key]); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Parse the name and value of the argument passed in 163 | * 164 | * @param string $cliArgument 165 | * @return string[] [$name, $value] 166 | */ 167 | protected function getNameAndValue($cliArgument) 168 | { 169 | // Look for arguments defined in the "key=value" format. 170 | if (strpos($cliArgument, '=') !== false) { 171 | return explode('=', $cliArgument, 2); 172 | } 173 | 174 | // If the argument isn't in "key=value" format then assume it's in 175 | // "key value" format and define the value after we've found the 176 | // matching CLImate argument. 177 | return [$cliArgument, null]; 178 | } 179 | 180 | /** 181 | * Attempt to set the an argument's value and remove applicable 182 | * arguments from array 183 | * 184 | * @param array $argv 185 | * @param int $key 186 | * @param string $passed_argument 187 | * 188 | * @return array The new $argv 189 | */ 190 | protected function trySettingArgumentValue($argv, $key, $passed_argument) 191 | { 192 | list($name, $value) = $this->getNameAndValue($passed_argument); 193 | 194 | // Look for the argument in our defined $arguments 195 | // and assign their value. 196 | if (!($argument = $this->findPrefixedArgument($name))) { 197 | return $argv; 198 | } 199 | 200 | // We found an argument key, so take it out of the array. 201 | unset($argv[$key]); 202 | 203 | return $this->setArgumentValue($argv, $argument, $key, $value); 204 | } 205 | 206 | /** 207 | * Set the argument's value 208 | * 209 | * @param array $argv 210 | * @param Argument $argument 211 | * @param int $key 212 | * @param string|null $value 213 | * 214 | * @return array The new $argv 215 | */ 216 | protected function setArgumentValue($argv, $argument, $key, $value) 217 | { 218 | // Arguments are given the value true if they only need to 219 | // be defined on the command line to be set. 220 | if ($argument->noValue()) { 221 | $argument->setValue(true); 222 | return $argv; 223 | } 224 | 225 | if (is_null($value)) { 226 | if (count($argv) === 0) { 227 | return $argv; 228 | } 229 | 230 | // If the value wasn't previously defined in "key=value" 231 | // format then define it from the next command argument. 232 | $argument->setValue($argv[++$key]); 233 | unset($argv[$key]); 234 | return $argv; 235 | } 236 | 237 | $argument->setValue($value); 238 | 239 | return $argv; 240 | } 241 | 242 | /** 243 | * Search for argument in defined prefix arguments 244 | * 245 | * @param string $name 246 | * 247 | * @return Argument|false 248 | */ 249 | protected function findPrefixedArgument($name) 250 | { 251 | foreach ($this->filter->withPrefix() as $argument) { 252 | if (in_array($name, ["-{$argument->prefix()}", "--{$argument->longPrefix()}"])) { 253 | return $argument; 254 | } 255 | } 256 | 257 | return false; 258 | } 259 | 260 | /** 261 | * Pull a command name and arguments from $argv. 262 | * 263 | * @param array $argv 264 | * @return array 265 | */ 266 | protected function getCommandAndArguments(array $argv = null) 267 | { 268 | // If no $argv is provided then use the global PHP defined $argv. 269 | if (is_null($argv)) { 270 | global $argv; 271 | } 272 | 273 | $arguments = $argv; 274 | $command = array_shift($arguments); 275 | 276 | return compact('arguments', 'command'); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/TerminalObject/Dynamic/Progress.php: -------------------------------------------------------------------------------- 1 | total($total); 72 | } 73 | } 74 | 75 | /** 76 | * Set the total property 77 | * 78 | * @param integer $total 79 | * 80 | * @return Progress 81 | */ 82 | public function total($total) 83 | { 84 | $this->total = $total; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Determines the current percentage we are at and re-writes the progress bar 91 | * 92 | * @param integer $current 93 | * @param mixed $label 94 | * @throws \Exception 95 | */ 96 | public function current($current, $label = null) 97 | { 98 | if ($this->total == 0) { 99 | // Avoid dividing by 0 100 | throw new \Exception('The progress total must be greater than zero.'); 101 | } 102 | 103 | if ($current > $this->total) { 104 | throw new \Exception('The current is greater than the total.'); 105 | } 106 | 107 | $this->drawProgressBar($current, $label); 108 | 109 | $this->current = $current; 110 | $this->label = $label; 111 | } 112 | 113 | /** 114 | * Increments the current position we are at and re-writes the progress bar 115 | * 116 | * @param integer $increment The number of items to increment by 117 | * @param string $label 118 | */ 119 | public function advance($increment = 1, $label = null) 120 | { 121 | $this->current($this->current + $increment, $label); 122 | } 123 | 124 | /** 125 | * Force the progress bar to redraw every time regardless of whether it has changed or not 126 | * 127 | * @param boolean $force 128 | * @return Progress 129 | */ 130 | public function forceRedraw($force = true) 131 | { 132 | $this->force_redraw = !!$force; 133 | 134 | return $this; 135 | } 136 | 137 | 138 | /** 139 | * Update a progress bar using an iterable. 140 | * 141 | * @param iterable $items Array or any other iterable object 142 | * @param callable $callback A handler to run on each item 143 | */ 144 | public function each($items, callable $callback = null) 145 | { 146 | if ($items instanceof \Traversable) { 147 | $items = iterator_to_array($items); 148 | } 149 | 150 | $total = count($items); 151 | if (!$total) { 152 | return; 153 | } 154 | $this->total($total); 155 | 156 | foreach ($items as $key => $item) { 157 | if ($callback) { 158 | $label = $callback($item, $key); 159 | } else { 160 | $label = null; 161 | } 162 | 163 | $this->advance(1, $label); 164 | } 165 | } 166 | 167 | 168 | /** 169 | * Draw the progress bar, if necessary 170 | * 171 | * @param string $current 172 | * @param string $label 173 | */ 174 | protected function drawProgressBar($current, $label) 175 | { 176 | $percentage = $this->percentageFormatted($current / $this->total); 177 | 178 | if ($this->shouldRedraw($percentage, $label)) { 179 | $progress_bar = $this->getProgressBar($current, $label); 180 | $this->output->write($this->parser->apply($progress_bar)); 181 | } 182 | 183 | $this->current_percentage = $percentage; 184 | } 185 | 186 | /** 187 | * Build the progress bar str and return it 188 | * 189 | * @param integer $current 190 | * @param string $label 191 | * 192 | * @return string 193 | */ 194 | protected function getProgressBar($current, $label) 195 | { 196 | if ($this->first_line) { 197 | // Drop down a line, we are about to 198 | // re-write this line for the progress bar 199 | $this->output->write(''); 200 | $this->first_line = false; 201 | } 202 | 203 | // Move the cursor up and clear it to the end 204 | $line_count = $this->has_label_line ? 2 : 1; 205 | 206 | $progress_bar = $this->util->cursor->up($line_count); 207 | $progress_bar .= $this->util->cursor->startOfCurrentLine(); 208 | $progress_bar .= $this->util->cursor->deleteCurrentLine(); 209 | $progress_bar .= $this->getProgressBarStr($current, $label); 210 | 211 | // If this line has a label then set that this progress bar has a label line 212 | if (strlen($label) > 0) { 213 | $this->has_label_line = true; 214 | } 215 | 216 | return $progress_bar; 217 | } 218 | 219 | /** 220 | * Get the progress bar string, basically: 221 | * =============> 50% label 222 | * 223 | * @param integer $current 224 | * @param string $label 225 | * 226 | * @return string 227 | */ 228 | protected function getProgressBarStr($current, $label) 229 | { 230 | $percentage = $current / $this->total; 231 | $bar_length = round($this->getBarStrLen() * $percentage); 232 | 233 | $bar = $this->getBar($bar_length); 234 | $number = $this->percentageFormatted($percentage); 235 | 236 | if ($label) { 237 | $label = $this->labelFormatted($label); 238 | // If this line doesn't have a label, but we've had one before, 239 | // then ensure the label line is cleared 240 | } elseif ($this->has_label_line) { 241 | $label = $this->labelFormatted(''); 242 | } 243 | 244 | return trim("{$bar} {$number}{$label}"); 245 | } 246 | 247 | /** 248 | * Get the string for the actual bar based on the current length 249 | * 250 | * @param integer $length 251 | * 252 | * @return string 253 | */ 254 | protected function getBar($length) 255 | { 256 | $bar = str_repeat('=', $length); 257 | $padding = str_repeat(' ', $this->getBarStrLen() - $length); 258 | 259 | return "{$bar}>{$padding}"; 260 | } 261 | 262 | /** 263 | * Get the length of the bar string based on the width of the terminal window 264 | * 265 | * @return integer 266 | */ 267 | protected function getBarStrLen() 268 | { 269 | if (!$this->bar_str_len) { 270 | // Subtract 10 because of the '> 100%' plus some padding, max 100 271 | $this->bar_str_len = min($this->util->width() - 10, 100); 272 | } 273 | 274 | return $this->bar_str_len; 275 | } 276 | 277 | /** 278 | * Format the percentage so it looks pretty 279 | * 280 | * @param integer $percentage 281 | * @return float 282 | */ 283 | protected function percentageFormatted($percentage) 284 | { 285 | return round($percentage * 100) . '%'; 286 | } 287 | 288 | /** 289 | * Format the label so it is positioned correctly 290 | * 291 | * @param string $label 292 | * @return string 293 | */ 294 | protected function labelFormatted($label) 295 | { 296 | return "\n" . $this->util->cursor->startOfCurrentLine() . $this->util->cursor->deleteCurrentLine() . $label; 297 | } 298 | 299 | /** 300 | * Determine whether the progress bar has changed and we need to redrew 301 | * 302 | * @param string $percentage 303 | * @param string $label 304 | * 305 | * @return boolean 306 | */ 307 | protected function shouldRedraw($percentage, $label) 308 | { 309 | return ($this->force_redraw || $percentage != $this->current_percentage || $label != $this->label); 310 | } 311 | } 312 | --------------------------------------------------------------------------------