Skip to content

Commit e1f4e22

Browse files
committed
Support parsing OSC sequences
1 parent 29a1382 commit e1f4e22

File tree

5 files changed

+91
-22
lines changed

5 files changed

+91
-22
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ $stream->on('csi', function ($sequence) {
5252
});
5353
```
5454

55+
Another common form of control code sequences is OSC (Operating System Command).
56+
For example, OSC is used to change the window title or window icon.
57+
Each OSC code gets emitted as an `osc` event with its raw byte sequence:
58+
59+
```php
60+
$stream->on('osc', function ($sequence) {
61+
// handle byte sequence
62+
});
63+
```
64+
5565
## Install
5666

5767
The recommended way to install this library is [through Composer](https://getcomposer.org).

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clue/term-react",
33
"description": "Streaming terminal emulator, built on top of React PHP",
4-
"keywords": ["terminal", "control codes", "xterm", "csi", "streaming", "ReactPHP"],
4+
"keywords": ["terminal", "control codes", "xterm", "csi", "osc", "streaming", "ReactPHP"],
55
"homepage": "https://github.com/clue/php-term-react",
66
"license": "MIT",
77
"authors": [

examples/stdin-codes.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
$stdin = new Stream(STDIN, $loop);
1616
$parser = new ControlCodeParser($stdin);
1717

18-
$parser->on('csi', function ($code) {
18+
$decoder = function ($code) {
1919
echo 'Code:';
2020
for ($i = 0; isset($code[$i]); ++$i) {
2121
echo sprintf(" %02X", ord($code[$i]));
2222
}
2323
echo PHP_EOL;
24-
});
24+
};
25+
26+
$parser->on('csi', $decoder);
27+
$parser->on('osc', $decoder);
2528

2629
$parser->on('data', function ($bytes) {
2730
echo 'Data: ' . $bytes . PHP_EOL;

src/ControlCodeParser.php

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,38 +89,59 @@ public function handleData($data)
8989
}
9090

9191
// ESC is now at start of buffer
92-
9392
// check following byte to determine type
9493
if (!isset($this->buffer[1])) {
9594
// type currently unknown, wait for next data chunk
9695
break;
9796
}
9897

98+
$found = false;
99+
99100
if ($this->buffer[1] === '[') {
100101
// followed by "[" means it's CSI
101-
} else {
102-
$data = substr($this->buffer, 0, 2);
103-
$this->buffer = (string)substr($this->buffer, 2);
104102

105-
$this->emit('data', array($data));
106-
continue;
107-
}
103+
// CSI is now at the start of the buffer, search final character
104+
for ($i = 2; isset($this->buffer[$i]); ++$i) {
105+
$code = ord($this->buffer[$i]);
108106

107+
// final character between \x40-\x7E
108+
if ($code >= 64 && $code <= 126) {
109+
$data = substr($this->buffer, 0, $i + 1);
110+
$this->buffer = (string)substr($this->buffer, $i + 1);
109111

110-
// CSI is now at the start of the buffer, search final character
111-
$found = false;
112-
for ($i = 2; isset($this->buffer[$i]); ++$i) {
113-
$code = ord($this->buffer[$i]);
112+
$this->emit('csi', array($data));
113+
$found = true;
114+
break;
115+
}
116+
}
117+
} elseif ($this->buffer[1] === ']') {
118+
// followed by "]" means it's OSC (Operating System Controls)
119+
120+
// terminated by ST or BEL (whichever comes first)
121+
$st = strpos($this->buffer, "\x1B\\");
122+
$bel = strpos($this->buffer, "\x07");
114123

115-
// final character between \x40-x7E
116-
if ($code >= 64 && $code <= 126) {
117-
$data = substr($this->buffer, 0, $i + 1);
118-
$this->buffer = (string)substr($this->buffer, $i + 1);
124+
if ($st !== false && ($bel === false || $bel > $st)) {
125+
// ST comes before BEL or no BEL found
126+
$data = substr($this->buffer, 0, $st + 2);
127+
$this->buffer = (string)substr($this->buffer, $st + 2);
119128

120-
$this->emit('csi', array($data));
129+
$this->emit('osc', array($data));
130+
$found = true;
131+
} elseif ($bel !== false) {
132+
// BEL comes before ST or no ST found
133+
$data = substr($this->buffer, 0, $bel + 1);
134+
$this->buffer = (string)substr($this->buffer, $bel + 1);
135+
136+
$this->emit('osc', array($data));
121137
$found = true;
122-
break;
123138
}
139+
} else {
140+
$data = substr($this->buffer, 0, 2);
141+
$this->buffer = (string)substr($this->buffer, 2);
142+
143+
$this->emit('data', array($data));
144+
continue;
124145
}
125146

126147
// no final character found => wait for next data chunk

tests/ControlCodeParserTest.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,44 @@ public function testEmitsNonCsiAsData()
108108
});
109109
$this->parser->on('csi', $this->expectCallableNever());
110110

111-
$this->input->emit('data', array("hello\x1B]world"));
111+
$this->input->emit('data', array("hello\x1B?world"));
112112

113-
$this->assertEquals("hello\x1B]world", $buffer);
113+
$this->assertEquals("hello\x1B?world", $buffer);
114+
}
115+
116+
117+
public function testEmitsOscBelAsOneChunk()
118+
{
119+
$this->parser->on('data', $this->expectCallableNever());
120+
$this->parser->on('osc', $this->expectCallableOnceWith("\x1B]asd\x07"));
121+
122+
$this->input->emit('data', array("\x1B]asd\x07"));
123+
}
124+
125+
public function testEmitsOscStAsOneChunk()
126+
{
127+
$this->parser->on('data', $this->expectCallableNever());
128+
$this->parser->on('osc', $this->expectCallableOnceWith("\x1B]asd\x1B\\"));
129+
130+
$this->input->emit('data', array("\x1B]asd\x1B\\"));
131+
}
132+
133+
public function testEmitsChunkedStartOscAsOneChunk()
134+
{
135+
$this->parser->on('data', $this->expectCallableNever());
136+
$this->parser->on('osc', $this->expectCallableOnceWith("\x1B]asd\x07"));
137+
138+
$this->input->emit('data', array("\x1B"));
139+
$this->input->emit('data', array("]asd\x07"));
140+
}
141+
142+
public function testEmitsChunkedMiddleOscAsOneChunk()
143+
{
144+
$this->parser->on('data', $this->expectCallableNever());
145+
$this->parser->on('osc', $this->expectCallableOnceWith("\x1B]asd\x07"));
146+
147+
$this->input->emit('data', array("\x1B]as"));
148+
$this->input->emit('data', array("d\x07"));
114149
}
115150

116151
public function testClosingInputWillCloseParser()

0 commit comments

Comments
 (0)