1- import { retainedTerminalHistory } from "@t3tools/contracts" ;
1+ import {
2+ retainedTerminalHistory ,
3+ TERMINAL_SEQUENCE_GROUND ,
4+ terminalSequenceEnd ,
5+ terminalSequenceStateAfter ,
6+ } from "@t3tools/contracts" ;
27
38export interface TerminalOutputChunk {
49 readonly startOffset : number ;
@@ -12,18 +17,24 @@ export interface TerminalOutputState {
1217 readonly retainedBytes : number ;
1318 readonly resetVersion : number ;
1419 readonly nextOffset : number ;
20+ // The terminal parser's state at the first retained code unit. Trimming can cut a control
21+ // sequence, and a replay has to skip the rest of it instead of printing it.
22+ readonly sequenceState : number ;
1523}
1624
1725export interface TerminalOutputCursor {
1826 readonly generation : number ;
1927 readonly resetVersion : number ;
2028 readonly offset : number ;
29+ // The cut control sequence still open at `offset`, which the reader's terminal never entered.
30+ readonly sequenceState : number ;
2131}
2232
2333export const INITIAL_TERMINAL_OUTPUT_CURSOR = Object . freeze < TerminalOutputCursor > ( {
2434 generation : - 1 ,
2535 resetVersion : - 1 ,
2636 offset : 0 ,
37+ sequenceState : TERMINAL_SEQUENCE_GROUND ,
2738} ) ;
2839
2940export type TerminalOutputUpdate =
@@ -54,6 +65,7 @@ export const EMPTY_TERMINAL_OUTPUT_STATE = Object.freeze<TerminalOutputState>({
5465 retainedBytes : 0 ,
5566 resetVersion : 0 ,
5667 nextOffset : 0 ,
68+ sequenceState : TERMINAL_SEQUENCE_GROUND ,
5769} ) ;
5870
5971interface Utf8Chunk {
@@ -175,6 +187,7 @@ function appendOutput(
175187 retainedBytes : 0 ,
176188 resetVersion : current . resetVersion + 1 ,
177189 nextOffset : current . nextOffset + data . length ,
190+ sequenceState : TERMINAL_SEQUENCE_GROUND ,
178191 } ;
179192 }
180193 const appended = splitOutputChunks (
@@ -185,12 +198,19 @@ function appendOutput(
185198
186199 const chunks = [ ...current . chunks , ...appended . chunks ] ;
187200 let retainedBytes = current . retainedBytes + appended . byteLength ;
201+ let sequenceState = current . sequenceState ;
188202 let firstRetainedIndex = 0 ;
189203 while ( retainedBytes > maxBufferBytes && firstRetainedIndex < chunks . length ) {
190204 const first = chunks [ firstRetainedIndex ] ! ;
191205 const bytesToDrop = retainedBytes - maxBufferBytes ;
192206 if ( bytesToDrop < first . byteLength ) {
193207 const trimmed = trimOutputChunkStart ( first , bytesToDrop ) ;
208+ sequenceState = terminalSequenceStateAfter (
209+ sequenceState ,
210+ first . data ,
211+ 0 ,
212+ first . data . length - trimmed . data . length ,
213+ ) ;
194214 retainedBytes -= first . byteLength - trimmed . byteLength ;
195215 if ( trimmed . byteLength > 0 ) {
196216 chunks [ firstRetainedIndex ] = trimmed ;
@@ -199,6 +219,7 @@ function appendOutput(
199219 }
200220 break ;
201221 }
222+ sequenceState = terminalSequenceStateAfter ( sequenceState , first . data ) ;
202223 retainedBytes -= first . byteLength ;
203224 firstRetainedIndex += 1 ;
204225 }
@@ -209,6 +230,7 @@ function appendOutput(
209230 const excessChunks = retainedChunks . length - MAX_TERMINAL_OUTPUT_CHUNKS ;
210231 if ( excessChunks > 0 ) {
211232 for ( const chunk of retainedChunks . slice ( 0 , excessChunks ) ) {
233+ sequenceState = terminalSequenceStateAfter ( sequenceState , chunk . data ) ;
212234 retainedBytes -= chunk . byteLength ;
213235 }
214236 retainedChunks = retainedChunks . slice ( excessChunks ) ;
@@ -221,6 +243,7 @@ function appendOutput(
221243 retainedBytes,
222244 resetVersion : current . resetVersion ,
223245 nextOffset : appended . nextOffset ,
246+ sequenceState,
224247 } ;
225248}
226249
@@ -241,18 +264,21 @@ function resetOutput(
241264 retainedBytes : reset . byteLength ,
242265 resetVersion : current . resetVersion + 1 ,
243266 nextOffset : reset . nextOffset ,
267+ sequenceState : TERMINAL_SEQUENCE_GROUND ,
244268 } ;
245269}
246270
271+ /** The retained output a fresh terminal can replay: it starts outside any control sequence. */
247272export function terminalOutputText ( output : TerminalOutputState ) : string {
248- return output . chunks . map ( ( chunk ) => chunk . data ) . join ( "" ) ;
273+ const text = output . chunks . map ( ( chunk ) => chunk . data ) . join ( "" ) ;
274+ return text . slice ( terminalSequenceEnd ( output . sequenceState , text ) ) ;
249275}
250276
251277export function readTerminalOutputUpdate (
252278 output : TerminalOutputState ,
253279 cursor : TerminalOutputCursor ,
254280) : TerminalOutputUpdate {
255- const nextCursor = {
281+ const position = {
256282 generation : output . generation ,
257283 resetVersion : output . resetVersion ,
258284 offset : output . nextOffset ,
@@ -263,7 +289,16 @@ export function readTerminalOutputUpdate(
263289 cursor . resetVersion !== output . resetVersion ||
264290 cursor . offset < ( firstChunk ?. startOffset ?? output . nextOffset )
265291 ) {
266- return { type : "reset" , data : terminalOutputText ( output ) , cursor : nextCursor } ;
292+ const text = output . chunks . map ( ( chunk ) => chunk . data ) . join ( "" ) ;
293+ const start = terminalSequenceEnd ( output . sequenceState , text ) ;
294+ return {
295+ type : "reset" ,
296+ data : text . slice ( start ) ,
297+ cursor : {
298+ ...position ,
299+ sequenceState : terminalSequenceStateAfter ( output . sequenceState , text , 0 , start ) ,
300+ } ,
301+ } ;
267302 }
268303
269304 // Appends and compaction preserve offset order, so unread chunks form a suffix.
@@ -273,17 +308,19 @@ export function readTerminalOutputUpdate(
273308 if ( ! ( chunk . startOffset + chunk . data . length > cursor . offset ) ) break ;
274309 firstUnread -= 1 ;
275310 }
276- const appended = output . chunks . slice ( firstUnread ) ;
277- if ( appended . length === 0 ) {
311+ const text = output . chunks
312+ . slice ( firstUnread )
313+ . map ( ( chunk ) => chunk . data . slice ( Math . max ( 0 , cursor . offset - chunk . startOffset ) ) )
314+ . join ( "" ) ;
315+ const start = terminalSequenceEnd ( cursor . sequenceState , text ) ;
316+ const nextCursor = {
317+ ...position ,
318+ sequenceState : terminalSequenceStateAfter ( cursor . sequenceState , text , 0 , start ) ,
319+ } ;
320+ if ( start === text . length ) {
278321 return { type : "none" , cursor : nextCursor } ;
279322 }
280- return {
281- type : "append" ,
282- data : appended
283- . map ( ( chunk ) => chunk . data . slice ( Math . max ( 0 , cursor . offset - chunk . startOffset ) ) )
284- . join ( "" ) ,
285- cursor : nextCursor ,
286- } ;
323+ return { type : "append" , data : text . slice ( start ) , cursor : nextCursor } ;
287324}
288325
289326export { appendOutput , resetOutput } ;
0 commit comments