├── 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["{$tag}>"] = $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 |

2 |
3 | [](https://github.com/thephpleague/climate/tags)
4 | [](LICENSE.md)
5 | [](https://travis-ci.org/thephpleague/climate)
6 | [](https://scrutinizer-ci.com/g/thephpleague/climate/code-structure)
7 | [](https://scrutinizer-ci.com/g/thephpleague/climate)
8 | [](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 |
--------------------------------------------------------------------------------