char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
b3a4fb7
main
1import { TERMINAL_PALETTE as palette } from "@berry/util/palette.ts" ; 2 3function indexedColor ( index :number ) :string | undefined { 4if ( ! Number . isInteger ( index ) || index < 0 || index > 255 ) return undefined ; 5if ( index < 16 ) return palette [ index ]; 6if ( index >= 232 ) { 7const gray = 8 + ( index - 232 ) * 10 ; 8return `rgb( ${ gray } , ${ gray } , ${ gray } )` ; 9} 10const value = index - 16 ; 11const levels = [ 0 , 95 , 135 , 175 , 215 , 255 ]; 12return `rgb( ${ levels [ Math . floor ( value / 36 )]} , ${ levels [ Math . floor ( value / 6 ) % 6 ]} , ${ 13levels [ value % 6 ] 14} )` ; 15} 16 17export type AnsiRun = { text :string ; style :Record < string , string > }; 18 19/** An incremental SGR decoder that hides terminal controls, even across chunk boundaries. */ 20export function ansiParser () { 21let foreground :string | undefined ; 22let background :string | undefined ; 23let bold = false ; 24let dim = false ; 25let italic = false ; 26let underline = false ; 27let strike = false ; 28let inverse = false ; 29let mode :"text" | "escape" | "csi" | "osc" | "oscEscape" = "text" ; 30let parametersText = "" ; 31let invalid = false ; 32 33return { 34write ( source :string ) :AnsiRun [] { 35const runs :AnsiRun [] = []; 36let offset = 0 ; 37// discard non-SGR controls, including OSC links and a streaming chunk's unfinished escape 38while ( offset < source . length ) { 39if ( mode === "text" ) { 40const escape = source . indexOf ( "\x1b" , offset ); 41const text = source . slice ( offset , escape === - 1 ?source . length :escape ); 42if ( text ) { 43const style :Record < string , string > = {}; 44const fg = inverse ?background ?? "var(--panel)" :foreground ; 45const bg = inverse ?foreground ?? "var(--text)" :background ; 46if ( fg ) style . color = fg ; 47if ( bg ) style [ "background-color" ] = bg ; 48if ( bold ) style [ "font-weight" ] = "bold" ; 49if ( dim ) style . opacity = "0.5" ; 50if ( italic ) style [ "font-style" ] = "italic" ; 51const decoration = [ underline ?"underline" :"" , strike ?"line-through" :"" ]. filter ( 52Boolean , 53); 54if ( decoration . length ) style [ "text-decoration" ] = decoration . join ( " " ); 55runs . push ({ text, style}); 56} 57if ( escape === - 1 ) break ; 58offset = escape + 1 ; 59mode = "escape" ; 60continue ; 61} 62const char = source [ offset ++ ]; 63if ( mode === "escape" ) { 64if ( char === "[" ) { 65mode = "csi" ; 66parametersText = "" ; 67invalid = false ; 68} else if ( char === "]" ) mode = "osc" ; 69else if ( char !== "\x1b" && ! ( char >= " " && char <= "/" )) mode = "text" ; 70} else if ( mode === "osc" ) { 71if ( char === "\x07" ) mode = "text" ; 72else if ( char === "\x1b" ) mode = "oscEscape" ; 73} else if ( mode === "oscEscape" ) { 74if ( char === "\\" || char === "\x07" ) mode = "text" ; 75else if ( char !== "\x1b" ) mode = "osc" ; 76} else if ( char >= "@" && char <= "~" ) { 77mode = "text" ; 78if ( char !== "m" || invalid || ! /^[\d;]*$ / . test ( parametersText )) continue ; 79const parameters = parametersText . split ( ";" ). map ( Number ); 80for ( let i = 0 ; i < parameters . length ; i ++ ) { 81const parameter = parameters [ i ]; 82if ( parameter === 0 ) { 83foreground = background = undefined ; 84bold = dim = false ; 85italic = underline = false ; 86strike = inverse = false ; 87} else if ( parameter === 1 ) bold = true ; 88else if ( parameter === 2 ) dim = true ; 89else if ( parameter === 3 ) italic = true ; 90else if ( parameter === 4 ) underline = true ; 91else if ( parameter === 7 ) inverse = true ; 92else if ( parameter === 9 ) strike = true ; 93else if ( parameter === 22 ) bold = dim = false ; 94else if ( parameter === 23 ) italic = false ; 95else if ( parameter === 24 ) underline = false ; 96else if ( parameter === 27 ) inverse = false ; 97else if ( parameter === 29 ) strike = false ; 98else if ( parameter === 39 ) foreground = undefined ; 99else if ( parameter === 49 ) background = undefined ; 100else if ( parameter >= 30 && parameter <= 37 ) foreground = palette [ parameter - 30 ]; 101else if ( parameter >= 90 && parameter <= 97 ) foreground = palette [ parameter - 82 ]; 102else if ( parameter >= 40 && parameter <= 47 ) background = palette [ parameter - 40 ]; 103else if ( parameter >= 100 && parameter <= 107 ) background = palette [ parameter - 92 ]; 104else if ( parameter === 38 || parameter === 48 ) { 105const mode = parameters [ ++ i ]; 106let color :string | undefined ; 107if ( mode === 5 ) color = indexedColor ( parameters [ ++ i ]); 108else if ( mode === 2 ) { 109const rgb = parameters . slice ( i + 1 , i + 4 ); 110i += 3 ; 111if ( 112rgb . length === 3 && rgb . every (( n ) => Number . isInteger ( n ) && n >= 0 && n <= 255 ) 113) { 114color = `rgb( ${ rgb . join ( ", " )} )` ; 115} 116} else break ; 117if ( color !== undefined ) { 118if ( parameter === 38 ) foreground = color ; 119else background = color ; 120} 121} 122} 123parametersText = "" ; 124} else if ( char >= " " && char <= "?" ) { 125// bound malformed CSI sequences; OSC payloads are never buffered 126if ( parametersText . length < 1024 && char >= "0" && ! invalid ) parametersText += char ; 127else invalid = true ; 128} else { 129mode = char === "\x1b" ?"escape" :"text" ; 130} 131} 132return runs ; 133}, 134}; 135} 136 137/** Text and SGR styles, without interpreting terminal commands or output as markup. */ 138export function ansiRuns ( source :string ) :AnsiRun [] { 139return ansiParser (). write ( source ); 140}