summaryrefslogtreecommitdiffstats
path: root/vendor/clue/term-react/src/ControlCodeParser.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/clue/term-react/src/ControlCodeParser.php')
-rw-r--r--vendor/clue/term-react/src/ControlCodeParser.php223
1 files changed, 223 insertions, 0 deletions
diff --git a/vendor/clue/term-react/src/ControlCodeParser.php b/vendor/clue/term-react/src/ControlCodeParser.php
new file mode 100644
index 0000000..abbe400
--- /dev/null
+++ b/vendor/clue/term-react/src/ControlCodeParser.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Clue\React\Term;
+
+use Evenement\EventEmitter;
+use React\Stream\ReadableStreamInterface;
+use React\Stream\WritableStreamInterface;
+use React\Stream\Util;
+
+class ControlCodeParser extends EventEmitter implements ReadableStreamInterface
+{
+ private $input;
+ private $closed = false;
+ private $buffer = '';
+
+ /**
+ * we know about the following C1 types (7 bit only)
+ *
+ * followed by "[" means it's CSI (Control Sequence Introducer)
+ * followed by "]" means it's OSC (Operating System Controls)
+ * followed by "_" means it's APC (Application Program-Control)
+ * followed by "P" means it's DPS (Device-Control string)
+ * followed by "^" means it's PM (Privacy Message)
+ *
+ * Each of these will be parsed until the sequence ends and then emitted
+ * under their respective name.
+ *
+ * All other C1 types will be emitted under the "c1" name without any
+ * further processing.
+ *
+ * C1 types in 8 bit are currently not supported, as they require special
+ * care with regards to whether UTF-8 mode is enabled. So far this has
+ * turned out to be a non-issue because most terminal emulators *accept*
+ * boths formats, but usually *send* in 7 bit mode exclusively.
+ */
+ private $types = array(
+ '[' => 'csi',
+ ']' => 'osc',
+ '_' => 'apc',
+ 'P' => 'dps',
+ '^' => 'pm',
+ );
+
+ public function __construct(ReadableStreamInterface $input)
+ {
+ $this->input = $input;
+
+ if (!$this->input->isReadable()) {
+ return $this->close();
+ }
+
+ $this->input->on('data', array($this, 'handleData'));
+ $this->input->on('end', array($this, 'handleEnd'));
+ $this->input->on('error', array($this, 'handleError'));
+ $this->input->on('close', array($this, 'close'));
+ }
+
+ public function isReadable()
+ {
+ return !$this->closed && $this->input->isReadable();
+ }
+
+ public function pause()
+ {
+ $this->input->pause();
+ }
+
+ public function resume()
+ {
+ $this->input->resume();
+ }
+
+ public function pipe(WritableStreamInterface $dest, array $options = array())
+ {
+ Util::pipe($this, $dest, $options);
+
+ return $dest;
+ }
+
+ public function close()
+ {
+ if ($this->closed) {
+ return;
+ }
+
+ $this->closed = true;
+ $this->buffer = '';
+
+ $this->input->close();
+
+ $this->emit('close');
+ $this->removeAllListeners();
+ }
+
+ /** @internal */
+ public function handleData($data)
+ {
+ $this->buffer .= $data;
+
+ while ($this->buffer !== '') {
+ // search for first control character (C0 and DEL)
+ $c0 = false;
+ for ($i = 0; isset($this->buffer[$i]); ++$i) {
+ $code = ord($this->buffer[$i]);
+ if ($code < 0x20 || $code === 0x7F) {
+ $c0 = $i;
+ break;
+ }
+ }
+
+ // no C0 found, emit whole buffer as data
+ if ($c0 === false) {
+ $data = $this->buffer;
+ $this->buffer = '';
+
+ $this->emit('data', array($data));
+ return;
+ }
+
+ // C0 found somewhere inbetween, emit everything before C0 as data
+ if ($c0 !== 0) {
+ $data = substr($this->buffer, 0, $c0);
+ $this->buffer = substr($this->buffer, $c0);
+
+ $this->emit('data', array($data));
+ continue;
+ }
+
+ // C0 is now at start of buffer
+ // check if this is a normal C0 code or an ESC (\x1B = \033)
+ // normal C0 will be emitted, ESC will be parsed further
+ if ($this->buffer[0] !== "\x1B") {
+ $data = $this->buffer[0];
+ $this->buffer = (string)substr($this->buffer, 1);
+
+ $this->emit('c0', array($data));
+ continue;
+ }
+
+ // check following byte to determine type
+ if (!isset($this->buffer[1])) {
+ // type currently unknown, wait for next data chunk
+ break;
+ }
+
+ // if this is an unknown type, just emit as "c1" without further parsing
+ if (!isset($this->types[$this->buffer[1]])) {
+ $data = substr($this->buffer, 0, 2);
+ $this->buffer = (string)substr($this->buffer, 2);
+
+ $this->emit('c1', array($data));
+ continue;
+ }
+
+ // this is known type, check for the sequence end
+ $type = $this->types[$this->buffer[1]];
+ $found = false;
+
+ if ($type === 'csi') {
+ // CSI is now at the start of the buffer, search final character
+ for ($i = 2; isset($this->buffer[$i]); ++$i) {
+ $code = ord($this->buffer[$i]);
+
+ // final character between \x40-\x7E
+ if ($code >= 64 && $code <= 126) {
+ $data = substr($this->buffer, 0, $i + 1);
+ $this->buffer = (string)substr($this->buffer, $i + 1);
+
+ $this->emit($type, array($data));
+ $found = true;
+ break;
+ }
+ }
+ } else {
+ // all other types are terminated by ST
+ // only OSC can also be terminted by BEL (whichever comes first)
+ $st = strpos($this->buffer, "\x1B\\");
+ $bel = ($type === 'osc') ? strpos($this->buffer, "\x07") : false;
+
+ if ($st !== false && ($bel === false || $bel > $st)) {
+ // ST comes before BEL or no BEL found
+ $data = substr($this->buffer, 0, $st + 2);
+ $this->buffer = (string)substr($this->buffer, $st + 2);
+
+ $this->emit($type, array($data));
+ $found = true;
+ } elseif ($bel !== false) {
+ // BEL comes before ST or no ST found
+ $data = substr($this->buffer, 0, $bel + 1);
+ $this->buffer = (string)substr($this->buffer, $bel + 1);
+
+ $this->emit($type, array($data));
+ $found = true;
+ }
+ }
+
+ // no final character found => wait for next data chunk
+ if (!$found) {
+ break;
+ }
+ }
+ }
+
+ /** @internal */
+ public function handleEnd()
+ {
+ if (!$this->closed) {
+ if ($this->buffer === '') {
+ $this->emit('end');
+ } else {
+ $this->emit('error', array(new \RuntimeException('Stream ended with incomplete control code sequence in buffer')));
+ }
+ $this->close();
+ }
+ }
+
+ /** @internal */
+ public function handleError(\Exception $e)
+ {
+ $this->emit('error', array($e));
+ $this->close();
+ }
+}