Completed
Pull Request — master (#1)
by Aydin
03:36
created

UnixTerminal   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 38
dl 0
loc 250
rs 8.3999
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A moveCursorToRow() 0 3 1
A enableCanonicalMode() 0 5 2
A moveCursorToColumn() 0 3 1
A isCanonicalMode() 0 3 1
A moveCursorToTop() 0 3 1
A getWidth() 0 3 2
A isEchoBack() 0 3 1
A getHeight() 0 3 2
A supportsColour() 0 7 4
A __destruct() 0 3 1
A disableCanonicalMode() 0 5 2
A clean() 0 5 2
A mustBeInteractive() 0 8 3
A enableCursor() 0 3 1
A __construct() 0 6 1
A getOriginalConfiguration() 0 3 2
A enableEchoBack() 0 4 1
A write() 0 3 1
A isInteractive() 0 3 2
A clear() 0 3 1
A restoreOriginalConfiguration() 0 3 1
A getOriginalCanonicalMode() 0 4 1
A clearLine() 0 3 1
A read() 0 7 1
A disableCursor() 0 3 1
A disableEchoBack() 0 4 1
1
<?php
2
3
namespace PhpSchool\Terminal;
4
5
use PhpSchool\Terminal\Exception\NotInteractiveTerminal;
6
use PhpSchool\Terminal\IO\InputStream;
7
use PhpSchool\Terminal\IO\OutputStream;
8
9
/**
10
 * @author Michael Woodward <[email protected]>
11
 * @author Aydin Hassan <[email protected]>
12
 */
13
class UnixTerminal implements Terminal
14
{
15
    /**
16
     * @var bool
17
     */
18
    private $isCanonical;
19
20
    /**
21
     * Whether terminal echo back is enabled or not.
22
     * Eg. user key presses and the terminal immediately shows it.
23
     *
24
     * @var bool
25
     */
26
    private $echoBack = true;
27
28
    /**
29
     * @var int
30
     */
31
    private $width;
32
33
    /**
34
     * @var int
35
     */
36
    private $height;
37
38
    /**
39
     * @var string
40
     */
41
    private $details;
0 ignored issues
show
introduced by
The private property $details is not used, and could be removed.
Loading history...
42
43
    /**
44
     * @var string
45
     */
46
    private $originalConfiguration;
47
48
    /**
49
     * @var InputStream
50
     */
51
    private $input;
52
53
    /**
54
     * @var OutputStream
55
     */
56
    private $output;
57
58
    public function __construct(InputStream $input, OutputStream $output)
59
    {
60
        $this->getOriginalConfiguration();
61
        $this->getOriginalCanonicalMode();
62
        $this->input = $input;
63
        $this->output = $output;
64
    }
65
66
    private function getOriginalCanonicalMode() : void
67
    {
68
        exec('stty -a', $output);
69
        $this->isCanonical = (strpos(implode("\n", $output), ' icanon') !== false);
70
    }
71
72
    public function getWidth() : int
73
    {
74
        return $this->width ?: $this->width = (int) exec('tput cols');
75
    }
76
77
    public function getHeight() : int
78
    {
79
        return $this->height ?: $this->height = (int) exec('tput lines');
80
    }
81
82
    private function getOriginalConfiguration() : string
83
    {
84
        return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g');
85
    }
86
87
    /**
88
     * Disables echoing every character back to the terminal. This means
89
     * we do not have to clear the line when reading.
90
     */
91
    public function disableEchoBack() : void
92
    {
93
        exec('stty -echo');
94
        $this->echoBack = false;
95
    }
96
97
    /**
98
     * Enable echoing back every character input to the terminal.
99
     */
100
    public function enableEchoBack() : void
101
    {
102
        exec('stty echo');
103
        $this->echoBack = true;
104
    }
105
106
    /**
107
     * Is echo back mode enabled
108
     */
109
    public function isEchoBack() : bool
110
    {
111
        return $this->echoBack;
112
    }
113
114
    /**
115
     * Disable canonical input (allow each key press for reading, rather than the whole line)
116
     *
117
     * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html
118
     */
119
    public function disableCanonicalMode() : void
120
    {
121
        if ($this->isCanonical) {
122
            exec('stty -icanon');
123
            $this->isCanonical = false;
124
        }
125
    }
126
127
    /**
128
     * Enable canonical input - read input by line
129
     *
130
     * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html
131
     */
132
    public function enableCanonicalMode() : void
133
    {
134
        if (!$this->isCanonical) {
135
            exec('stty canon');
136
            $this->isCanonical = true;
137
        }
138
    }
139
140
    /**
141
     * Is canonical mode enabled or not
142
     */
143
    public function isCanonicalMode() : bool
144
    {
145
        return $this->isCanonical;
146
    }
147
148
    /**
149
     * Restore the original terminal configuration
150
     */
151
    public function restoreOriginalConfiguration() : void
152
    {
153
        exec('stty ' . $this->getOriginalConfiguration());
154
    }
155
156
    /**
157
     * Check if the Input & Output streams are interactive. Eg - they are
158
     * connected to a terminal.
159
     *
160
     * @return bool
161
     */
162
    public function isInteractive() : bool
163
    {
164
        return $this->input->isInteractive() && $this->output->isInteractive();
165
    }
166
167
    /**
168
     * Assert that both the Input & Output streams are interactive. Throw
169
     * `NotInteractiveTerminal` if not.
170
     */
171
    public function mustBeInteractive() : void
172
    {
173
        if (!$this->input->isInteractive()) {
174
            throw NotInteractiveTerminal::inputNotInteractive();
175
        }
176
177
        if (!$this->output->isInteractive()) {
178
            throw NotInteractiveTerminal::outputNotInteractive();
179
        }
180
    }
181
182
    /**
183
     * @see https://github.com/symfony/Console/blob/master/Output/StreamOutput.php#L95-L102
184
     */
185
    public function supportsColour() : bool
186
    {
187
        if (DIRECTORY_SEPARATOR === '\\') {
188
            return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM');
189
        }
190
191
        return $this->isInteractive();
192
    }
193
194
    public function clear() : void
195
    {
196
        $this->output->write("\033[2J");
197
    }
198
199
    public function clearLine() : void
200
    {
201
        $this->output->write(sprintf("\033[%dD\033[K", $this->getWidth()));
202
    }
203
204
    public function clean() : void
205
    {
206
        foreach (range(0, $this->getHeight()) as $rowNum) {
207
            $this->moveCursorToRow($rowNum);
208
            $this->clearLine();
209
        }
210
    }
211
212
    public function enableCursor() : void
213
    {
214
        $this->output->write("\033[?25h");
215
    }
216
217
    public function disableCursor() : void
218
    {
219
        $this->output->write("\033[?25l");
220
    }
221
222
    public function moveCursorToTop() : void
223
    {
224
        $this->output->write("\033[H");
225
    }
226
227
    public function moveCursorToRow(int $rowNumber) : void
228
    {
229
        $this->output->write(sprintf("\033[%d;0H", $rowNumber));
230
    }
231
232
    public function moveCursorToColumn(int $column) : void
233
    {
234
        $this->output->write(sprintf("\033[%dC", $column));
235
    }
236
237
    /**
238
     * Read bytes from the input stream
239
     */
240
    public function read(int $bytes): string
241
    {
242
        $buffer = '';
243
        $this->input->read($bytes, function ($data) use (&$buffer) {
244
            $buffer .= $data;
245
        });
246
        return $buffer;
247
    }
248
249
    /**
250
     * Write to the output stream
251
     */
252
    public function write(string $buffer): void
253
    {
254
        $this->output->write($buffer);
255
    }
256
257
    /**
258
     * Restore the original terminal configuration on shutdown.
259
     */
260
    public function __destruct()
261
    {
262
        $this->restoreOriginalConfiguration();
263
    }
264
}
265