File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -148,8 +148,22 @@ export function spawnPipe(options: SpawnBackendOptions): TerminalProcess {
148148 const exitCbs : ( ( code : number ) => void ) [ ] = [ ]
149149 let exited = false
150150
151+ // A piped child has no controlling TTY, so its stdout/stderr carry bare `\n`
152+ // line endings — a real PTY would apply the kernel's ONLCR translation. xterm
153+ // only returns the cursor to column 0 on `\r`, so forwarding bare `\n` renders
154+ // a staircase. Translate lone `\n` to `\r\n`, tracking a `\r` left dangling at
155+ // a chunk boundary so an existing `\r\n` split across chunks isn't doubled.
156+ let pendingCr = false
157+ const normalizeNewlines = ( data : string ) : string => {
158+ const out = data . replace ( / \r ? \n / g, ( match , offset : number ) =>
159+ match === '\n' && ! ( offset === 0 && pendingCr ) ? '\r\n' : match )
160+ pendingCr = out . endsWith ( '\r' )
161+ return out
162+ }
163+
151164 const emitData = ( data : string ) : void => {
152- for ( const cb of dataCbs ) cb ( data )
165+ const normalized = normalizeNewlines ( data )
166+ for ( const cb of dataCbs ) cb ( normalized )
153167 }
154168 const emitExit = ( code : number ) : void => {
155169 if ( exited )
Original file line number Diff line number Diff line change @@ -62,6 +62,26 @@ describe('@devframes/plugin-terminals', () => {
6262 } )
6363 } )
6464
65+ it ( 'normalizes bare LF from a piped readonly session to CRLF' , async ( ) => {
66+ const client = bootClient ( server . port )
67+ await new Promise ( r => setTimeout ( r , 50 ) )
68+
69+ // A piped child has no TTY to apply ONLCR, so it emits bare `\n`. Without
70+ // normalization xterm renders a staircase; the backend must translate lone
71+ // `\n` to `\r\n` while leaving an existing `\r\n` untouched.
72+ const info = await call < TerminalSessionInfo > ( client , 'devframes-plugin-terminals:spawn' , {
73+ command : NODE ,
74+ args : [ '-e' , 'process.stdout.write("a\\nb\\r\\nc")' ] ,
75+ mode : 'readonly' ,
76+ } )
77+
78+ const reader = subscribe ( client , info . id )
79+ const output = await collectUntil ( reader , acc => acc . includes ( 'a' ) && acc . includes ( 'b' ) && acc . includes ( 'c' ) )
80+ expect ( output ) . toContain ( 'a\r\nb\r\nc' )
81+ expect ( output ) . not . toContain ( 'a\nb' )
82+ expect ( output ) . not . toContain ( '\r\r\n' )
83+ } )
84+
6585 it ( 'rejects writes to a readonly session' , async ( ) => {
6686 const client = bootClient ( server . port )
6787 await new Promise ( r => setTimeout ( r , 50 ) )
You can’t perform that action at this time.
0 commit comments