Passed
Pull Request — master (#12)
by Aydin
01:54
created

UnixTerminal::onSignal()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 4
c 2
b 1
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 2
1
<?php declare(ticks=1);
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 int;
40
     */
41
    private $colourSupport;
42
43
    /**
44
     * @var string
45
     */
46
    private $originalConfiguration;
47
48
    /**
49
     * @var array
50
     */
51
    private $signalHandlers = [];
52
53
    /**
54
     * @var InputStream
55
     */
56
    private $input;
57
58
    /**
59
     * @var OutputStream
60
     */
61
    private $output;
62
63
    public function __construct(InputStream $input, OutputStream $output)
64
    {
65
        $this->initTerminal();
66
67
        $this->input = $input;
68
        $this->output = $output;
69
    }
70
71
    private function initTerminal() : void
72
    {
73
        $this->getOriginalConfiguration();
74
        $this->getOriginalCanonicalMode();
75
76
        pcntl_async_signals(true);
77
78
        $this->onSignal(SIGWINCH, [$this, 'refreshDimensions']);
79
    }
80
81
    private function getOriginalCanonicalMode() : void
82
    {
83
        exec('stty -a', $output);
84
        $this->isCanonical = (strpos(implode("\n", $output), ' icanon') !== false);
85
    }
86
87
    private function getOriginalConfiguration() : string
88
    {
89
        return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g');
90
    }
91
92
    public function getWidth() : int
93
    {
94
        return $this->width ?: $this->width = (int) exec('tput cols');
95
    }
96
97
    public function getHeight() : int
98
    {
99
        return $this->height ?: $this->height = (int) exec('tput lines');
100
    }
101
102
    public function getColourSupport() : int
103
    {
104
        return $this->colourSupport ?: $this->colourSupport = (int) exec('tput colors');
105
    }
106
107
    private function refreshDimensions() : void
108
    {
109
        $this->width  = (int) exec('tput cols');
110
        $this->height = (int) exec('tput lines');
111
    }
112
113
    public function onSignal(int $signo, callable $handler) : void
114
    {
115
        if (!isset($this->signalHandlers[$signo])) {
116
            $this->signalHandlers[$signo] = [];
117
            pcntl_signal($signo, [$this, 'handleSignal']);
118
        }
119
        $this->signalHandlers[$signo][] = $handler;
120
    }
121
122
    public function handleSignal(int $signo) : void
123
    {
124
        foreach ($this->signalHandlers[$signo] as $signalHandler) {
125
            $signalHandler();
126
        }
127
    }
128
129
    /**
130
     * Disables echoing every character back to the terminal. This means
131
     * we do not have to clear the line when reading.
132
     */
133
    public function disableEchoBack() : void
134
    {
135
        exec('stty -echo');
136
        $this->echoBack = false;
137
    }
138
139
    /**
140
     * Enable echoing back every character input to the terminal.
141
     */
142
    public function enableEchoBack() : void
143
    {
144
        exec('stty echo');
145
        $this->echoBack = true;
146
    }
147
148
    /**
149
     * Is echo back mode enabled
150
     */
151
    public function isEchoBack() : bool
152
    {
153
        return $this->echoBack;
154
    }
155
156
    /**
157
     * Disable canonical input (allow each key press for reading, rather than the whole line)
158
     *
159
     * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html
160
     */
161
    public function disableCanonicalMode() : void
162
    {
163
        if ($this->isCanonical) {
164
            exec('stty -icanon');
165
            $this->isCanonical = false;
166
        }
167
    }
168
169
    /**
170
     * Enable canonical input - read input by line
171
     *
172
     * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html
173
     */
174
    public function enableCanonicalMode() : void
175
    {
176
        if (!$this->isCanonical) {
177
            exec('stty icanon');
178
            $this->isCanonical = true;
179
        }
180
    }
181
182
    /**
183
     * Is canonical mode enabled or not
184
     */
185
    public function isCanonicalMode() : bool
186
    {
187
        return $this->isCanonical;
188
    }
189
190
    /**
191
     * Restore the original terminal configuration
192
     */
193
    public function restoreOriginalConfiguration() : void
194
    {
195
        exec('stty ' . $this->getOriginalConfiguration());
196
    }
197
198
    /**
199
     * Check if the Input & Output streams are interactive. Eg - they are
200
     * connected to a terminal.
201
     *
202
     * @return bool
203
     */
204
    public function isInteractive() : bool
205
    {
206
        return $this->input->isInteractive() && $this->output->isInteractive();
207
    }
208
209
    /**
210
     * Assert that both the Input & Output streams are interactive. Throw
211
     * `NotInteractiveTerminal` if not.
212
     */
213
    public function mustBeInteractive() : void
214
    {
215
        if (!$this->input->isInteractive()) {
216
            throw NotInteractiveTerminal::inputNotInteractive();
217
        }
218
219
        if (!$this->output->isInteractive()) {
220
            throw NotInteractiveTerminal::outputNotInteractive();
221
        }
222
    }
223
224
    /**
225
     * @see https://github.com/symfony/Console/blob/master/Output/StreamOutput.php#L95-L102
226
     */
227
    public function supportsColour() : bool
228
    {
229
        if (DIRECTORY_SEPARATOR === '\\') {
230
            return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM');
231
        }
232
233
        return $this->isInteractive();
234
    }
235
236
    public function clear() : void
237
    {
238
        $this->output->write("\033[2J");
239
    }
240
241
    public function clearLine() : void
242
    {
243
        $this->output->write("\033[2K");
244
    }
245
246
    /**
247
     * Erase screen from the current line down to the bottom of the screen
248
     */
249
    public function clearDown() : void
250
    {
251
        $this->output->write("\033[J");
252
    }
253
254
    public function clean() : void
255
    {
256
        foreach (range(0, $this->getHeight()) as $rowNum) {
257
            $this->moveCursorToRow($rowNum);
258
            $this->clearLine();
259
        }
260
    }
261
262
    public function enableCursor() : void
263
    {
264
        $this->output->write("\033[?25h");
265
    }
266
267
    public function disableCursor() : void
268
    {
269
        $this->output->write("\033[?25l");
270
    }
271
272
    public function moveCursorToTop() : void
273
    {
274
        $this->output->write("\033[H");
275
    }
276
277
    public function moveCursorToRow(int $rowNumber) : void
278
    {
279
        $this->output->write(sprintf("\033[%d;0H", $rowNumber));
280
    }
281
282
    public function moveCursorToColumn(int $column) : void
283
    {
284
        $this->output->write(sprintf("\033[%dC", $column));
285
    }
286
287
    public function showSecondaryScreen() : void
288
    {
289
        $this->output->write("\033[?47h");
290
    }
291
292
    public function showPrimaryScreen() : void
293
    {
294
        $this->output->write("\033[?47l");
295
    }
296
297
    /**
298
     * Read bytes from the input stream
299
     */
300
    public function read(int $bytes): string
301
    {
302
        $buffer = '';
303
        $this->input->read($bytes, function ($data) use (&$buffer) {
304
            $buffer .= $data;
305
        });
306
        return $buffer;
307
    }
308
309
    /**
310
     * Write to the output stream
311
     */
312
    public function write(string $buffer): void
313
    {
314
        $this->output->write($buffer);
315
    }
316
317
    /**
318
     * Restore the original terminal configuration on shutdown.
319
     */
320
    public function __destruct()
321
    {
322
        $this->restoreOriginalConfiguration();
323
    }
324
}
325