Skip to content

Commit 697679a

Browse files
Fix screen rendering issues (#70)
A massive amount of work to make rendering more consistent. --------- Co-authored-by: aarondfrancis <[email protected]> Co-authored-by: Hambrook <[email protected]>
1 parent 7650203 commit 697679a

File tree

269 files changed

+1715
-393
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

269 files changed

+1715
-393
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,11 @@ I wouldn't! Use supervisor or similar tools for production environments.
214214

215215
This is free! If you want to support me:
216216

217+
- Sponsor my open source work: [aaronfrancis.com/backstage](https://aaronfrancis.com/backstage)
217218
- Check out my courses:
218219
- [Mastering Postgres](https://masteringpostgres.com)
219220
- [High Performance SQLite](https://highperformancesqlite.com)
220221
- [Screencasting](https://screencasting.com)
221-
- Share them with friends
222222
- Help spread the word about things I make
223223

224224
## Credits

composer.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"ext-pcntl": "*",
1919
"ext-posix": "*",
2020
"laravel/prompts": "^0.1.21|^0.2|^0.3",
21-
"joetannenbaum/chewie": "^0.1"
21+
"joetannenbaum/chewie": "^0.1",
22+
"soloterm/grapheme": "^1"
2223
},
2324
"require-dev": {
2425
"illuminate/database": "^10|^11|^12",
@@ -32,8 +33,6 @@
3233
}
3334
},
3435
"autoload-dev": {
35-
"files": [
36-
],
3736
"psr-4": {
3837
"SoloTerm\\Solo\\Tests\\": "tests/",
3938
"App\\": "workbench/app/"

home-end.log

Whitespace-only changes.

src/Support/AnsiTracker.php renamed to src/Buffers/AnsiBuffer.php

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@
77
* @link https://x.com/aarondfrancis
88
*/
99

10-
namespace SoloTerm\Solo\Support;
10+
namespace SoloTerm\Solo\Buffers;
1111

1212
use InvalidArgumentException;
1313
use RuntimeException;
1414

15-
class AnsiTracker
15+
class AnsiBuffer extends Buffer
1616
{
17-
/**
18-
* The buffer that stores ANSI codes for each cell as either a pure integer
19-
* or as an array, if there are extended foreground and backgrounds.
20-
*/
21-
public Buffer $buffer;
17+
protected mixed $valueForClearing = 0;
2218

2319
/**
2420
* The active integer bitmask representing current standard ANSI
@@ -92,7 +88,7 @@ class AnsiTracker
9288
9 => 29,
9389
];
9490

95-
public function __construct()
91+
public function initialize()
9692
{
9793
if (PHP_INT_SIZE < 8) {
9894
throw new RuntimeException(static::class . ' requires a 64-bit PHP environment.');
@@ -119,9 +115,6 @@ public function __construct()
119115

120116
return $carry;
121117
}, []);
122-
123-
// This buffer uses integers and arrays to keep track of active ANSI codes.
124-
$this->buffer = new Buffer(usesStrings: false);
125118
}
126119

127120
/**
@@ -144,7 +137,7 @@ protected function cellValue(): int|array
144137

145138
public function fillBufferWithActiveFlags(int $row, int $startCol, int $endCol): void
146139
{
147-
$this->buffer->fill($this->cellValue(), $row, $startCol, $endCol);
140+
$this->fill($this->cellValue(), $row, $startCol, $endCol);
148141
}
149142

150143
public function getActive(): int
@@ -157,9 +150,27 @@ public function getActiveAsAnsi(): string
157150
return $this->ansiStringFromBits($this->active);
158151
}
159152

153+
public function getActiveBackground(): array|int
154+
{
155+
// If we have an extended background (256-color or RGB), that counts as "active"
156+
if ($this->extendedBackground !== null) {
157+
return $this->extendedBackground;
158+
}
159+
160+
// Build a bitmask that represents all possible background codes.
161+
// Then see if any of those bits are set in $this->active.
162+
$backgroundBitmask = 0;
163+
foreach ($this->background as $code) {
164+
$backgroundBitmask |= $this->codes[$code];
165+
}
166+
167+
// If any background bits are set, this expression will be non-zero.
168+
return $this->active & $backgroundBitmask;
169+
}
170+
160171
public function compressedAnsiBuffer(): array
161172
{
162-
$lines = $this->buffer->getBuffer();
173+
$lines = $this->buffer;
163174

164175
return array_map(function ($line) {
165176
// We reset the previous cell on every single line, because we're not guaranteed that the

src/Support/Buffer.php renamed to src/Buffers/Buffer.php

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @link https://x.com/aarondfrancis
88
*/
99

10-
namespace SoloTerm\Solo\Support;
10+
namespace SoloTerm\Solo\Buffers;
1111

1212
use ArrayAccess;
1313
use ReturnTypeWillChange;
@@ -16,9 +16,13 @@ class Buffer implements ArrayAccess
1616
{
1717
public array $buffer = [];
1818

19-
public function __construct(public bool $usesStrings = false, public int $max = 5000)
19+
protected mixed $valueForClearing = 0;
20+
21+
public function __construct(public int $max = 5000)
2022
{
21-
//
23+
if (method_exists($this, 'initialize')) {
24+
$this->initialize();
25+
}
2226
}
2327

2428
public function getBuffer()
@@ -53,27 +57,22 @@ public function clear(
5357
if ($cols[0] === 0 && $cols[1] === $length) {
5458
// Clearing an entire line. Benchmarked slightly
5559
// faster to just replace the entire row.
56-
$this->buffer[$row] = $this->usesStrings ? '' : [];
60+
$this->buffer[$row] = [];
5761
} elseif ($cols[0] > 0 && $cols[1] === $length) {
5862
// Clearing from cols[0] to the end of the line.
59-
$this->buffer[$row] = $this->usesStrings
60-
// Chop off the end of the string.
61-
? mb_substr($line, 0, $cols[0], 'UTF-8')
62-
// Chop off the end of the array.
63-
: array_slice($line, 0, $cols[0]);
63+
// Chop off the end of the array.
64+
$this->buffer[$row] = array_slice($line, 0, $cols[0]);
6465
} else {
65-
// Clearing the middle of a row. Fill with either 0s or spaces.
66-
$this->fill(
67-
($this->usesStrings ? ' ' : 0), $row, $cols[0], $cols[1]
68-
);
66+
// Clearing the middle of a row. Fill with the replacement value.
67+
$this->fill($this->valueForClearing, $row, $cols[0], $cols[1]);
6968
}
7069
}
7170
}
7271

7372
public function expand($rows)
7473
{
7574
while (count($this->buffer) <= $rows) {
76-
$this->buffer[] = $this->usesStrings ? '' : [];
75+
$this->buffer[] = [];
7776
}
7877
}
7978

@@ -83,26 +82,16 @@ public function fill(mixed $value, int $row, int $startCol, int $endCol)
8382

8483
$line = $this->buffer[$row];
8584

86-
if ($this->usesStrings) {
87-
$replacement = str_repeat($value, $endCol - $startCol + 1);
88-
$beforeStart = mb_substr($line, 0, $startCol, 'UTF-8');
89-
$afterEnd = mb_substr($line, $endCol + 1, null, 'UTF-8');
90-
91-
$this->buffer[$row] = $beforeStart . $replacement . $afterEnd;
92-
} else {
93-
$this->buffer[$row] = array_replace(
94-
$line, array_fill_keys(range($startCol, $endCol), $value)
95-
);
96-
}
85+
$this->buffer[$row] = array_replace(
86+
$line, array_fill_keys(range($startCol, $endCol), $value)
87+
);
9788

9889
$this->trim();
9990
}
10091

10192
public function rowMax($row)
10293
{
103-
$line = $this->buffer[$row];
104-
105-
return $this->usesStrings ? mb_strlen($line, 'UTF-8') : count($line) - 1;
94+
return count($this->buffer[$row]) - 1;
10695
}
10796

10897
public function trim()
@@ -119,7 +108,7 @@ public function trim()
119108
if ($excess > 0) {
120109
$keys = array_keys($this->buffer);
121110
$remove = array_slice($keys, 0, $excess);
122-
$nulls = array_fill_keys($remove, $this->usesStrings ? '' : []);
111+
$nulls = array_fill_keys($remove, []);
123112

124113
$this->buffer = array_replace($this->buffer, $nulls);
125114
}

src/Buffers/PrintableBuffer.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace SoloTerm\Solo\Buffers;
4+
5+
use Exception;
6+
use SoloTerm\Grapheme\Grapheme;
7+
8+
class PrintableBuffer extends Buffer
9+
{
10+
public int $width;
11+
12+
protected mixed $valueForClearing = ' ';
13+
14+
public function setWidth(int $width): static
15+
{
16+
$this->width = $width;
17+
18+
return $this;
19+
}
20+
21+
/**
22+
* Writes a string into the buffer at the specified row and starting column.
23+
* The string is split into "units" (either single characters or grapheme clusters),
24+
* and each unit is inserted into one or more cells based on its display width.
25+
* If a unit has width > 1, its first cell gets the unit, and the remaining cells are set to PHP null.
26+
*
27+
* If the text overflows the available width on that row, the function stops writing and returns
28+
* an array containing the number of columns advanced and a string of the remaining characters.
29+
*
30+
* @param int $row Row index (0-based).
31+
* @param int $col Starting column index (0-based).
32+
* @param string $text The text to write.
33+
* @return array [$advanceCursor, $remainder]
34+
*
35+
* @throws Exception if splitting into graphemes fails.
36+
*/
37+
public function writeString(int $row, int $col, string $text): array
38+
{
39+
// Determine the units to iterate over: if the text is ASCII-only, we can split by character,
40+
// otherwise we split into grapheme clusters.
41+
if (strlen($text) === mb_strlen($text)) {
42+
$units = str_split($text);
43+
} else {
44+
if (preg_match_all('/\X/u', $text, $matches) === false) {
45+
throw new Exception('Error splitting text into grapheme clusters.');
46+
}
47+
48+
$units = $matches[0];
49+
}
50+
51+
$currentCol = $col;
52+
$advanceCursor = 0;
53+
$totalUnits = count($units);
54+
55+
// Ensure that the row is not sparse.
56+
// If the row already exists, fill any missing indices before the starting column with a space.
57+
// Otherwise, initialize the row and fill indices 0 through $col-1 with spaces.
58+
if (!isset($this->buffer[$row])) {
59+
$this->buffer[$row] = [];
60+
}
61+
62+
for ($i = 0; $i < $col; $i++) {
63+
if (!array_key_exists($i, $this->buffer[$row])) {
64+
$this->buffer[$row][$i] = ' ';
65+
}
66+
}
67+
68+
// Make sure we don't splice a wide character.
69+
if (array_key_exists($col, $this->buffer[$row]) && $this->buffer[$row][$col] === null) {
70+
for ($i = $col; $i >= 0; $i--) {
71+
// Replace null values with a space.
72+
if (!isset($this->buffer[$row][$i]) || $this->buffer[$row][$i] === null) {
73+
$this->buffer[$row][$i] = ' ';
74+
} else {
75+
// Also replace the first non-null value with a space, then exit.
76+
$this->buffer[$row][$i] = ' ';
77+
break;
78+
}
79+
}
80+
}
81+
82+
for ($i = 0; $i < $totalUnits; $i++) {
83+
$unit = $units[$i];
84+
85+
// Check if the unit is a tab character.
86+
if ($unit === "\t") {
87+
// Calculate tab width as the number of spaces needed to reach the next tab stop.
88+
$unitWidth = 8 - ($currentCol % 8);
89+
} else {
90+
$unitWidth = Grapheme::wcwidth($unit);
91+
}
92+
93+
// If adding this unit would overflow the available width, break out.
94+
if ($currentCol + $unitWidth > $this->width) {
95+
break;
96+
}
97+
98+
// Write the unit into the first cell.
99+
$this->buffer[$row][$currentCol] = $unit;
100+
101+
// Fill any additional columns that the unit occupies with PHP null.
102+
for ($j = 1; $j < $unitWidth; $j++) {
103+
if (($currentCol + $j) < $this->width) {
104+
$this->buffer[$row][$currentCol + $j] = null;
105+
}
106+
}
107+
108+
$currentCol += $unitWidth;
109+
110+
// Clear out any leftover continuation nulls
111+
if (array_key_exists($currentCol, $this->buffer[$row]) && $this->buffer[$row][$currentCol] === null) {
112+
$k = $currentCol;
113+
114+
while (array_key_exists($k, $this->buffer[$row]) && $this->buffer[$row][$k] === null) {
115+
$this->buffer[$row][$k] = ' ';
116+
$k++;
117+
}
118+
}
119+
120+
$advanceCursor += $unitWidth;
121+
}
122+
123+
// The remainder is the unprocessed units joined back into a string.
124+
$remainder = implode('', array_slice($units, $i));
125+
126+
return [$advanceCursor, $remainder];
127+
}
128+
129+
public function lines(): array
130+
{
131+
return array_map(fn($line) => implode('', $line), $this->buffer);
132+
}
133+
}

src/Commands/Command.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Chewie\Concerns\Ticks;
1313
use Chewie\Contracts\Loopable;
1414
use Illuminate\Support\Collection;
15+
use Illuminate\Support\Str;
1516
use SoloTerm\Solo\Commands\Concerns\ManagesProcess;
1617
use SoloTerm\Solo\Hotkeys\Hotkey;
1718
use SoloTerm\Solo\Hotkeys\KeyHandler;
@@ -168,13 +169,13 @@ public function isInteractive(): bool
168169
*/
169170
public function dd()
170171
{
171-
$this->wrappedLines()->dd();
172-
exit();
172+
dd($this->screen->printable->buffer);
173173
}
174174

175175
public function addOutput($text)
176176
{
177-
$text = str_replace('[screen is terminating]', '', $text);
177+
$text = Str::before($text, $this->outputEndMarker);
178+
$text = Str::after($text, $this->outputStartMarker);
178179

179180
$this->screen->write($text);
180181
}

0 commit comments

Comments
 (0)